apostrophe 3.61.1 → 3.63.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 (104) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +1 -1
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +6 -4
  4. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +9 -1
  5. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +8 -0
  6. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +6 -3
  7. package/modules/@apostrophecms/doc/index.js +256 -7
  8. package/modules/@apostrophecms/doc/ui/apos/mixins/AposFieldMetaUtilsMixin.js +93 -0
  9. package/modules/@apostrophecms/doc-type/index.js +78 -12
  10. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +9 -1
  11. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +24 -6
  12. package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
  13. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +8 -7
  14. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +1 -5
  15. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +5 -2
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +1 -5
  17. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +4 -2
  18. package/modules/@apostrophecms/login/index.js +25 -19
  19. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +11 -1
  20. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +46 -2
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +8 -3
  22. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +3 -0
  23. package/modules/@apostrophecms/page/index.js +118 -27
  24. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +3 -1
  25. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +7 -0
  26. package/modules/@apostrophecms/page-type/index.js +81 -4
  27. package/modules/@apostrophecms/permission/index.js +60 -31
  28. package/modules/@apostrophecms/piece-type/index.js +19 -41
  29. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +9 -1
  30. package/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue +16 -1
  31. package/modules/@apostrophecms/rich-text-widget/index.js +141 -1
  32. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +8 -0
  33. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +7 -7
  34. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +38 -79
  35. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +11 -0
  36. package/modules/@apostrophecms/schema/index.js +9 -0
  37. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +22 -2
  38. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  39. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +4 -1
  40. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +7 -8
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -0
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -0
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +1 -0
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +76 -30
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +2 -4
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  47. package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +7 -0
  48. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +13 -1
  49. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +5 -1
  50. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +21 -0
  51. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +12 -8
  52. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +35 -0
  53. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +6 -0
  54. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +41 -0
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -0
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +1 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposTree.vue +7 -0
  58. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +8 -0
  59. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +10 -4
  60. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -2
  61. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +7 -0
  62. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +6 -0
  63. package/package.json +2 -2
  64. package/test/areas.js +1 -1
  65. package/test/assets.js +2 -2
  66. package/test/attachments.js +2 -2
  67. package/test/base-module.js +2 -1
  68. package/test/base-url-env-var.js +2 -2
  69. package/test/change-doc-ids.js +33 -31
  70. package/test/command-menu.js +2 -2
  71. package/test/content-i18n.js +47 -46
  72. package/test/db.js +2 -2
  73. package/test/docs.js +301 -126
  74. package/test/draft-published.js +2 -2
  75. package/test/email.js +2 -1
  76. package/test/express.js +3 -2
  77. package/test/external-front.js +4 -4
  78. package/test/field-meta.js +363 -0
  79. package/test/global.js +2 -1
  80. package/test/http.js +4 -2
  81. package/test/images.js +87 -88
  82. package/test/job.js +34 -34
  83. package/test/locks.js +2 -2
  84. package/test/login-requirements.js +3 -2
  85. package/test/middleware-and-route-order.js +2 -2
  86. package/test/page-type.js +2 -1
  87. package/test/pages-autocomplete.js +192 -0
  88. package/test/pages-public-api.js +2 -2
  89. package/test/pages-rest.js +4 -4
  90. package/test/pages.js +389 -57
  91. package/test/parked-pages.js +47 -47
  92. package/test/permissions.js +76 -0
  93. package/test/pieces-page-type.js +2 -1
  94. package/test/pieces-public-api.js +38 -38
  95. package/test/pieces.js +4 -4
  96. package/test/published-pages.js +16 -16
  97. package/test/rich-text-widget.js +164 -0
  98. package/test/schema-queryBuilders.js +180 -0
  99. package/test/schemaBuilders.js +4 -4
  100. package/test/schemas.js +220 -221
  101. package/test/search.js +2 -2
  102. package/test/soft-redirects.js +2 -1
  103. package/test/templates.js +2 -2
  104. package/test/users.js +2 -1
@@ -128,7 +128,7 @@ module.exports = {
128
128
  return {
129
129
  add: {
130
130
  new: {
131
- canEdit: true,
131
+ canCreate: true,
132
132
  relationship: true,
133
133
  label: {
134
134
  key: 'apostrophe:newDocType',
@@ -156,7 +156,7 @@ module.exports = {
156
156
  description: 'apostrophe:publishingBatchConfirmation',
157
157
  confirmationButton: 'apostrophe:publishingBatchConfirmationButton'
158
158
  },
159
- permission: 'edit'
159
+ permission: 'publish'
160
160
  },
161
161
  archive: {
162
162
  label: 'apostrophe:archive',
@@ -173,7 +173,7 @@ module.exports = {
173
173
  description: 'apostrophe:archivingBatchConfirmation',
174
174
  confirmationButton: 'apostrophe:archivingBatchConfirmationButton'
175
175
  },
176
- permission: 'edit'
176
+ permission: 'delete'
177
177
  },
178
178
  restore: {
179
179
  label: 'apostrophe:restore',
@@ -670,10 +670,18 @@ module.exports = {
670
670
  },
671
671
  // Similar to insertDraftOf, invoked on first publication.
672
672
  insertPublishedOf(req, doc, published, options) {
673
+ // Check publish permission up front because we won't check it
674
+ // in insert
675
+ if (!self.apos.permission.can(req, 'publish', doc)) {
676
+ throw self.apos.error('forbidden');
677
+ }
673
678
  return self.insert(
674
679
  req.clone({ mode: 'published' }),
675
680
  published,
676
- options
681
+ {
682
+ ...options,
683
+ permissions: false
684
+ }
677
685
  );
678
686
  },
679
687
  // Returns one editable piece matching the criteria, throws `notfound`
@@ -703,39 +711,6 @@ module.exports = {
703
711
  async delete(req, piece, options = {}) {
704
712
  return self.apos.doc.delete(req, piece, options);
705
713
  },
706
- composeFilters() {
707
- self.filters = Object.keys(self.filters).map((key) => ({
708
- name: key,
709
- ...self.filters[key],
710
- inputType: self.filters[key].inputType || 'select'
711
- }));
712
- // Add a null choice if not already added or set to `required`
713
- self.filters.forEach((filter) => {
714
- if (filter.choices) {
715
- if (
716
- !filter.required &&
717
- filter.choices &&
718
- !filter.choices.find((choice) => choice.value === null)
719
- ) {
720
- filter.def = null;
721
- filter.choices.push({
722
- value: null,
723
- label: 'apostrophe:none'
724
- });
725
- }
726
- } else {
727
- // Dynamic choices from the REST API, but
728
- // we need a label for "no opinion"
729
- filter.nullLabel = 'Choose One';
730
- }
731
- });
732
- },
733
- composeColumns() {
734
- self.columns = Object.keys(self.columns).map((key) => ({
735
- name: key,
736
- ...self.columns[key]
737
- }));
738
- },
739
714
  // Enable inclusion of this type in sitewide search results
740
715
  searchDetermineTypes(types) {
741
716
  if (self.options.searchable !== false) {
@@ -850,7 +825,10 @@ module.exports = {
850
825
  return self.findOneForEditing(
851
826
  req,
852
827
  { _id: piece._id },
853
- { attachments: true }
828
+ {
829
+ attachments: true,
830
+ permission: 'create'
831
+ }
854
832
  );
855
833
  },
856
834
 
@@ -1153,14 +1131,14 @@ module.exports = {
1153
1131
  browserOptions.batchOperations = self.checkBatchOperationsPermissions(req);
1154
1132
  browserOptions.utilityOperations = self.utilityOperations;
1155
1133
  browserOptions.insertViaUpload = self.options.insertViaUpload;
1156
- browserOptions.quickCreate = !self.options.singleton && self.options.quickCreate && self.apos.permission.can(req, 'edit', self.name, 'draft');
1134
+ browserOptions.quickCreate = !self.options.singleton && self.options.quickCreate && browserOptions.canCreate;
1157
1135
  browserOptions.singleton = self.options.singleton;
1158
1136
  browserOptions.showCreate = !self.options.singleton && self.options.showCreate;
1159
1137
  browserOptions.showDismissSubmission = self.options.showDismissSubmission;
1160
1138
  browserOptions.showArchive = self.options.showArchive;
1161
1139
  browserOptions.showDiscardDraft = self.options.showDiscardDraft;
1162
- browserOptions.canEdit = self.apos.permission.can(req, 'edit', self.name, 'draft');
1163
- browserOptions.canPublish = self.apos.permission.can(req, 'edit', self.name, 'publish');
1140
+ browserOptions.canDeleteDraft = self.apos.permission.can(req, 'delete', self.name, 'draft');
1141
+
1164
1142
  _.defaults(browserOptions, {
1165
1143
  components: {}
1166
1144
  });
@@ -30,7 +30,7 @@
30
30
  @click="saveRelationship"
31
31
  />
32
32
  <AposButton
33
- v-else-if="moduleOptions.canEdit && moduleOptions.showCreate"
33
+ v-else-if="moduleOptions.canCreate && moduleOptions.showCreate"
34
34
  :label="{
35
35
  key: 'apostrophe:newDocType',
36
36
  type: $t(moduleOptions.label)
@@ -297,6 +297,14 @@ export default {
297
297
  withPublished: 1
298
298
  };
299
299
 
300
+ const type = this.relationshipField?.withType;
301
+ const isPage = apos.modules['@apostrophecms/page'].validPageTypes
302
+ .includes(type);
303
+
304
+ if (isPage) {
305
+ options.type = type;
306
+ }
307
+
300
308
  // Avoid undefined properties.
301
309
  const qs = Object.entries(options)
302
310
  .reduce((acc, [ key, val ]) => ({
@@ -95,7 +95,13 @@ export default {
95
95
  }
96
96
  },
97
97
  setUtilityOperations () {
98
- const { utilityOperations, canEdit } = this.moduleOptions;
98
+ const {
99
+ utilityOperations,
100
+ canPublish,
101
+ canEdit,
102
+ canArchive,
103
+ canCreate
104
+ } = this.moduleOptions;
99
105
 
100
106
  const operations = ((Array.isArray(utilityOperations) && utilityOperations) || []).filter(operation => {
101
107
  let ok = true;
@@ -106,9 +112,18 @@ export default {
106
112
  ok = !operation.relationship;
107
113
  }
108
114
  }
115
+ if (operation.canCreate) {
116
+ ok = ok && canCreate;
117
+ }
109
118
  if (operation.canEdit) {
110
119
  ok = ok && canEdit;
111
120
  }
121
+ if (operation.canArchive) {
122
+ ok = ok && canArchive;
123
+ }
124
+ if (operation.canPublish) {
125
+ ok = ok && canPublish;
126
+ }
112
127
  return ok;
113
128
  });
114
129
 
@@ -6,6 +6,84 @@ const cheerio = require('cheerio');
6
6
 
7
7
  module.exports = {
8
8
  extend: '@apostrophecms/widget-type',
9
+ cascades: [ 'linkFields' ],
10
+ linkFields(self, options) {
11
+ const linkWithType = (Array.isArray(options.linkWithType)
12
+ ? options.linkWithType
13
+ : [ options.linkWithType ]);
14
+
15
+ // Labels are not available at the time the schema is built,
16
+ // they are added on modulesRegistered.
17
+ const linkWithTypeChoices = linkWithType
18
+ .map(type => ({
19
+ label: type,
20
+ value: type
21
+ }))
22
+ .concat([
23
+ {
24
+ label: 'apostrophe:url',
25
+ value: '_url'
26
+ }
27
+ ]);
28
+
29
+ const linkWithTypeFields = linkWithType.reduce((fields, type) => {
30
+ const name = `_${type}`;
31
+ fields[name] = {
32
+ type: 'relationship',
33
+ label: type,
34
+ withType: type,
35
+ required: true,
36
+ max: 1,
37
+ browse: true,
38
+ if: {
39
+ linkTo: type
40
+ }
41
+ };
42
+ return fields;
43
+ }, {});
44
+ linkWithTypeFields.updateTitle = {
45
+ label: 'apostrophe:updateTitle',
46
+ type: 'boolean',
47
+ def: true,
48
+ if: {
49
+ $or: linkWithType.map(type => ({
50
+ linkTo: type
51
+ }))
52
+ }
53
+ };
54
+
55
+ return {
56
+ add: {
57
+ linkTo: {
58
+ label: 'apostrophe:linkTo',
59
+ type: 'select',
60
+ choices: linkWithTypeChoices,
61
+ required: true,
62
+ def: linkWithTypeChoices[0].value
63
+ },
64
+ ...linkWithTypeFields,
65
+ href: {
66
+ label: 'apostrophe:url',
67
+ type: 'string',
68
+ required: true,
69
+ if: {
70
+ linkTo: '_url'
71
+ }
72
+ },
73
+ target: {
74
+ label: 'apostrophe:linkTarget',
75
+ type: 'checkboxes',
76
+ htmlAttribute: 'target',
77
+ choices: [
78
+ {
79
+ label: 'apostrophe:openLinkInNewTab',
80
+ value: '_blank'
81
+ }
82
+ ]
83
+ }
84
+ }
85
+ };
86
+ },
9
87
  options: {
10
88
  icon: 'format-text-icon',
11
89
  label: 'apostrophe:richText',
@@ -270,6 +348,18 @@ module.exports = {
270
348
  'format-color-highlight-icon': 'FormatColorHighlight',
271
349
  'table-icon': 'Table'
272
350
  },
351
+ handlers(self) {
352
+ return {
353
+ 'apostrophe:modulesRegistered': {
354
+ validateAndFixLinkWithTypes() {
355
+ self.validateAndFixLinkWithTypes();
356
+ },
357
+ composeLinkSchema() {
358
+ self.composeLinkSchema();
359
+ }
360
+ }
361
+ };
362
+ },
273
363
  methods(self) {
274
364
  return {
275
365
  // Return just the rich text of the widget, which may be undefined or null if it has not yet been edited
@@ -404,7 +494,9 @@ module.exports = {
404
494
  'href',
405
495
  'id',
406
496
  'name',
407
- 'target'
497
+ ...self.linkSchema
498
+ .filter(field => field.htmlAttribute)
499
+ .map(field => field.htmlAttribute)
408
500
  ]
409
501
  },
410
502
  alignLeft: {
@@ -675,6 +767,53 @@ module.exports = {
675
767
  }
676
768
  }
677
769
  return content;
770
+ },
771
+ // Validate the types provided for links, update labels derived from
772
+ // corresponding modules, as they are not available at the time
773
+ // the schema is generated.
774
+ validateAndFixLinkWithTypes() {
775
+ const linkWithType = (Array.isArray(self.options.linkWithType)
776
+ ? self.options.linkWithType
777
+ : [ self.options.linkWithType ]);
778
+
779
+ for (const type of linkWithType) {
780
+ if (!self.apos.modules[type]) {
781
+ throw new Error(
782
+ `The linkWithType option of rich text widget "${type}" must be a valid module type`
783
+ );
784
+ }
785
+
786
+ self.linkFields[`_${type}`].label = getLabel(type);
787
+ const choice = self.linkFields.linkTo.choices
788
+ .find(choice => choice.value === type);
789
+
790
+ choice.label = getLabel(type);
791
+ }
792
+
793
+ function getLabel(type) {
794
+ if ([ '@apostrophecms/any-page-type', '@apostrophecms/page' ].includes(type)) {
795
+ return 'apostrophe:page';
796
+ }
797
+ return self.apos.modules[type].options?.label ?? type;
798
+ }
799
+ },
800
+ // Compose and register the link schema.
801
+ composeLinkSchema() {
802
+ self.linkSchema = self.apos.schema.compose({
803
+ addFields: self.apos.schema.fieldsToArray(`Links ${self.__meta.name}`, self.linkFields),
804
+ arrangeFields: self.apos.schema.groupsToArray(self.linkFieldsGroups)
805
+ }, self);
806
+
807
+ self.apos.schema.validate(self.linkSchema, {
808
+ type: 'link',
809
+ subtype: self.__meta.name
810
+ });
811
+
812
+ // Don't allow htmlAttribute `href`, it's a special case.
813
+ const hrefField = self.linkSchema.find(field => field.htmlAttribute === 'href');
814
+ if (hrefField) {
815
+ throw new Error(`Field "${hrefField.name}" validation error: "htmlAttribute: href" is not allowed.`);
816
+ }
678
817
  }
679
818
  };
680
819
  },
@@ -739,6 +878,7 @@ module.exports = {
739
878
  // Not optional in presence of an insert menu, it's not acceptable UX without it
740
879
  placeholderTextWithInsertMenu: self.options.placeholderTextWithInsertMenu,
741
880
  linkWithType: Array.isArray(self.options.linkWithType) ? self.options.linkWithType : [ self.options.linkWithType ],
881
+ linkSchema: self.linkSchema,
742
882
  imageStyles: self.options.imageStyles
743
883
  };
744
884
  return finalData;
@@ -161,6 +161,14 @@ export default {
161
161
  return {};
162
162
  }
163
163
  },
164
+ // not used, but we need to keep it here to avoid
165
+ // an attribute [object Object]
166
+ meta: {
167
+ type: Object,
168
+ default() {
169
+ return {};
170
+ }
171
+ },
164
172
  docId: {
165
173
  type: String,
166
174
  required: false,
@@ -42,13 +42,6 @@ export default {
42
42
  active: false
43
43
  };
44
44
  },
45
- watch: {
46
- hasSelection(newVal, oldVal) {
47
- if (!newVal) {
48
- this.close();
49
- }
50
- }
51
- },
52
45
  computed: {
53
46
  attributes() {
54
47
  return this.editor.getAttributes('image');
@@ -71,6 +64,13 @@ export default {
71
64
  return text !== '' || type?.name === 'image';
72
65
  }
73
66
  },
67
+ watch: {
68
+ hasSelection(newVal, oldVal) {
69
+ if (!newVal) {
70
+ this.close();
71
+ }
72
+ }
73
+ },
74
74
  methods: {
75
75
  click() {
76
76
  if (this.hasSelection) {
@@ -86,11 +86,11 @@ export default {
86
86
  }
87
87
  },
88
88
  data() {
89
- const linkWithType = getOptions().linkWithType;
89
+
90
90
  return {
91
91
  generation: 1,
92
92
  href: null,
93
- target: null,
93
+ // target: null,
94
94
  active: false,
95
95
  hasLinkOnOpen: false,
96
96
  triggerValidation: false,
@@ -98,73 +98,7 @@ export default {
98
98
  data: {}
99
99
  },
100
100
  formModifiers: [ 'small', 'margin-micro' ],
101
- originalSchema: [
102
- {
103
- name: 'linkTo',
104
- label: this.$t('apostrophe:linkTo'),
105
- type: 'select',
106
- def: linkWithType[0],
107
- required: true,
108
- choices: [
109
- ...(linkWithType.map(type => {
110
- return {
111
- // Should already be localized server side
112
- label: apos.modules[type].label,
113
- value: type
114
- };
115
- })),
116
- {
117
- // TODO this needs i18n
118
- label: this.$t('apostrophe:url'),
119
- // Value that will never be a doc type
120
- value: '_url'
121
- }
122
- ]
123
- },
124
- ...getOptions().linkWithType.map(type => ({
125
- name: `_${type}`,
126
- type: 'relationship',
127
- label: apos.modules[type].label,
128
- withType: type,
129
- required: true,
130
- max: 1,
131
- browse: true,
132
- if: {
133
- linkTo: type
134
- }
135
- })),
136
- {
137
- name: 'updateTitle',
138
- label: this.$t('apostrophe:updateTitle'),
139
- type: 'boolean',
140
- def: true,
141
- if: {
142
- $or: linkWithType.map(type => ({
143
- linkTo: type
144
- }))
145
- }
146
- },
147
- {
148
- name: 'href',
149
- label: this.$t('apostrophe:url'),
150
- type: 'string',
151
- required: true,
152
- if: {
153
- linkTo: '_url'
154
- }
155
- },
156
- {
157
- name: 'target',
158
- label: this.$t('apostrophe:linkTarget'),
159
- type: 'checkboxes',
160
- choices: [
161
- {
162
- label: this.$t('apostrophe:openLinkInNewTab'),
163
- value: '_blank'
164
- }
165
- ]
166
- }
167
- ]
101
+ originalSchema: getOptions().linkSchema
168
102
  };
169
103
  },
170
104
  computed: {
@@ -186,6 +120,9 @@ export default {
186
120
  },
187
121
  schema() {
188
122
  return this.originalSchema;
123
+ },
124
+ schemaHtmlAttributes() {
125
+ return this.schema.filter(item => !!item.htmlAttribute);
189
126
  }
190
127
  },
191
128
  watch: {
@@ -249,10 +186,22 @@ export default {
249
186
  if (this.docFields.data.target && !this.docFields.data.href) {
250
187
  delete this.docFields.data.target;
251
188
  }
252
- this.editor.commands.setLink({
253
- target: this.docFields.data.target[0],
254
- href: this.docFields.data.href
255
- });
189
+
190
+ const attrs = this.schemaHtmlAttributes.reduce((acc, field) => {
191
+ const value = this.docFields.data[field.name];
192
+ if (field.type === 'checkboxes' && !value?.[0]) {
193
+ return acc;
194
+ }
195
+ if (field.type === 'boolean') {
196
+ acc[field.htmlAttribute] = value === true ? '' : null;
197
+ return acc;
198
+ }
199
+ acc[field.htmlAttribute] = Array.isArray(value) ? value[0] : value;
200
+ return acc;
201
+ }, {});
202
+ attrs.href = this.docFields.data.href;
203
+ this.editor.commands.setLink(attrs);
204
+
256
205
  this.close();
257
206
  });
258
207
  },
@@ -272,13 +221,23 @@ export default {
272
221
  },
273
222
  async populateFields() {
274
223
  try {
275
- const attrs = this.attributes;
276
- if (attrs.target) {
277
- // checkboxes field expects an array
278
- attrs.target = [ attrs.target ];
279
- }
224
+ const attrs = { ...this.attributes };
280
225
  this.docFields.data = {};
281
226
  this.schema.forEach((item) => {
227
+ if (item.htmlAttribute && item.type === 'checkboxes') {
228
+ this.docFields.data[item.name] = attrs[item.htmlAttribute] ? [ attrs[item.htmlAttribute] ] : [];
229
+ return;
230
+ }
231
+ if (item.htmlAttribute && item.type === 'boolean') {
232
+ this.docFields.data[item.name] = attrs[item.htmlAttribute] === null
233
+ ? null
234
+ : (attrs[item.htmlAttribute] === '');
235
+ return;
236
+ }
237
+ if (item.htmlAttribute) {
238
+ this.docFields.data[item.name] = attrs[item.htmlAttribute] || '';
239
+ return;
240
+ }
282
241
  this.docFields.data[item.name] = attrs[item.name] || '';
283
242
  });
284
243
  const matches = this.docFields.data.href.match(/^#apostrophe-permalink-(.*)\?updateTitle=(\d)$/);
@@ -361,7 +320,7 @@ function getOptions() {
361
320
  }
362
321
 
363
322
  // special schema style for this use
364
- .apos-link-control ::v-deep .apos-field--target {
323
+ .apos-link-control ::v-deep .apos-field--checkboxes {
365
324
  .apos-field__label {
366
325
  display: none;
367
326
  }
@@ -8,6 +8,17 @@ export default (options) => {
8
8
  linkOnPaste: true,
9
9
  HTMLAttributes: {}
10
10
  };
11
+ },
12
+ addAttributes() {
13
+ return {
14
+ ...this.parent?.(),
15
+ ...apos.modules['@apostrophecms/rich-text-widget'].linkSchema
16
+ .filter(field => !!field.htmlAttribute)
17
+ .reduce((obj, field) => {
18
+ obj[field.htmlAttribute] = { default: field.def ?? null };
19
+ return obj;
20
+ }, {})
21
+ };
11
22
  }
12
23
  });
13
24
  };
@@ -28,6 +28,7 @@ module.exports = {
28
28
  self.fieldsById = {};
29
29
  self.arrayManagers = {};
30
30
  self.objectManagers = {};
31
+ self.fieldMetadataComponents = [];
31
32
 
32
33
  self.enableBrowserData();
33
34
 
@@ -1127,6 +1128,13 @@ module.exports = {
1127
1128
  return self.fieldTypes[typeName];
1128
1129
  },
1129
1130
 
1131
+ addFieldMetadataComponent(namespace, component) {
1132
+ self.fieldMetadataComponents.push({
1133
+ name: component,
1134
+ namespace
1135
+ });
1136
+ },
1137
+
1130
1138
  // Given a schema and a query, add query builders to the query
1131
1139
  // for each of the fields in the schema, based on their field type,
1132
1140
  // if supported by the field type. If the field already has a
@@ -1762,6 +1770,7 @@ module.exports = {
1762
1770
  }
1763
1771
  browserOptions.action = self.action;
1764
1772
  browserOptions.components = { fields: fields };
1773
+ browserOptions.fieldMetadataComponents = self.fieldMetadataComponents;
1765
1774
  return browserOptions;
1766
1775
  }
1767
1776
  };
@@ -192,6 +192,9 @@ module.exports = (self) => {
192
192
  query.and(criteria);
193
193
  }
194
194
  },
195
+ launder: function (s) {
196
+ return self.apos.launder.string(s);
197
+ },
195
198
  choices: async function () {
196
199
  return self.sortedDistinct(field.name, query);
197
200
  }
@@ -461,6 +464,7 @@ module.exports = (self) => {
461
464
  $gte: value[0],
462
465
  $lte: value[1]
463
466
  };
467
+ query.and(criteria);
464
468
  } else {
465
469
  criteria = {};
466
470
  criteria[field.name] = self.apos.launder.integer(value);
@@ -471,8 +475,14 @@ module.exports = (self) => {
471
475
  choices: async function () {
472
476
  return self.sortedDistinct(field.name, query);
473
477
  },
474
- launder: function (s) {
475
- return self.apos.launder.integer(s, null);
478
+ launder: function (value) {
479
+ const launderInteger = (v) => self.apos.launder.integer(v, null);
480
+
481
+ if (Array.isArray(value)) {
482
+ return value.map(launderInteger);
483
+ } else {
484
+ return launderInteger(value);
485
+ }
476
486
  }
477
487
  });
478
488
  }
@@ -505,6 +515,7 @@ module.exports = (self) => {
505
515
  $gte: value[0],
506
516
  $lte: value[1]
507
517
  };
518
+ query.and(criteria);
508
519
  } else {
509
520
  criteria = {};
510
521
  criteria[field.name] = self.apos.launder.float(value);
@@ -514,6 +525,15 @@ module.exports = (self) => {
514
525
  },
515
526
  choices: async function () {
516
527
  return self.sortedDistinct(field.name, query);
528
+ },
529
+ launder: function(value) {
530
+ const launderFloat = (v) => self.apos.launder.float(v, null);
531
+
532
+ if (Array.isArray(value)) {
533
+ return value.map(launderFloat);
534
+ } else {
535
+ return launderFloat(value);
536
+ }
517
537
  }
518
538
  });
519
539
  }
@@ -70,6 +70,7 @@
70
70
  :following-values="followingValues()"
71
71
  :conditional-fields="conditionalFields"
72
72
  :value="currentDoc"
73
+ :meta="currentDocMeta"
73
74
  :server-errors="currentDocServerErrors"
74
75
  ref="schema"
75
76
  :doc-id="docId"
@@ -3,7 +3,9 @@
3
3
  :field="field"
4
4
  :error="effectiveError" :uid="uid"
5
5
  :display-options="displayOptions"
6
- :modifiers="modifiers"
6
+ :modifiers="[...modifiers, 'full-width']"
7
+ :items="next.items"
8
+ :meta="areaMeta"
7
9
  >
8
10
  <template #body>
9
11
  <!-- data-apos-schema-area lets all the child areas know that this area is in a schema (which is in a modal)
@@ -18,6 +20,7 @@
18
20
  :is="editorComponent"
19
21
  :options="field.options"
20
22
  :items="next.items"
23
+ :meta="areaMeta"
21
24
  :choices="choices"
22
25
  :id="next._id"
23
26
  :field-id="field._id"