apostrophe 4.28.0 → 4.29.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 (88) hide show
  1. package/CHANGELOG.md +33 -3
  2. package/README.md +142 -0
  3. package/defaults.js +1 -0
  4. package/lib/safe-json-script.js +27 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +1 -1
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -0
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +3 -5
  8. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +13 -1
  9. package/modules/@apostrophecms/asset/lib/globalIcons.js +3 -0
  10. package/modules/@apostrophecms/attachment/index.js +43 -1
  11. package/modules/@apostrophecms/color-field/index.js +7 -1
  12. package/modules/@apostrophecms/doc/index.js +11 -1
  13. package/modules/@apostrophecms/doc-type/index.js +165 -32
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
  15. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +104 -59
  16. package/modules/@apostrophecms/file/index.js +109 -9
  17. package/modules/@apostrophecms/i18n/i18n/de.json +0 -2
  18. package/modules/@apostrophecms/i18n/i18n/en.json +40 -1
  19. package/modules/@apostrophecms/i18n/i18n/es.json +0 -1
  20. package/modules/@apostrophecms/i18n/i18n/fr.json +0 -1
  21. package/modules/@apostrophecms/i18n/i18n/it.json +0 -1
  22. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
  23. package/modules/@apostrophecms/i18n/i18n/sk.json +0 -1
  24. package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nBatchReporting.js +18 -1
  25. package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nLocalizeActions.js +50 -0
  26. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +56 -13
  27. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +8 -2
  28. package/modules/@apostrophecms/layout-column-widget/index.js +156 -163
  29. package/modules/@apostrophecms/layout-widget/index.js +7 -2
  30. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +6 -11
  31. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +3 -5
  32. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +4 -4
  33. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -16
  34. package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-state.mjs +7 -27
  35. package/modules/@apostrophecms/layout-widget/views/column.html +7 -9
  36. package/modules/@apostrophecms/login/index.js +39 -40
  37. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +17 -2
  38. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +3 -2
  39. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -0
  40. package/modules/@apostrophecms/page/index.js +2 -0
  41. package/modules/@apostrophecms/piece-type/index.js +3 -1
  42. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
  43. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +5 -0
  44. package/modules/@apostrophecms/recently-edited/index.js +831 -0
  45. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +54 -0
  46. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedCombo.vue +454 -0
  47. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilterTag.vue +75 -0
  48. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilters.vue +287 -0
  49. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +16 -0
  50. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedManager.vue +346 -0
  51. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedBatch.js +193 -0
  52. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedData.js +276 -0
  53. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFetch.js +199 -0
  54. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFilters.js +100 -0
  55. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +8 -4
  56. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +1 -1
  57. package/modules/@apostrophecms/styles/index.js +10 -0
  58. package/modules/@apostrophecms/styles/lib/apiRoutes.js +6 -0
  59. package/modules/@apostrophecms/styles/lib/handlers.js +5 -0
  60. package/modules/@apostrophecms/styles/lib/methods.js +9 -3
  61. package/modules/@apostrophecms/styles/lib/presets.js +119 -0
  62. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +3 -8
  63. package/modules/@apostrophecms/styles/ui/apos/composables/AposStyles.js +1 -3
  64. package/modules/@apostrophecms/styles/ui/apos/render-factory.js +29 -0
  65. package/modules/@apostrophecms/styles/ui/apos/universal/backgroundHelpers.mjs +140 -0
  66. package/modules/@apostrophecms/styles/ui/apos/universal/customRules.mjs +105 -0
  67. package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +195 -15
  68. package/modules/@apostrophecms/template/index.js +22 -6
  69. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +2 -0
  70. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +18 -4
  71. package/modules/@apostrophecms/ui/ui/apos/composables/useInfiniteScroll.js +91 -0
  72. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
  73. package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +5 -2
  74. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
  75. package/modules/@apostrophecms/url/index.js +38 -4
  76. package/modules/@apostrophecms/widget-type/index.js +22 -6
  77. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +8 -4
  78. package/package.json +19 -19
  79. package/test/files.js +129 -0
  80. package/test/layout-widget-migration.js +719 -0
  81. package/test/login-requirements.js +1 -1
  82. package/test/pieces-public-api.js +80 -0
  83. package/test/pieces.js +25 -0
  84. package/test/recently-edited.js +2311 -0
  85. package/test/schemas.js +39 -3
  86. package/test/static-build.js +642 -0
  87. package/test/styles.js +2569 -0
  88. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposLayoutColControlDialog.vue +0 -171
@@ -2698,4 +2698,646 @@ describe('Static Build Support', function () {
2698
2698
  });
2699
2699
  });
2700
2700
  });
2701
+
2702
+ describe('getBaseUrl with relative option', function () {
2703
+
2704
+ describe('no prefix configured', function () {
2705
+ let apos;
2706
+
2707
+ before(async function () {
2708
+ apos = await t.create({
2709
+ root: module,
2710
+ baseUrl: 'http://localhost:3000',
2711
+ staticBaseUrl: 'https://www.example.com',
2712
+ modules: {
2713
+ '@apostrophecms/url': {
2714
+ options: { static: true }
2715
+ }
2716
+ }
2717
+ });
2718
+ });
2719
+
2720
+ after(async function () {
2721
+ await t.destroy(apos);
2722
+ apos = null;
2723
+ });
2724
+
2725
+ it('returns empty string when relative is true and no prefix', function () {
2726
+ const req = apos.task.getAnonReq({ mode: 'published' });
2727
+ const result = apos.url.getBaseUrl(req, { relative: true });
2728
+ assert.strictEqual(result, '');
2729
+ });
2730
+
2731
+ it('returns origin-based URL when relative is false', function () {
2732
+ const req = apos.task.getAnonReq({ mode: 'published' });
2733
+ const result = apos.url.getBaseUrl(req, { relative: false });
2734
+ assert.strictEqual(result, 'http://localhost:3000');
2735
+ });
2736
+
2737
+ it('relative ignores staticBaseUrl', function () {
2738
+ const req = apos.task.getAnonReq({
2739
+ mode: 'published',
2740
+ staticBuild: true
2741
+ });
2742
+ const result = apos.url.getBaseUrl(req, { relative: true });
2743
+ assert.strictEqual(result, '');
2744
+ });
2745
+ });
2746
+
2747
+ describe('with prefix configured', function () {
2748
+ let apos;
2749
+
2750
+ before(async function () {
2751
+ apos = await t.create({
2752
+ root: module,
2753
+ baseUrl: 'http://localhost:3000',
2754
+ prefix: '/cms',
2755
+ modules: {
2756
+ '@apostrophecms/url': {
2757
+ options: { static: true }
2758
+ }
2759
+ }
2760
+ });
2761
+ });
2762
+
2763
+ after(async function () {
2764
+ await t.destroy(apos);
2765
+ apos = null;
2766
+ });
2767
+
2768
+ it('returns prefix when relative is true', function () {
2769
+ const req = apos.task.getAnonReq({ mode: 'published' });
2770
+ const result = apos.url.getBaseUrl(req, { relative: true });
2771
+ assert.strictEqual(result, '/cms');
2772
+ });
2773
+
2774
+ it('returns origin + prefix when relative is false', function () {
2775
+ const req = apos.task.getAnonReq({ mode: 'published' });
2776
+ const result = apos.url.getBaseUrl(req, { relative: false });
2777
+ assert.strictEqual(result, 'http://localhost:3000/cms');
2778
+ });
2779
+ });
2780
+ });
2781
+
2782
+ describe('Pretty URL file methods', function () {
2783
+
2784
+ describe('prettyUrls disabled (default)', function () {
2785
+ let apos;
2786
+
2787
+ before(async function () {
2788
+ apos = await t.create({
2789
+ root: module,
2790
+ modules: {
2791
+ '@apostrophecms/url': {
2792
+ options: { static: true }
2793
+ }
2794
+ }
2795
+ });
2796
+ });
2797
+
2798
+ after(async function () {
2799
+ await t.destroy(apos);
2800
+ apos = null;
2801
+ });
2802
+
2803
+ it('getPrettyUrlBase returns null', function () {
2804
+ const req = apos.task.getAnonReq({ mode: 'published' });
2805
+ assert.strictEqual(apos.file.getPrettyUrlBase(req), null);
2806
+ });
2807
+
2808
+ it('getPrettyPath returns null', function () {
2809
+ assert.strictEqual(
2810
+ apos.file.getPrettyPath({
2811
+ slug: 'file-test',
2812
+ attachment: { extension: 'pdf' }
2813
+ }),
2814
+ null
2815
+ );
2816
+ });
2817
+
2818
+ it('applyPrettyUrlPaths is a no-op', async function () {
2819
+ const req = apos.task.getAnonReq({ mode: 'published' });
2820
+ const meta = {
2821
+ uploadsUrl: '/uploads',
2822
+ results: [
2823
+ {
2824
+ _id: 'att-1',
2825
+ urls: [ { path: '/attachments/original.pdf' } ]
2826
+ }
2827
+ ]
2828
+ };
2829
+ await apos.file.applyPrettyUrlPaths(req, meta);
2830
+ // Should remain unchanged
2831
+ assert.strictEqual(meta.results[0].urls[0].path, '/attachments/original.pdf');
2832
+ assert.strictEqual(meta.results[0].base, undefined);
2833
+ });
2834
+ });
2835
+
2836
+ describe('prettyUrls enabled, no prefix', function () {
2837
+ let apos;
2838
+
2839
+ before(async function () {
2840
+ apos = await t.create({
2841
+ root: module,
2842
+ baseUrl: 'http://localhost:3000',
2843
+ modules: {
2844
+ '@apostrophecms/url': {
2845
+ options: { static: true }
2846
+ },
2847
+ '@apostrophecms/file': {
2848
+ options: {
2849
+ prettyUrls: true
2850
+ }
2851
+ }
2852
+ }
2853
+ });
2854
+ });
2855
+
2856
+ after(async function () {
2857
+ await t.destroy(apos);
2858
+ apos = null;
2859
+ });
2860
+
2861
+ it('getPrettyUrlBase returns base with prettyUrlDir', function () {
2862
+ const req = apos.task.getAnonReq({ mode: 'published' });
2863
+ assert.strictEqual(
2864
+ apos.file.getPrettyUrlBase(req),
2865
+ 'http://localhost:3000/files'
2866
+ );
2867
+ });
2868
+
2869
+ it('getPrettyUrlBase with relative returns just prettyUrlDir', function () {
2870
+ const req = apos.task.getAnonReq({ mode: 'published' });
2871
+ assert.strictEqual(
2872
+ apos.file.getPrettyUrlBase(req, { relative: true }),
2873
+ '/files'
2874
+ );
2875
+ });
2876
+
2877
+ it('getPrettyPath returns slug-based path', function () {
2878
+ const doc = {
2879
+ slug: 'file-my-document',
2880
+ attachment: { extension: 'pdf' }
2881
+ };
2882
+ assert.strictEqual(apos.file.getPrettyPath(doc), '/my-document.pdf');
2883
+ });
2884
+
2885
+ it('getPrettyPath strips slugPrefix', function () {
2886
+ const doc = {
2887
+ slug: 'file-design-principles',
2888
+ attachment: { extension: 'pdf' }
2889
+ };
2890
+ assert.strictEqual(apos.file.getPrettyPath(doc), '/design-principles.pdf');
2891
+ });
2892
+
2893
+ it('getPrettyPath returns null when no attachment', function () {
2894
+ assert.strictEqual(apos.file.getPrettyPath({ slug: 'file-test' }), null);
2895
+ });
2896
+ });
2897
+
2898
+ describe('prettyUrls enabled with prefix', function () {
2899
+ let apos;
2900
+
2901
+ before(async function () {
2902
+ apos = await t.create({
2903
+ root: module,
2904
+ baseUrl: 'http://localhost:3000',
2905
+ prefix: '/cms',
2906
+ modules: {
2907
+ '@apostrophecms/url': {
2908
+ options: { static: true }
2909
+ },
2910
+ '@apostrophecms/file': {
2911
+ options: {
2912
+ prettyUrls: true
2913
+ }
2914
+ }
2915
+ }
2916
+ });
2917
+ });
2918
+
2919
+ after(async function () {
2920
+ await t.destroy(apos);
2921
+ apos = null;
2922
+ });
2923
+
2924
+ it('getPrettyUrlBase includes prefix', function () {
2925
+ const req = apos.task.getAnonReq({ mode: 'published' });
2926
+ assert.strictEqual(
2927
+ apos.file.getPrettyUrlBase(req),
2928
+ 'http://localhost:3000/cms/files'
2929
+ );
2930
+ });
2931
+
2932
+ it('getPrettyUrlBase with relative includes prefix', function () {
2933
+ const req = apos.task.getAnonReq({ mode: 'published' });
2934
+ assert.strictEqual(
2935
+ apos.file.getPrettyUrlBase(req, { relative: true }),
2936
+ '/cms/files'
2937
+ );
2938
+ });
2939
+ });
2940
+ });
2941
+
2942
+ describe('applyPrettyUrlPaths', function () {
2943
+ let apos;
2944
+
2945
+ before(async function () {
2946
+ apos = await t.create({
2947
+ root: module,
2948
+ modules: {
2949
+ '@apostrophecms/url': {
2950
+ options: { static: true }
2951
+ },
2952
+ '@apostrophecms/file': {
2953
+ options: {
2954
+ prettyUrls: true
2955
+ }
2956
+ }
2957
+ }
2958
+ });
2959
+
2960
+ const req = apos.task.getReq();
2961
+
2962
+ // Insert a file piece with a known attachment.
2963
+ // We seed the attachment directly in the DB to avoid
2964
+ // needing real uploaded files.
2965
+ const file = await apos.file.insert(req, {
2966
+ title: 'Design Principles',
2967
+ visibility: 'public',
2968
+ attachment: {
2969
+ _id: 'att-pretty-1',
2970
+ name: 'design-principles',
2971
+ extension: 'pdf',
2972
+ group: 'office',
2973
+ type: 'attachment'
2974
+ }
2975
+ });
2976
+
2977
+ // Also seed the attachment record so getStaticMetadata
2978
+ // can find it.
2979
+ await apos.attachment.db.insertOne({
2980
+ _id: 'att-pretty-1',
2981
+ name: 'design-principles',
2982
+ extension: 'pdf',
2983
+ group: 'office',
2984
+ archived: false,
2985
+ docIds: [ `${file.aposDocId}:en:published` ],
2986
+ crops: [],
2987
+ used: true,
2988
+ utilized: true
2989
+ });
2990
+
2991
+ // Insert a second file.
2992
+ const file2 = await apos.file.insert(req, {
2993
+ title: 'Annual Report',
2994
+ visibility: 'public',
2995
+ attachment: {
2996
+ _id: 'att-pretty-2',
2997
+ name: 'annual-report',
2998
+ extension: 'pdf',
2999
+ group: 'office',
3000
+ type: 'attachment'
3001
+ }
3002
+ });
3003
+
3004
+ await apos.attachment.db.insertOne({
3005
+ _id: 'att-pretty-2',
3006
+ name: 'annual-report',
3007
+ extension: 'pdf',
3008
+ group: 'office',
3009
+ archived: false,
3010
+ docIds: [ `${file2.aposDocId}:en:published` ],
3011
+ crops: [],
3012
+ used: true,
3013
+ utilized: true
3014
+ });
3015
+
3016
+ // Seed a non-file attachment (image) that should NOT
3017
+ // be touched by applyPrettyUrlPaths.
3018
+ await apos.attachment.db.insertOne({
3019
+ _id: 'att-image-notouch',
3020
+ name: 'photo',
3021
+ extension: 'jpg',
3022
+ group: 'images',
3023
+ width: 800,
3024
+ height: 600,
3025
+ archived: false,
3026
+ docIds: [ 'some-img:en:published' ],
3027
+ crops: [],
3028
+ used: true,
3029
+ utilized: true
3030
+ });
3031
+ });
3032
+
3033
+ after(async function () {
3034
+ await t.destroy(apos);
3035
+ apos = null;
3036
+ });
3037
+
3038
+ it('mutates matching entries with base and pretty urls', async function () {
3039
+ const req = apos.task.getAnonReq({ mode: 'published' });
3040
+ const meta = {
3041
+ uploadsUrl: '/uploads',
3042
+ results: [
3043
+ {
3044
+ _id: 'att-pretty-1',
3045
+ urls: [ { path: '/attachments/design-principles.pdf' } ]
3046
+ },
3047
+ {
3048
+ _id: 'att-image-notouch',
3049
+ urls: [
3050
+ {
3051
+ size: 'full',
3052
+ path: '/attachments/photo-full.jpg'
3053
+ },
3054
+ {
3055
+ size: 'one-half',
3056
+ path: '/attachments/photo-one-half.jpg'
3057
+ }
3058
+ ]
3059
+ },
3060
+ {
3061
+ _id: 'att-pretty-2',
3062
+ urls: [ { path: '/attachments/annual-report.pdf' } ]
3063
+ }
3064
+ ]
3065
+ };
3066
+
3067
+ await apos.file.applyPrettyUrlPaths(req, meta);
3068
+
3069
+ // File attachment should be mutated
3070
+ const pretty1 = meta.results.find(a => a._id === 'att-pretty-1');
3071
+ assert.strictEqual(pretty1.base, '/files');
3072
+ assert.strictEqual(pretty1.urls.length, 1);
3073
+ assert.strictEqual(pretty1.urls[0].path, '/design-principles.pdf');
3074
+ assert.strictEqual(pretty1.urls[0].size, undefined);
3075
+
3076
+ // Second file attachment should also be mutated
3077
+ const pretty2 = meta.results.find(a => a._id === 'att-pretty-2');
3078
+ assert.strictEqual(pretty2.base, '/files');
3079
+ assert.strictEqual(pretty2.urls.length, 1);
3080
+ assert.strictEqual(pretty2.urls[0].path, '/annual-report.pdf');
3081
+
3082
+ // Image attachment should be untouched
3083
+ const img = meta.results.find(a => a._id === 'att-image-notouch');
3084
+ assert.strictEqual(img.base, undefined);
3085
+ assert.strictEqual(img.urls.length, 2);
3086
+ assert.strictEqual(img.urls[0].size, 'full');
3087
+ });
3088
+
3089
+ it('handles empty results gracefully', async function () {
3090
+ const req = apos.task.getAnonReq({ mode: 'published' });
3091
+ const meta = {
3092
+ uploadsUrl: '/uploads',
3093
+ results: []
3094
+ };
3095
+ await apos.file.applyPrettyUrlPaths(req, meta);
3096
+ assert.strictEqual(meta.results.length, 0);
3097
+ });
3098
+
3099
+ it('handles null attachmentMeta gracefully', async function () {
3100
+ const req = apos.task.getAnonReq({ mode: 'published' });
3101
+ // Should not throw
3102
+ await apos.file.applyPrettyUrlPaths(req, null);
3103
+ });
3104
+
3105
+ it('does not touch entries whose _id has no matching file doc', async function () {
3106
+ const req = apos.task.getAnonReq({ mode: 'published' });
3107
+ const meta = {
3108
+ uploadsUrl: '/uploads',
3109
+ results: [
3110
+ {
3111
+ _id: 'att-nonexistent',
3112
+ urls: [ { path: '/attachments/ghost.pdf' } ]
3113
+ }
3114
+ ]
3115
+ };
3116
+ await apos.file.applyPrettyUrlPaths(req, meta);
3117
+ const entry = meta.results[0];
3118
+ assert.strictEqual(entry.base, undefined);
3119
+ assert.strictEqual(entry.urls[0].path, '/attachments/ghost.pdf');
3120
+ });
3121
+ });
3122
+
3123
+ describe('getAllUrlMetadata with prettyUrls', function () {
3124
+ let apos;
3125
+
3126
+ before(async function () {
3127
+ apos = await t.create({
3128
+ root: module,
3129
+ modules: {
3130
+ '@apostrophecms/url': {
3131
+ options: { static: true }
3132
+ },
3133
+ '@apostrophecms/file': {
3134
+ options: {
3135
+ prettyUrls: true
3136
+ }
3137
+ },
3138
+ article: {
3139
+ extend: '@apostrophecms/piece-type',
3140
+ options: {
3141
+ name: 'article',
3142
+ label: 'Article',
3143
+ alias: 'article'
3144
+ },
3145
+ fields: {
3146
+ add: {
3147
+ _file: {
3148
+ type: 'relationship',
3149
+ withType: '@apostrophecms/file',
3150
+ label: 'File',
3151
+ max: 1
3152
+ }
3153
+ }
3154
+ }
3155
+ },
3156
+ 'article-page': {
3157
+ extend: '@apostrophecms/piece-page-type',
3158
+ options: {
3159
+ name: 'articlePage',
3160
+ label: 'Articles',
3161
+ alias: 'articlePage',
3162
+ perPage: 10
3163
+ }
3164
+ },
3165
+ '@apostrophecms/page': {
3166
+ options: {
3167
+ park: [
3168
+ {
3169
+ title: 'Articles',
3170
+ type: 'articlePage',
3171
+ slug: '/articles',
3172
+ parkedId: 'articles'
3173
+ }
3174
+ ]
3175
+ }
3176
+ }
3177
+ }
3178
+ });
3179
+
3180
+ const req = apos.task.getReq();
3181
+
3182
+ // Insert a file piece
3183
+ const file = await apos.file.insert(req, {
3184
+ title: 'Spec Document',
3185
+ visibility: 'public',
3186
+ attachment: {
3187
+ _id: 'att-spec-pdf',
3188
+ name: 'spec-document',
3189
+ extension: 'pdf',
3190
+ group: 'office',
3191
+ type: 'attachment'
3192
+ }
3193
+ });
3194
+
3195
+ // Seed attachment record
3196
+ await apos.attachment.db.insertOne({
3197
+ _id: 'att-spec-pdf',
3198
+ name: 'spec-document',
3199
+ extension: 'pdf',
3200
+ group: 'office',
3201
+ archived: false,
3202
+ docIds: [ `${file.aposDocId}:en:published` ],
3203
+ crops: [],
3204
+ used: true,
3205
+ utilized: true
3206
+ });
3207
+
3208
+ // Insert an article referencing the file
3209
+ const article = await apos.article.insert(req, {
3210
+ title: 'Article With File',
3211
+ visibility: 'public'
3212
+ });
3213
+
3214
+ // Wire the relationship via idsStorage
3215
+ await apos.doc.db.updateMany(
3216
+ { aposDocId: article.aposDocId },
3217
+ { $set: { fileIds: [ file.aposDocId ] } }
3218
+ );
3219
+ });
3220
+
3221
+ after(async function () {
3222
+ await t.destroy(apos);
3223
+ apos = null;
3224
+ });
3225
+
3226
+ it('attachment entries with pretty URLs should have base property', async function () {
3227
+ const req = apos.task.getAnonReq({ mode: 'published' });
3228
+ const result = await apos.url.getAllUrlMetadata(req, {
3229
+ attachments: { scope: 'used' }
3230
+ });
3231
+ assert(result.attachments);
3232
+ const pdfAtt = result.attachments.results.find(a => a._id === 'att-spec-pdf');
3233
+ assert(pdfAtt, 'Should find the pretty URL attachment');
3234
+ assert.strictEqual(pdfAtt.base, '/files');
3235
+ assert.strictEqual(pdfAtt.urls.length, 1);
3236
+ assert(
3237
+ pdfAtt.urls[0].path.endsWith('.pdf'),
3238
+ 'Pretty URL path should end with .pdf'
3239
+ );
3240
+ assert(
3241
+ !pdfAtt.urls[0].path.includes('/attachments/'),
3242
+ 'Pretty URL path should not contain /attachments/'
3243
+ );
3244
+ });
3245
+
3246
+ it('uploadsUrl is unchanged by applyPrettyUrlPaths', async function () {
3247
+ const req = apos.task.getAnonReq({ mode: 'published' });
3248
+ const result = await apos.url.getAllUrlMetadata(req, {
3249
+ attachments: { scope: 'used' }
3250
+ });
3251
+ assert(typeof result.attachments.uploadsUrl === 'string');
3252
+ assert(
3253
+ result.attachments.uploadsUrl.includes('/uploads'),
3254
+ 'uploadsUrl should still reference /uploads'
3255
+ );
3256
+ });
3257
+
3258
+ it('non-file attachments should not have base property', async function () {
3259
+ // Seed a regular image attachment
3260
+ await apos.attachment.db.insertOne({
3261
+ _id: 'att-regular-img',
3262
+ name: 'regular-photo',
3263
+ extension: 'jpg',
3264
+ group: 'images',
3265
+ width: 400,
3266
+ height: 300,
3267
+ archived: false,
3268
+ docIds: [],
3269
+ crops: [],
3270
+ used: true,
3271
+ utilized: true
3272
+ });
3273
+
3274
+ const req = apos.task.getAnonReq({ mode: 'published' });
3275
+ const result = await apos.url.getAllUrlMetadata(req, {
3276
+ attachments: { scope: 'all' }
3277
+ });
3278
+ const imgAtt = result.attachments.results.find(a => a._id === 'att-regular-img');
3279
+ assert(imgAtt, 'Should find the regular image attachment');
3280
+ assert.strictEqual(imgAtt.base, undefined, 'Regular attachment should not have base');
3281
+ assert(imgAtt.urls.length > 1, 'Image should have multiple size variants');
3282
+ });
3283
+ });
3284
+
3285
+ describe('addUrls with prettyUrls', function () {
3286
+ let apos;
3287
+
3288
+ before(async function () {
3289
+ apos = await t.create({
3290
+ root: module,
3291
+ baseUrl: 'http://localhost:3000',
3292
+ modules: {
3293
+ '@apostrophecms/file': {
3294
+ options: {
3295
+ prettyUrls: true
3296
+ }
3297
+ }
3298
+ }
3299
+ });
3300
+ });
3301
+
3302
+ after(async function () {
3303
+ await t.destroy(apos);
3304
+ apos = null;
3305
+ });
3306
+
3307
+ it('sets _url to pretty URL on file docs', function () {
3308
+ const req = apos.task.getAnonReq({ mode: 'published' });
3309
+ const files = [
3310
+ {
3311
+ slug: 'file-my-document',
3312
+ attachment: { extension: 'pdf' }
3313
+ }
3314
+ ];
3315
+ apos.file.addUrls(req, files);
3316
+ assert.strictEqual(files[0]._url, 'http://localhost:3000/files/my-document.pdf');
3317
+ });
3318
+
3319
+ it('sets attachment._prettyUrl', function () {
3320
+ const req = apos.task.getAnonReq({ mode: 'published' });
3321
+ const files = [
3322
+ {
3323
+ slug: 'file-report',
3324
+ attachment: { extension: 'pdf' }
3325
+ }
3326
+ ];
3327
+ apos.file.addUrls(req, files);
3328
+ assert.strictEqual(files[0].attachment._prettyUrl, 'http://localhost:3000/files/report.pdf');
3329
+ });
3330
+
3331
+ it('uses relative URL when relative option is true', function () {
3332
+ const req = apos.task.getAnonReq({ mode: 'published' });
3333
+ const files = [
3334
+ {
3335
+ slug: 'file-guide',
3336
+ attachment: { extension: 'pdf' }
3337
+ }
3338
+ ];
3339
+ apos.file.addUrls(req, files, { relative: true });
3340
+ assert.strictEqual(files[0]._url, '/files/guide.pdf');
3341
+ });
3342
+ });
2701
3343
  });