apostrophe 3.62.0 → 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 (84) hide show
  1. package/CHANGELOG.md +29 -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 +254 -5
  8. package/modules/@apostrophecms/doc/ui/apos/mixins/AposFieldMetaUtilsMixin.js +93 -0
  9. package/modules/@apostrophecms/doc-type/index.js +70 -10
  10. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +2 -0
  11. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +12 -3
  12. package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
  13. package/modules/@apostrophecms/login/index.js +25 -19
  14. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +11 -1
  15. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +46 -2
  16. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +8 -3
  17. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +3 -0
  18. package/modules/@apostrophecms/page/index.js +87 -20
  19. package/modules/@apostrophecms/page-type/index.js +67 -2
  20. package/modules/@apostrophecms/piece-type/index.js +1 -34
  21. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +8 -0
  22. package/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue +16 -1
  23. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +8 -0
  24. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +7 -7
  25. package/modules/@apostrophecms/schema/index.js +9 -0
  26. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -0
  27. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  28. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +3 -0
  29. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -8
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -0
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -0
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +1 -0
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +74 -29
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  36. package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +7 -0
  37. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +13 -1
  38. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +5 -1
  39. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +21 -0
  40. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +35 -0
  41. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +6 -0
  42. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +41 -0
  43. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +10 -4
  44. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +7 -0
  45. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +6 -0
  46. package/package.json +2 -2
  47. package/test/areas.js +1 -1
  48. package/test/assets.js +2 -2
  49. package/test/attachments.js +2 -2
  50. package/test/base-module.js +2 -1
  51. package/test/base-url-env-var.js +2 -2
  52. package/test/change-doc-ids.js +33 -31
  53. package/test/command-menu.js +2 -2
  54. package/test/content-i18n.js +47 -46
  55. package/test/db.js +2 -2
  56. package/test/docs.js +301 -126
  57. package/test/draft-published.js +2 -2
  58. package/test/email.js +2 -1
  59. package/test/express.js +3 -2
  60. package/test/external-front.js +4 -4
  61. package/test/field-meta.js +363 -0
  62. package/test/global.js +2 -1
  63. package/test/http.js +4 -2
  64. package/test/images.js +87 -88
  65. package/test/job.js +34 -34
  66. package/test/locks.js +2 -2
  67. package/test/login-requirements.js +3 -2
  68. package/test/middleware-and-route-order.js +2 -2
  69. package/test/page-type.js +2 -1
  70. package/test/pages-autocomplete.js +0 -11
  71. package/test/pages-public-api.js +2 -2
  72. package/test/pages-rest.js +4 -4
  73. package/test/pages.js +389 -57
  74. package/test/parked-pages.js +47 -47
  75. package/test/pieces-page-type.js +2 -1
  76. package/test/pieces-public-api.js +38 -38
  77. package/test/pieces.js +4 -4
  78. package/test/published-pages.js +16 -16
  79. package/test/schemaBuilders.js +4 -4
  80. package/test/schemas.js +220 -221
  81. package/test/search.js +2 -2
  82. package/test/soft-redirects.js +2 -1
  83. package/test/templates.js +2 -2
  84. package/test/users.js +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.63.0 (2024-02-21)
4
+
5
+ ### Adds
6
+
7
+ * Adds a `launder` method to the `slug` schema field query builder to allow for use in API queries.
8
+ * Adds support for browsing specific pages in a relationship field when `withType` is set to a page type, like `@apostrophecms/home-page`, `default-page`, `article-page`...
9
+ * Add support for `canCreate`, `canPreview` & `canShareDraft` in context operations conditions.
10
+ * Add support for `canCreate`, `canEdit`, `canArchive` & `canPublish` in utility operations definitions.
11
+ * Add `uponSubmit` requirement in the `@apostrophecms/login` module. `uponSubmit` requirements are checked each time the user submit the login form. See the documentation for more information.
12
+ * Add field metadata feature, where every module can add metadata to fields via public API offered by `apos.doc.setMeta()`, `apos.doc.getMeta()`, `apos.doc.getMetaPath()` and `apos.doc.removeMeta()`. The metadata is stored in the database and can be used to store additional information about a field.
13
+ * Add new `apos.schema.addFieldMetadataComponent(namespace, component)` method to allow adding custom components. They have access to the server-side added field metadata and can decide to show indicators on the admin UI fields. Currently supported fields are "string", "slug", "array", "object" and "area".
14
+
15
+ ### Fixes
16
+
17
+ * When deleting a draft document, we remove related reverse IDs of documents having a relation to the deleted one.
18
+ * Fix publishing or moving published page after a draft page on the same tree level to work as expected.
19
+ * Check create permissions on create keyboard shortcut.
20
+ * Copy requires create and edit permission.
21
+ * Display a more informative error message when publishing a page because the parent page is not published and the current user has no permission to publish the parent page (while having permission to publish the current one).
22
+ * The `content-changed` event for the submit draft action now uses a complete document.
23
+ * Fix the context bar overlap on palette for non-admin users that have the permission to modify it.
24
+ * Show widget icons in the editor area context menu.
25
+
26
+ ### Changes
27
+
28
+ * Share Drafts modal styles made larger and it's toggle input has a larger hitbox.
29
+
3
30
  ## 3.62.0 (2024-01-25)
4
31
 
5
32
  ### Adds
@@ -11,6 +38,7 @@
11
38
  * Adds support in `can` and `criteria` methods for `create` and `delete`.
12
39
  * Changes support for image upload from `canEdit` to `canCreate`.
13
40
  * The media manager is compatible with per-doc permissions granted via the `@apostrophecms-pro/advanced-permission` module.
41
+ * In inline arrays, the trash icon has been replaced by a close icon.
14
42
 
15
43
  ### Fixes
16
44
 
@@ -19,6 +47,7 @@
19
47
  * A user who has permission to `publish` a particular page should always be allowed to insert it into the
20
48
  published version of the site even if they could not otherwise insert a child of the published
21
49
  parent.
50
+ * Display the "Browse" button in a relationship inside an inline array.
22
51
 
23
52
  ## 3.61.1 (2023-01-08)
24
53
 
@@ -16,7 +16,7 @@
16
16
  :items="userItems"
17
17
  />
18
18
  </div>
19
- <TheAposContextBar @mounted="setSpacer" />
19
+ <TheAposContextBar @visibility-changed="setSpacer" />
20
20
  <component
21
21
  v-for="bar in bars"
22
22
  v-bind="bar.props || {}"
@@ -42,7 +42,7 @@ import AposAdvisoryLockMixin from 'Modules/@apostrophecms/ui/mixins/AposAdvisory
42
42
  export default {
43
43
  name: 'TheAposContextBar',
44
44
  mixins: [ AposPublishMixin, AposAdvisoryLockMixin ],
45
- emits: [ 'mounted' ],
45
+ emits: [ 'visibility-changed' ],
46
46
  data() {
47
47
  const query = apos.http.parseQuery(location.search);
48
48
  // If the URL references a draft, go into draft mode but then clean up the URL
@@ -137,6 +137,11 @@ export default {
137
137
  watch: {
138
138
  editMode(newVal) {
139
139
  window.apos.adminBar.editMode = newVal;
140
+ },
141
+ contextBarActive() {
142
+ this.$nextTick(() => {
143
+ this.$emit('visibility-changed');
144
+ });
140
145
  }
141
146
  },
142
147
  async mounted() {
@@ -173,9 +178,6 @@ export default {
173
178
  await this.updateDraftIsEditable();
174
179
  this.rememberLastBaseContext();
175
180
  this.published = await this.getPublished();
176
- this.$nextTick(() => {
177
- this.$emit('mounted');
178
- });
179
181
 
180
182
  apos.util.onReadyAndRefresh(() => {
181
183
  if (window.apos.adminBar.scrollPosition) {
@@ -39,6 +39,7 @@
39
39
  :area-id="areaId"
40
40
  :key="widget._id"
41
41
  :widget="widget"
42
+ :meta="meta[widget._id]"
42
43
  :generation="generation"
43
44
  :i="i"
44
45
  :options="options"
@@ -111,6 +112,12 @@ export default {
111
112
  return [];
112
113
  }
113
114
  },
115
+ meta: {
116
+ type: Object,
117
+ default() {
118
+ return {};
119
+ }
120
+ },
114
121
  followingValues: {
115
122
  type: Object,
116
123
  default() {
@@ -379,7 +386,8 @@ export default {
379
386
  options: this.widgetOptionsByType(widget.type),
380
387
  type: widget.type,
381
388
  docId: this.docId,
382
- parentFollowingValues: this.followingValues
389
+ parentFollowingValues: this.followingValues,
390
+ meta: this.meta[widget._id]?.aposMeta
383
391
  });
384
392
  apos.area.activeEditor = null;
385
393
  apos.bus.$off('apos-refreshing', cancelRefresh);
@@ -109,6 +109,7 @@
109
109
  :options="widgetOptions"
110
110
  :type="widget.type"
111
111
  :value="widget"
112
+ :meta="meta"
112
113
  @update="$emit('update', $event)"
113
114
  :doc-id="docId"
114
115
  :focused="isFocused"
@@ -124,6 +125,7 @@
124
125
  :area-field="field"
125
126
  :following-values="followingValuesWithParent"
126
127
  :value="widget"
128
+ :meta="meta"
127
129
  :foreign="foreign"
128
130
  @edit="$emit('edit', i);"
129
131
  :doc-id="docId"
@@ -193,6 +195,12 @@ export default {
193
195
  return {};
194
196
  }
195
197
  },
198
+ meta: {
199
+ type: Object,
200
+ default() {
201
+ return {};
202
+ }
203
+ },
196
204
  followingValues: {
197
205
  type: Object,
198
206
  default() {
@@ -35,12 +35,15 @@
35
35
  }"
36
36
  :modifiers="[ 'inline' ]"
37
37
  />
38
- <!-- <AposButton
38
+ <AposButton
39
39
  v-bind="copyButton"
40
40
  v-if="!foreign"
41
41
  @click="$emit('copy')"
42
- tooltip="Copy"
43
- /> -->
42
+ :tooltip="{
43
+ content: 'apostrophe:copy',
44
+ placement: 'left'
45
+ }"
46
+ />
44
47
  <AposButton
45
48
  v-if="!foreign"
46
49
  :disabled="disabled || maxReached"
@@ -717,7 +717,6 @@ module.exports = {
717
717
  // This operation ignores the locale and mode of `req`
718
718
  // in favor of the actual document's locale and mode.
719
719
  async delete(req, doc, options = {}) {
720
- options = options || {};
721
720
  const m = self.getManager(doc.type);
722
721
  await m.emit('beforeDelete', req, doc, options);
723
722
  await self.deleteBody(req, doc, options);
@@ -938,6 +937,252 @@ module.exports = {
938
937
  return self.db.insertOne(self.apos.util.clonePermanent(doc));
939
938
  });
940
939
  },
940
+ // Set meta data for a given field, that will be live under `aposMeta`
941
+ // doc property. It returns the path to the meta property withouth the
942
+ // key. See `getMetaPath` method for more information.
943
+ //
944
+ // Signature:
945
+ // `apos.doc.setMeta(doc, namespace, [subobject], ...pathComponents, key, value);`
946
+ // where arguments are as follows:
947
+ // - `doc`: the document to attach the meta property to.
948
+ // - `namespace`: the namespace of the meta property, by convention the
949
+ // module name that is setting the meta property.
950
+ // - `subobject`: (optional) the name of the field subobject (e.g. array
951
+ // item, widget, or any other field type object that have `_id` property).
952
+ // This argument dictates how `pathComponents` are interpreted. If
953
+ // `subobject` is not provided, `pathComponents` are interpreted as
954
+ // a path starting from `doc`. If `subobject` is provided, `pathComponents`
955
+ // are interpreted as a relative path from the `subobject` field.
956
+ // - `pathComponents`: the dot path to the field value. It can be any number
957
+ // of strings with or without dot-separated components. If `subobject` is
958
+ // provided, `pathComponents` are interpreted as a relative path from the
959
+ // `subobject` field. If `subobject` is not provided, `pathComponents` are
960
+ // interpreted as a top-level path. `pathComponents` is optional when
961
+ // `subobject` field is provided. This way you can set a meta property
962
+ // directly for e.g. array or widget field. See examples below.
963
+ // - `key`: the key of the meta property. Should be a string. Dot-path is
964
+ // not supported, dots will be treated as part of the key. It's prefixed
965
+ // automatically with the `namespace` (`namespace:key`) to avoid
966
+ // conflicts with other modules.
967
+ // - `value`: the value of the meta property. Can be any JSON-serializable value.
968
+ //
969
+ // The document field metadata can be consumed by admin UI components. See
970
+ // `schema.addFieldMetadataComponent()` method for more information.
971
+ //
972
+ // Examples:
973
+ // - Set value of a top-level meta property of a generic field (e.g. string,
974
+ // number, boolean, etc.):
975
+ // `apos.doc.setMeta(doc, 'my-module', 'title', 'myMetaKey', 'myMetaValue');`
976
+ //
977
+ // - Set value of a top-level meta property of an object field (can be
978
+ // further nested):
979
+ // `apos.doc.setMeta(doc, 'my-module', 'address', 'city', 'myMetaKey', 'myMetaValue');`
980
+ //
981
+ // - Set value of a meta property of a field inside of an array field type:
982
+ // `apos.doc.setMeta(doc, 'my-module', arrayItemObject, 'city', 'myMetaKey', 'myMetaValue');`
983
+ //
984
+ // - Set value of a meta property of a rich text widget
985
+ // `apos.doc.setMeta(doc, 'my-module', widgetObject, 'myMetaKey', 'myMetaValue');`
986
+ //
987
+ // - Dots in the `key` are treated as part of the key, dots in `pathComponents`
988
+ // are treated as dot-path and are not altered:
989
+ // `apos.doc.setMeta(doc, 'my-module', 'address', 'city.name', 'myMetaKey.with.dots', 'myMetaValue');`
990
+ // will set `doc.aposMeta.address.aposMeta.city.name['my-module:myMetaKey.with.dots']: 'myMetaValue'`.
991
+ setMeta(doc, namespace, ...pathArgsWithKeyAndValue) {
992
+ if (!_.isPlainObject(doc) || !namespace) {
993
+ throw self.apos.error('invalid', 'Valid document and namespace are required.', {
994
+ cause: 'invalidArguments'
995
+ });
996
+ }
997
+
998
+ const pathArgs = [ ...pathArgsWithKeyAndValue ];
999
+ const value = pathArgs.pop();
1000
+ const key = pathArgs.pop();
1001
+
1002
+ if (!key) {
1003
+ throw self.apos.error('invalid', 'Key and value are required.', {
1004
+ cause: 'invalidArguments'
1005
+ });
1006
+ }
1007
+ if (typeof key !== 'string') {
1008
+ throw self.apos.error('invalid', 'Key must be a string.', {
1009
+ cause: 'invalidArguments'
1010
+ });
1011
+ }
1012
+
1013
+ const metaPath = self.getMetaPath(...pathArgs);
1014
+ const metaPathFull = `aposMeta.${metaPath}`;
1015
+ const nsKey = `${namespace}:${key}`;
1016
+
1017
+ const existingValue = _.get(doc, metaPathFull) || {};
1018
+ existingValue[nsKey] = value;
1019
+ _.set(doc, metaPathFull, existingValue);
1020
+
1021
+ return metaPath;
1022
+ },
1023
+ // Get meta data for a given field. It has exactly the same signature as
1024
+ // `setMeta` method, except the last `value` argument.
1025
+ getMeta(doc, namespace, ...pathArgsWithKey) {
1026
+ if (!doc || !namespace) {
1027
+ throw self.apos.error('invalid', 'Document and namespace are required.', {
1028
+ cause: 'invalidArguments'
1029
+ });
1030
+ }
1031
+
1032
+ const pathArgs = [ ...pathArgsWithKey ];
1033
+ const key = pathArgs.pop();
1034
+
1035
+ if (!key) {
1036
+ throw self.apos.error('invalid', 'Key and value are required.', {
1037
+ cause: 'invalidArguments'
1038
+ });
1039
+ }
1040
+ if (typeof key !== 'string') {
1041
+ throw self.apos.error('invalid', 'Key must be a string.', {
1042
+ cause: 'invalidArguments'
1043
+ });
1044
+ }
1045
+ const nsKey = `${namespace}:${key}`;
1046
+
1047
+ return _.get(
1048
+ doc,
1049
+ `aposMeta.${self.getMetaPath(...pathArgs)}`
1050
+ )?.[nsKey];
1051
+ },
1052
+ // Remove meta data key for a given field. It has exactly the same signature as
1053
+ // `setMeta` method, except the last `value` argument.
1054
+ // A cleanup is performed to remove empty meta properties on each call.
1055
+ removeMeta(doc, namespace, ...pathArgsWithKey) {
1056
+ if (!doc || !namespace) {
1057
+ throw self.apos.error('invalid', 'Document and namespace are required.', {
1058
+ cause: 'invalidArguments'
1059
+ });
1060
+ }
1061
+
1062
+ const pathArgs = [ ...pathArgsWithKey ];
1063
+ const key = pathArgs.pop();
1064
+ const metaPath = self.getMetaPath(...pathArgs);
1065
+ const metaPathFull = `aposMeta.${metaPath}`;
1066
+
1067
+ if (!_.has(doc, metaPathFull)) {
1068
+ return;
1069
+ }
1070
+
1071
+ if (!key) {
1072
+ throw self.apos.error('invalid', 'Key and value are required.', {
1073
+ cause: 'invalidArguments'
1074
+ });
1075
+ }
1076
+ if (typeof key !== 'string') {
1077
+ throw self.apos.error('invalid', 'Key must be a string.', {
1078
+ cause: 'invalidArguments'
1079
+ });
1080
+ }
1081
+ const nsKey = `${namespace}:${key}`;
1082
+
1083
+ const existingValue = _.get(doc, metaPathFull) || {};
1084
+ delete existingValue[nsKey];
1085
+ _.set(doc, metaPathFull, existingValue);
1086
+
1087
+ cleanup(doc.aposMeta, 'aposMeta');
1088
+
1089
+ return metaPath;
1090
+
1091
+ function cleanup(object, path) {
1092
+ if (_.isEmpty(object)) {
1093
+ _.unset(object, path);
1094
+ return true;
1095
+ }
1096
+
1097
+ for (const key of Object.keys(object)) {
1098
+ if (key.includes(':')) {
1099
+ return false;
1100
+ }
1101
+ if (!_.isPlainObject(object[key])) {
1102
+ delete object[key];
1103
+ continue;
1104
+ }
1105
+ if (!cleanup(object[key], `${path}.${key}`)) {
1106
+ return false;
1107
+ }
1108
+
1109
+ delete object[key];
1110
+ }
1111
+
1112
+ return true;
1113
+ }
1114
+ },
1115
+ // Get all meta keys for a given field. It has exactly the same signature as
1116
+ // `setMeta` method, except no key/value should be provided.
1117
+ getMetaKeys(doc, namespace, ...pathArgs) {
1118
+ return Object.keys(
1119
+ _.get(
1120
+ doc,
1121
+ `aposMeta.${self.getMetaPath(...pathArgs)}`
1122
+ ) || {}
1123
+ )
1124
+ .filter(key => key.startsWith(`${namespace}:`))
1125
+ .map(key => key.replace(`${namespace}:`, ''));
1126
+ },
1127
+ // Get the meta path for a given field.
1128
+ // Signature:
1129
+ // `apos.doc.getMetaPath([subobject,] ...pathComponents);`
1130
+ // See `setMeta` for more information about `subobject` and `pathComponents` arguments.
1131
+ //
1132
+ // Returns the path to the meta property withouth the namespace and key.
1133
+ // The returned path can be directly used to access or modify the meta property.
1134
+ // It's supported by all meta API methods.
1135
+ //
1136
+ // Example:
1137
+ // ```js
1138
+ // const path = apos.doc.getMetaPath(subobject, 'address', 'city', 'name');
1139
+ // apos.doc.setMeta(doc, ns, path, 'myMetaKey', 'myMetaValue');
1140
+ // apos.doc.getMeta(doc, ns, path, 'myMetaKey');
1141
+ // apos.doc.removeMeta(doc, ns, path, 'myMetaKey');
1142
+ getMetaPath(...pathArgs) {
1143
+ const args = pathArgs
1144
+ .filter(arg => typeof arg !== 'undefined' && arg !== null);
1145
+
1146
+ let subObject;
1147
+ if (_.isPlainObject(args[0])) {
1148
+ subObject = args.shift();
1149
+ }
1150
+
1151
+ if (args.some(arg => typeof arg !== 'string')) {
1152
+ throw self.apos.error('invalid', 'All path components must be strings.', {
1153
+ cause: 'invalidArguments'
1154
+ });
1155
+ }
1156
+ const pathComponents = args.join('.aposMeta.');
1157
+
1158
+ if (!subObject && !pathComponents) {
1159
+ throw self.apos.error(
1160
+ 'invalid',
1161
+ 'You must provide at least a "subobject" or at least one "pathComponent" string.',
1162
+ { cause: 'invalidArguments' }
1163
+ );
1164
+ }
1165
+
1166
+ if (subObject && !subObject._id) {
1167
+ throw self.apos.error(
1168
+ 'invalid',
1169
+ 'Provided subobject must have an _id property.',
1170
+ { cause: 'subObjectNoId' }
1171
+ );
1172
+ }
1173
+
1174
+ if (!subObject) {
1175
+ return pathComponents;
1176
+ }
1177
+
1178
+ const metaPath = [];
1179
+ metaPath.push(`@${subObject._id}`);
1180
+ if (pathComponents) {
1181
+ metaPath.push(`aposMeta.${pathComponents}`);
1182
+ }
1183
+
1184
+ return metaPath.join('.');
1185
+ },
941
1186
 
942
1187
  // Given either an id (as a string) or a criteria
943
1188
  // object, return a criteria object.
@@ -1062,12 +1307,12 @@ module.exports = {
1062
1307
  _id: tabId,
1063
1308
  updatedAt: new Date()
1064
1309
  };
1065
- const result = await self.db.updateOne(criteria, {
1310
+ const { result } = await self.db.updateOne(criteria, {
1066
1311
  $set: {
1067
1312
  advisoryLock: doc.advisoryLock
1068
1313
  }
1069
1314
  });
1070
- if (!result.result.nModified) {
1315
+ if (!result.nModified) {
1071
1316
  const info = await self.db.findOne({
1072
1317
  _id
1073
1318
  }, {
@@ -1200,7 +1445,8 @@ module.exports = {
1200
1445
  // `conditions` may be an array containing one or multiple of these values:
1201
1446
  //
1202
1447
  // 'canPublish', 'canEdit', 'canDismissSubmission', 'canDiscardDraft',
1203
- // 'canLocalize', 'canArchive', 'canUnpublish', 'canCopy', 'canRestore'.
1448
+ // 'canLocalize', 'canArchive', 'canUnpublish', 'canCopy', 'canRestore',
1449
+ // 'canCreate', 'canPreview', 'canShareDraft'
1204
1450
 
1205
1451
  addContextOperation(operation) {
1206
1452
  if (arguments.length === 2) {
@@ -1230,7 +1476,10 @@ module.exports = {
1230
1476
  'canArchive',
1231
1477
  'canUnpublish',
1232
1478
  'canCopy',
1233
- 'canRestore'
1479
+ 'canRestore',
1480
+ 'canCreate',
1481
+ 'canPreview',
1482
+ 'canShareDraft'
1234
1483
  ];
1235
1484
 
1236
1485
  if (!action || !context || !label || !modal) {
@@ -0,0 +1,93 @@
1
+ // A mixin to be implemented by the registered external metadata components.
2
+ // Registers the component props and provides useful util methods.
3
+
4
+ import isPlainObject from 'lodash/isPlainObject';
5
+
6
+ export default {
7
+ props: {
8
+ field: {
9
+ type: Object,
10
+ required: true
11
+ },
12
+ items: {
13
+ type: Array,
14
+ default() {
15
+ return [];
16
+ }
17
+ },
18
+ meta: {
19
+ type: Object,
20
+ default: () => ({})
21
+ },
22
+ namespace: {
23
+ type: String,
24
+ default: ''
25
+ },
26
+ metaRaw: {
27
+ type: Object,
28
+ default: () => ({})
29
+ }
30
+ },
31
+
32
+ methods: {
33
+ /**
34
+ * Recursively search for a namespace and optional key. Optionally
35
+ * match a value if a key and value are provided. If no key is provided,
36
+ * the namespace is matched.
37
+ * Meta keys (inside `metaObject`) starting with '@' are ignored,
38
+ * if not explicitely set otherwise, as they are absolute paths
39
+ * and will be yielding false positives.
40
+ *
41
+ * @param {object} metaObject the meta object or any object inside it
42
+ * @param {string} namespace
43
+ * @param {string} [key] optional key to match
44
+ * @param {string|number|boolean} [value] optional value to match
45
+ * @param {boolean} [searchAbsolute] whether to search in absolute paths
46
+ * @returns {boolean}
47
+ */
48
+ hasMeta(metaObject, namespace, key, value, searchAbsolute = false) {
49
+ if (!metaObject) {
50
+ return false;
51
+ }
52
+ const theKey = `${namespace}:${key || ''}`;
53
+ const withValue = key && typeof value !== 'undefined' && value !== null;
54
+
55
+ for (const [ k, v ] of Object.entries(metaObject)) {
56
+ // Do not search in absolute paths as it will
57
+ // lead to false positives.
58
+ if (!searchAbsolute && k.startsWith('@')) {
59
+ continue;
60
+ }
61
+ if (evaluateKey(k)) {
62
+ return evaluateValue(v);
63
+ }
64
+
65
+ if (isPlainObject(v) && this.hasMeta(v, namespace, key, value)) {
66
+ return true;
67
+ }
68
+ }
69
+
70
+ return false;
71
+
72
+ function evaluateKey(aKey) {
73
+ if (key) {
74
+ return aKey === theKey;
75
+ }
76
+
77
+ return aKey.startsWith(theKey);
78
+ }
79
+
80
+ function evaluateValue(aValue) {
81
+ if (!withValue) {
82
+ return true;
83
+ }
84
+
85
+ if (isPlainObject(aValue)) {
86
+ return this.hasMeta(aValue, namespace, key, value);
87
+ }
88
+
89
+ return aValue === value;
90
+ }
91
+ }
92
+ }
93
+ };
@@ -119,7 +119,7 @@ module.exports = {
119
119
  type: 'command-menu-manager-create-new'
120
120
  },
121
121
  permission: {
122
- action: 'edit',
122
+ action: 'create',
123
123
  type: self.__meta.name
124
124
  },
125
125
  shortcut: 'C'
@@ -147,7 +147,7 @@ module.exports = {
147
147
  type: 'command-menu-manager-archive-selected'
148
148
  },
149
149
  permission: {
150
- action: 'edit',
150
+ action: 'delete',
151
151
  type: self.__meta.name
152
152
  },
153
153
  shortcut: 'E'
@@ -328,6 +328,15 @@ module.exports = {
328
328
  }
329
329
  }
330
330
  },
331
+ afterDelete: {
332
+ async deleteRelatedReverseId(req, doc) {
333
+ // When deleting an unlocalized or draft document,
334
+ // we remove related reverse IDs of documents having a relation to the deleted one
335
+ if (!doc.aposMode || doc.aposMode === 'draft') {
336
+ await self.deleteRelatedReverseId(doc, true);
337
+ }
338
+ }
339
+ },
331
340
  afterRescue: {
332
341
  async revertDeduplication(req, doc) {
333
342
  const $set = await self.getRevertDeduplicationSet(req, doc);
@@ -371,18 +380,27 @@ module.exports = {
371
380
 
372
381
  methods(self) {
373
382
  return {
374
- async updateCacheField(req, doc) {
375
- const relatedDocsIds = self.getRelatedDocsIds(req, doc);
376
-
377
- // - Remove current doc reference from docs that include it
378
- // - Update these docs' cache field
379
- await self.apos.doc.db.updateMany({
383
+ async deleteRelatedReverseId(doc, deleting = false) {
384
+ const locales = doc.aposLocale && deleting
385
+ ? [
386
+ doc.aposLocale.replace(':draft', ':published'),
387
+ doc.aposLocale.replace(':published', ':draft')
388
+ ]
389
+ : [ doc.aposLocale ];
390
+ return self.apos.doc.db.updateMany({
380
391
  relatedReverseIds: { $in: [ doc.aposDocId ] },
381
- aposLocale: { $in: [ doc.aposLocale, null ] }
392
+ aposLocale: { $in: [ ...locales, null ] }
382
393
  }, {
383
394
  $pull: { relatedReverseIds: doc.aposDocId },
384
395
  $set: { cacheInvalidatedAt: doc.updatedAt }
385
396
  });
397
+ },
398
+ async updateCacheField(req, doc) {
399
+ const relatedDocsIds = self.getRelatedDocsIds(req, doc);
400
+
401
+ // - Remove current doc reference from docs that include it
402
+ // - Update these docs' cache field
403
+ await this.deleteRelatedReverseId(doc);
386
404
 
387
405
  if (relatedDocsIds.length) {
388
406
  // - Add current doc reference to related docs
@@ -423,7 +441,8 @@ module.exports = {
423
441
  label: 'apostrophe:shareDraft',
424
442
  modal: 'AposModalShareDraft',
425
443
  manuallyPublished: true,
426
- hasUrl: true
444
+ hasUrl: true,
445
+ conditions: [ 'canShareDraft' ]
427
446
  });
428
447
  },
429
448
  getRelatedDocsIds(req, doc) {
@@ -1478,11 +1497,52 @@ module.exports = {
1478
1497
  return field.viewPermission && !self.apos.permission.can(req, field.viewPermission.action, field.viewPermission.type);
1479
1498
  });
1480
1499
 
1500
+ if (doc.aposMeta && !self.apos.permission.can(req, 'edit', doc)) {
1501
+ forbiddenSchemaFields.push({
1502
+ name: 'aposMeta'
1503
+ });
1504
+ }
1505
+
1481
1506
  forbiddenSchemaFields.forEach(field => {
1482
1507
  delete doc[field.name];
1483
1508
  });
1484
1509
 
1485
1510
  return doc;
1511
+ },
1512
+
1513
+ composeFilters() {
1514
+ self.filters = Object.keys(self.filters).map((key) => ({
1515
+ name: key,
1516
+ ...self.filters[key],
1517
+ inputType: self.filters[key].inputType || 'select'
1518
+ }));
1519
+ // Add a null choice if not already added or set to `required`
1520
+ self.filters.forEach((filter) => {
1521
+ if (filter.choices) {
1522
+ if (
1523
+ !filter.required &&
1524
+ filter.choices &&
1525
+ !filter.choices.find((choice) => choice.value === null)
1526
+ ) {
1527
+ filter.def = null;
1528
+ filter.choices.push({
1529
+ value: null,
1530
+ label: 'apostrophe:none'
1531
+ });
1532
+ }
1533
+ } else {
1534
+ // Dynamic choices from the REST API, but
1535
+ // we need a label for "no opinion"
1536
+ filter.nullLabel = 'Choose One';
1537
+ }
1538
+ });
1539
+ },
1540
+
1541
+ composeColumns() {
1542
+ self.columns = Object.keys(self.columns).map((key) => ({
1543
+ name: key,
1544
+ ...self.columns[key]
1545
+ }));
1486
1546
  }
1487
1547
  };
1488
1548
  },