apostrophe 3.66.0 → 3.67.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.67.0 (2024-06-12)
4
+
5
+ ### Changes
6
+
7
+ * When moving a page, recognize when the slug of a new child
8
+ already contains the new parent's slug and not double it.
9
+ For example, given we have two pages as children of the home page, page A and page B.
10
+ Page A and page B are siblings.
11
+ Page A has the slug `/peer` and page B has the slug `/peer/page`.
12
+ Now we want page B to be the child of page A.
13
+ We will now end up with page B slug as `/peer/page` and not `/peer/peer/page` as before.
14
+
15
+ ### Fixes
16
+
17
+ * Updating schema fields as read-only no longer reset the value when updating the document.
18
+
3
19
  ## 3.66.0 (2024-05-15)
4
20
 
5
21
  ### Fixes
@@ -7,8 +23,6 @@
7
23
  * Autocrop image attachments for referenced documents when replacing an image in the Media Manager.
8
24
  * Backports some internal A4 UI logic for metadata to make the new `document-versions` comparison feature compatible with A3.
9
25
 
10
- # Changelog
11
-
12
26
  ## 3.65.0 (2024-05-06)
13
27
 
14
28
  * Adds a `publicBundle` option to `@apostrophecms/asset`. When set to `false`, the `ui/src` public asset bundle is not built at all in most cases
@@ -1275,11 +1275,14 @@ database.`);
1275
1275
  if (parent._id !== oldParent._id) {
1276
1276
  const matchOldParentSlugPrefix = new RegExp('^' + self.apos.util.regExpQuote(self.apos.util.addSlashIfNeeded(oldParent.slug)));
1277
1277
  if (moved.slug.match(matchOldParentSlugPrefix)) {
1278
- let slugStem = parent.slug;
1279
- if (slugStem !== '/') {
1280
- slugStem += '/';
1281
- }
1282
- moved.slug = moved.slug.replace(matchOldParentSlugPrefix, self.apos.util.addSlashIfNeeded(parent.slug));
1278
+ const movedSlugCandidate = moved.slug
1279
+ .split('/')
1280
+ .slice(0, -1)
1281
+ .join('/');
1282
+
1283
+ moved.slug = parent.slug.endsWith(movedSlugCandidate)
1284
+ ? parent.slug.replace(movedSlugCandidate, '').concat(moved.slug)
1285
+ : moved.slug.replace(matchOldParentSlugPrefix, self.apos.util.addSlashIfNeeded(parent.slug));
1283
1286
  changed.push({
1284
1287
  _id: moved._id,
1285
1288
  slug: moved.slug
@@ -786,8 +786,12 @@ module.exports = (self) => {
786
786
  }
787
787
  const errors = [];
788
788
  for (const datum of data) {
789
- const result = {};
790
- result._id = self.apos.launder.id(datum._id) || self.apos.util.generateId();
789
+ const _id = self.apos.launder.id(datum._id) || self.apos.util.generateId();
790
+ const [ found ] = destination[field.name]?.filter?.(item => item._id === _id) || [];
791
+ const result = {
792
+ ...(found || {}),
793
+ _id
794
+ };
791
795
  result.metaType = 'arrayItem';
792
796
  result.scopedArrayName = field.scopedArrayName;
793
797
  try {
@@ -859,7 +863,7 @@ module.exports = (self) => {
859
863
  if (one[field.name].length !== two[field.name].length) {
860
864
  return false;
861
865
  }
862
- for (let i = 0; (i < one.length); i++) {
866
+ for (let i = 0; (i < one[field.name].length); i++) {
863
867
  if (!self.isEqual(req, field.schema, one[field.name][i], two[field.name][i])) {
864
868
  return false;
865
869
  }
@@ -876,6 +880,7 @@ module.exports = (self) => {
876
880
  const schema = field.schema;
877
881
  const errors = [];
878
882
  const result = {
883
+ ...(destination[field.name] || {}),
879
884
  _id: self.apos.launder.id(data && data._id) || self.apos.util.generateId()
880
885
  };
881
886
  if (data == null || typeof data !== 'object' || Array.isArray(data)) {
@@ -1027,7 +1032,7 @@ module.exports = (self) => {
1027
1032
  const result = results.find(doc => (doc._id === item._id));
1028
1033
  if (result) {
1029
1034
  if (field.schema) {
1030
- result._fields = {};
1035
+ result._fields = { ...(destination[field.name]?.find?.(doc => doc._id === item._id)?._fields || {}) };
1031
1036
  if (item && ((typeof item._fields === 'object'))) {
1032
1037
  await self.convert(req, field.schema, item._fields || {}, result._fields);
1033
1038
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.66.0",
3
+ "version": "3.67.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/pages.js CHANGED
@@ -457,6 +457,233 @@ describe('Pages', function() {
457
457
  assert.strictEqual(page.path, `${homeId.replace(':en:published', '')}/another-parent/parent/sibling`);
458
458
  });
459
459
 
460
+ describe('move peer pages', function () {
461
+ this.afterEach(async function() {
462
+ await apos.doc.db.deleteMany({
463
+ type: 'test-page'
464
+ });
465
+ });
466
+
467
+ it('moving /bar under /foo should wind up with /foo/bar', async function() {
468
+ const fooPage = await apos.page.insert(
469
+ apos.task.getReq(),
470
+ '_home',
471
+ 'lastChild',
472
+ {
473
+ slug: '/foo',
474
+ visibility: 'public',
475
+ type: 'test-page',
476
+ title: 'Foo Page'
477
+ }
478
+ );
479
+ const barPage = await apos.page.insert(
480
+ apos.task.getReq(),
481
+ '_home',
482
+ 'lastChild',
483
+ {
484
+ slug: '/bar',
485
+ visibility: 'public',
486
+ type: 'test-page',
487
+ title: 'Bar Page'
488
+ }
489
+ );
490
+ const childPage = await apos.page.insert(
491
+ apos.task.getReq(),
492
+ barPage._id,
493
+ 'lastChild',
494
+ {
495
+ slug: '/bar/child',
496
+ visibility: 'public',
497
+ type: 'test-page',
498
+ title: 'Child Page'
499
+ }
500
+ );
501
+
502
+ await apos.page.move(
503
+ apos.task.getReq(),
504
+ barPage._id,
505
+ fooPage._id,
506
+ 'lastChild'
507
+ );
508
+
509
+ const movedPage = await apos.page.find(apos.task.getAnonReq(), { _id: barPage._id }).toObject();
510
+ const movedChildPage = await apos.page.find(apos.task.getAnonReq(), { _id: childPage._id }).toObject();
511
+
512
+ const actual = {
513
+ bar: {
514
+ path: movedPage.path,
515
+ rank: movedPage.rank,
516
+ slug: movedPage.slug
517
+ },
518
+ child: {
519
+ path: movedChildPage.path,
520
+ rank: movedChildPage.rank,
521
+ slug: movedChildPage.slug
522
+ }
523
+ };
524
+ const expected = {
525
+ bar: {
526
+ path: fooPage.path.concat('/', barPage.aposDocId),
527
+ rank: 0,
528
+ slug: '/foo/bar'
529
+ },
530
+ child: {
531
+ path: movedPage.path.concat('/', childPage.aposDocId),
532
+ rank: 0,
533
+ slug: '/foo/bar/child'
534
+ }
535
+ };
536
+
537
+ assert.deepEqual(actual, expected);
538
+ });
539
+
540
+ it('moving peer /foo/bar under /foo should wind up with /foo/bar', async function() {
541
+ const fooPage = await apos.page.insert(
542
+ apos.task.getReq(),
543
+ '_home',
544
+ 'lastChild',
545
+ {
546
+ slug: '/foo',
547
+ visibility: 'public',
548
+ type: 'test-page',
549
+ title: 'Foo Page'
550
+ }
551
+ );
552
+ const barPage = await apos.page.insert(
553
+ apos.task.getReq(),
554
+ '_home',
555
+ 'lastChild',
556
+ {
557
+ slug: '/foo/bar',
558
+ visibility: 'public',
559
+ type: 'test-page',
560
+ title: 'Bar Page'
561
+ }
562
+ );
563
+ const childPage = await apos.page.insert(
564
+ apos.task.getReq(),
565
+ barPage._id,
566
+ 'lastChild',
567
+ {
568
+ slug: '/foo/bar/child',
569
+ visibility: 'public',
570
+ type: 'test-page',
571
+ title: 'Child Page'
572
+ }
573
+ );
574
+
575
+ await apos.page.move(
576
+ apos.task.getReq(),
577
+ barPage._id,
578
+ fooPage._id,
579
+ 'lastChild'
580
+ );
581
+
582
+ const movedPage = await apos.page.find(apos.task.getAnonReq(), { _id: barPage._id }).toObject();
583
+ const movedChildPage = await apos.page.find(apos.task.getAnonReq(), { _id: childPage._id }).toObject();
584
+
585
+ const actual = {
586
+ bar: {
587
+ path: movedPage.path,
588
+ rank: movedPage.rank,
589
+ slug: movedPage.slug
590
+ },
591
+ child: {
592
+ path: movedChildPage.path,
593
+ rank: movedChildPage.rank,
594
+ slug: movedChildPage.slug
595
+ }
596
+ };
597
+ const expected = {
598
+ bar: {
599
+ path: fooPage.path.concat('/', barPage.aposDocId),
600
+ rank: 0,
601
+ slug: '/foo/bar'
602
+ },
603
+ child: {
604
+ path: movedPage.path.concat('/', childPage.aposDocId),
605
+ rank: 0,
606
+ slug: '/foo/bar/child'
607
+ }
608
+ };
609
+
610
+ assert.deepEqual(actual, expected);
611
+ });
612
+
613
+ it('moving /foobar under /foo should wind up with /foo/foobar', async function() {
614
+ const fooPage = await apos.page.insert(
615
+ apos.task.getReq(),
616
+ '_home',
617
+ 'lastChild',
618
+ {
619
+ slug: '/foo',
620
+ visibility: 'public',
621
+ type: 'test-page',
622
+ title: 'Foo Page'
623
+ }
624
+ );
625
+ const foobarPage = await apos.page.insert(
626
+ apos.task.getReq(),
627
+ '_home',
628
+ 'lastChild',
629
+ {
630
+ slug: '/foobar',
631
+ visibility: 'public',
632
+ type: 'test-page',
633
+ title: 'Foobar Page'
634
+ }
635
+ );
636
+ const childPage = await apos.page.insert(
637
+ apos.task.getReq(),
638
+ foobarPage._id,
639
+ 'lastChild',
640
+ {
641
+ slug: '/foobar/child',
642
+ visibility: 'public',
643
+ type: 'test-page',
644
+ title: 'Child Page'
645
+ }
646
+ );
647
+
648
+ await apos.page.move(
649
+ apos.task.getReq(),
650
+ foobarPage._id,
651
+ fooPage._id,
652
+ 'lastChild'
653
+ );
654
+
655
+ const movedPage = await apos.page.find(apos.task.getAnonReq(), { _id: foobarPage._id }).toObject();
656
+ const movedChildPage = await apos.page.find(apos.task.getAnonReq(), { _id: childPage._id }).toObject();
657
+
658
+ const actual = {
659
+ foobar: {
660
+ path: movedPage.path,
661
+ rank: movedPage.rank,
662
+ slug: movedPage.slug
663
+ },
664
+ child: {
665
+ path: movedChildPage.path,
666
+ rank: movedChildPage.rank,
667
+ slug: movedChildPage.slug
668
+ }
669
+ };
670
+ const expected = {
671
+ foobar: {
672
+ path: fooPage.path.concat('/', foobarPage.aposDocId),
673
+ rank: 0,
674
+ slug: '/foo/foobar'
675
+ },
676
+ child: {
677
+ path: movedPage.path.concat('/', childPage.aposDocId),
678
+ rank: 0,
679
+ slug: '/foo/foobar/child'
680
+ }
681
+ };
682
+
683
+ assert.deepEqual(actual, expected);
684
+ });
685
+ });
686
+
460
687
  it('inferred page relationships are correct', async function() {
461
688
  const req = apos.task.getReq();
462
689
  const pages = await apos.page.find(req, {}).toArray();
package/test/schemas.js CHANGED
@@ -288,6 +288,44 @@ describe('Schemas', function() {
288
288
  }
289
289
  };
290
290
  }
291
+ },
292
+ article: {
293
+ extend: '@apostrophecms/piece-type',
294
+ options: {
295
+ alias: 'article'
296
+ },
297
+ fields(self) {
298
+ return {
299
+ add: {
300
+ title: {
301
+ label: '',
302
+ type: 'string',
303
+ required: true
304
+ },
305
+ area: {
306
+ label: 'Area',
307
+ type: 'area',
308
+ options: {
309
+ widgets: {
310
+ '@apostrophecms/rich-text': {}
311
+ }
312
+ }
313
+ },
314
+ array: {
315
+ label: 'Array',
316
+ type: 'array',
317
+ fields: {
318
+ add: {
319
+ arrayTitle: {
320
+ label: 'Array Title',
321
+ type: 'string'
322
+ }
323
+ }
324
+ }
325
+ }
326
+ }
327
+ };
328
+ }
291
329
  }
292
330
  }
293
331
  });
@@ -2263,6 +2301,290 @@ describe('Schemas', function() {
2263
2301
  assert(output.goodValue === '2022-05-09T22:36:00.000Z');
2264
2302
  });
2265
2303
 
2304
+ it('should compare two document properly with the method getChanges', async function() {
2305
+ const req = apos.task.getReq();
2306
+ const instance = apos.article.newInstance();
2307
+ const article1 = {
2308
+ ...instance,
2309
+ title: 'article 1',
2310
+ area: {
2311
+ _id: 'clrth36680007mnmd3jj7cta0',
2312
+ items: [
2313
+ {
2314
+ _id: 'clt79l48g001h2061j5ihxjkv',
2315
+ metaType: 'widget',
2316
+ type: '@apostrophecms/rich-text',
2317
+ aposPlaceholder: false,
2318
+ content: '<p>Some text here.</p>',
2319
+ permalinkIds: [],
2320
+ imageIds: []
2321
+ }
2322
+ ],
2323
+ metaType: 'area'
2324
+ },
2325
+ array: [
2326
+ {
2327
+ _id: 'clt79llm800242061v4dx9kv5',
2328
+ metaType: 'arrayItem',
2329
+ scopedArrayName: 'doc.article.array',
2330
+ arrayTitle: 'array title 1'
2331
+ },
2332
+ {
2333
+ _id: 'clt79llm800242061v4d47364',
2334
+ metaType: 'arrayItem',
2335
+ scopedArrayName: 'doc.article.array',
2336
+ arrayTitle: 'array title 2'
2337
+ }
2338
+ ]
2339
+ };
2340
+ const article2 = {
2341
+ ...article1,
2342
+ title: 'article 2'
2343
+ };
2344
+ const article3 = {
2345
+ ...instance,
2346
+ title: 'article 3',
2347
+
2348
+ area: {
2349
+ _id: 'clrth36680007mnmd3jj7cta0',
2350
+ items: [
2351
+ {
2352
+ _id: 'clt79l48g001h2061j5ihxjkv',
2353
+ metaType: 'widget',
2354
+ type: '@apostrophecms/rich-text',
2355
+ aposPlaceholder: false,
2356
+ content: '<p>Some text here changed.</p>',
2357
+ permalinkIds: [],
2358
+ imageIds: []
2359
+ }
2360
+ ],
2361
+ metaType: 'area'
2362
+ },
2363
+ array: [
2364
+ {
2365
+ _id: 'clt79llm800242061v4dx9kv5',
2366
+ metaType: 'arrayItem',
2367
+ scopedArrayName: 'doc.article.array',
2368
+ arrayTitle: 'array title 1 changed'
2369
+ },
2370
+ {
2371
+ _id: 'clt79llm800242061v4d47364',
2372
+ metaType: 'arrayItem',
2373
+ scopedArrayName: 'doc.article.array',
2374
+ arrayTitle: 'array title 2'
2375
+ }
2376
+ ]
2377
+ };
2378
+
2379
+ await apos.article.insert(req, article1);
2380
+ await apos.article.insert(req, article2);
2381
+ await apos.article.insert(req, article3);
2382
+
2383
+ const art1 = await apos.doc.db.findOne({ title: 'article 1' });
2384
+ const art2 = await apos.doc.db.findOne({ title: 'article 2' });
2385
+ const art3 = await apos.doc.db.findOne({ title: 'article 3' });
2386
+
2387
+ const changes11 = apos.schema.getChanges(req, apos.article.schema, art1, art1);
2388
+ const changes12 = apos.schema.getChanges(req, apos.article.schema, art1, art2);
2389
+ const changes23 = apos.schema.getChanges(req, apos.article.schema, art2, art3);
2390
+ const actual = {
2391
+ changes11,
2392
+ changes12,
2393
+ changes23
2394
+ };
2395
+ const expected = {
2396
+ changes11: [],
2397
+ changes12: [ 'title', 'slug' ],
2398
+ changes23: [ 'title', 'slug', 'area', 'array' ]
2399
+ };
2400
+
2401
+ assert.deepEqual(actual, expected);
2402
+ });
2403
+
2404
+ describe('field.readOnly with default value', function() {
2405
+ const givenSchema = [
2406
+ {
2407
+ name: 'title',
2408
+ type: 'string'
2409
+ },
2410
+ {
2411
+ name: 'array',
2412
+ type: 'array',
2413
+ schema: [
2414
+ {
2415
+ name: 'planet',
2416
+ type: 'string',
2417
+ def: 'Earth',
2418
+ readOnly: true
2419
+ },
2420
+ {
2421
+ name: 'moon',
2422
+ type: 'string'
2423
+ }
2424
+ ]
2425
+ },
2426
+ {
2427
+ name: 'object',
2428
+ type: 'object',
2429
+ schema: [
2430
+ {
2431
+ name: 'planet',
2432
+ type: 'string',
2433
+ def: 'Earth',
2434
+ readOnly: true
2435
+ },
2436
+ {
2437
+ name: 'moon',
2438
+ type: 'string'
2439
+ }
2440
+ ]
2441
+ },
2442
+ {
2443
+ name: '_relationship',
2444
+ type: 'relationship',
2445
+ limit: 1,
2446
+ withType: '@apostrophecms/any-page-type',
2447
+ label: 'Page Title',
2448
+ idsStorage: 'pageId',
2449
+ schema: [
2450
+ {
2451
+ name: 'planet',
2452
+ type: 'string',
2453
+ def: 'Earth',
2454
+ readOnly: true
2455
+ },
2456
+ {
2457
+ name: 'moon',
2458
+ type: 'string'
2459
+ }
2460
+ ]
2461
+ }
2462
+ ];
2463
+
2464
+ it('should keep read only values when editing a document', async function() {
2465
+ const req = apos.task.getReq();
2466
+ const schema = apos.schema.compose({
2467
+ addFields: givenSchema
2468
+ });
2469
+ const home = await apos.page.find(req, { slug: '/' }).toObject();
2470
+
2471
+ const data = {
2472
+ _relationship: [
2473
+ {
2474
+ ...home,
2475
+ _fields: {
2476
+ planet: 'Saturn',
2477
+ moon: 'Titan'
2478
+ }
2479
+ }
2480
+ ],
2481
+ array: [
2482
+ {
2483
+ _id: 'Jupiter-Io',
2484
+ moon: 'Io'
2485
+ },
2486
+ {
2487
+ _id: 'Mars-Phobos',
2488
+ moon: 'Phobos'
2489
+ }
2490
+ ],
2491
+ object: {
2492
+ _id: 'Neptune-Triton',
2493
+ moon: 'Triton'
2494
+ },
2495
+ pageId: [ home._id ],
2496
+ pageFields: {
2497
+ [home._id]: {
2498
+ planet: 'Saturn'
2499
+ }
2500
+ },
2501
+ title: 'Sol'
2502
+ };
2503
+ const destination = {
2504
+ _relationship: [
2505
+ {
2506
+ ...home,
2507
+ _fields: {
2508
+ planet: 'Saturn'
2509
+ }
2510
+ }
2511
+ ],
2512
+ array: [
2513
+ {
2514
+ _id: 'Jupiter-Io',
2515
+ planet: 'Jupiter'
2516
+ },
2517
+ {
2518
+ _id: 'Mars-Phobos',
2519
+ planet: 'Mars'
2520
+ }
2521
+ ],
2522
+ object: {
2523
+ _id: 'Neptune-Triton',
2524
+ planet: 'Neptune'
2525
+ },
2526
+ pageId: [ home._id ],
2527
+ pageFields: {
2528
+ [home._id]: {
2529
+ planet: 'Saturn'
2530
+ }
2531
+ },
2532
+ title: 'Default'
2533
+ };
2534
+ await apos.schema.convert(
2535
+ req,
2536
+ schema,
2537
+ data,
2538
+ destination
2539
+ );
2540
+
2541
+ const actual = destination;
2542
+ const expected = {
2543
+ _relationship: [
2544
+ {
2545
+ _fields: {
2546
+ planet: 'Saturn',
2547
+ moon: 'Titan'
2548
+ },
2549
+ ...home
2550
+ }
2551
+ ],
2552
+ array: [
2553
+ {
2554
+ _id: 'Jupiter-Io',
2555
+ metaType: 'arrayItem',
2556
+ moon: 'Io',
2557
+ planet: 'Jupiter',
2558
+ scopedArrayName: undefined
2559
+ },
2560
+ {
2561
+ _id: 'Mars-Phobos',
2562
+ metaType: 'arrayItem',
2563
+ moon: 'Phobos',
2564
+ planet: 'Mars',
2565
+ scopedArrayName: undefined
2566
+ }
2567
+ ],
2568
+ object: {
2569
+ _id: 'Neptune-Triton',
2570
+ metaType: 'objectItem',
2571
+ moon: 'Triton',
2572
+ planet: 'Neptune',
2573
+ scopedObjectName: undefined
2574
+ },
2575
+ pageId: [ home._id ],
2576
+ pageFields: {
2577
+ [home._id]: {
2578
+ planet: 'Saturn'
2579
+ }
2580
+ },
2581
+ title: 'Sol'
2582
+ };
2583
+
2584
+ assert.deepEqual(actual, expected);
2585
+ });
2586
+ });
2587
+
2266
2588
  describe('field editPermission|viewPermission', function() {
2267
2589
  const schema = [
2268
2590
  {