apostrophe 3.53.0 → 3.55.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 (77) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/defaults.js +1 -0
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +5 -2
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +28 -19
  5. package/modules/@apostrophecms/any-doc-type/index.js +2 -2
  6. package/modules/@apostrophecms/any-page-type/index.js +2 -2
  7. package/modules/@apostrophecms/doc/index.js +55 -29
  8. package/modules/@apostrophecms/doc-type/index.js +11 -6
  9. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +4 -440
  10. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +445 -0
  11. package/modules/@apostrophecms/i18n/i18n/de.json +113 -105
  12. package/modules/@apostrophecms/i18n/i18n/es.json +10 -0
  13. package/modules/@apostrophecms/i18n/i18n/fr.json +8 -0
  14. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +10 -0
  15. package/modules/@apostrophecms/i18n/i18n/sk.json +8 -0
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +1 -0
  17. package/modules/@apostrophecms/log/index.js +429 -0
  18. package/modules/@apostrophecms/login/index.js +47 -4
  19. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +14 -1
  20. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -1
  21. package/modules/@apostrophecms/module/index.js +32 -6
  22. package/modules/@apostrophecms/module/lib/log.js +68 -0
  23. package/modules/@apostrophecms/page/index.js +71 -19
  24. package/modules/@apostrophecms/page/lib/legacy-migrations.js +0 -57
  25. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +8 -285
  26. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +291 -0
  27. package/modules/@apostrophecms/page-type/index.js +39 -26
  28. package/modules/@apostrophecms/piece-type/index.js +19 -11
  29. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +2 -357
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +2 -86
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +2 -254
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +2 -77
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputBoolean.vue +2 -44
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +2 -64
  36. package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +2 -94
  37. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +3 -47
  38. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -82
  39. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +2 -37
  40. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +2 -26
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -57
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +2 -259
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +2 -38
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +2 -275
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +2 -167
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +2 -115
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +3 -279
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +2 -83
  49. package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +10 -1
  50. package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +361 -0
  51. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +89 -0
  52. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +257 -0
  53. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputAttachment.js +81 -0
  54. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputBoolean.js +48 -0
  55. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +68 -0
  56. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputColor.js +98 -0
  57. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +49 -0
  58. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +86 -0
  59. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +41 -0
  60. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +29 -0
  61. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +60 -0
  62. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +262 -0
  63. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js +41 -0
  64. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +278 -0
  65. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +170 -0
  66. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +118 -0
  67. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +281 -0
  68. package/modules/@apostrophecms/schema/ui/apos/logic/AposSearchList.js +85 -0
  69. package/modules/@apostrophecms/template/index.js +1 -1
  70. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -2
  71. package/modules/@apostrophecms/util/index.js +83 -13
  72. package/modules/@apostrophecms/util/lib/logger.js +19 -17
  73. package/package.json +1 -1
  74. package/test/docs.js +35 -2
  75. package/test/log.js +1765 -0
  76. package/test/pages.js +57 -0
  77. package/test-lib/util.js +1 -1
@@ -0,0 +1,262 @@
1
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2
+ import { klona } from 'klona';
3
+
4
+ export default {
5
+ name: 'AposInputRelationship',
6
+ mixins: [ AposInputMixin ],
7
+ emits: [ 'input' ],
8
+ data () {
9
+ const next = (this.value && Array.isArray(this.value.data))
10
+ ? klona(this.value.data) : (klona(this.field.def) || []);
11
+
12
+ // Remember relationship subfield values even if a document
13
+ // is temporarily deselected, easing the user's pain if they
14
+ // inadvertently deselect something for a moment
15
+ const subfields = Object.fromEntries(
16
+ (next || []).filter(doc => doc._fields)
17
+ .map(doc => [ doc._id, doc._fields ])
18
+ );
19
+
20
+ return {
21
+ searchTerm: '',
22
+ searchList: [],
23
+ next,
24
+ subfields,
25
+ disabled: false,
26
+ searching: false,
27
+ choosing: false,
28
+ relationshipSchema: null
29
+ };
30
+ },
31
+ computed: {
32
+ limitReached() {
33
+ return this.field.max === this.next.length;
34
+ },
35
+ pluralLabel() {
36
+ return apos.modules[this.field.withType].pluralLabel;
37
+ },
38
+ // TODO get 'Search' server for better i18n
39
+ placeholder() {
40
+ return this.field.placeholder || {
41
+ key: 'apostrophe:searchDocType',
42
+ type: this.$t(this.pluralLabel)
43
+ };
44
+ },
45
+ // TODO get 'Browse' for better i18n
46
+ browseLabel() {
47
+ return {
48
+ key: 'apostrophe:browseDocType',
49
+ type: this.$t(this.pluralLabel)
50
+ };
51
+ },
52
+ suggestion() {
53
+ return {
54
+ disabled: true,
55
+ tooltip: false,
56
+ icon: false,
57
+ classes: [ 'suggestion' ],
58
+ title: this.$t(this.field.suggestionLabel),
59
+ help: this.$t({
60
+ key: this.field.suggestionHelp || 'apostrophe:relationshipSuggestionHelp',
61
+ type: this.$t(this.pluralLabel)
62
+ }),
63
+ customFields: [ 'help' ]
64
+ };
65
+ },
66
+ hint() {
67
+ return {
68
+ disabled: true,
69
+ tooltip: false,
70
+ icon: 'binoculars-icon',
71
+ iconSize: 35,
72
+ classes: [ 'hint' ],
73
+ title: this.$t('apostrophe:relationshipSuggestionNoResults'),
74
+ help: this.$t({
75
+ key: this.field.browse
76
+ ? 'apostrophe:relationshipSuggestionSearchAndBrowse'
77
+ : 'apostrophe:relationshipSuggestionSearch',
78
+ type: this.$t(this.pluralLabel)
79
+ }),
80
+ customFields: [ 'help' ]
81
+ };
82
+ },
83
+ chooserComponent () {
84
+ return apos.modules[this.field.withType].components.managerModal;
85
+ },
86
+ disableUnpublished() {
87
+ return apos.modules[this.field.withType].localized;
88
+ },
89
+ buttonModifiers() {
90
+ const modifiers = [ 'small' ];
91
+ if (this.modifiers.includes('no-search')) {
92
+ modifiers.push('block');
93
+ }
94
+ return modifiers;
95
+ },
96
+ minSize() {
97
+ const [ widgetOptions = {} ] = apos.area.widgetOptions;
98
+
99
+ return widgetOptions.minSize || [];
100
+ },
101
+ duplicate () {
102
+ return this.value.duplicate ? 'apos-input--error' : null;
103
+ }
104
+ },
105
+ watch: {
106
+ next(after, before) {
107
+ for (const doc of before) {
108
+ this.subfields[doc._id] = doc._fields;
109
+ }
110
+ for (const doc of after) {
111
+ if (Object.keys(doc._fields || {}).length) {
112
+ continue;
113
+ }
114
+ doc._fields = this.field.schema && (this.subfields[doc._id]
115
+ ? this.subfields[doc._id]
116
+ : this.getDefault());
117
+ }
118
+ }
119
+ },
120
+ mounted () {
121
+ this.checkLimit();
122
+ },
123
+ methods: {
124
+ validate(value) {
125
+ this.checkLimit();
126
+
127
+ if (this.field.required && !value.length) {
128
+ return { message: 'required' };
129
+ }
130
+
131
+ if (this.field.min && this.field.min > value.length) {
132
+ return { message: `minimum of ${this.field.min} required` };
133
+ }
134
+
135
+ return false;
136
+ },
137
+ checkLimit() {
138
+ if (this.limitReached) {
139
+ this.searchTerm = 'Limit reached!';
140
+ } else if (this.searchTerm === 'Limit reached!') {
141
+ this.searchTerm = '';
142
+ }
143
+
144
+ this.disabled = !!this.limitReached;
145
+ },
146
+ updateSelected(items) {
147
+ this.next = items;
148
+ },
149
+ async search(qs) {
150
+ if (this.field.suggestionLimit) {
151
+ qs.perPage = this.field.suggestionLimit;
152
+ }
153
+ if (this.field.suggestionSort) {
154
+ qs.sort = this.field.suggestionSort;
155
+ }
156
+ if (this.field.withType === '@apostrophecms/image') {
157
+ apos.bus.$emit('piece-relationship-query', qs);
158
+ }
159
+
160
+ this.searching = true;
161
+ const list = await apos.http.get(
162
+ apos.modules[this.field.withType].action,
163
+ {
164
+ busy: false,
165
+ draft: true,
166
+ qs
167
+ }
168
+ );
169
+
170
+ const removeSelectedItem = item => !this.next.map(i => i._id).includes(item._id);
171
+ const formatItems = item => ({
172
+ ...item,
173
+ disabled: this.disableUnpublished && !item.lastPublishedAt
174
+ });
175
+
176
+ const results = (list.results || [])
177
+ .filter(removeSelectedItem)
178
+ .map(formatItems);
179
+
180
+ const suggestion = !qs.autocomplete && this.suggestion;
181
+ const hint = (!qs.autocomplete || !results.length) && this.hint;
182
+
183
+ this.searchList = [ suggestion, ...results, hint ].filter(Boolean);
184
+ this.searching = false;
185
+ },
186
+ async input () {
187
+ if (this.searching) {
188
+ return;
189
+ }
190
+
191
+ const trimmed = this.searchTerm.trim();
192
+ const qs = trimmed.length
193
+ ? {
194
+ autocomplete: trimmed
195
+ }
196
+ : {};
197
+
198
+ await this.search(qs);
199
+ },
200
+ handleFocusOut() {
201
+ // hide search list when click outside the input
202
+ // timeout to execute "@select" method before
203
+ setTimeout(() => {
204
+ this.searchList = [];
205
+ }, 200);
206
+ },
207
+ watchValue () {
208
+ this.error = this.value.error;
209
+ // Ensure the internal state is an array.
210
+ this.next = Array.isArray(this.value.data) ? this.value.data : [];
211
+ },
212
+ async choose () {
213
+ const result = await apos.modal.execute(this.chooserComponent, {
214
+ title: this.field.label || this.field.name,
215
+ moduleName: this.field.withType,
216
+ chosen: this.next,
217
+ relationshipField: this.field
218
+ });
219
+ if (result) {
220
+ this.updateSelected(result);
221
+ }
222
+ },
223
+ async editRelationship (item) {
224
+ const editor = this.field.editor || 'AposRelationshipEditor';
225
+
226
+ const result = await apos.modal.execute(editor, {
227
+ schema: this.field.schema,
228
+ item,
229
+ title: item.title,
230
+ value: item._fields
231
+ });
232
+
233
+ if (result) {
234
+ const index = this.next.findIndex(_item => _item._id === item._id);
235
+ this.$set(this.next, index, {
236
+ ...this.next[index],
237
+ _fields: result
238
+ });
239
+ }
240
+ },
241
+ getEditRelationshipLabel () {
242
+ if (this.field.editor === 'AposImageRelationshipEditor') {
243
+ return 'apostrophe:editImageAdjustments';
244
+ }
245
+ },
246
+ getDefault() {
247
+ const object = {};
248
+ this.field.schema.forEach(field => {
249
+ if (field.name.startsWith('_')) {
250
+ return;
251
+ }
252
+ // Using `hasOwn` here, not simply checking if `field.def` is truthy
253
+ // so that `false`, `null`, `''` or `0` are taken into account:
254
+ const hasDefaultValue = Object.hasOwn(field, 'def');
255
+ object[field.name] = hasDefaultValue
256
+ ? klona(field.def)
257
+ : null;
258
+ });
259
+ return object;
260
+ }
261
+ }
262
+ };
@@ -0,0 +1,41 @@
1
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2
+ import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInputChoicesMixin';
3
+
4
+ export default {
5
+ name: 'AposInputSelect',
6
+ mixins: [ AposInputMixin, AposInputChoicesMixin ],
7
+ props: {
8
+ icon: {
9
+ type: String,
10
+ default: 'menu-down-icon'
11
+ }
12
+ },
13
+ data() {
14
+ return {
15
+ next: (this.value.data == null) ? null : this.value.data,
16
+ choices: []
17
+ };
18
+ },
19
+ computed: {
20
+ classes () {
21
+ return [ this.value.duplicate && 'apos-input--error' ];
22
+ }
23
+ },
24
+ methods: {
25
+ validate(value) {
26
+ if (this.field.required && (value === null)) {
27
+ return 'required';
28
+ }
29
+
30
+ if (value && !this.choices.find(choice => choice.value === value)) {
31
+ return 'invalid';
32
+ }
33
+
34
+ return false;
35
+ },
36
+ change(value) {
37
+ // Allows expression of non-string values
38
+ this.next = this.choices.find(choice => choice.value === value).value;
39
+ }
40
+ }
41
+ };
@@ -0,0 +1,278 @@
1
+ // NOTE: This is a temporary component, copying AposInputString. Base modules
2
+ // already have `type: 'slug'` fields, so this is needed to avoid distracting
3
+ // errors.
4
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
5
+ import sluggo from 'sluggo';
6
+ import debounce from 'debounce-async';
7
+ import { klona } from 'klona';
8
+
9
+ export default {
10
+ name: 'AposInputSlug',
11
+ mixins: [ AposInputMixin ],
12
+ emits: [ 'return' ],
13
+ data() {
14
+ return {
15
+ conflict: false,
16
+ isArchived: null,
17
+ originalSlugPartsLength: null
18
+ };
19
+ },
20
+ computed: {
21
+ tabindex () {
22
+ return this.field.disableFocus ? '-1' : '0';
23
+ },
24
+ type () {
25
+ if (this.field.type) {
26
+ return this.field.type;
27
+ } else {
28
+ return 'text';
29
+ }
30
+ },
31
+ classes () {
32
+ return [ 'apos-input', 'apos-input--text', 'apos-input--slug' ];
33
+ },
34
+ wrapperClasses () {
35
+ return [ 'apos-input-wrapper' ].concat(this.localePrefix ? [ 'apos-input-wrapper--with-prefix' ] : []);
36
+ },
37
+ icon () {
38
+ if (this.error) {
39
+ return 'circle-medium-icon';
40
+ } else if (this.field.icon) {
41
+ return this.field.icon;
42
+ } else {
43
+ return null;
44
+ }
45
+ },
46
+ prefix () {
47
+ return this.field.prefix || '';
48
+ },
49
+ localePrefix() {
50
+ return this.field.page && apos.i18n.locales[apos.i18n.locale].prefix;
51
+ }
52
+ },
53
+ watch: {
54
+ followingValues: {
55
+ // We are usually interested in followingValue.title, but a
56
+ // secondary slug field could be configured to watch
57
+ // one or more other fields
58
+ deep: true,
59
+ handler(newValue, oldValue) {
60
+ const newClone = klona(newValue);
61
+ const oldClone = klona(oldValue);
62
+
63
+ // Track whether the slug is archived for prefixing.
64
+ this.isArchived = newValue.archived;
65
+ // We only want the string properties to build the slug itself.
66
+ delete newClone.archived;
67
+ delete oldClone.archived;
68
+
69
+ oldValue = Object.values(oldClone).join(' ');
70
+ newValue = Object.values(newClone).join(' ');
71
+
72
+ if (this.compatible(oldValue, this.next) && !newValue.archived) {
73
+ // If this is a page slug, we only replace the last section of the slug.
74
+ if (this.field.page) {
75
+ let parts = this.next.split('/');
76
+ parts = parts.filter(part => part.length > 0);
77
+ if ((!this.originalSlugPartsLength && parts.length) || (this.originalSlugPartsLength && parts.length === (this.originalSlugPartsLength - 1))) {
78
+ // Remove last path component so we can replace it
79
+ parts.pop();
80
+ }
81
+ parts.push(this.slugify(newValue, { componentOnly: true }));
82
+ if (parts[0].length) {
83
+ // TODO: handle page archives.
84
+ this.next = `/${parts.join('/')}`;
85
+ }
86
+ } else {
87
+ this.next = this.slugify(newValue);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ },
93
+ async mounted() {
94
+ this.debouncedCheckConflict = debounce(() => this.checkConflict(), 250);
95
+ if (this.next.length) {
96
+ await this.debouncedCheckConflict();
97
+ }
98
+ this.originalSlugPartsLength = this.next.split('/').length;
99
+ },
100
+ methods: {
101
+ async watchNext() {
102
+ this.next = this.slugify(this.next);
103
+ this.validateAndEmit();
104
+ try {
105
+ await this.debouncedCheckConflict();
106
+ } catch (e) {
107
+ if (e === 'canceled') {
108
+ // That's fine
109
+ } else {
110
+ throw e;
111
+ }
112
+ }
113
+ },
114
+ validate(value) {
115
+ if (this.conflict) {
116
+ return {
117
+ name: 'conflict',
118
+ message: 'apostrophe:slugInUse'
119
+ };
120
+ }
121
+ if (this.field.required) {
122
+ if (!value.length) {
123
+ return 'required';
124
+ }
125
+ }
126
+ if (this.field.min) {
127
+ if (value.length && (value.length < this.field.min)) {
128
+ return 'min';
129
+ }
130
+ }
131
+ if (this.field.max) {
132
+ if (value.length && (value.length > this.field.max)) {
133
+ return 'max';
134
+ }
135
+ }
136
+ return false;
137
+ },
138
+ compatible(title, slug) {
139
+ if ((typeof title) !== 'string') {
140
+ title = '';
141
+ }
142
+ if (this.field.page) {
143
+ const matches = slug.match(/[^/]+$/);
144
+ slug = (matches && matches[0]) || '';
145
+ }
146
+ return ((title === '') && (slug === `${this.prefix}`)) ||
147
+ this.slugify(title) === this.slugify(slug);
148
+ },
149
+ // if componentOnly is true, we are slugifying just one component of
150
+ // a slug as part of following the title field, and so we do *not*
151
+ // want to allow slashes (when editing a page) or set a prefix.
152
+ slugify(s, { componentOnly = false } = {}) {
153
+ const options = {
154
+ def: ''
155
+ };
156
+
157
+ if (this.field.page && !componentOnly) {
158
+ options.allow = '/';
159
+ }
160
+
161
+ let preserveDash = false;
162
+ // When you are typing a slug it feels wrong for hyphens you typed
163
+ // to disappear as you go, so if the last character is not valid in a slug,
164
+ // restore it after we call sluggo for the full string
165
+ if (this.focus && s.length && (sluggo(s.charAt(s.length - 1), options) === '')) {
166
+ preserveDash = true;
167
+ }
168
+
169
+ s = sluggo(s, options);
170
+ if (preserveDash) {
171
+ s += '-';
172
+ }
173
+
174
+ if (this.field.page && !componentOnly) {
175
+ if (!this.followingValues?.title) {
176
+ const nextParts = this.next.split('/');
177
+ if (s === nextParts[nextParts.length - 1]) {
178
+ s = '';
179
+ if (this.originalSlugPartsLength === nextParts.length) {
180
+ nextParts.pop();
181
+ }
182
+ this.next = nextParts.join('/');
183
+ }
184
+ }
185
+ if (!s.charAt(0) !== '/') {
186
+ s = `/${s}`;
187
+ }
188
+ s = s.replace(/\/+/g, '/');
189
+ if (s !== '/') {
190
+ s = s.replace(/\/$/, '');
191
+ }
192
+ if (!this.followingValues?.title && s.length) {
193
+ s += '/';
194
+ }
195
+ }
196
+
197
+ if (!componentOnly) {
198
+ s = this.setPrefix(s);
199
+ }
200
+
201
+ return s;
202
+ },
203
+ setPrefix (slug) {
204
+ // Get a fresh clone of the slug.
205
+ let updated = slug;
206
+ const archivedRegexp = new RegExp(`^deduplicate-[a-z0-9]+-${this.prefix}`);
207
+
208
+ // Prefix if the slug doesn't start with the prefix OR if its archived
209
+ // and it doesn't start with the dedupe+prefix pattern.
210
+ if (
211
+ !updated.startsWith(this.prefix) ||
212
+ (this.isArchived && !updated.match(archivedRegexp))
213
+ ) {
214
+ let archivePrefix = '';
215
+ // If archived, remove the dedupe pattern to add again later.
216
+ if (this.isArchived) {
217
+ archivePrefix = updated.match(/^deduplicate-[a-z0-9]+-/);
218
+ updated = updated.replace(archivePrefix, '');
219
+ }
220
+
221
+ if (this.prefix.startsWith(updated)) {
222
+ // If they delete the `-`, and the prefix is `recipe-`,
223
+ // we want to restore `recipe-`, not set it to `recipe-recipe`
224
+ updated = this.prefix;
225
+ } else {
226
+ // Make sure we're not double prefixing archived slugs.
227
+ updated = updated.startsWith(this.prefix) ? updated : this.prefix + updated;
228
+ }
229
+ // Reapply the dedupe pattern if archived. If being restored from the
230
+ // doc editor modal it will momentarily be tracked as archived but
231
+ // without not have the archive prefix, so check that too.
232
+ updated = this.isArchived && archivePrefix ? `${archivePrefix}${updated}` : updated;
233
+ }
234
+
235
+ return updated;
236
+ },
237
+ async checkConflict() {
238
+ let slug;
239
+ try {
240
+ slug = this.next;
241
+ if (slug.length) {
242
+ await apos.http.post(`${apos.doc.action}/slug-taken`, {
243
+ body: {
244
+ slug,
245
+ _id: this.docId
246
+ },
247
+ draft: true
248
+ });
249
+ // Still relevant?
250
+ if (slug === this.next) {
251
+ this.conflict = false;
252
+ this.validateAndEmit();
253
+ } else {
254
+ // Can ignore it, another request
255
+ // probably already in-flight
256
+ }
257
+ }
258
+ } catch (e) {
259
+ // 409: Conflict (slug in use)
260
+ if (e.status === 409) {
261
+ // Still relevant?
262
+ if (slug === this.next) {
263
+ this.conflict = true;
264
+ this.validateAndEmit();
265
+ } else {
266
+ // Can ignore it, another request
267
+ // probably already in-flight
268
+ }
269
+ } else {
270
+ throw e;
271
+ }
272
+ }
273
+ },
274
+ passFocus() {
275
+ this.$refs.input.focus();
276
+ }
277
+ }
278
+ };