apostrophe 3.48.0 → 3.50.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 (45) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/index.js +20 -2
  3. package/lib/locales.js +1 -1
  4. package/lib/moog-require.js +3 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +12 -2
  6. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -24
  8. package/modules/@apostrophecms/asset/index.js +27 -2
  9. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +23 -2
  10. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +26 -2
  11. package/modules/@apostrophecms/doc/index.js +149 -0
  12. package/modules/@apostrophecms/doc-type/index.js +9 -1
  13. package/modules/@apostrophecms/global/index.js +4 -15
  14. package/modules/@apostrophecms/i18n/i18n/en.json +3 -2
  15. package/modules/@apostrophecms/i18n/index.js +76 -61
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +14 -1
  17. package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +3 -60
  18. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +3 -231
  19. package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +3 -96
  20. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -99
  21. package/modules/@apostrophecms/login/ui/apos/logic/AposForgotPasswordForm.js +68 -0
  22. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +239 -0
  23. package/modules/@apostrophecms/login/ui/apos/logic/AposResetPasswordForm.js +105 -0
  24. package/modules/@apostrophecms/login/ui/apos/logic/TheAposLogin.js +107 -0
  25. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +9 -3
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue +1 -0
  27. package/modules/@apostrophecms/page/index.js +124 -1
  28. package/modules/@apostrophecms/piece-type/index.js +57 -9
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +11 -8
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +226 -72
  31. package/modules/@apostrophecms/schema/index.js +0 -1
  32. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +35 -7
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +21 -1
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +12 -7
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
  36. package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +178 -20
  37. package/modules/@apostrophecms/ui/ui/apos/components/AposFilterMenu.vue +1 -1
  38. package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -6
  39. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
  40. package/modules/@apostrophecms/util/index.js +5 -6
  41. package/modules/@apostrophecms/util/ui/src/http.js +6 -3
  42. package/package.json +20 -3
  43. package/test/change-doc-ids.js +134 -0
  44. package/test/i18n.js +310 -0
  45. package/test/static-i18n.js +0 -105
@@ -20,8 +20,8 @@
20
20
  <li
21
21
  class="apos-combo__selected"
22
22
  v-for="checked in selectedItems"
23
- :key="checked"
24
- @click="selectOption(getSelectedOption(checked))"
23
+ :key="objectValues ? checked.value : checked"
24
+ @click.stop="selectOption(getSelectedOption(checked))"
25
25
  >
26
26
  {{ getSelectedOption(checked)?.label }}
27
27
  <AposIndicator
@@ -44,14 +44,30 @@
44
44
  :class="{'apos-combo__list--showed': showedList}"
45
45
  :style="{top: boxHeight + 'px'}"
46
46
  tabindex="0"
47
- @keydown.prevent.space="selectOption(options[focusedItemIndex])"
48
- @keydown.prevent.enter="selectOption(options[focusedItemIndex])"
49
- @keydown.prevent.arrow-down="focusListItem()"
50
- @keydown.prevent.arrow-up="focusListItem(true)"
51
- @keydown.prevent.delete="closeList(null, true)"
52
- @keydown.prevent.stop.esc="closeList(null, true)"
47
+ @keydown="onListKey"
53
48
  @blur="closeList()"
54
49
  >
50
+ <li
51
+ v-if="typehead"
52
+ class="apos-combo__list-typehead"
53
+ key="__typehead"
54
+ @click.stop="$refs.input.focus()"
55
+ >
56
+ <input
57
+ class="apos-combo__typehead"
58
+ type="text"
59
+ :placeholder="$t('apostrophe:search')"
60
+ :value="thInput"
61
+ ref="input"
62
+ @input="onTypeheadInput"
63
+ @keydown="onTypeheadKey"
64
+ >
65
+ <AposSpinner
66
+ v-if="busy"
67
+ class="apos-combo__spinner"
68
+ color="--a-base-5"
69
+ />
70
+ </li>
55
71
  <li
56
72
  :key="choice.value"
57
73
  class="apos-combo__list-item"
@@ -88,30 +104,54 @@ export default {
88
104
  value: {
89
105
  type: Object,
90
106
  required: true
107
+ },
108
+ typehead: {
109
+ type: Boolean,
110
+ default: false
111
+ },
112
+ busy: {
113
+ type: Boolean,
114
+ default: false
115
+ },
116
+ // When true, indicates that the values and selected items are arrays of objects,
117
+ // array of primitive values otherwise.
118
+ objectValues: {
119
+ type: Boolean,
120
+ default: false
91
121
  }
92
122
  },
93
123
 
94
- emits: [ 'select-items' ],
124
+ emits: [ 'select-items', 'toggle', 'input' ],
95
125
  data () {
96
- const showSelectAll = this.field.all !== false &&
97
- (!this.field.max || this.field.max > this.choices.length);
98
126
 
99
127
  return {
100
128
  showedList: false,
101
129
  boxHeight: 0,
102
- showSelectAll,
103
- options: this.renderOptions(showSelectAll),
104
130
  boxResizeObserver: this.getBoxResizeObserver(),
105
- focusedItemIndex: null
131
+ focusedItemIndex: null,
132
+ thInput: ''
106
133
  };
107
134
  },
108
135
  computed: {
136
+ showSelectAll() {
137
+ return this.field.all !== false &&
138
+ (!this.field.max || this.field.max > this.choices.length);
139
+ },
140
+ options() {
141
+ return this.renderOptions(this.showSelectAll);
142
+ },
109
143
  selectedItems() {
110
- if (this.allItemsSelected()) {
111
- return [ '__all' ];
144
+ if (!this.showSelectAll || !this.allItemsSelected()) {
145
+ return this.value.data;
112
146
  }
113
-
114
- return this.value.data;
147
+ if (this.objectValues) {
148
+ const { listLabel } = this.getSelectAllLabel();
149
+ return [ {
150
+ label: listLabel,
151
+ value: '__all'
152
+ } ];
153
+ }
154
+ return [ '__all' ];
115
155
  }
116
156
  },
117
157
  mounted() {
@@ -121,6 +161,37 @@ export default {
121
161
  this.boxResizeObserver.unobserve(this.$refs.select);
122
162
  },
123
163
  methods: {
164
+ onTypeheadInput(e) {
165
+ this.thInput = e.target.value;
166
+ this.$emit('input', this.thInput);
167
+ },
168
+ onTypeheadKey(e) {
169
+ const stop = () => {
170
+ e.preventDefault();
171
+ e.stopPropagation();
172
+ };
173
+ switch (e.key) {
174
+ case 'ArrowDown':
175
+ stop();
176
+ this.focusListItem();
177
+ break;
178
+
179
+ case 'ArrowUp':
180
+ stop();
181
+ this.focusListItem(true);
182
+ break;
183
+
184
+ case 'Enter':
185
+ stop();
186
+ this.selectOption(this.options[this.focusedItemIndex]);
187
+ break;
188
+
189
+ case 'Tab':
190
+ stop();
191
+ this.closeList(null, true);
192
+ break;
193
+ }
194
+ },
124
195
  toggleList() {
125
196
  if (this.field.readOnly) {
126
197
  return;
@@ -128,15 +199,68 @@ export default {
128
199
  this.showedList = !this.showedList;
129
200
 
130
201
  if (this.showedList) {
202
+ this.$emit('toggle', true);
131
203
  this.$nextTick(() => {
132
- this.$refs.list.focus();
133
204
  this.focusedItemIndex = 0;
205
+ if (this.typehead) {
206
+ this.$refs.input.focus();
207
+ } else {
208
+ this.$refs.list.focus();
209
+ }
134
210
  });
135
211
  } else {
136
212
  this.$refs.select.focus();
137
213
  this.resetList();
138
214
  }
139
215
  },
216
+ onListKey(e) {
217
+ const stop = () => {
218
+ e.preventDefault();
219
+ e.stopPropagation();
220
+ };
221
+ switch (e.key) {
222
+ case ' ': // Brave issues
223
+ case 'Space': {
224
+ if (!this.typehead) {
225
+ stop();
226
+ this.selectOption(this.options[this.focusedItemIndex]);
227
+ }
228
+ break;
229
+ }
230
+
231
+ case 'Enter': {
232
+ stop();
233
+ this.selectOption(this.options[this.focusedItemIndex]);
234
+ break;
235
+ }
236
+
237
+ case 'ArrowDown': {
238
+ stop();
239
+ this.focusListItem();
240
+ break;
241
+ }
242
+
243
+ case 'ArrowUp': {
244
+ stop();
245
+ this.focusListItem(true);
246
+ break;
247
+ }
248
+
249
+ case 'Delete': {
250
+ if (!this.typehead) {
251
+ stop();
252
+ this.closeList(null, true);
253
+ }
254
+ break;
255
+ }
256
+
257
+ case 'Escape': {
258
+ stop();
259
+ this.closeList(null, true);
260
+ break;
261
+ }
262
+ }
263
+ },
140
264
  closeList(_, focusSelect) {
141
265
  this.showedList = false;
142
266
  this.resetList();
@@ -148,8 +272,11 @@ export default {
148
272
  }
149
273
  },
150
274
  resetList() {
275
+ this.$emit('toggle', false);
151
276
  this.focusedItemIndex = null;
152
277
  this.$refs.list.scrollTo({ top: 0 });
278
+ this.thInput = '';
279
+ this.$emit('input', this.thInput);
153
280
  },
154
281
  getBoxResizeObserver() {
155
282
  return new ResizeObserver(([ { target } ]) => {
@@ -174,12 +301,21 @@ export default {
174
301
  ];
175
302
  },
176
303
  isSelected(choice) {
177
- return this.value.data.some((val) => val === choice.value);
304
+ const condition = (entry) => (
305
+ this.objectValues
306
+ ? entry.value === choice.value
307
+ : entry === choice.value
308
+ );
309
+
310
+ return this.value.data.some(condition);
178
311
  },
179
312
  allItemsSelected () {
180
313
  return this.choices.length && this.value.data.length === this.choices.length;
181
314
  },
182
315
  getSelectedOption(checked) {
316
+ if (this.objectValues) {
317
+ return checked;
318
+ }
183
319
  if (checked === '__all') {
184
320
  const { selectedLabel } = this.getSelectAllLabel();
185
321
  return {
@@ -365,6 +501,12 @@ export default {
365
501
  &--showed {
366
502
  display: block;
367
503
  }
504
+
505
+ &-typehead {
506
+ box-sizing: border-box;
507
+ display: flex;
508
+ align-items: center;
509
+ }
368
510
  }
369
511
 
370
512
  .apos-combo__list-item {
@@ -378,4 +520,20 @@ export default {
378
520
  }
379
521
  }
380
522
 
523
+ .apos-combo__typehead {
524
+ flex-grow: 1;
525
+ margin: 0;
526
+ padding: 10px 10px 10px 20px;
527
+ border: none;
528
+ box-sizing: border-box;
529
+ outline: none;
530
+ background-color: transparent;
531
+ @include type-base;
532
+ }
533
+
534
+ .apos-combo__spinner {
535
+ margin-right: 15px;
536
+ opacity: 0.7;
537
+ }
538
+
381
539
  </style>
@@ -48,7 +48,7 @@ export default {
48
48
  return {
49
49
  label: 'apostrophe:filter',
50
50
  icon: 'chevron-down-icon',
51
- modifiers: [ 'icon-right' ],
51
+ modifiers: [ 'icon-right', 'small' ],
52
52
  type: 'outline'
53
53
  };
54
54
  }
@@ -3,6 +3,7 @@
3
3
  <AposButton
4
4
  :disabled="currentPage == 1"
5
5
  class="apos-pager__btn"
6
+ :modifiers="['small']"
6
7
  type="outline" @click="incrementPage(-1)"
7
8
  :icon-only="true" icon="chevron-left-icon"
8
9
  :label="prevButtonLabel"
@@ -10,7 +11,7 @@
10
11
  <div class="apos-input-wrapper">
11
12
  <select
12
13
  :disabled="totalPages <= 1"
13
- class="apos-input apos-input--select"
14
+ class="apos-pager__page-select apos-input apos-input--select"
14
15
  v-model="selectedPage" :aria-label="$t('apostrophe:selectPage')"
15
16
  >
16
17
  <option
@@ -25,6 +26,7 @@
25
26
  <AposButton
26
27
  :disabled="currentPage >= totalPages"
27
28
  class="apos-pager__btn"
29
+ :modifiers="['small']"
28
30
  type="outline" @click="incrementPage(1)"
29
31
  :icon-only="true" icon="chevron-right-icon"
30
32
  :label="nextButtonLabel"
@@ -94,13 +96,9 @@ export default {
94
96
  align-items: center;
95
97
  }
96
98
 
97
- .apos-input-wrapper {
98
- display: inline-flex;
99
- align-self: stretch;
100
- }
101
-
102
99
  .apos-input--select {
103
100
  background-color: transparent;
101
+ height: 32px;
104
102
  padding: 0 $spacing-double 0 $spacing-base;
105
103
  }
106
104
 
@@ -20,6 +20,7 @@ $input-color-disabled: var(--a-base-4);
20
20
 
21
21
  @mixin apos-primary-mixin($color) {
22
22
  --a-primary: #{$color};
23
+ --a-primary-transparent-10: #{transparentize($color, 0.1)};
23
24
  --a-primary-transparent-50: #{transparentize($color, 0.5)};
24
25
  --a-primary-transparent-10: #{transparentize($color, 0.9)};
25
26
  --a-primary-dark-10: #{mix(#000, $color, 10%)};
@@ -563,12 +563,11 @@ module.exports = {
563
563
  // Also see `warnDevOnce` which is less likely to irritate
564
564
  // the developer until they stop paying attention.
565
565
 
566
- warnDev(msg) {
566
+ warnDev(...args) {
567
567
  if (process.env.NODE_ENV === 'production') {
568
568
  return;
569
569
  }
570
- const args = [ '\n⚠️', ...arguments ];
571
- self.warn.apply(self, args);
570
+ self.warn(...[ '\n⚠️ ', ...args, '\n' ]);
572
571
  },
573
572
 
574
573
  // Identical to `apos.util.warnDev`, except that the warning is
@@ -578,7 +577,7 @@ module.exports = {
578
577
  // `--all-[name]` is present on the command line. You can
579
578
  // also suppress these with `--ignore-[name]`.
580
579
 
581
- warnDevOnce(name, msg) {
580
+ warnDevOnce(name, ...args) {
582
581
  const always = self.apos.argv[`all-${name}`];
583
582
  const hide = self.apos.argv[`ignore-${name}`];
584
583
  if (hide) {
@@ -588,13 +587,13 @@ module.exports = {
588
587
  return;
589
588
  }
590
589
  if (always || (!self.warnedDev[name])) {
591
- self.warn.apply(self, Array.prototype.slice.call(arguments, 1));
590
+ self.warnDev(...args);
592
591
  if (!always) {
593
592
  self.warnedDev[name] = true;
594
593
  self.info(stripIndent`
595
594
  This warning appears only once to save space. Pass --all-${name}
596
595
  on the command line to see the warning for all cases.
597
- `);
596
+ ` + '\n');
598
597
  }
599
598
  }
600
599
  },
@@ -75,6 +75,10 @@ export default () => {
75
75
  // `uploadProgress` (may be a function accepting `sent` and `total` parameters. May never be called. If
76
76
  // called, `sent` will be the bytes sent so far, and `total` will be the total bytes to be
77
77
  // sent. If the total is unknown, it will be `null`)
78
+ // `prefix`: If explicitly set to `false`, do not automatically prefix the URL,
79
+ // even if the site has a site-wide prefix or locale prefix.
80
+ // It can become handy when the given url is already prefixed,
81
+ // which is the case when using the document's computed `_url` field for instance.
78
82
  //
79
83
  // If the status code is >= 400 an error is thrown. The error object will be
80
84
  // similar to a `fullResponse` object, with a `status` property.
@@ -104,9 +108,8 @@ export default () => {
104
108
  });
105
109
  }
106
110
 
107
- if (apos.prefix) {
108
- // We don't need a prefix if the target URL is already prefixed,
109
- // which any absolute URL should be
111
+ if (apos.prefix && options.prefix !== false) {
112
+ // Prepend the prefix if the URL is absolute:
110
113
  if (url.substring(0, 1) === '/') {
111
114
  url = apos.prefix + url;
112
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.48.0",
3
+ "version": "3.50.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,20 +34,37 @@
34
34
  "@apostrophecms/vue-color": "^2.8.2",
35
35
  "@opentelemetry/api": "^1.0.4",
36
36
  "@opentelemetry/semantic-conventions": "^1.0.1",
37
+ "@tiptap/core": "^2.0.0-beta.220",
38
+ "@tiptap/extension-blockquote": "^2.0.0-beta.220",
39
+ "@tiptap/extension-bold": "^2.0.0-beta.220",
40
+ "@tiptap/extension-bullet-list": "^2.0.0-beta.220",
41
+ "@tiptap/extension-code": "^2.0.0-beta.220",
42
+ "@tiptap/extension-code-block": "^2.0.0-beta.220",
43
+ "@tiptap/extension-dropcursor": "^2.0.0-beta.220",
37
44
  "@tiptap/extension-floating-menu": "^2.0.0-beta.220",
45
+ "@tiptap/extension-gapcursor": "^2.0.0-beta.220",
46
+ "@tiptap/extension-hard-break": "^2.0.0-beta.220",
47
+ "@tiptap/extension-heading": "^2.0.0-beta.220",
38
48
  "@tiptap/extension-highlight": "^2.0.0-beta.220",
49
+ "@tiptap/extension-history": "^2.0.0-beta.220",
50
+ "@tiptap/extension-horizontal-rule": "^2.0.0-beta.220",
51
+ "@tiptap/extension-italic": "^2.0.0-beta.220",
39
52
  "@tiptap/extension-link": "^2.0.0-beta.220",
53
+ "@tiptap/extension-list-item": "^2.0.0-beta.220",
54
+ "@tiptap/extension-ordered-list": "^2.0.0-beta.220",
55
+ "@tiptap/extension-paragraph": "^2.0.0-beta.220",
40
56
  "@tiptap/extension-placeholder": "^2.0.0-beta.220",
57
+ "@tiptap/extension-strike": "^2.0.0-beta.220",
41
58
  "@tiptap/extension-subscript": "^2.0.0-beta.220",
42
59
  "@tiptap/extension-superscript": "^2.0.0-beta.220",
43
60
  "@tiptap/extension-table": "^2.0.0-beta.220",
44
61
  "@tiptap/extension-table-cell": "^2.0.0-beta.220",
45
62
  "@tiptap/extension-table-header": "^2.0.0-beta.220",
46
63
  "@tiptap/extension-table-row": "^2.0.0-beta.220",
64
+ "@tiptap/extension-text": "^2.0.0-beta.220",
47
65
  "@tiptap/extension-text-align": "^2.0.0-beta.220",
48
66
  "@tiptap/extension-text-style": "^2.0.0-beta.220",
49
67
  "@tiptap/extension-underline": "^2.0.0-beta.220",
50
- "@tiptap/starter-kit": "^2.0.0-beta.220",
51
68
  "@tiptap/vue-2": "^2.0.0-beta.220",
52
69
  "autoprefixer": "^10.4.1",
53
70
  "bluebird": "^3.7.2",
@@ -83,7 +100,7 @@
83
100
  "jsdom": "^22.0.0",
84
101
  "klona": "^2.0.4",
85
102
  "launder": "^1.4.0",
86
- "lodash": "^4.17.20",
103
+ "lodash": "^4.17.21",
87
104
  "mini-css-extract-plugin": "^1.6.0",
88
105
  "minimatch": "^3.0.4",
89
106
  "mkdirp": "^0.5.5",
@@ -0,0 +1,134 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+ let apos;
4
+ const articles = []; const categories = [];
5
+
6
+ describe('change-doc-ids', function() {
7
+
8
+ this.timeout(t.timeout);
9
+
10
+ after(function() {
11
+ return t.destroy(apos);
12
+ });
13
+
14
+ it('should initialize the apos object', async function() {
15
+ apos = await t.create({
16
+ root: module,
17
+ modules: {
18
+ article: {
19
+ extend: '@apostrophecms/piece-type',
20
+ options: {
21
+ alias: 'article'
22
+ },
23
+ fields: {
24
+ add: {
25
+ _categories: {
26
+ type: 'relationship',
27
+ withType: 'category'
28
+ }
29
+ }
30
+ }
31
+ },
32
+ category: {
33
+ extend: '@apostrophecms/piece-type',
34
+ options: {
35
+ alias: 'category'
36
+ }
37
+ },
38
+ 'default-page': {},
39
+ '@apostrophecms/page': {
40
+ options: {
41
+ park: [
42
+ {
43
+ slug: '/test',
44
+ type: 'default-page',
45
+ title: 'Test',
46
+ parkedId: 'test',
47
+ visibility: 'public',
48
+ _children: [
49
+ {
50
+ slug: '/test/child',
51
+ type: 'default-page',
52
+ title: 'Test Child',
53
+ parkedId: 'test-child',
54
+ visibility: 'public'
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ }
61
+ }
62
+ });
63
+ });
64
+
65
+ it('should insert categories', async function() {
66
+ for (let i = 0; (i < 10); i++) {
67
+ const category = apos.category.newInstance();
68
+ categories.push(await apos.category.insert(apos.task.getReq(), {
69
+ ...category,
70
+ title: 'Category ' + i,
71
+ slug: 'category-' + i
72
+ }));
73
+ }
74
+ });
75
+
76
+ it('should insert articles with relationships to categories', async function() {
77
+ for (let i = 0; (i < 10); i++) {
78
+ const article = apos.article.newInstance();
79
+ articles.push(await apos.article.insert(apos.task.getReq(), {
80
+ ...article,
81
+ title: 'Article ' + i,
82
+ slug: 'article-' + i,
83
+ _categories: [ categories[i % 4], categories[(i + 1) % 4] ]
84
+ }));
85
+ }
86
+ });
87
+
88
+ it('changeDocIds should work across a mix of pages and pieces', async function() {
89
+
90
+ await sanityCheck();
91
+
92
+ const pages = await apos.page.find(apos.task.getReq(), {}).toArray();
93
+ const test = pages.find(page => page.slug === '/test');
94
+ const newPageId = `new-test-page-id:${test.aposLocale}`;
95
+ const newCategoryId = `new-test-category-id:${categories[0].aposLocale}`;
96
+ const pairs = [
97
+ [ test._id, newPageId ],
98
+ [ categories[0]._id, newCategoryId ]
99
+ ];
100
+ await apos.doc.changeDocIds(pairs);
101
+
102
+ await sanityCheck(newPageId, newCategoryId);
103
+ });
104
+
105
+ });
106
+
107
+ async function sanityCheck(newPageId, newCategoryId) {
108
+ const pages = await apos.page.find(apos.task.getReq(), {}).children(true).toArray();
109
+ const test = pages.find(page => page.slug === '/test');
110
+ assert(test);
111
+ assert(test._children[0]);
112
+ assert(!test._children[1]);
113
+ assert.strictEqual(test._children[0].slug, '/test/child');
114
+ if (newPageId) {
115
+ assert.strictEqual(test._id, newPageId);
116
+ const newPageDocId = newPageId.replace(/:.+$/, '');
117
+ assert.strictEqual(test.aposDocId, newPageDocId);
118
+ assert(test.path.includes(newPageDocId));
119
+ assert(test._children[0].path.includes(newPageDocId));
120
+ }
121
+ const articles = await apos.article.find(apos.task.getReq(), {}).sort({ slug: 1 }).toArray();
122
+ assert.strictEqual(articles[0].title, 'Article 0');
123
+ assert(articles[0]._categories);
124
+ assert.strictEqual(articles[0]._categories.length, 2);
125
+ assert(articles[0]._categories.find(category => category.slug === 'category-0'));
126
+ assert(articles[0]._categories.find(category => category.slug === 'category-1'));
127
+ if (newCategoryId) {
128
+ assert.strictEqual(articles[0]._categories[0]._id, newCategoryId);
129
+ const newCategory = articles[0]._categories.find(category => category._id === newCategoryId);
130
+ assert(newCategory);
131
+ assert.strictEqual(newCategory._id, newCategoryId);
132
+ assert.strictEqual(newCategory.aposDocId, newCategoryId.replace(/:.+$/, ''));
133
+ }
134
+ }