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/test/job.js CHANGED
@@ -1,10 +1,11 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
  const Promise = require('bluebird');
4
- let apos;
5
4
 
6
5
  describe('Job module', function() {
7
6
 
7
+ let apos;
8
+
8
9
  this.timeout(t.timeout);
9
10
 
10
11
  after(async function() {
@@ -182,43 +183,42 @@ describe('Job module', function() {
182
183
  // Tests failure()
183
184
  assert(bad === (articleIds.length / 2));
184
185
  });
185
- });
186
-
187
- function padInteger (i, places) {
188
- let s = i + '';
189
- while (s.length < places) {
190
- s = '0' + s;
186
+ function padInteger (i, places) {
187
+ let s = i + '';
188
+ while (s.length < places) {
189
+ s = '0' + s;
190
+ }
191
+ return s;
191
192
  }
192
- return s;
193
- }
194
-
195
- async function insert (req, pieceModule, title, data, i) {
196
- const docData = Object.assign(pieceModule.newInstance(), {
197
- title: `${title} #${padInteger(i, 5)}`,
198
- slug: `${title}-${padInteger(i, 5)}`,
199
- ...data
200
- });
201
193
 
202
- return pieceModule.insert(req, docData);
203
- };
204
-
205
- async function pollJob(job, { jar }) {
206
- const {
207
- processed,
208
- total,
209
- good,
210
- bad
211
- } = await apos.http.get(job.route, { jar });
194
+ async function insert (req, pieceModule, title, data, i) {
195
+ const docData = Object.assign(pieceModule.newInstance(), {
196
+ title: `${title} #${padInteger(i, 5)}`,
197
+ slug: `${title}-${padInteger(i, 5)}`,
198
+ ...data
199
+ });
212
200
 
213
- if (processed < total) {
214
- Promise.delay(100);
201
+ return pieceModule.insert(req, docData);
202
+ };
215
203
 
216
- return await pollJob(job, { jar });
217
- } else {
218
- return {
219
- completed: processed,
204
+ async function pollJob(job, { jar }) {
205
+ const {
206
+ processed,
207
+ total,
220
208
  good,
221
209
  bad
222
- };
210
+ } = await apos.http.get(job.route, { jar });
211
+
212
+ if (processed < total) {
213
+ Promise.delay(100);
214
+
215
+ return await pollJob(job, { jar });
216
+ } else {
217
+ return {
218
+ completed: processed,
219
+ good,
220
+ bad
221
+ };
222
+ }
223
223
  }
224
- }
224
+ });
package/test/locks.js CHANGED
@@ -2,10 +2,10 @@ const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
  const Promise = require('bluebird');
4
4
 
5
- let apos;
6
-
7
5
  describe('Locks', function() {
8
6
 
7
+ let apos;
8
+
9
9
  this.timeout(t.timeout);
10
10
 
11
11
  after(function() {
@@ -1,9 +1,10 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
 
4
- let apos;
5
-
6
4
  describe('Login', function() {
5
+
6
+ let apos;
7
+
7
8
  const extraSecretErr = 'extra secret incorrect';
8
9
  const captchaErr = 'captcha code incorrect';
9
10
 
@@ -1,10 +1,10 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
 
4
- let apos;
5
-
6
4
  describe('Middleware and Route Order', function() {
7
5
 
6
+ let apos;
7
+
8
8
  this.timeout(t.timeout);
9
9
 
10
10
  after(async function () {
package/test/page-type.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
- let apos;
4
3
 
5
4
  describe('page-type', function() {
6
5
 
6
+ let apos;
7
+
7
8
  this.timeout(t.timeout);
8
9
 
9
10
  after(async function() {
@@ -145,17 +145,6 @@ describe('Pages Autocomplete', function() {
145
145
  assert.equal(suggestions.results[0].slug, '/complex-page', 'complex page slug not found');
146
146
  }
147
147
  });
148
-
149
- it('should ignore type when no autocomplete', async function () {
150
- const result = await apos.http.get('/api/v1/@apostrophecms/page', {
151
- jar,
152
- qs: {
153
- type: 'test-page'
154
- }
155
- });
156
- assert.equal(result.slug, '/', 'home page not found');
157
- assert.equal(result._children.length, 2, 'children result does not match');
158
- });
159
148
  });
160
149
 
161
150
  async function login(apos) {
@@ -1,10 +1,10 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
 
4
- let apos;
5
-
6
4
  describe('Pages Public API', function() {
7
5
 
6
+ let apos;
7
+
8
8
  this.timeout(t.timeout);
9
9
 
10
10
  after(async function () {
@@ -1,10 +1,6 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
 
4
- let apos;
5
- let homeId;
6
- let jar;
7
-
8
4
  const areaConfig = {
9
5
  '@apostrophecms/image': {},
10
6
  '@apostrophecms/video': {},
@@ -38,6 +34,10 @@ const areaConfig = {
38
34
 
39
35
  describe('Pages REST', function() {
40
36
 
37
+ let apos;
38
+ let homeId;
39
+ let jar;
40
+
41
41
  this.timeout(t.timeout);
42
42
 
43
43
  after(function() {
package/test/pages.js CHANGED
@@ -2,21 +2,15 @@ const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
  const _ = require('lodash');
4
4
 
5
- let apos;
6
- let homeId;
7
- const apiKey = 'this is a test api key';
8
-
9
5
  describe('Pages', function() {
6
+ let apos;
7
+ let home;
8
+ let homeId;
9
+ const apiKey = 'this is a test api key';
10
10
 
11
11
  this.timeout(t.timeout);
12
12
 
13
- after(function() {
14
- return t.destroy(apos);
15
- });
16
-
17
- // EXISTENCE
18
-
19
- it('should be a property of the apos object', async function() {
13
+ before(async function() {
20
14
  apos = await t.create({
21
15
  root: module,
22
16
  modules: {
@@ -54,54 +48,9 @@ describe('Pages', function() {
54
48
  }
55
49
  });
56
50
 
57
- assert(apos.page.__meta.name === '@apostrophecms/page');
58
- });
59
-
60
- // SETUP
61
-
62
- it('should make sure all of the expected indexes are configured', async function() {
63
- const expectedIndexes = [ 'path' ];
64
- const actualIndexes = [];
65
-
66
- const info = await apos.doc.db.indexInformation();
67
-
68
- // Extract the actual index info we care about
69
- _.each(info, function(index) {
70
- actualIndexes.push(index[0][0]);
71
- });
72
-
73
- // Now make sure everything in expectedIndexes is in actualIndexes
74
- _.each(expectedIndexes, function(index) {
75
- assert(_.includes(actualIndexes, index));
76
- });
77
- });
78
-
79
- it('parked homepage exists', async function() {
80
- const home = await apos.page.find(apos.task.getAnonReq(), { level: 0 }).toObject();
81
-
82
- assert(home);
51
+ home = await apos.page.find(apos.task.getAnonReq(), { level: 0 }).toObject();
83
52
  homeId = home._id;
84
- assert(home.slug === '/');
85
- assert(`${home.path}:en:published` === home._id);
86
- assert(home.type === '@apostrophecms/home-page');
87
- assert(home.parked);
88
- assert(home.visibility === 'public');
89
- });
90
53
 
91
- it('parked archive page exists', async function() {
92
- const archive = await apos.page.find(apos.task.getReq(), { slug: '/archive' }).archived(null).toObject();
93
- assert(archive);
94
- assert(archive.slug === '/archive');
95
- assert(archive.path === `${homeId.replace(':en:published', '')}/${archive._id.replace(':en:published', '')}`);
96
- assert(archive.type === '@apostrophecms/archive-page');
97
- assert(archive.parked);
98
- // Verify that clonePermanent did its
99
- // job and removed properties not meant
100
- // to be stored in mongodb
101
- assert(!archive._children);
102
- });
103
-
104
- it('should be able to use db to insert documents', async function() {
105
54
  const testItems = [
106
55
  {
107
56
  _id: 'parent:en:published',
@@ -172,9 +121,11 @@ describe('Pages', function() {
172
121
  }
173
122
  ];
174
123
  // Insert draft versions too to match the A3 data model
124
+ const lastPublishedAt = new Date();
175
125
  const draftItems = await apos.doc.db.insertMany(testItems.map(item => ({
176
126
  ...item,
177
127
  aposLocale: item.aposLocale.replace(':published', ':draft'),
128
+ lastPublishedAt,
178
129
  _id: item._id.replace(':published', ':draft')
179
130
  })));
180
131
  assert(draftItems.result.ok === 1);
@@ -186,6 +137,57 @@ describe('Pages', function() {
186
137
  assert(items.insertedCount === 6);
187
138
  });
188
139
 
140
+ after(function() {
141
+ return t.destroy(apos);
142
+ });
143
+
144
+ // EXISTENCE
145
+
146
+ it('should be a property of the apos object', async function() {
147
+ assert(apos.page.__meta.name === '@apostrophecms/page');
148
+ });
149
+
150
+ // SETUP
151
+
152
+ it('should make sure all of the expected indexes are configured', async function() {
153
+ const expectedIndexes = [ 'path' ];
154
+ const actualIndexes = [];
155
+
156
+ const info = await apos.doc.db.indexInformation();
157
+
158
+ // Extract the actual index info we care about
159
+ _.each(info, function(index) {
160
+ actualIndexes.push(index[0][0]);
161
+ });
162
+
163
+ // Now make sure everything in expectedIndexes is in actualIndexes
164
+ _.each(expectedIndexes, function(index) {
165
+ assert(_.includes(actualIndexes, index));
166
+ });
167
+ });
168
+
169
+ it('parked homepage exists', async function() {
170
+ assert(home);
171
+ assert(home.slug === '/');
172
+ assert(`${home.path}:en:published` === home._id);
173
+ assert(home.type === '@apostrophecms/home-page');
174
+ assert(home.parked);
175
+ assert(home.visibility === 'public');
176
+ });
177
+
178
+ it('parked archive page exists', async function() {
179
+ const archive = await apos.page.find(apos.task.getReq(), { slug: '/archive' }).archived(null).toObject();
180
+ assert(archive);
181
+ assert(archive.slug === '/archive');
182
+ assert(archive.path === `${homeId.replace(':en:published', '')}/${archive._id.replace(':en:published', '')}`);
183
+ assert(archive.type === '@apostrophecms/archive-page');
184
+ assert(archive.parked);
185
+ // Verify that clonePermanent did its
186
+ // job and removed properties not meant
187
+ // to be stored in mongodb
188
+ assert(!archive._children);
189
+ });
190
+
189
191
  // FINDING
190
192
 
191
193
  it('should have a find method on pages that returns a cursor', async function() {
@@ -278,6 +280,55 @@ describe('Pages', function() {
278
280
  assert.strictEqual(page._ancestors[1]._children[1].path, `${homeId.replace(':en:published', '')}/parent/sibling`);
279
281
  });
280
282
 
283
+ it('should return pages from a specific type when type is provided', async function () {
284
+ const result = await apos.http.get('/api/v1/@apostrophecms/page', {
285
+ qs: {
286
+ type: 'test-page'
287
+ }
288
+ });
289
+
290
+ const expected = {
291
+ results: [
292
+ {
293
+ type: 'test-page',
294
+ slug: '/parent'
295
+ },
296
+ {
297
+ type: 'test-page',
298
+ slug: '/parent/child'
299
+ },
300
+ {
301
+ type: 'test-page',
302
+ slug: '/parent/child/grandchild'
303
+ },
304
+ {
305
+ type: 'test-page',
306
+ slug: '/parent/sibling'
307
+ },
308
+ {
309
+ type: 'test-page',
310
+ slug: '/parent/sibling/cousin'
311
+ },
312
+ {
313
+ type: 'test-page',
314
+ slug: '/another-parent'
315
+ }
316
+ ],
317
+ pages: 1,
318
+ currentPage: 1
319
+ };
320
+
321
+ const mappedResult = result.results.map(({ type, slug }) => ({
322
+ type,
323
+ slug
324
+ }));
325
+
326
+ assert.deepEqual({
327
+ ...result,
328
+ results: mappedResult
329
+ }, expected);
330
+ });
331
+
281
332
  // INSERTING
282
333
 
283
334
  it('is able to insert a new page', async function() {
@@ -1309,4 +1360,285 @@ describe('Pages', function() {
1309
1360
  });
1310
1361
  });
1311
1362
  });
1363
+
1364
+ describe('publish, move and draft', function () {
1365
+ beforeEach(async function() {
1366
+ await t.destroy(apos);
1367
+ apos = await t.create({
1368
+ root: module,
1369
+ modules: {
1370
+ '@apostrophecms/page': {
1371
+ options: {
1372
+ park: [],
1373
+ types: [
1374
+ {
1375
+ name: '@apostrophecms/home-page',
1376
+ label: 'Home'
1377
+ },
1378
+ {
1379
+ name: 'test-page',
1380
+ label: 'Test Page'
1381
+ }
1382
+ ]
1383
+ }
1384
+ },
1385
+ 'test-page': {
1386
+ extend: '@apostrophecms/page-type'
1387
+ }
1388
+ }
1389
+ });
1390
+ });
1391
+
1392
+ it('should publish and move published after draft doc', async function () {
1393
+ const req = apos.task.getReq({ mode: 'draft' });
1394
+
1395
+ const page1 = await apos.page.insert(req, '_home', 'lastChild', {
1396
+ title: 'Root First Page',
1397
+ type: 'test-page',
1398
+ slug: 'root-first-page'
1399
+ });
1400
+ const page2 = await apos.page.insert(req, '_home', 'lastChild', {
1401
+ title: 'Root Second Page',
1402
+ type: 'test-page',
1403
+ slug: 'root-second-page'
1404
+ });
1405
+ const page3 = await apos.page.insert(req, '_home', 'lastChild', {
1406
+ title: 'Root Third Page',
1407
+ type: 'test-page',
1408
+ slug: 'root-third-page'
1409
+ });
1410
+
1411
+ // Publish and assert.
1412
+ // Obviously, should not throw an error.
1413
+ await apos.page.publish(req, page3);
1414
+ {
1415
+ const pages = await apos.doc.db.find({
1416
+ type: 'test-page'
1417
+ }).sort({ rank: 1 }).toArray();
1418
+
1419
+ const p1Idx = pages.findIndex(p => p._id === page1._id);
1420
+ assert(p1Idx !== -1);
1421
+ const p2Idx = pages.findIndex(p => p._id === page2._id);
1422
+ assert(p2Idx !== -1);
1423
+ const p3Idx = pages.findIndex(p => p._id === page3._id);
1424
+ assert(p3Idx !== -1);
1425
+ const p3IdxPublished = pages.findIndex(p => p._id === page3._id.replace(':draft', ':published'));
1426
+ assert(p3IdxPublished !== -1);
1427
+
1428
+ // first, second, third/third-published
1429
+ assert(p1Idx < p2Idx);
1430
+ assert(p2Idx < p3Idx);
1431
+ assert(p2Idx < p3IdxPublished);
1432
+ }
1433
+
1434
+ // Move and assert.
1435
+ await apos.page.move(req, page3._id, page1._id, 'after');
1436
+ {
1437
+ const pages = await apos.doc.db.find({
1438
+ type: 'test-page'
1439
+ }).sort({ rank: 1 }).toArray();
1440
+
1441
+ const p1Idx = pages.findIndex(p => p._id === page1._id);
1442
+ assert(p1Idx !== -1);
1443
+ const p2Idx = pages.findIndex(p => p._id === page2._id);
1444
+ assert(p2Idx !== -1);
1445
+ const p3Idx = pages.findIndex(p => p._id === page3._id);
1446
+ assert(p3Idx !== -1);
1447
+ const p3IdxPublished = pages.findIndex(p => p._id === page3._id.replace(':draft', ':published'));
1448
+ assert(p3IdxPublished !== -1);
1449
+
1450
+ // first, third/third-published, second
1451
+ assert(p1Idx < p3Idx);
1452
+ assert(p1Idx < p3IdxPublished);
1453
+ assert(p3Idx < p2Idx);
1454
+ assert(p3IdxPublished < p2Idx);
1455
+ }
1456
+ });
1457
+
1458
+ it('should publish and move published after draft doc (nested)', async function () {
1459
+ const req = apos.task.getReq({ mode: 'draft' });
1460
+
1461
+ const root = await apos.page.insert(req, '_home', 'lastChild', {
1462
+ title: 'Root Page',
1463
+ type: 'test-page'
1464
+ });
1465
+ await apos.page.publish(req, root);
1466
+
1467
+ const page1 = await apos.page.insert(req, root._id, 'lastChild', {
1468
+ title: 'First Page',
1469
+ type: 'test-page'
1470
+ });
1471
+ const page2 = await apos.page.insert(req, root._id, 'lastChild', {
1472
+ title: 'Second Page',
1473
+ type: 'test-page'
1474
+ });
1475
+ const page3 = await apos.page.insert(req, root._id, 'lastChild', {
1476
+ title: 'Third Page',
1477
+ type: 'test-page'
1478
+ });
1479
+
1480
+ // Publish and assert.
1481
+ // Obviously, should not throw an error.
1482
+ await apos.page.publish(req, page3);
1483
+ {
1484
+ const pages = await apos.doc.db.find({
1485
+ type: 'test-page',
1486
+ level: 2
1487
+ }).sort({
1488
+ level: 1,
1489
+ rank: 1
1490
+ }).toArray();
1491
+
1492
+ const p1Idx = pages.findIndex(p => p._id === page1._id);
1493
+ assert(p1Idx !== -1);
1494
+ assert.equal(pages[p1Idx].path, `${root.path}/${page1.aposDocId}`);
1495
+ const p2Idx = pages.findIndex(p => p._id === page2._id);
1496
+ assert(p2Idx !== -1);
1497
+ assert.equal(pages[p2Idx].path, `${root.path}/${page2.aposDocId}`);
1498
+ const p3Idx = pages.findIndex(p => p._id === page3._id);
1499
+ assert(p3Idx !== -1);
1500
+ assert.equal(pages[p3Idx].path, `${root.path}/${page3.aposDocId}`);
1501
+ const p3IdxPublished = pages.findIndex(p => p._id === page3._id.replace(':draft', ':published'));
1502
+ assert(p3IdxPublished !== -1);
1503
+ assert.equal(pages[p3IdxPublished].path, `${root.path}/${page3.aposDocId}`);
1504
+
1505
+ // first, second, third/third-published
1506
+ assert(p1Idx < p2Idx);
1507
+ assert(p2Idx < p3Idx);
1508
+ assert(p2Idx < p3IdxPublished);
1509
+ }
1510
+
1511
+ // Move and assert.
1512
+ await apos.page.move(req, page3._id, page1._id, 'after');
1513
+ {
1514
+ const pages = await apos.doc.db.find({
1515
+ type: 'test-page',
1516
+ level: 2
1517
+ }).sort({
1518
+ level: 1,
1519
+ rank: 1
1520
+ }).toArray();
1521
+
1522
+ const p1Idx = pages.findIndex(p => p._id === page1._id);
1523
+ assert(p1Idx !== -1);
1524
+ const p2Idx = pages.findIndex(p => p._id === page2._id);
1525
+ assert(p2Idx !== -1);
1526
+ const p3Idx = pages.findIndex(p => p._id === page3._id);
1527
+ assert(p3Idx !== -1);
1528
+ const p3IdxPublished = pages.findIndex(p => p._id === page3._id.replace(':draft', ':published'));
1529
+ assert(p3IdxPublished !== -1);
1530
+
1531
+ // first, third/third-published, second
1532
+ assert(p1Idx < p3Idx);
1533
+ assert(p1Idx < p3IdxPublished);
1534
+ assert(p3Idx < p2Idx);
1535
+ assert(p3IdxPublished < p2Idx);
1536
+ }
1537
+
1538
+ // Publish the last draft and assert it's sorted right.
1539
+ await apos.page.publish(req, page2);
1540
+ {
1541
+ const pages = await apos.doc.db.find({
1542
+ type: 'test-page',
1543
+ level: 2
1544
+ }).sort({
1545
+ level: 1,
1546
+ rank: 1
1547
+ }).toArray();
1548
+
1549
+ const p1Idx = pages.findIndex(p => p._id === page1._id);
1550
+ assert(p1Idx !== -1);
1551
+ const p2Idx = pages.findIndex(p => p._id === page2._id);
1552
+ assert(p2Idx !== -1);
1553
+ const p2IdxPublished = pages.findIndex(p => p._id === page2._id.replace(':draft', ':published'));
1554
+ assert(p2IdxPublished !== -1);
1555
+ const p3Idx = pages.findIndex(p => p._id === page3._id);
1556
+ assert(p3Idx !== -1);
1557
+ const p3IdxPublished = pages.findIndex(p => p._id === page3._id.replace(':draft', ':published'));
1558
+ assert(p3IdxPublished !== -1);
1559
+
1560
+ // first, third/third-published, second
1561
+ assert(p1Idx < p3Idx);
1562
+ assert(p1Idx < p3IdxPublished);
1563
+ assert(p3Idx < p2Idx);
1564
+ assert(p3IdxPublished < p2IdxPublished);
1565
+ }
1566
+ });
1567
+
1568
+ it('should not be able to move published inside draft doc', async function () {
1569
+ const req = apos.task.getReq({ mode: 'draft' });
1570
+
1571
+ const root = await apos.page.insert(req, '_home', 'lastChild', {
1572
+ title: 'Root Page',
1573
+ type: 'test-page'
1574
+ });
1575
+
1576
+ const page1 = await apos.page.insert(req, '_home', 'lastChild', {
1577
+ title: 'First Page',
1578
+ type: 'test-page'
1579
+ });
1580
+ await apos.page.publish(req, page1);
1581
+
1582
+ await assert.rejects(apos.page.move(req, page1._id, root._id, 'firstChild'), {
1583
+ name: 'forbidden',
1584
+ message: 'Publish the parent page first.'
1585
+ });
1586
+ });
1587
+
1588
+ it('should not be able to publish inside draft doc', async function () {
1589
+ const req = apos.task.getReq({ mode: 'draft' });
1590
+
1591
+ const root = await apos.page.insert(req, '_home', 'lastChild', {
1592
+ title: 'Root Page',
1593
+ type: 'test-page'
1594
+ });
1595
+
1596
+ const page1 = await apos.page.insert(req, root._id, 'lastChild', {
1597
+ title: 'First Page',
1598
+ type: 'test-page'
1599
+ });
1600
+
1601
+ await assert.rejects(
1602
+ apos.page.publish(req, page1),
1603
+ (error) => {
1604
+ assert.equal(error.name, 'invalid');
1605
+ assert.equal(error.data?.unpublishedAncestors?.length, 1);
1606
+ assert.equal(error.data.unpublishedAncestors[0].title, 'Root Page');
1607
+ return true;
1608
+ }
1609
+ );
1610
+ });
1611
+
1612
+ it('should not be able to insert publish inside draft doc', async function () {
1613
+ const req = apos.task.getReq({ mode: 'draft' });
1614
+
1615
+ const root = await apos.page.insert(req, '_home', 'lastChild', {
1616
+ title: 'Root Page',
1617
+ type: 'test-page'
1618
+ });
1619
+
1620
+ // Publish directly via insert and req.mode = 'published'.
1621
+ // This can really be done only programmatically, but it's a valid use case.
1622
+ await assert.rejects(
1623
+ apos.page.insert(req.clone({ mode: 'published' }), root._id, 'lastChild', {
1624
+ title: 'First Page',
1625
+ type: 'test-page'
1626
+ }),
1627
+ {
1628
+ name: 'forbidden',
1629
+ message: 'Publish the parent page first.'
1630
+ }
1631
+ );
1632
+
1633
+ // IMPORTANT - assert the published page is gone, the draft is still there.
1634
+ const pages = await apos.doc.db.find({
1635
+ type: 'test-page'
1636
+ }).sort({ rank: 1 }).toArray();
1637
+ const draftPage = pages.find(p => p.title === 'First Page' && p.aposMode === 'draft');
1638
+ const publishedPage = pages.find(p => p.title === 'First Page' && p.aposMode === 'published');
1639
+ assert(draftPage);
1640
+ assert(draftPage.level === 2);
1641
+ assert.equal(typeof publishedPage, 'undefined');
1642
+ });
1643
+ });
1312
1644
  });