apostrophe 4.30.0-alpha.1 → 4.30.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/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +30 -2
- package/eslint.config.js +1 -2
- package/lib/mongodb-connect.js +62 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +0 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +25 -8
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +9 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +20 -2
- package/modules/@apostrophecms/area/index.js +10 -5
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +2 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +11 -1
- package/modules/@apostrophecms/db/index.js +27 -68
- package/modules/@apostrophecms/http/index.js +1 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
- package/modules/@apostrophecms/i18n/index.js +1 -8
- package/modules/@apostrophecms/image-widget/index.js +29 -1
- package/modules/@apostrophecms/job/index.js +7 -9
- package/modules/@apostrophecms/layout-widget/index.js +124 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +89 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +2 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +2 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/layout.css +8 -0
- package/modules/@apostrophecms/layout-widget/ui/src/layout.css +1 -1
- package/modules/@apostrophecms/layout-widget/views/widget.html +3 -3
- package/modules/@apostrophecms/login/index.js +13 -15
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +2 -1
- package/modules/@apostrophecms/oembed/index.js +18 -13
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +2 -0
- package/modules/@apostrophecms/rich-text-widget/index.js +36 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +1 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +1 -1
- package/modules/@apostrophecms/styles/index.js +16 -0
- package/modules/@apostrophecms/styles/lib/handlers.js +6 -0
- package/modules/@apostrophecms/styles/lib/methods.js +93 -0
- package/modules/@apostrophecms/styles/lib/presets.js +17 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +10 -1
- package/modules/@apostrophecms/submitted-draft/ui/apos/components/AposSubmittedDraftIcon.vue +1 -0
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +8 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +5 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +14 -1
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss +13 -1
- package/modules/@apostrophecms/util/index.js +4 -0
- package/modules/@apostrophecms/widget-type/index.js +6 -0
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +32 -0
- package/package.json +13 -13
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/add-missing-schema-fields-project/test.js +3 -11
- package/test/assets.js +67 -110
- package/test/db.js +15 -24
- package/test/job.js +1 -1
- package/test/layout-widget-gap.js +530 -0
- package/test/login.js +122 -1
- package/test/rich-text-widget.js +200 -0
- package/test/styles.js +50 -0
- package/test-lib/util.js +14 -50
- package/claude-tools/detect-handles.js +0 -46
- package/claude-tools/minimal-hang-test.js +0 -28
- package/claude-tools/mongo-close-test.js +0 -11
- package/claude-tools/stdin-ref-test.js +0 -14
- package/test/db-tools.js +0 -365
- package/test/default-adapter.js +0 -256
|
@@ -102,7 +102,7 @@ module.exports = {
|
|
|
102
102
|
linkHref: {
|
|
103
103
|
label: 'apostrophe:url',
|
|
104
104
|
help: 'apostrophe:linkHrefHelp',
|
|
105
|
-
type: '
|
|
105
|
+
type: 'url',
|
|
106
106
|
required: true,
|
|
107
107
|
if: {
|
|
108
108
|
linkTo: '_url'
|
|
@@ -147,6 +147,7 @@ module.exports = {
|
|
|
147
147
|
self.showPlaceholder = self.options.placeholder !== false;
|
|
148
148
|
self.options.placeholder = true;
|
|
149
149
|
self.determineBestAssetUrl('placeholder');
|
|
150
|
+
self.addCleanLinkHrefMigration();
|
|
150
151
|
},
|
|
151
152
|
handlers(self) {
|
|
152
153
|
return {
|
|
@@ -159,6 +160,33 @@ module.exports = {
|
|
|
159
160
|
},
|
|
160
161
|
methods(self) {
|
|
161
162
|
return {
|
|
163
|
+
// Clear link hrefs that use dangerous URL schemes (e.g.
|
|
164
|
+
// `javascript:`) and may have been stored before the linkHref
|
|
165
|
+
// schema field was changed to `type: 'url'`. Registered per
|
|
166
|
+
// module instance so subclasses of this widget are migrated
|
|
167
|
+
// under their own widget type and migration name.
|
|
168
|
+
addCleanLinkHrefMigration() {
|
|
169
|
+
self.apos.migration.add(
|
|
170
|
+
`${self.__meta.name}:clean-naughty-link-href`,
|
|
171
|
+
async () => {
|
|
172
|
+
await self.apos.migration.eachWidget({}, async (doc, widget, dotPath) => {
|
|
173
|
+
if (widget.type !== self.name) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!widget.linkHref) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!self.apos.launder.naughtyHref(widget.linkHref)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await self.apos.doc.db.updateOne(
|
|
183
|
+
{ _id: doc._id },
|
|
184
|
+
{ $set: { [`${dotPath}.linkHref`]: '' } }
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
},
|
|
162
190
|
validateAndAddSchemaLabels() {
|
|
163
191
|
const linkWithType = self.options.linkWithType;
|
|
164
192
|
|
|
@@ -244,9 +244,7 @@ module.exports = {
|
|
|
244
244
|
},
|
|
245
245
|
setTotal (n) {
|
|
246
246
|
total = n;
|
|
247
|
-
|
|
248
|
-
promises.push(result);
|
|
249
|
-
return result;
|
|
247
|
+
return self.setTotal(job, n);
|
|
250
248
|
},
|
|
251
249
|
setResults (_results) {
|
|
252
250
|
results = _results;
|
|
@@ -414,12 +412,12 @@ module.exports = {
|
|
|
414
412
|
//
|
|
415
413
|
// No promise is returned as this method just updates
|
|
416
414
|
// the job tracking information in the background.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
415
|
+
setTotal(job, total) {
|
|
416
|
+
self.db.updateOne({ _id: job._id }, { $set: { total } }, function (err) {
|
|
417
|
+
if (err) {
|
|
418
|
+
self.apos.util.error(err);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
423
421
|
},
|
|
424
422
|
// Mark the given job as ended. If `success`
|
|
425
423
|
// is true the job is reported as an overall
|
|
@@ -19,6 +19,10 @@ module.exports = {
|
|
|
19
19
|
gap: '1.5rem',
|
|
20
20
|
defaultCellHorizontalAlignment: null,
|
|
21
21
|
defaultCellVerticalAlignment: null,
|
|
22
|
+
// Extra class name(s) to append to the rendered layout-widget area
|
|
23
|
+
// wrapper, in addition to the built-in `layout-widget` class. Accepts
|
|
24
|
+
// a string of space-separated class names.
|
|
25
|
+
className: '',
|
|
22
26
|
injectStyles: true,
|
|
23
27
|
minifyStyles: true
|
|
24
28
|
},
|
|
@@ -98,6 +102,25 @@ module.exports = {
|
|
|
98
102
|
validateAndIdentifyTypes() {
|
|
99
103
|
const { column } = self.validateAndIdentifyTypes();
|
|
100
104
|
self.columnWidgetName = column;
|
|
105
|
+
},
|
|
106
|
+
// Detect the widget-style "gap" field (any styles field whose
|
|
107
|
+
// CSS `property` resolves to `gap`). Only the first match is used.
|
|
108
|
+
// Also detect whether the @apostrophecms/styles module has a
|
|
109
|
+
// site-wide layout gap field configured (via the `layoutGap`
|
|
110
|
+
// preset / `layoutGapDefault: true` marker).
|
|
111
|
+
detectGapFields() {
|
|
112
|
+
const widgetGapFields = self.apos.styles
|
|
113
|
+
.fieldsWithProperty(self.schema, 'gap');
|
|
114
|
+
if (widgetGapFields.length > 1) {
|
|
115
|
+
self.apos.util.warn(
|
|
116
|
+
`[${self.__meta.name}] Multiple style fields produce the ` +
|
|
117
|
+
`CSS \`gap\` property (${widgetGapFields.join(', ')}). ` +
|
|
118
|
+
'Only the first one will be honoured as the widget-scope gap.'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
self.widgetGapFieldName = widgetGapFields[0] || null;
|
|
122
|
+
self.globalGapEnabled = !!self.apos.modules['@apostrophecms/styles']
|
|
123
|
+
?.layoutGapFieldName;
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
};
|
|
@@ -118,6 +141,17 @@ module.exports = {
|
|
|
118
141
|
defaultCellHorizontalAlignment: self.options.defaultCellHorizontalAlignment,
|
|
119
142
|
defaultCellVerticalAlignment: self.options.defaultCellVerticalAlignment
|
|
120
143
|
},
|
|
144
|
+
widgetGapFieldName: self.widgetGapFieldName || null,
|
|
145
|
+
widgetGapFieldUnit: self.widgetGapFieldName
|
|
146
|
+
? (self.apos.styles
|
|
147
|
+
.getFieldByPath(self.schema, self.widgetGapFieldName)
|
|
148
|
+
?.unit || '')
|
|
149
|
+
: '',
|
|
150
|
+
globalGapEnabled: !!self.globalGapEnabled,
|
|
151
|
+
// Opt-in flag read by the generic widget editor: when true,
|
|
152
|
+
// it broadcasts `apos-widget-live-preview` events on the apos bus
|
|
153
|
+
// during the style-only fast path.
|
|
154
|
+
subscribesToLivePreview: !!self.widgetGapFieldName,
|
|
121
155
|
columnWidgetName: self.columnWidgetName
|
|
122
156
|
};
|
|
123
157
|
},
|
|
@@ -132,6 +166,7 @@ module.exports = {
|
|
|
132
166
|
defaultCellHorizontalAlignment,
|
|
133
167
|
defaultCellVerticalAlignment
|
|
134
168
|
} = self.options;
|
|
169
|
+
const widgetGap = self.resolveWidgetGap(widget);
|
|
135
170
|
return {
|
|
136
171
|
..._super(widget, { scene }),
|
|
137
172
|
columns,
|
|
@@ -141,13 +176,53 @@ module.exports = {
|
|
|
141
176
|
tablet,
|
|
142
177
|
gap,
|
|
143
178
|
defaultCellHorizontalAlignment,
|
|
144
|
-
defaultCellVerticalAlignment
|
|
179
|
+
defaultCellVerticalAlignment,
|
|
180
|
+
_gap: widgetGap,
|
|
181
|
+
_gapHasGlobal: !!self.globalGapEnabled
|
|
145
182
|
};
|
|
146
183
|
}
|
|
147
184
|
};
|
|
148
185
|
},
|
|
149
186
|
methods(self) {
|
|
150
187
|
return {
|
|
188
|
+
// Resolve the widget-scope gap for a widget instance, if this
|
|
189
|
+
// module declares a styles field with `property: 'gap'`. Returns
|
|
190
|
+
// the resolved value (with unit, when applicable). When the
|
|
191
|
+
// widget has no explicit value, falls back to the field's `def`.
|
|
192
|
+
// Returns `null` only when no widget gap field is
|
|
193
|
+
// configured, no widget value is set, and no `def` is declared
|
|
194
|
+
// on the field.
|
|
195
|
+
resolveWidgetGap(widget) {
|
|
196
|
+
if (!self.widgetGapFieldName || !widget) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const field = self.apos.styles.getFieldByPath(
|
|
200
|
+
self.schema, self.widgetGapFieldName
|
|
201
|
+
);
|
|
202
|
+
let value = self.apos.util.get(widget, self.widgetGapFieldName);
|
|
203
|
+
if (value === null || value === undefined || value === '') {
|
|
204
|
+
if (field?.def === null || field?.def === undefined || field?.def === '') {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
value = field.def;
|
|
208
|
+
}
|
|
209
|
+
if (field?.unit && typeof value !== 'string') {
|
|
210
|
+
return `${value}${field.unit}`;
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
},
|
|
214
|
+
// Determine whether the inline `--grid-gap` CSS variable should
|
|
215
|
+
// be omitted on the grid container so the global cascade
|
|
216
|
+
// (`var(--apos-layout-gap, …)`) can take effect. The inline var
|
|
217
|
+
// is omitted whenever:
|
|
218
|
+
// - no widget-scope gap value is set, AND
|
|
219
|
+
// - the global layout-gap field is configured.
|
|
220
|
+
shouldOmitInlineGap(widget, global) {
|
|
221
|
+
if (!self.globalGapEnabled) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return self.resolveWidgetGap(widget) === null;
|
|
225
|
+
},
|
|
151
226
|
publicCssNodes(req) {
|
|
152
227
|
return [
|
|
153
228
|
{
|
|
@@ -277,11 +352,13 @@ module.exports = {
|
|
|
277
352
|
const mobileBreakpointPlus = mobileBreakpoint + 1;
|
|
278
353
|
const tabletBreakpoint = self.options.tablet?.breakpoint || 1024;
|
|
279
354
|
const tabletBreakpointPlus = tabletBreakpoint + 1;
|
|
355
|
+
const gapDefault = self.options.gap || '0';
|
|
280
356
|
cssContent = cssContent
|
|
281
357
|
.replace(/\{\$mobile\}/g, mobileBreakpoint)
|
|
282
358
|
.replace(/\{\$mobile-plus\}/g, mobileBreakpointPlus)
|
|
283
359
|
.replace(/\{\$tablet\}/g, tabletBreakpoint)
|
|
284
|
-
.replace(/\{\$tablet-plus\}/g, tabletBreakpointPlus)
|
|
360
|
+
.replace(/\{\$tablet-plus\}/g, tabletBreakpointPlus)
|
|
361
|
+
.replace(/\{\$gap-default\}/g, gapDefault);
|
|
285
362
|
|
|
286
363
|
return self.processCss(cssContent, scene);
|
|
287
364
|
},
|
|
@@ -371,6 +448,51 @@ module.exports = {
|
|
|
371
448
|
(a.order ?? 0) - (b.order ?? 0)
|
|
372
449
|
);
|
|
373
450
|
return items[items.length - 1]._id;
|
|
451
|
+
},
|
|
452
|
+
// Compute the `--grid-gap: <value>;` declaration to inline on the
|
|
453
|
+
// grid container, or an empty string when the cascade should
|
|
454
|
+
// resolve gap via `var(--apos-layout-gap, …)` instead.
|
|
455
|
+
// Honours the priority order:
|
|
456
|
+
// 1. Widget-style gap (when set on this widget instance).
|
|
457
|
+
// 2. Static module option (BC) — when no global gap field is
|
|
458
|
+
// configured, or it has no value.
|
|
459
|
+
// 3. Otherwise, omit the inline var so the global cascade wins.
|
|
460
|
+
// Must be invoked via the widget's own module namespace —
|
|
461
|
+
// `apos.modules[data.manager.__meta.name].gapInlineCss(...)` —
|
|
462
|
+
// so that `self` resolves to the actual subclass and picks up
|
|
463
|
+
// its `widgetGapFieldName` / `globalGapEnabled`.
|
|
464
|
+
gapInlineCss(widget, options, global) {
|
|
465
|
+
const widgetGap = self.resolveWidgetGap(widget);
|
|
466
|
+
if (widgetGap !== null) {
|
|
467
|
+
return ` --grid-gap: ${widgetGap};`;
|
|
468
|
+
}
|
|
469
|
+
if (self.shouldOmitInlineGap(widget, global)) {
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
const fallback = (options && options.gap) || self.options.gap || '0';
|
|
473
|
+
return ` --grid-gap: ${fallback};`;
|
|
474
|
+
},
|
|
475
|
+
// Build the `aposParentOptions` payload passed by the rendered
|
|
476
|
+
// layout-widget area to the in-place editor (AposAreaLayoutEditor).
|
|
477
|
+
// Includes the widget's resolved gap (from its `gap` styles field,
|
|
478
|
+
// when present) so the live editor's grid container reflects the
|
|
479
|
+
// saved per-widget value rather than only the static module
|
|
480
|
+
// option. Sets `gap: null` to signal the editor to omit
|
|
481
|
+
// `--grid-gap` so the global cascade resolves it through
|
|
482
|
+
// `:root { --apos-layout-gap }`. Must be invoked via the widget's
|
|
483
|
+
// own module namespace, like `gapInlineCss`.
|
|
484
|
+
parentOptionsForArea(widget, options, global) {
|
|
485
|
+
const opts = {
|
|
486
|
+
...(options || {}),
|
|
487
|
+
widgetId: widget._id
|
|
488
|
+
};
|
|
489
|
+
const widgetGap = self.resolveWidgetGap(widget);
|
|
490
|
+
if (widgetGap !== null) {
|
|
491
|
+
opts.gap = widgetGap;
|
|
492
|
+
} else if (self.shouldOmitInlineGap(widget, global)) {
|
|
493
|
+
opts.gap = null;
|
|
494
|
+
}
|
|
495
|
+
return opts;
|
|
374
496
|
}
|
|
375
497
|
};
|
|
376
498
|
}
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
|
|
123
123
|
<script>
|
|
124
124
|
import { mapActions } from 'pinia';
|
|
125
|
+
import get from 'lodash/get';
|
|
125
126
|
import AposAreaEditorLogic from 'Modules/@apostrophecms/area/logic/AposAreaEditor.js';
|
|
126
127
|
import walkWidgets from 'Modules/@apostrophecms/area/lib/walk-widgets.js';
|
|
127
128
|
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget.js';
|
|
@@ -144,7 +145,11 @@ export default {
|
|
|
144
145
|
return {
|
|
145
146
|
// 'layout' | 'focus' | 'content'
|
|
146
147
|
layoutMode: 'content',
|
|
147
|
-
layoutDeviceMode: 'desktop'
|
|
148
|
+
layoutDeviceMode: 'desktop',
|
|
149
|
+
// Live snapshot of the parent layout-widget's in-modal style
|
|
150
|
+
// values, fed by `apos-widget-live-preview` bus events from
|
|
151
|
+
// AposWidgetEditor while the user drags style sliders.
|
|
152
|
+
liveWidgetData: null
|
|
148
153
|
};
|
|
149
154
|
},
|
|
150
155
|
computed: {
|
|
@@ -152,11 +157,48 @@ export default {
|
|
|
152
157
|
return window.apos.modules[this.moduleName] || {};
|
|
153
158
|
},
|
|
154
159
|
gridModuleOptions() {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
const baseGrid = this.layoutModuleOptions.grid ?? {};
|
|
161
|
+
const parent = this.parentOptions || {};
|
|
162
|
+
const opts = Object.assign({}, baseGrid, parent);
|
|
163
|
+
// Resolve `gap` priority for the live editor:
|
|
164
|
+
const liveGap = this.liveGapValue;
|
|
165
|
+
if (liveGap != null) {
|
|
166
|
+
opts.gap = liveGap;
|
|
167
|
+
} else if (parent.gap != null) {
|
|
168
|
+
opts.gap = parent.gap;
|
|
169
|
+
} else if (
|
|
170
|
+
parent.gap === null ||
|
|
171
|
+
this.layoutModuleOptions.globalGapEnabled
|
|
172
|
+
) {
|
|
173
|
+
// Parent signalled "use the cascade" (omit `--grid-gap` and let
|
|
174
|
+
// `var(--apos-layout-gap, …)` resolve it on `:root`).
|
|
175
|
+
// The snapshot is recomputed whenever `layoutMode`
|
|
176
|
+
// changes, which is the only cadence the editor needs.
|
|
177
|
+
if (this.layoutMode !== 'content') {
|
|
178
|
+
opts.gap = this.resolvedCascadeGap();
|
|
179
|
+
} else {
|
|
180
|
+
opts.gap = undefined;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
opts.gap = baseGrid.gap;
|
|
184
|
+
}
|
|
185
|
+
return opts;
|
|
186
|
+
},
|
|
187
|
+
// Reactive bridge to the parent layout-widget's styles modal.
|
|
188
|
+
liveGapValue() {
|
|
189
|
+
if (this.layoutMode !== 'content') {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const fieldName = this.layoutModuleOptions.widgetGapFieldName;
|
|
193
|
+
if (!fieldName || !this.liveWidgetData) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const value = get(this.liveWidgetData, fieldName);
|
|
197
|
+
if (value === null || value === undefined || value === '') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const unit = this.layoutModuleOptions.widgetGapFieldUnit || '';
|
|
201
|
+
return typeof value === 'string' ? value : `${value}${unit}`;
|
|
160
202
|
},
|
|
161
203
|
// Meta storage is not yet implemented, return a default meta object
|
|
162
204
|
layoutMeta() {
|
|
@@ -259,6 +301,12 @@ export default {
|
|
|
259
301
|
this.emphasizeGrid();
|
|
260
302
|
} else {
|
|
261
303
|
this.deEmphasizeGrid();
|
|
304
|
+
// Leaving layout mode: clear the control-suppression flag that was
|
|
305
|
+
// set on the focused widget when we entered layout mode. Without
|
|
306
|
+
// this the layout widget keeps `isSuppressingWidgetControls=true`
|
|
307
|
+
// and its side operation bar stays hidden even though it is the
|
|
308
|
+
// focused widget.
|
|
309
|
+
apos.bus.$emit('clear-focused-widget-control-suppression');
|
|
262
310
|
}
|
|
263
311
|
}
|
|
264
312
|
},
|
|
@@ -266,6 +314,8 @@ export default {
|
|
|
266
314
|
apos.bus.$on('apos-switch-layout-mode', this.switchLayoutMode);
|
|
267
315
|
apos.bus.$on('apos-layout-col-delete', this.onRemoveLayoutColumn);
|
|
268
316
|
apos.bus.$on('apos-edit-styles', this.editStyles);
|
|
317
|
+
apos.bus.$on('apos-widget-live-preview', this.onWidgetLivePreview);
|
|
318
|
+
apos.bus.$on('apos-widget-live-preview-end', this.onWidgetLivePreviewEnd);
|
|
269
319
|
if (!this.hasLayoutColumnWidgets) {
|
|
270
320
|
this.onCreateProvision();
|
|
271
321
|
}
|
|
@@ -275,6 +325,8 @@ export default {
|
|
|
275
325
|
apos.bus.$off('apos-switch-layout-mode', this.switchLayoutMode);
|
|
276
326
|
apos.bus.$off('apos-layout-col-delete', this.onRemoveLayoutColumn);
|
|
277
327
|
apos.bus.$off('apos-edit-styles', this.editStyles);
|
|
328
|
+
apos.bus.$off('apos-widget-live-preview', this.onWidgetLivePreview);
|
|
329
|
+
apos.bus.$off('apos-widget-live-preview-end', this.onWidgetLivePreviewEnd);
|
|
278
330
|
},
|
|
279
331
|
methods: {
|
|
280
332
|
...mapActions(useWidgetStore, [
|
|
@@ -284,6 +336,37 @@ export default {
|
|
|
284
336
|
'removeEmphasizedWidget',
|
|
285
337
|
'setFocusedWidget'
|
|
286
338
|
]),
|
|
339
|
+
// Read the current resolved value of the global cascade variable
|
|
340
|
+
// `--apos-layout-gap` from `:root`. Used by `gridModuleOptions` to
|
|
341
|
+
// hand layout/focus modes a real gap value.
|
|
342
|
+
resolvedCascadeGap() {
|
|
343
|
+
if (!document?.documentElement) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
const value = window.getComputedStyle(document.documentElement)
|
|
347
|
+
.getPropertyValue('--apos-layout-gap')
|
|
348
|
+
.trim();
|
|
349
|
+
return value || undefined;
|
|
350
|
+
},
|
|
351
|
+
onWidgetLivePreview({ widgetId, data }) {
|
|
352
|
+
if (!widgetId || widgetId !== this.parentOptions?.widgetId) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
this.liveWidgetData = data;
|
|
356
|
+
},
|
|
357
|
+
onWidgetLivePreviewEnd({ widgetId, reason }) {
|
|
358
|
+
if (!widgetId || widgetId !== this.parentOptions?.widgetId) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// On `save` keep the live snapshot in place: clearing
|
|
362
|
+
// here would expose the old `parent.gap` value for one frame.
|
|
363
|
+
// On `cancel` / `unmount` drop the live value immediately so the
|
|
364
|
+
// editor falls back to the SSR-rendered parent options.
|
|
365
|
+
if (reason === 'save') {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.liveWidgetData = null;
|
|
369
|
+
},
|
|
287
370
|
clickOnGrid() {
|
|
288
371
|
if (this.parentOptions.widgetId) {
|
|
289
372
|
this.setFocusedWidget(this.parentOptions.widgetId, this.areaId);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
data-mobile-auto="true"
|
|
17
17
|
:style="{
|
|
18
18
|
'--grid-columns': gridState.columns,
|
|
19
|
-
'--grid-gap': gridState.options.gap
|
|
19
|
+
'--grid-gap': gridState.options.gap ?? null,
|
|
20
20
|
'--grid-rows': 'auto',
|
|
21
21
|
'--mobile-grid-rows': 'auto',
|
|
22
22
|
'--tablet-grid-rows': 'auto',
|
|
@@ -310,7 +310,7 @@ export default {
|
|
|
310
310
|
display: grid;
|
|
311
311
|
grid-template-columns: repeat(var(--grid-columns, 12), 1fr);
|
|
312
312
|
grid-template-rows: repeat(var(--grid-rows), auto);
|
|
313
|
-
|
|
313
|
+
gap: var(--grid-gap, var(--apos-layout-gap, 0));
|
|
314
314
|
justify-items: var(--justify-items);
|
|
315
315
|
/* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */
|
|
316
316
|
align-items: var(--align-items);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
data-apos-test="aposLayoutContainerClone"
|
|
6
6
|
:style="{
|
|
7
7
|
'--grid-columns': gridState.columns,
|
|
8
|
-
'--grid-gap': gridState.options.gap,
|
|
8
|
+
'--grid-gap': gridState.options.gap ?? null,
|
|
9
9
|
}"
|
|
10
10
|
@mousemove="onMouseMove($event)"
|
|
11
11
|
>
|
|
@@ -938,7 +938,7 @@ $resize-ui-z-index: 2;
|
|
|
938
938
|
inset: 0;
|
|
939
939
|
grid-template-columns: repeat(var(--grid-columns, 12), 1fr);
|
|
940
940
|
grid-template-rows: repeat(var(--grid-rows), auto);
|
|
941
|
-
|
|
941
|
+
gap: var(--grid-gap, var(--apos-layout-gap, 0));
|
|
942
942
|
|
|
943
943
|
&.is-moving,
|
|
944
944
|
&.is-resizing {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Reserve the same vertical footprint for the SSR markup.
|
|
3
|
+
This stylesheet is only injected on the apos scene in edit mode.
|
|
4
|
+
*/
|
|
5
|
+
.layout-widget > div {
|
|
6
|
+
min-height: 150px;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
/* Tablet rules for admin interface */
|
|
2
10
|
/* stylelint-disable-next-line media-feature-name-allowed-list */
|
|
3
11
|
@media screen and (min-width: {$mobile-plus}px) and (max-width: {$tablet}px) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
display: grid;
|
|
3
3
|
grid-template-columns: repeat(var(--grid-columns, 12), 1fr);
|
|
4
4
|
grid-template-rows: repeat(var(--grid-rows), auto);
|
|
5
|
-
|
|
5
|
+
gap: var(--grid-gap, var(--apos-layout-gap, {$gap-default}));
|
|
6
6
|
justify-items: var(--justify-items);
|
|
7
7
|
/* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */
|
|
8
8
|
align-items: var(--align-items);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{% area data.widget, 'columns' with {
|
|
2
2
|
aposStyle: '--grid-columns: ' + (data.options.columns or data.manager.options.columns) + ';' +
|
|
3
|
-
|
|
3
|
+
apos.modules[data.manager.__meta.name].gapInlineCss(data.widget, data.options, data.global) +
|
|
4
4
|
' --grid-rows: auto;' +
|
|
5
5
|
' --mobile-grid-rows: auto;' +
|
|
6
6
|
' --tablet-grid-rows: auto;' +
|
|
7
7
|
' --justify-items: ' + (data.options.defaultCellHorizontalAlignment or data.manager.options.defaultCellHorizontalAlignment or 'stretch') + ';' +
|
|
8
8
|
' --align-items: ' + (data.options.defaultCellVerticalAlignment or data.manager.options.defaultCellVerticalAlignment or 'stretch') + ';',
|
|
9
|
-
aposClassName: 'layout-widget',
|
|
10
|
-
aposParentOptions: data.
|
|
9
|
+
aposClassName: ('layout-widget' + ((' ' + (data.options.className or data.manager.options.className)) if (data.options.className or data.manager.options.className))),
|
|
10
|
+
aposParentOptions: apos.modules[data.manager.__meta.name].parentOptionsForArea(data.widget, data.options, data.global),
|
|
11
11
|
aposAttrs: {
|
|
12
12
|
'tablet-auto': true,
|
|
13
13
|
'mobile-auto': true
|
|
@@ -295,7 +295,17 @@ module.exports = {
|
|
|
295
295
|
async resetRequest(req) {
|
|
296
296
|
const MIN_RESPONSE_TIME = 2000;
|
|
297
297
|
const startTime = Date.now();
|
|
298
|
-
|
|
298
|
+
// Refuse to construct reset URLs from the request's Host header,
|
|
299
|
+
// which is attacker-controlled. Operators must configure the
|
|
300
|
+
// baseUrl option (or the APOS_BASE_URL environment variable) so
|
|
301
|
+
// the link in the reset email points to the real site rather than
|
|
302
|
+
// wherever the attacker aimed their crafted request.
|
|
303
|
+
if (!self.apos.baseUrl) {
|
|
304
|
+
throw self.apos.error(
|
|
305
|
+
'invalid',
|
|
306
|
+
'The baseUrl option (or APOS_BASE_URL environment variable) must be configured to enable password reset'
|
|
307
|
+
);
|
|
308
|
+
}
|
|
299
309
|
const email = self.apos.launder.string(req.body.email);
|
|
300
310
|
if (!email.length) {
|
|
301
311
|
throw self.apos.error('invalid', req.t('apostrophe:loginResetEmailRequired'));
|
|
@@ -319,22 +329,10 @@ module.exports = {
|
|
|
319
329
|
user.passwordReset = reset;
|
|
320
330
|
user.passwordResetAt = new Date();
|
|
321
331
|
await self.apos.user.update(req, user, { permissions: false });
|
|
322
|
-
|
|
323
|
-
if (!port || [ '80', '443' ].includes(port)) {
|
|
324
|
-
port = '';
|
|
325
|
-
} else {
|
|
326
|
-
port = `:${port}`;
|
|
327
|
-
}
|
|
328
|
-
const parsed = new URL(
|
|
329
|
-
req.absoluteUrl,
|
|
330
|
-
self.apos.baseUrl
|
|
331
|
-
? undefined
|
|
332
|
-
: `${req.protocol}://${req.hostname}${port}`
|
|
333
|
-
);
|
|
334
|
-
parsed.pathname = self.login();
|
|
335
|
-
parsed.search = '?';
|
|
332
|
+
const parsed = new URL(self.login(), self.apos.baseUrl);
|
|
336
333
|
parsed.searchParams.append('reset', reset);
|
|
337
334
|
parsed.searchParams.append('email', user.email);
|
|
335
|
+
const site = parsed.hostname;
|
|
338
336
|
try {
|
|
339
337
|
await self.email(req, 'passwordResetEmail', {
|
|
340
338
|
user,
|
|
@@ -212,7 +212,8 @@ const nonDraggableElements = [
|
|
|
212
212
|
'.apos-field--inline-array-field',
|
|
213
213
|
'.apos-field--inline-array-table-with-remove-button-field',
|
|
214
214
|
'.apos-field--inline-array-table-field',
|
|
215
|
-
'.apos-input-color__sample-picker'
|
|
215
|
+
'.apos-input-color__sample-picker',
|
|
216
|
+
'.apos-input-array-inline-table'
|
|
216
217
|
];
|
|
217
218
|
|
|
218
219
|
const resizeSides = [
|
|
@@ -16,10 +16,13 @@ const cheerio = require('cheerio');
|
|
|
16
16
|
// widely trusted sites are already allowlisted.
|
|
17
17
|
//
|
|
18
18
|
// Your `allowlist` option is concatenated with `oembetter`'s standard
|
|
19
|
-
// allowlist, plus wufoo.com, infogr.am
|
|
19
|
+
// allowlist, plus wufoo.com, infogr.am and slideshare.net.
|
|
20
20
|
//
|
|
21
21
|
// Your `endpoints` option is concatenated with `oembetter`'s standard
|
|
22
22
|
// endpoints list.
|
|
23
|
+
//
|
|
24
|
+
// If you wish to completely override the behavior, set
|
|
25
|
+
// `minimumAllowlist` and `minimumEndpoints` instead.
|
|
23
26
|
|
|
24
27
|
module.exports = {
|
|
25
28
|
options: {
|
|
@@ -42,28 +45,30 @@ module.exports = {
|
|
|
42
45
|
// Don't permit oembed of untrusted sites, which could
|
|
43
46
|
// lead to XSS attacks
|
|
44
47
|
|
|
45
|
-
self.
|
|
46
|
-
self.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
);
|
|
48
|
+
const minimumAllowlist = self.options.minimumAllowlist || [
|
|
49
|
+
...self.oembetter.suggestedAllowlist,
|
|
50
|
+
'wufoo.com',
|
|
51
|
+
'infogr.am',
|
|
52
|
+
'slideshare.net'
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
self.oembetter.allowlist(minimumAllowlist.concat(self.options.allowlist || []));
|
|
56
|
+
|
|
57
|
+
const minimumEndpoints = self.options.minimumEndpoints || self.oembetter.suggestedEndpoints;
|
|
53
58
|
self.oembetter.endpoints(
|
|
54
|
-
|
|
59
|
+
minimumEndpoints.concat(self.options.endpoints || [])
|
|
55
60
|
);
|
|
56
61
|
},
|
|
57
62
|
|
|
58
63
|
// Enhances oembetter to support services better or to support services
|
|
59
|
-
// that have no oembed support by default.
|
|
60
|
-
//
|
|
64
|
+
// that have no oembed support by default.
|
|
65
|
+
//
|
|
66
|
+
// Extend or override this method to change or add oembetter filters.
|
|
61
67
|
|
|
62
68
|
enhanceOembetter() {
|
|
63
69
|
require('./lib/youtube.js')(self, self.oembetter);
|
|
64
70
|
require('./lib/vimeo.js')(self, self.oembetter);
|
|
65
71
|
require('./lib/wufoo.js')(self, self.oembetter);
|
|
66
|
-
require('./lib/infogram.js')(self, self.oembetter);
|
|
67
72
|
},
|
|
68
73
|
|
|
69
74
|
// This method fetches the specified URL, determines its best embedded
|
package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
:modifiers="['small', 'no-motion']"
|
|
7
7
|
:tooltip="$t('apostrophe:recentlyEditedDocuments')"
|
|
8
8
|
:icon-only="true"
|
|
9
|
+
:label="'apostrophe:recentlyEditedManagerOpen'"
|
|
10
|
+
:attrs="{ 'aria-label': $t('apostrophe:recentlyEditedManagerOpen') }"
|
|
9
11
|
@click="open"
|
|
10
12
|
/>
|
|
11
13
|
</template>
|