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
@@ -174,7 +174,10 @@ module.exports = {
174
174
  origin: null,
175
175
  preview: true,
176
176
  // Set to false to opt out of the automatic styling wrapping of widget output
177
- stylesWrapper: true
177
+ stylesWrapper: true,
178
+ // If set to true, the widget editor will hide tabs when there is only one,
179
+ // even if groups are defined. Defaults to false for backwards compatibility.
180
+ hideSingleTab: false
178
181
  },
179
182
  init(self) {
180
183
  self.isExplicitOrigin = self.options.origin !== null;
@@ -306,14 +309,20 @@ module.exports = {
306
309
  label: 'apostrophe:styles',
307
310
  fields: groupFields
308
311
  };
309
- // Create a default group if none exist
312
+ // Create a default group if none exist.
313
+ // Ignore utility group if it's the only one, since it's now the way
314
+ // to hide fields (see layout column widget).
315
+ const visibleFields = Object.keys(self.fields)
316
+ .filter(
317
+ fieldName => !self.fieldsGroups.utility?.fields?.includes(fieldName)
318
+ );
310
319
  if (
311
- !Object.keys(self.fieldsGroups).length &&
312
- Object.keys(self.fields).length
320
+ !Object.keys(self.fieldsGroups).filter(g => g !== 'utility').length &&
321
+ visibleFields.length
313
322
  ) {
314
323
  self.fieldsGroups.basics = {
315
324
  label: 'apostrophe:basics',
316
- fields: Object.keys(self.fields)
325
+ fields: visibleFields
317
326
  };
318
327
  }
319
328
  self.fieldsGroups.styles = stylesGroup;
@@ -530,6 +539,12 @@ module.exports = {
530
539
  // then feeding it our widgets as if they were docs.
531
540
 
532
541
  if (!(widgets.length && widgets[0]._virtual)) {
542
+ // For non-virtual widgets whose loading was deferred,
543
+ // relationships were just populated above but the page-level
544
+ // `attachments` query builder already ran before widget
545
+ // loading occurred. Re-annotate so that attachment `_urls`
546
+ // reflect crop/focal-point data from relationship `_fields`.
547
+ self.apos.attachment.all(widgets, { annotate: true });
533
548
  return;
534
549
  }
535
550
 
@@ -772,7 +787,8 @@ module.exports = {
772
787
  preview: self.options.preview,
773
788
  isExplicitOrigin: self.isExplicitOrigin,
774
789
  widgetOperations: self.getWidgetOperations(req),
775
- widgetBreadcrumbOperations: self.getWidgetBreadcrumbOperations(req)
790
+ widgetBreadcrumbOperations: self.getWidgetBreadcrumbOperations(req),
791
+ hideSingleTab: self.options.hideSingleTab
776
792
  });
777
793
  return result;
778
794
  }
@@ -56,7 +56,7 @@
56
56
  :items="breadcrumbs"
57
57
  />
58
58
  <AposWidgetModalTabs
59
- v-if="tabs.length && tabs[0].name !== 'ungrouped'"
59
+ v-if="shouldShowTabs"
60
60
  :key="tabKey"
61
61
  :current="currentTab"
62
62
  :tabs="tabs"
@@ -120,8 +120,7 @@ import AposModalTabsMixin from 'Modules/@apostrophecms/modal/mixins/AposModalTab
120
120
  import { detectDocChange } from 'Modules/@apostrophecms/schema/lib/detectChange';
121
121
  import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
122
122
  import { debounceAsync } from 'Modules/@apostrophecms/ui/utils';
123
- import { renderScopedStyles } from 'Modules/@apostrophecms/styles/universal/render.mjs';
124
- import checkIfConditions from 'apostrophe/lib/universal/check-if-conditions.mjs';
123
+ import { renderScopedStyles } from 'Modules/@apostrophecms/styles/render-factory.js';
125
124
  import breakpointPreviewTransformer from 'postcss-viewport-to-container-toggle/standalone.js';
126
125
  import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
127
126
 
@@ -227,6 +226,12 @@ export default {
227
226
  moduleOptions() {
228
227
  return window.apos.modules[apos.area.widgetManagers[this.type]];
229
228
  },
229
+ shouldShowTabs() {
230
+ if (this.moduleOptions.hideSingleTab) {
231
+ return this.tabs.length > 1;
232
+ }
233
+ return this.tabs.length && this.tabs[0].name !== 'ungrouped';
234
+ },
230
235
  typeLabel() {
231
236
  return this.moduleOptions.label;
232
237
  },
@@ -553,7 +558,6 @@ export default {
553
558
  if (recomputeOnlyStyles) {
554
559
  const styles = renderScopedStyles(this.schema, value.data, {
555
560
  rootSelector: '__placeholder_root_selector__',
556
- checkIfConditionsFn: checkIfConditions,
557
561
  subset: this.moduleOptions.stylesFields
558
562
  });
559
563
  this.applyPreviewStyles(styles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "4.28.0",
3
+ "version": "4.29.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "autoprefixer": "^10.4.1",
51
51
  "bluebird": "^3.7.2",
52
52
  "body-parser": "^1.18.2",
53
- "cheerio": "^1.0.0-rc.10",
53
+ "cheerio": "^1.1.0",
54
54
  "chokidar": "^3.5.2",
55
55
  "common-tags": "^1.8.0",
56
56
  "concat-with-sourcemaps": "^1.1.0",
@@ -61,9 +61,9 @@
61
61
  "cssnano": "^7.1.1",
62
62
  "csv-parse": "^5.6.0",
63
63
  "dayjs": "^1.9.8",
64
- "dompurify": "^3.2.5",
64
+ "dompurify": "^3.3.2",
65
65
  "encodeurl": "^2.0.0",
66
- "express": "^4.16.4",
66
+ "express": "^4.22.0",
67
67
  "express-bearer-token": "^3.0.0",
68
68
  "express-session": "^1.18.2",
69
69
  "fs-extra": "^7.0.1",
@@ -77,15 +77,14 @@
77
77
  "jsdom": "^24.1.0",
78
78
  "klona": "^2.0.4",
79
79
  "launder": "^1.4.0",
80
- "lodash": "^4.17.21",
80
+ "lodash": "^4.18.1",
81
81
  "mini-css-extract-plugin": "^1.6.0",
82
- "minimatch": "^3.0.4",
82
+ "minimatch": "^3.1.4",
83
83
  "mkdirp": "^0.5.5",
84
- "multer": "^2.0.2",
84
+ "multer": "^2.1.1",
85
85
  "node-fetch": "^2.6.1",
86
- "nodemailer": "^7.0.10",
86
+ "nodemailer": "^8.0.5",
87
87
  "nunjucks": "^3.2.1",
88
- "oembetter": "^1.1.3",
89
88
  "parseurl": "^1.3.3",
90
89
  "passport": "^0.6.0",
91
90
  "passport-local": "^1.0.0",
@@ -101,7 +100,7 @@
101
100
  "regexp-quote": "0.0.0",
102
101
  "resolve": "^1.19.0",
103
102
  "resolve-from": "^5.0.0",
104
- "sass": "^1.80.3",
103
+ "sass": "^1.85.0",
105
104
  "sass-loader": "^16.0.0",
106
105
  "server-destroy": "^1.0.1",
107
106
  "sluggo": "^1.0.0",
@@ -110,30 +109,31 @@
110
109
  "tiny-emitter": "^2.1.0",
111
110
  "tough-cookie": "^4.0.0",
112
111
  "underscore.string": "^3.3.4",
113
- "uploadfs": "^1.26.1",
114
112
  "void-elements": "^3.1.0",
115
113
  "vue": "^3.5.20",
116
114
  "vue-advanced-cropper": "^2.8.8",
117
115
  "vue-loader": "^17.1.0",
118
116
  "vue-style-loader": "^4.1.3",
119
- "webpack": "^5.72.0",
117
+ "webpack": "^5.106.1",
120
118
  "webpack-merge": "^5.7.3",
121
119
  "xregexp": "^2.0.0",
122
- "boring": "^1.1.1",
123
- "postcss-viewport-to-container-toggle": "^2.3.0",
124
- "sanitize-html": "^2.17.2",
125
120
  "@apostrophecms/emulate-mongo-3-driver": "^1.0.6",
121
+ "express-cache-on-demand": "^1.0.4",
126
122
  "broadband": "^1.1.0",
127
- "express-cache-on-demand": "^1.0.4"
123
+ "boring": "^1.1.1",
124
+ "oembetter": "^1.1.4",
125
+ "postcss-viewport-to-container-toggle": "^2.3.0",
126
+ "uploadfs": "^1.26.1",
127
+ "sanitize-html": "^2.17.3"
128
128
  },
129
129
  "devDependencies": {
130
130
  "eslint": "^9.39.1",
131
131
  "form-data": "^4.0.4",
132
- "mocha": "^11.7.1",
132
+ "mocha": "^11.7.5",
133
133
  "nyc": "^17.1.0",
134
134
  "stylelint": "^16.5.0",
135
- "eslint-config-apostrophe": "^6.0.2",
136
- "stylelint-config-apostrophe": "^4.4.0"
135
+ "stylelint-config-apostrophe": "^4.4.0",
136
+ "eslint-config-apostrophe": "^6.0.2"
137
137
  },
138
138
  "browserslist": [
139
139
  "ie >= 10"
package/test/files.js CHANGED
@@ -133,3 +133,132 @@ describe('Files', function() {
133
133
  });
134
134
 
135
135
  });
136
+
137
+ describe('Files with i18n locale prefixes', function() {
138
+
139
+ let apos;
140
+
141
+ const mockFiles = [
142
+ {
143
+ type: '@apostrophecms/file',
144
+ slug: 'file-locale-test',
145
+ visibility: 'public',
146
+ attachment: {
147
+ type: 'attachment',
148
+ _id: 'testid-locale',
149
+ name: 'localename',
150
+ extension: 'pdf',
151
+ data: 'I am a fake localized PDF'
152
+ }
153
+ }
154
+ ];
155
+
156
+ this.timeout(t.timeout);
157
+
158
+ after(async function() {
159
+ return t.destroy(apos);
160
+ });
161
+
162
+ before(async function() {
163
+ this.timeout(t.timeout);
164
+ this.slow(2000);
165
+
166
+ apos = await t.create({
167
+ root: module,
168
+ modules: {
169
+ '@apostrophecms/file': {
170
+ options: {
171
+ prettyUrls: true
172
+ }
173
+ },
174
+ '@apostrophecms/i18n': {
175
+ options: {
176
+ locales: {
177
+ en: {},
178
+ fr: {
179
+ prefix: '/fr'
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ });
186
+
187
+ apos.baseUrl = apos.http.getBase();
188
+
189
+ try {
190
+ await apos.doc.db.deleteMany({ type: '@apostrophecms/file' });
191
+ try {
192
+ fs.mkdirSync(`${__dirname}/public/uploads/attachments`);
193
+ } catch (e) {
194
+ // May already exist
195
+ }
196
+ } catch (e) {
197
+ assert(false);
198
+ }
199
+
200
+ // Insert the file doc in the default (en) locale
201
+ const req = apos.task.getReq();
202
+ for (const file of mockFiles) {
203
+ await apos.file.insert(req, file);
204
+ const {
205
+ _id, name, extension, data
206
+ } = file.attachment;
207
+ fs.writeFileSync(
208
+ `${__dirname}/public/uploads/attachments/${_id}-${name}.${extension}`,
209
+ data
210
+ );
211
+ }
212
+
213
+ // Localize the file to FR and give it a distinct French slug
214
+ const enFile = await apos.file.find(req, {}).toObject();
215
+ await apos.file.localize(req, enFile, 'fr');
216
+ const frReq = apos.task.getReq({ locale: 'fr' });
217
+ const frDraft = await apos.file.find(frReq.clone({ mode: 'draft' }), {}).toObject();
218
+ frDraft.slug = 'file-test-locale-fr';
219
+ frDraft.title = 'Test Locale FR';
220
+ await apos.file.update(frReq, frDraft);
221
+ });
222
+
223
+ it('should include locale prefix and FR slug in pretty URL for FR locale', async function() {
224
+ const frReq = apos.task.getAnonReq({ locale: 'fr' });
225
+ const files = await apos.file.find(frReq).toArray();
226
+ assert.strictEqual(files.length, 1);
227
+ const file = files[0];
228
+ const attachment = apos.attachment.first(file);
229
+ const url = apos.attachment.url(attachment);
230
+ assert(url);
231
+ // Must use the FR slug (test-locale-fr) and include /fr/ prefix
232
+ assert.strictEqual(
233
+ url,
234
+ `${apos.http.getBase()}/fr/files/test-locale-fr.${attachment.extension}`
235
+ );
236
+ });
237
+
238
+ it('should serve the file at the locale-prefixed pretty URL with FR slug', async function() {
239
+ const frReq = apos.task.getAnonReq({ locale: 'fr' });
240
+ const files = await apos.file.find(frReq).toArray();
241
+ assert.strictEqual(files.length, 1);
242
+ const file = files[0];
243
+ const attachment = apos.attachment.first(file);
244
+ const url = apos.attachment.url(attachment);
245
+ // Verify the locale-prefixed, FR-slug URL actually serves the content
246
+ const body = await apos.http.get(url);
247
+ assert.strictEqual(body, attachment.data);
248
+ });
249
+
250
+ it('should use EN slug without locale prefix for default EN locale', async function() {
251
+ const enReq = apos.task.getAnonReq({ locale: 'en' });
252
+ const files = await apos.file.find(enReq).toArray();
253
+ assert.strictEqual(files.length, 1);
254
+ const file = files[0];
255
+ const attachment = apos.attachment.first(file);
256
+ const url = apos.attachment.url(attachment);
257
+ assert(url);
258
+ // EN keeps its original slug (locale-test) with no locale prefix
259
+ assert.strictEqual(
260
+ url,
261
+ `${apos.http.getBase()}/files/locale-test.${attachment.extension}`
262
+ );
263
+ });
264
+ });