open-consul/ui-v2/app/utils/form/builder.js
John Cowen 338812f5c2
ui: UI Release Merge (ui-staging merge) (#6527)
## HTTPAdapter (#5637)

## Ember upgrade 2.18 > 3.12 (#6448)

### Proxies can no longer get away with not calling _super

This means that we can't use create anymore to define dynamic methods.
Therefore we dynamically make 2 extended Proxies on demand, and then
create from those. Therefore we can call _super in the init method of
the extended Proxies.

### We aren't allowed to reset a service anymore

We never actually need to now anyway, this is a remnant of the refactor
from browser based confirmations. We fix it as simply as possible here
but will revisit and remove the old browser confirm functionality at a
later date

### Revert classes to use ES5 style to workaround babel transp. probs

Using a mixture of ES6 classes (and hence super) and arrow functions
means that when babel transpiles the arrow functions down to ES5, a
reference to this is moved before the call to super, hence causing a js
error.

Furthermore, we the testing environment no longer lets use use
apply/call on the constructor.

These errors only manifests during testing (only in the testing
environment), the application itself runs fine with no problems without
this change.

Using ES5 style class definitions give us freedom to do all of the above
without causing any errors, so we reverted these classes back to ES5
class definitions

### Skip test that seems to have changed due to a change in RSVP timing

This test tests a usecase/area of the API that will probably never ever
be used, it was more testing out the API. We've skipped the test for now
as this doesn't affect the application itself, but left a note to come
back here later to investigate further

### Remove enumerableContentDidChange

Initial testing looks like we don't need to call this function anymore,
the function no longer exists

### Rework Changeset.isSaving to take into account new ember APIs

Setting/hanging a computedProperty of an instantiated object no longer
works. Move to setting it on the prototype/class definition instead

### Change how we detect whether something requires listening

New ember API's have changed how you can detect whether something is a
computedProperty or not. It's not immediately clear if its even possible
now. Therefore we change how we detect whether something should be
listened to or not by just looking for presence of `addEventListener`

### Potentially temporary change of ci test scripts to ensure deps exist

All our tooling scripts run through a Makefile (for people familiar with
only using those), which then call yarn scripts which can be called
independently (for people familar with only using yarn).

The Makefile targets always check to make sure all the dependencies are
installed before running anything that requires them (building, testing
etc).

The CI scripts/targets didn't follow this same route and called the yarn
scripts directly (usually CI builds a cache of the dependencies first).

For some reason this cache isn't doing what it usually does, and it
looks as though, in CI, ember isn't installed.

This commit makes the CI scripts consistently use the same method as all
of the other tooling scripts (Makefile target > Install Deps if
required > call yarn script). This should install the dependencies if
for some reason the CI cache building doesn't complete/isn't successful.

Potentially this commit may be reverted if, the root of the problem is
elsewhere, although consistency is always good, so it might be a good
idea to leave this commit as is even if we need to debug and fix things
elsewhere.

### Make test-parallel consistent with the rest of the tooling scripts

As we are here making changes for CI purposes (making test-ci
consistent), we spotted that test-parallel is also inconsistent and also
the README manual instructions won't work without `ember` installed
globally.

This commit makes everything consistent and changes the manual
instructions to use the local ember instance that gets installed via
yarn

### Re-wrangle catchable to fit with new ember 3.12 APIs

In the upgrade from ember 3.8 > 3.12 the public interfaces for
ComputedProperties have changed slightly. `meta` is no longer a public
property of ComputedProperty but of a ComputedDecoratorImpl mixin
instead.

7e4ba1096e/packages/%40ember/-internals/metal/lib/computed.ts (L725)

There seems to be no way, by just using publically available
methods, to replicate this behaviour so that we can create our own
'ComputedProperty` factory via injecting the ComputedProperty class as
we did previously.

3f333bada1/ui-v2/app/utils/computed/factory.js (L1-L18)

Instead we dynamically hang our `Catchable` `catch` method off the
instantiated ComputedProperty. In doing it like this `ComputedProperty`
has already has its `meta` method mixed in so we don't have to manually
mix it in ourselves (which doesn't seem possible)

This functionality is only used during our work in trying to ensure
our EventSource/BlockingQuery work was as 'ember-like' as possible (i.e.
using the traditional Route.model hooks and ember-like Controller
properties). Our ongoing/upcoming work on a componentized approach to
data a.k.a `<DataSource />` means we will be able to remove the majority
of the code involved here now that it seems to be under an amount of
flux in ember.

### Build bindata_assetfs.go with new UI changes
2019-09-30 14:47:49 +01:00

182 lines
7.1 KiB
JavaScript

import { get, set, computed } from '@ember/object';
import Changeset from 'ember-changeset';
import lookupValidator from 'ember-changeset-validations';
// Keep these here for now so forms are easy to make
// TODO: Probably move this to utils/form/parse-element-name
import parseElementName from 'consul-ui/utils/get-form-name-property';
// TODO: Currently supporting ember-data nicely like this
// Unfortunately since post-ember 2.18, the only way to get this to work
// is to hang stuff off the prototype (which then makes it available everywhere)
// we should either try and figure out another way of doing this, or move this code
// somewhere where it is more 'global' like an initializer
Changeset.prototype.isSaving = computed('data.isSaving', function() {
return this.data.isSaving;
});
const defaultChangeset = function(data, validators) {
return new Changeset(data, lookupValidator(validators), validators);
};
/**
* Form builder/Form factory
* Deals with handling (generally change) events and updating data in response to the change
* in a typical data down event up manner
* validations are included currently using ember-changeset-validations
*
* @param {string} name - The name of the form, generally this is the name of your model
* Generally (until view building is complete) you should name your form elements as `name="modelName[property]"`
* or pass this name through using you action and create an Event-like object instead
* You can also just not set a name and use `name="property"`, but if you want to use combinations
* if multiple forms at least form children should use names
*
* @param {object} config - Form configuration object. Just a plain object to configure the form should be a hash
* with property names relating to the form data. Each property is the configuration for that model/data property
* currently the only supported property of these configuration objects is `type` which currently allows you to
* set a property as 'array-like'
*/
export default function(changeset = defaultChangeset, getFormNameProperty = parseElementName) {
return function(name = '', obj = {}) {
const _children = {};
let _validators = null;
// TODO make this into a class to reuse prototype
const form = {
data: null,
name: name,
getName: function() {
return this.name;
},
setData: function(data) {
// Array check temporarily for when we get an empty array from repo.status
if (_validators && !Array.isArray(data)) {
data = changeset(data, _validators);
}
set(this, 'data', data);
return this;
},
getData: function() {
return this.data;
},
add: function(child) {
_children[child.getName()] = child;
return this;
},
handleEvent: function(e, targetName) {
const target = e.target;
// currently we only use targetName in {{form-component}} for handling deeply
// nested forms, once {{form-component}} handles deeply nested forms targetName can go
const parts = getFormNameProperty(targetName || target.name);
// split the form element name from `name[prop]`
const name = parts[0];
const prop = parts[1];
//
let config = obj;
// if the name (usually the name of the model) isn't this form, look at its children
if (name !== this.getName()) {
if (this.has(name)) {
// is its a child form then use the child form
return this.form(name).handleEvent(e);
}
// should probably throw here, unless we support having a name
// even if you are referring to this form
config = config[name];
}
const data = this.getData();
// ember-data/changeset dance
// TODO: This works for ember-data RecordSets and Changesets but not for plain js Objects
// see settings
const json = typeof data.toJSON === 'function' ? data.toJSON() : get(data, 'data').toJSON();
// if the form doesn't include a property then throw so it can be
// caught outside, therefore the user can deal with things that aren't in the data
// TODO: possibly need to add support for deeper properties using `get` here
// for example `client.blocking` instead of just `blocking`
if (!Object.keys(json).includes(prop)) {
const error = new Error(`${prop} property doesn't exist`);
error.target = target;
throw error;
}
// deal with the change of property
let currentValue = get(data, prop);
// if the value is an array-like or config says its an array
if (
Array.isArray(currentValue) ||
(typeof config[prop] !== 'undefined' &&
typeof config[prop].type === 'string' &&
config[prop].type.toLowerCase() === 'array')
) {
// array specific set
if (currentValue == null) {
currentValue = [];
}
const method = target.checked ? 'pushObject' : 'removeObject';
currentValue[method](target.value);
set(data, prop, currentValue);
} else {
// deal with booleans
// but only booleans that aren't checkboxes/radios with values
if (
typeof target.checked !== 'undefined' &&
(target.value.toLowerCase() === 'on' || target.value.toLowerCase() === 'off')
) {
set(data, prop, target.checked);
} else {
// text and non-boolean checkboxes/radios
set(data, prop, target.value);
}
}
// validate everything
return this.validate();
},
reset: function() {
const data = this.getData();
if (typeof data.rollbackAttributes === 'function') {
this.getData().rollbackAttributes();
}
return this;
},
clear: function(cb = {}) {
if (typeof cb === 'function') {
return (this.clearer = cb);
} else {
return this.setData(this.clearer(cb)).getData();
}
},
submit: function(cb = {}) {
if (typeof cb === 'function') {
return (this.submitter = cb);
} else {
this.submitter(this.getData());
}
},
setValidators: function(validators) {
_validators = validators;
return this;
},
validate: function() {
const data = this.getData();
// just pass along to the Changeset for now
if (typeof data.validate === 'function') {
data.validate();
}
return this;
},
addError: function(name, message) {
const data = this.getData();
if (typeof data.addError === 'function') {
data.addError(...arguments);
}
},
form: function(name) {
if (name == null) {
return this;
}
return _children[name];
},
has: function(name) {
return typeof _children[name] !== 'undefined';
},
};
form.submit = form.submit.bind(form);
form.reset = form.reset.bind(form);
return form;
};
}