apostrophe 3.24.0 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/index.js +10 -5
  3. package/modules/@apostrophecms/asset/index.js +19 -26
  4. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +2 -6
  5. package/modules/@apostrophecms/db/index.js +1 -1
  6. package/modules/@apostrophecms/i18n/i18n/en.json +0 -1
  7. package/modules/@apostrophecms/i18n/i18n/es.json +0 -1
  8. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
  9. package/modules/@apostrophecms/i18n/i18n/sk.json +0 -1
  10. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +1 -1
  11. package/modules/@apostrophecms/module/index.js +10 -1
  12. package/modules/@apostrophecms/permission/index.js +29 -21
  13. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +15 -24
  14. package/modules/@apostrophecms/schema/index.js +11 -1
  15. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +6 -8
  16. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +5 -4
  17. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +5 -4
  18. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +5 -23
  19. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputChoicesMixin.js +32 -0
  20. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +20 -2
  21. package/modules/@apostrophecms/util/index.js +1 -1
  22. package/package.json +10 -15
  23. package/test/assets.js +19 -102
  24. package/test/permissions.js +158 -105
  25. package/test/utils/permissions.js +638 -0
  26. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.es5.js +0 -33
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.26.0
4
+
5
+ ### Adds
6
+
7
+ * Tasks can now be registered with the `afterModuleReady` flag, which is more useful than `afterModuleInit` because it waits for the module to be more fully initialized, including all "improvements" loaded via npm. The original `afterModuleInit` flag is still supported in case someone was counting on its behavior.
8
+ * Add `/grid` `POST` route in permission module, in addition to the existing `GET` one, to improve extensibility.
9
+
10
+ ### Changes
11
+
12
+ * Since Microsoft has ended support for IE11 and support for ES5 builds is responsible for a significant chunk of Apostrophe's installation time, the `es5: true` option no longer produces an IE11 build. For backwards compatibility, developers will receive a warning, but their build will proceed without IE11 support. IE11 ES5 builds can be brought back by installing the optional [@apostrophecms/asset-es5](https://github.com/apostrophecms/asset-es5) module.
13
+
14
+ ### Fixes
15
+
16
+ * `testModule: true` works in unit tests of external Apostrophe modules again even with modern versions of `mocha`, thanks to [Amin Shazrin](https://github.com/ammein).
17
+ * `getObjectManager` is now implemented for `Object` field types, fixing a bug that prevented the use of areas found in `object` schema fields within templates. Thanks to [James R T](https://github.com/jamestiotio).
18
+
19
+ ## 3.25.0 (2022-07-20)
20
+
21
+ ### Adds
22
+
23
+ * `radio` and `checkboxes` input field types now support a server side `choices` function for supplying their `choices` array dynamically, just like `select` fields do. Future custom field types can opt into this functionality with the field type flag `dynamicChoices: true`.
24
+
25
+ ### Fixes
26
+
27
+ * `AposSelect` now emits values on `change` event as they were originally given. Their values "just work" so you do not have to think about JSON anymore when you receive it.
28
+ * Unpinned tiptap as the tiptap team has made releases that resolve the packaging errors that caused us to pin it in 3.22.1.
29
+ * Pinned `vue-loader` to the `15.9.x` minor release series for now. The `15.10.0` release breaks support for using `npm link` to develop the `apostrophe` module itself.
30
+ * Minimum version of `sanitize-html` bumped to ensure a potential denial-of-service vector is closed.
31
+
3
32
  ## 3.24.0 (2022-07-06)
4
33
 
5
34
  ### Adds
package/index.js CHANGED
@@ -505,14 +505,18 @@ async function apostrophe(options, telemetry, rootSpan) {
505
505
  // and throws an exception if we don't
506
506
  function findTestModule() {
507
507
  let m = module;
508
+ const nodeModuleRegex = new RegExp(`node_modules${path.sep}mocha`);
509
+ if (!require.main.filename.match(nodeModuleRegex)) {
510
+ throw new Error('mocha does not seem to be running, is this really a test?');
511
+ }
508
512
  while (m) {
509
- if (m.parent && m.parent.filename.match(/node_modules\/mocha/)) {
513
+ if (m.parent && m.parent.filename.match(nodeModuleRegex)) {
514
+ return m;
515
+ } else if (!m.parent) {
516
+ // Mocha v10 doesn't inject mocha paths inside `module`, therefore, we only detect the parent until the last parent. But we can get Mocha running using `require.main` - Amin
510
517
  return m;
511
518
  }
512
519
  m = m.parent;
513
- if (!m) {
514
- throw new Error('mocha does not seem to be running, is this really a test?');
515
- }
516
520
  }
517
521
  }
518
522
  }
@@ -562,7 +566,8 @@ async function apostrophe(options, telemetry, rootSpan) {
562
566
  self.modules = {};
563
567
  for (const item of modulesToBeInstantiated()) {
564
568
  // module registers itself in self.modules
565
- await self.synth.create(item, { apos: self });
569
+ const module = await self.synth.create(item, { apos: self });
570
+ await module.emit('moduleReady');
566
571
  }
567
572
  }
568
573
 
@@ -42,6 +42,7 @@ module.exports = {
42
42
  },
43
43
 
44
44
  async init(self) {
45
+
45
46
  self.restartId = self.apos.util.generateId();
46
47
  self.iconMap = {
47
48
  ...globalIcons
@@ -63,6 +64,7 @@ module.exports = {
63
64
  self.buildWatcherEnable = process.env.APOS_ASSET_WATCH !== '0' && self.options.watch !== false;
64
65
  self.buildWatcherDebounceMs = parseInt(self.options.watchDebounceMs || 1000, 10);
65
66
  self.buildWatcher = null;
67
+
66
68
  },
67
69
  handlers (self) {
68
70
  return {
@@ -121,6 +123,15 @@ module.exports = {
121
123
  usage: 'Build Apostrophe frontend CSS and JS bundles',
122
124
  afterModuleInit: true,
123
125
  async task(argv = {}) {
126
+ if (self.options.es5 && !self.es5TaskFn) {
127
+ self.apos.util.warnDev(stripIndent`
128
+ es5: true is set. IE11 compatibility builds now require that you
129
+ install the optional @apostrophecms/asset-es5 module. Until then,
130
+ for backwards compatibility, your build will succeed but
131
+ will not be IE11 compatible.
132
+ `);
133
+ self.options.es5 = false;
134
+ }
124
135
  // The lock could become huge, cache it, see computeCacheMeta()
125
136
  let packageLockContentCached;
126
137
  const req = self.apos.task.getReq();
@@ -350,7 +361,11 @@ module.exports = {
350
361
  modulesDir,
351
362
  outputPath: bundleDir,
352
363
  outputFilename,
353
- bundles: webpackExtraBundles
364
+ bundles: webpackExtraBundles,
365
+ // Added on the fly by the
366
+ // @apostrophecms/asset-es5 module,
367
+ // if it is present
368
+ es5TaskFn: self.es5TaskFn
354
369
  }, self.apos);
355
370
 
356
371
  const webpackInstanceConfigMerged = self.webpackExtensions
@@ -841,7 +856,7 @@ module.exports = {
841
856
 
842
857
  'clear-cache': {
843
858
  usage: 'Clear build cache',
844
- afterModuleInit: true,
859
+ afterModuleReady: true,
845
860
  async task(argv) {
846
861
  const cacheBaseDir = self.getCacheBasePath();
847
862
 
@@ -1161,7 +1176,7 @@ module.exports = {
1161
1176
  // If you are trying to enable IE11 support for ui/src, use the
1162
1177
  // `es5: true` option (es5 builds are disabled by default).
1163
1178
  configureBuilds() {
1164
- const srcPrologue = stripIndent`
1179
+ self.srcPrologue = stripIndent`
1165
1180
  (function() {
1166
1181
  window.apos = window.apos || {};
1167
1182
  var data = document.body && document.body.getAttribute('data-apos');
@@ -1188,26 +1203,7 @@ module.exports = {
1188
1203
  index: true,
1189
1204
  // Load only in browsers that support ES6 modules
1190
1205
  condition: 'module',
1191
- prologue: srcPrologue
1192
- },
1193
- 'src-es5': {
1194
- // An alternative build from the same sources for IE11
1195
- source: 'src',
1196
- webpack: true,
1197
- scenes: [ 'public', 'apos' ],
1198
- // The CSS from the src build is identical, do not duplicate it
1199
- outputs: [ 'js' ],
1200
- label: 'apostrophe:ie11Build',
1201
- // Load index.js and index.scss from each module
1202
- index: true,
1203
- // The polyfills babel will be expecting
1204
- prologue: stripIndent`
1205
- import "core-js/stable";
1206
- import "regenerator-runtime/runtime";
1207
- ${srcPrologue}
1208
- `,
1209
- // Load only in browsers that do not support ES6 modules
1210
- condition: 'nomodule'
1206
+ prologue: self.srcPrologue
1211
1207
  },
1212
1208
  public: {
1213
1209
  scenes: [ 'public', 'apos' ],
@@ -1236,9 +1232,6 @@ module.exports = {
1236
1232
  // We could add an apos-ie11 bundle that just pushes a "sorry charlie" prologue,
1237
1233
  // if we chose
1238
1234
  };
1239
- if (!self.options.es5) {
1240
- delete self.builds['src-es5'];
1241
- }
1242
1235
  },
1243
1236
  // Filter the given css performing any necessary transformations,
1244
1237
  // such as support for the /modules path regardless of where
@@ -1,7 +1,6 @@
1
1
  const path = require('path');
2
2
  const merge = require('webpack-merge').merge;
3
3
  const scssTask = require('./webpack.scss');
4
- const es5Task = require('./webpack.es5');
5
4
  const srcBuildNames = [ 'src-build', 'src-es5-build' ];
6
5
 
7
6
  let BundleAnalyzerPlugin;
@@ -11,14 +10,11 @@ if (process.env.APOS_BUNDLE_ANALYZER) {
11
10
  }
12
11
 
13
12
  module.exports = ({
14
- importFile, modulesDir, outputPath, outputFilename, bundles = {}, es5
13
+ importFile, modulesDir, outputPath, outputFilename, bundles = {}, es5, es5TaskFn
15
14
  }, apos) => {
16
15
  const mainBundleName = outputFilename.replace('.js', '');
17
- const taskFns = [ scssTask ];
16
+ const taskFns = [ scssTask, ...(es5 ? [ es5TaskFn ] : []) ];
18
17
 
19
- if (es5) {
20
- taskFns.push(es5Task);
21
- }
22
18
  const tasks = taskFns.map(task =>
23
19
  task(
24
20
  {
@@ -168,7 +168,7 @@ This database contains an Apostrophe 2.x website. Exiting to avoid content loss.
168
168
  // and start up your app, which will recreate them.
169
169
  reset: {
170
170
  usage: 'Usage: node app @apostrophecms/db:reset\n\nThis destroys ALL of your content. EVERYTHING in your database.\n',
171
- afterModuleInit: true,
171
+ afterModuleReady: true,
172
172
  exitAfter: false,
173
173
  task: async () => {
174
174
  const argv = self.apos.argv;
@@ -288,7 +288,6 @@
288
288
  "previousPage": "Previous Page",
289
289
  "public": "Public",
290
290
  "modernBuild": "Public-facing modern JavaScript and Sass",
291
- "ie11Build": "Public-facing modern JavaScript and Sass (IE11 build)",
292
291
  "publish": "Publish",
293
292
  "publishBeforeUsingTooltip": "Publish this content before using it in a relationship",
294
293
  "published": "Published",
@@ -265,7 +265,6 @@
265
265
  "previousPage": "Página Previa",
266
266
  "public": "Público",
267
267
  "modernBuild": "JavaScript y Sass moderno orientado al público",
268
- "ie11Build": "JavaScript y Sass (edición IE11) moderno orientado al público",
269
268
  "publish": "Publicar",
270
269
  "publishBeforeUsingTooltip": "Publique este contenido antes de utilizarlo en una relación",
271
270
  "published": "Publicado",
@@ -263,7 +263,6 @@
263
263
  "previousPage": "Página Anterior",
264
264
  "public": "Público",
265
265
  "modernBuild": "JavaScript and Sass modernos públicos",
266
- "ie11Build": "JavaScript and Sass (IE11 build) modernos públicos",
267
266
  "publish": "Publicar",
268
267
  "publishBeforeUsingTooltip": "Publique esse conteúdo antes de usá-lo em um relacionamento",
269
268
  "published": "Publicado",
@@ -277,7 +277,6 @@
277
277
  "previousPage": "Predchádzajúca strana",
278
278
  "public": "Verejné",
279
279
  "modernBuild": "Verejnosti orientovaný moderný JavaScript a Sass",
280
- "ie11Build": "Verejný moderný JavaScript a Sass (zostava IE11)",
281
280
  "publish": "Publikovať",
282
281
  "publishBeforeUsingTooltip": "Pred použitím v kontexte zverejnite tento obsah",
283
282
  "published": "Publikovaný",
@@ -396,7 +396,7 @@ export default {
396
396
  };
397
397
  },
398
398
  updateAspectRatio(value) {
399
- this.aspectRatio = parseFloat(value);
399
+ this.aspectRatio = value;
400
400
  this.computeMaxSizes();
401
401
  this.computeMinSizes();
402
402
  },
@@ -705,8 +705,12 @@ module.exports = {
705
705
  },
706
706
 
707
707
  async executeAfterModuleInitTask() {
708
+ return self.executeAfterModuleTask('afterModuleInit');
709
+ },
710
+
711
+ async executeAfterModuleTask(when) {
708
712
  for (const [ name, info ] of Object.entries(self.tasks || {})) {
709
- if (info.afterModuleInit) {
713
+ if (info[when]) {
710
714
  // Execute a task like @apostrophecms/asset:build or
711
715
  // @apostrophecms/db:reset which
712
716
  // must run before most modules are awake
@@ -764,6 +768,11 @@ module.exports = {
764
768
 
765
769
  handlers(self) {
766
770
  return {
771
+ moduleReady: {
772
+ executeAfterModuleReadyTask() {
773
+ return self.executeAfterModuleTask('afterModuleReady');
774
+ }
775
+ },
767
776
  'apostrophe:modulesRegistered': {
768
777
  addHelpers() {
769
778
  // We check this just to allow init in bootstrap tests that
@@ -302,6 +302,29 @@ module.exports = {
302
302
  const manager = self.apos.doc.getManager(permissionSet.name);
303
303
  return manager.options.showPermissions;
304
304
  },
305
+ grid(req, role) {
306
+ if (!self.can(req, 'edit', '@apostrophecms/user')) {
307
+ throw self.apos.error('forbidden');
308
+ }
309
+ const permissionSets = [];
310
+ const effectiveRole = self.apos.launder.select(role, [ 'guest', 'contributor', 'editor', 'admin' ]);
311
+ if (!effectiveRole) {
312
+ throw self.apos.error('invalid', { role: effectiveRole });
313
+ }
314
+ const _req = self.apos.task.getReq({
315
+ role: effectiveRole,
316
+ mode: 'draft'
317
+ });
318
+ for (const module of Object.values(self.apos.modules)) {
319
+ if (self.apos.synth.instanceOf(module, '@apostrophecms/piece-type')) {
320
+ permissionSets.push(self.describePermissionSet(_req, module, { piece: true }));
321
+ }
322
+ }
323
+ permissionSets.push(self.describePermissionSet(_req, self.apos.modules['@apostrophecms/any-page-type']));
324
+ return {
325
+ permissionSets: self.presentPermissionSets(permissionSets)
326
+ };
327
+ },
305
328
  ...require('./lib/legacy-migrations')(self)
306
329
  };
307
330
  },
@@ -309,27 +332,12 @@ module.exports = {
309
332
  return {
310
333
  get: {
311
334
  async grid(req) {
312
- if (!self.apos.permission.can(req, 'edit', '@apostrophecms/user')) {
313
- throw self.apos.error('forbidden');
314
- }
315
- const permissionSets = [];
316
- const effectiveRole = self.apos.launder.select(req.query.role, [ 'guest', 'contributor', 'editor', 'admin' ]);
317
- if (!effectiveRole) {
318
- throw self.apos.error('invalid', { role: effectiveRole });
319
- }
320
- const _req = self.apos.task.getReq({
321
- role: effectiveRole,
322
- mode: 'draft'
323
- });
324
- for (const module of Object.values(self.apos.modules)) {
325
- if (self.apos.synth.instanceOf(module, '@apostrophecms/piece-type')) {
326
- permissionSets.push(self.describePermissionSet(_req, module, { piece: true }));
327
- }
328
- }
329
- permissionSets.push(self.describePermissionSet(_req, self.apos.modules['@apostrophecms/any-page-type']));
330
- return {
331
- permissionSets: self.presentPermissionSets(permissionSets)
332
- };
335
+ return self.grid(req, req.query.role);
336
+ }
337
+ },
338
+ post: {
339
+ async grid(req) {
340
+ return self.grid(req, req.body.role);
333
341
  }
334
342
  }
335
343
  };
@@ -7,26 +7,15 @@
7
7
  :display-options="displayOptions"
8
8
  >
9
9
  <template #body>
10
- <div class="apos-input-wrapper apos-input__role">
11
- <select
12
- class="apos-input apos-input--select apos-input--role" :id="uid"
13
- @change="change($event.target.value)"
14
- :disabled="field.readOnly"
15
- >
16
- <option
17
- v-for="choice in choices" :key="JSON.stringify(choice.value)"
18
- :value="JSON.stringify(choice.value)"
19
- :selected="choice.value === value.data"
20
- >
21
- {{ $t(choice.label) }}
22
- </option>
23
- </select>
24
- <AposIndicator
25
- icon="menu-down-icon"
26
- class="apos-input-icon"
27
- :icon-size="20"
28
- />
29
- </div>
10
+ <AposSelect
11
+ :choices="choices"
12
+ :disabled="field.readOnly"
13
+ :selected="value.data"
14
+ :id="uid"
15
+ :classes="[ 'apos-input__role' ]"
16
+ :wrapper-classes="[ 'apos-input__role' ]"
17
+ @change="change"
18
+ />
30
19
  <div class="apos-input__role__permission-grid">
31
20
  <div
32
21
  v-for="permissionSet in permissionSets"
@@ -152,15 +141,17 @@ export default {
152
141
  },
153
142
  change(value) {
154
143
  // Allows expression of non-string values
155
- this.next = this.choices.find(choice => choice.value === JSON.parse(value)).value;
144
+ this.next = this.choices.find(choice => choice.value === value).value;
156
145
  },
157
146
  async getPermissionSets(role) {
158
- return (await apos.http.get(`${apos.permission.action}/grid`, {
159
- qs: {
147
+ const { permissionSets } = await apos.http.post(`${apos.permission.action}/grid`, {
148
+ body: {
160
149
  role
161
150
  },
162
151
  busy: true
163
- })).permissionSets;
152
+ });
153
+
154
+ return permissionSets;
164
155
  }
165
156
  }
166
157
  };
@@ -1405,6 +1405,10 @@ module.exports = {
1405
1405
  getArrayManager(name) {
1406
1406
  return self.arrayManagers[name];
1407
1407
  },
1408
+ // This allows the getManagerOf method to operate on objects of type "object".
1409
+ getObjectManager(name) {
1410
+ return self.objectManagers[name];
1411
+ },
1408
1412
  // Regenerate all array item, area and widget ids so they are considered
1409
1413
  // new. Useful when copying an entire doc.
1410
1414
  regenerateIds(req, schema, doc) {
@@ -1453,6 +1457,12 @@ module.exports = {
1453
1457
  _.each(self.apos.area.widgetManagers, function (manager, type) {
1454
1458
  self.register('widget', type, manager.schema);
1455
1459
  });
1460
+ },
1461
+
1462
+ async getChoices(req, field) {
1463
+ return typeof field.choices === 'string'
1464
+ ? self.apos.modules[field.moduleName][field.choices](req)
1465
+ : field.choices;
1456
1466
  }
1457
1467
 
1458
1468
  };
@@ -1466,7 +1476,7 @@ module.exports = {
1466
1476
  let choices = [];
1467
1477
  if (
1468
1478
  !field ||
1469
- field.type !== 'select' ||
1479
+ !self.fieldTypes[field.type].dynamicChoices ||
1470
1480
  !(field.choices && typeof field.choices === 'string')
1471
1481
  ) {
1472
1482
  throw self.apos.error('invalid');
@@ -231,7 +231,9 @@ module.exports = (self) => {
231
231
 
232
232
  self.addFieldType({
233
233
  name: 'checkboxes',
234
+ dynamicChoices: true,
234
235
  async convert(req, field, data, destination) {
236
+ const choices = await self.getChoices(req, field);
235
237
  if (typeof data[field.name] === 'string') {
236
238
  data[field.name] = self.apos.launder.string(data[field.name]).split(',');
237
239
 
@@ -241,14 +243,14 @@ module.exports = (self) => {
241
243
  }
242
244
 
243
245
  destination[field.name] = _.filter(data[field.name], function (choice) {
244
- return _.includes(_.map(field.choices, 'value'), choice);
246
+ return _.includes(_.map(choices, 'value'), choice);
245
247
  });
246
248
  } else {
247
249
  if (!Array.isArray(data[field.name])) {
248
250
  destination[field.name] = [];
249
251
  } else {
250
252
  destination[field.name] = _.filter(data[field.name], function (choice) {
251
- return _.includes(_.map(field.choices, 'value'), choice);
253
+ return _.includes(_.map(choices, 'value'), choice);
252
254
  });
253
255
  }
254
256
  }
@@ -303,13 +305,9 @@ module.exports = (self) => {
303
305
 
304
306
  self.addFieldType({
305
307
  name: 'select',
308
+ dynamicChoices: true,
306
309
  async convert(req, field, data, destination) {
307
- let choices;
308
- if ((typeof field.choices) === 'string') {
309
- choices = await self.apos.modules[field.moduleName][field.choices](req);
310
- } else {
311
- choices = field.choices;
312
- }
310
+ const choices = await self.getChoices(req, field);
313
311
  destination[field.name] = self.apos.launder.select(data[field.name], choices, field.def);
314
312
  },
315
313
  index: function (value, field, texts) {
@@ -8,7 +8,7 @@
8
8
  <template #body>
9
9
  <AposCheckbox
10
10
  :for="getChoiceId(uid, choice.value)"
11
- v-for="choice in field.choices"
11
+ v-for="choice in choices"
12
12
  :key="choice.value"
13
13
  :id="getChoiceId(uid, choice.value)"
14
14
  :choice="choice"
@@ -21,10 +21,11 @@
21
21
 
22
22
  <script>
23
23
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
24
+ import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInputChoicesMixin';
24
25
 
25
26
  export default {
26
27
  name: 'AposInputCheckboxes',
27
- mixins: [ AposInputMixin ],
28
+ mixins: [ AposInputMixin, AposInputChoicesMixin ],
28
29
  beforeMount: function () {
29
30
  this.value.data = Array.isArray(this.value.data) ? this.value.data : [];
30
31
  },
@@ -38,7 +39,7 @@ export default {
38
39
  },
39
40
  validate(values) {
40
41
  // The choices and values should always be arrays.
41
- if (!Array.isArray(this.field.choices) || !Array.isArray(values)) {
42
+ if (!Array.isArray(this.choices) || !Array.isArray(values)) {
42
43
  return 'malformed';
43
44
  }
44
45
 
@@ -48,7 +49,7 @@ export default {
48
49
 
49
50
  if (Array.isArray(values)) {
50
51
  values.forEach(chosen => {
51
- if (!this.field.choices.map(choice => {
52
+ if (!this.choices.map(choice => {
52
53
  return choice.value;
53
54
  }).includes(chosen)) {
54
55
  return 'invalid';
@@ -8,7 +8,7 @@
8
8
  <template #body>
9
9
  <label
10
10
  class="apos-choice-label" :for="getChoiceId(uid, choice.value)"
11
- v-for="choice in field.choices" :key="choice.value"
11
+ v-for="choice in choices" :key="choice.value"
12
12
  :class="{'apos-choice-label--disabled': field.readOnly}"
13
13
  >
14
14
  <input
@@ -43,12 +43,13 @@
43
43
 
44
44
  <script>
45
45
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
46
+ import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInputChoicesMixin';
46
47
  import InformationIcon from 'vue-material-design-icons/Information.vue';
47
48
 
48
49
  export default {
49
50
  name: 'AposInputRadio',
50
51
  components: { InformationIcon },
51
- mixins: [ AposInputMixin ],
52
+ mixins: [ AposInputMixin, AposInputChoicesMixin ],
52
53
  methods: {
53
54
  getChoiceId(uid, value) {
54
55
  return (uid + JSON.stringify(value)).replace(/\s+/g, '');
@@ -58,7 +59,7 @@ export default {
58
59
  return 'required';
59
60
  }
60
61
 
61
- if (value && !this.field.choices.find(choice => choice.value === value)) {
62
+ if (value && !this.choices.find(choice => choice.value === value)) {
62
63
  return 'invalid';
63
64
  }
64
65
 
@@ -66,7 +67,7 @@ export default {
66
67
  },
67
68
  change(value) {
68
69
  // Allows expression of non-string values
69
- this.next = this.field.choices.find(choice => choice.value === JSON.parse(value)).value;
70
+ this.next = this.choices.find(choice => choice.value === JSON.parse(value)).value;
70
71
  }
71
72
  }
72
73
  };
@@ -20,10 +20,11 @@
20
20
 
21
21
  <script>
22
22
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
23
+ import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInputChoicesMixin';
23
24
 
24
25
  export default {
25
26
  name: 'AposInputSelect',
26
- mixins: [ AposInputMixin ],
27
+ mixins: [ AposInputMixin, AposInputChoicesMixin ],
27
28
  props: {
28
29
  icon: {
29
30
  type: String,
@@ -37,34 +38,15 @@ export default {
37
38
  };
38
39
  },
39
40
  async mounted() {
40
- let choices;
41
- if (typeof this.field.choices === 'string') {
42
- const action = this.options.action;
43
- const response = await apos.http.get(
44
- `${action}/choices`,
45
- {
46
- qs: {
47
- fieldId: this.field._id
48
- },
49
- busy: true
50
- }
51
- );
52
- if (response.choices) {
53
- choices = response.choices;
54
- }
55
- } else {
56
- choices = this.field.choices;
57
- }
58
41
  // Add an null option if there isn't one already
59
- if (!this.field.required && !choices.find(choice => {
42
+ if (!this.field.required && !this.choices.find(choice => {
60
43
  return choice.value === null;
61
44
  })) {
62
- this.choices.push({
45
+ this.choices.unshift({
63
46
  label: '',
64
47
  value: null
65
48
  });
66
49
  }
67
- this.choices = this.choices.concat(choices);
68
50
  this.$nextTick(() => {
69
51
  // this has to happen on nextTick to avoid emitting before schemaReady is
70
52
  // set in AposSchema
@@ -87,7 +69,7 @@ export default {
87
69
  },
88
70
  change(value) {
89
71
  // Allows expression of non-string values
90
- this.next = this.choices.find(choice => choice.value === JSON.parse(value)).value;
72
+ this.next = this.choices.find(choice => choice.value === value).value;
91
73
  }
92
74
  }
93
75
  };
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Provides prep work for fetching choices from the server
3
+ * or defaulting to the choices provided with the field.
4
+ */
5
+
6
+ export default {
7
+ data() {
8
+ return {
9
+ choices: []
10
+ };
11
+ },
12
+
13
+ async mounted() {
14
+ if (typeof this.field.choices === 'string') {
15
+ const action = this.options.action;
16
+ const response = await apos.http.get(
17
+ `${action}/choices`,
18
+ {
19
+ qs: {
20
+ fieldId: this.field._id
21
+ },
22
+ busy: true
23
+ }
24
+ );
25
+ if (response.choices) {
26
+ this.choices = response.choices;
27
+ }
28
+ } else {
29
+ this.choices = this.field.choices;
30
+ }
31
+ }
32
+ };