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.
- package/CHANGELOG.md +29 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +1 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +6 -4
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +9 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +8 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +6 -3
- package/modules/@apostrophecms/doc/index.js +254 -5
- package/modules/@apostrophecms/doc/ui/apos/mixins/AposFieldMetaUtilsMixin.js +93 -0
- package/modules/@apostrophecms/doc-type/index.js +70 -10
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +2 -0
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +12 -3
- package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
- package/modules/@apostrophecms/login/index.js +25 -19
- package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +11 -1
- package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +46 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +8 -3
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +3 -0
- package/modules/@apostrophecms/page/index.js +87 -20
- package/modules/@apostrophecms/page-type/index.js +67 -2
- package/modules/@apostrophecms/piece-type/index.js +1 -34
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +8 -0
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue +16 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +8 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +7 -7
- package/modules/@apostrophecms/schema/index.js +9 -0
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +3 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -8
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +74 -29
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +7 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +13 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +5 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +21 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +35 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +6 -0
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +41 -0
- package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +10 -4
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +7 -0
- package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +6 -0
- package/package.json +2 -2
- package/test/areas.js +1 -1
- package/test/assets.js +2 -2
- package/test/attachments.js +2 -2
- package/test/base-module.js +2 -1
- package/test/base-url-env-var.js +2 -2
- package/test/change-doc-ids.js +33 -31
- package/test/command-menu.js +2 -2
- package/test/content-i18n.js +47 -46
- package/test/db.js +2 -2
- package/test/docs.js +301 -126
- package/test/draft-published.js +2 -2
- package/test/email.js +2 -1
- package/test/express.js +3 -2
- package/test/external-front.js +4 -4
- package/test/field-meta.js +363 -0
- package/test/global.js +2 -1
- package/test/http.js +4 -2
- package/test/images.js +87 -88
- package/test/job.js +34 -34
- package/test/locks.js +2 -2
- package/test/login-requirements.js +3 -2
- package/test/middleware-and-route-order.js +2 -2
- package/test/page-type.js +2 -1
- package/test/pages-autocomplete.js +0 -11
- package/test/pages-public-api.js +2 -2
- package/test/pages-rest.js +4 -4
- package/test/pages.js +389 -57
- package/test/parked-pages.js +47 -47
- package/test/pieces-page-type.js +2 -1
- package/test/pieces-public-api.js +38 -38
- package/test/pieces.js +4 -4
- package/test/published-pages.js +16 -16
- package/test/schemaBuilders.js +4 -4
- package/test/schemas.js +220 -221
- package/test/search.js +2 -2
- package/test/soft-redirects.js +2 -1
- package/test/templates.js +2 -2
- 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
|
|
|
@@ -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: [ '
|
|
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
|
-
|
|
38
|
+
<AposButton
|
|
39
39
|
v-bind="copyButton"
|
|
40
40
|
v-if="!foreign"
|
|
41
41
|
@click="$emit('copy')"
|
|
42
|
-
tooltip="
|
|
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.
|
|
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: '
|
|
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: '
|
|
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
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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: [
|
|
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
|
},
|