apostrophe 4.27.1 → 4.28.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 +35 -0
- package/index.js +3 -0
- package/lib/stream-proxy.js +49 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
- package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
- package/modules/@apostrophecms/asset/index.js +3 -2
- package/modules/@apostrophecms/attachment/index.js +270 -0
- package/modules/@apostrophecms/doc/index.js +8 -2
- package/modules/@apostrophecms/doc-type/index.js +81 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
- package/modules/@apostrophecms/express/index.js +30 -1
- package/modules/@apostrophecms/file/index.js +71 -6
- package/modules/@apostrophecms/i18n/index.js +20 -1
- package/modules/@apostrophecms/image/index.js +11 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
- package/modules/@apostrophecms/login/index.js +43 -11
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
- package/modules/@apostrophecms/page/index.js +9 -11
- package/modules/@apostrophecms/page-type/index.js +6 -1
- package/modules/@apostrophecms/piece-page-type/index.js +100 -13
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
- package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
- package/modules/@apostrophecms/styles/lib/methods.js +35 -12
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
- package/modules/@apostrophecms/task/index.js +9 -1
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
- package/modules/@apostrophecms/ui/index.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
- package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
- package/modules/@apostrophecms/uploadfs/index.js +15 -1
- package/modules/@apostrophecms/url/index.js +419 -1
- package/package.json +6 -6
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/external-front.js +1 -0
- package/test/files.js +135 -0
- package/test/login-requirements.js +145 -3
- package/test/static-build.js +2701 -0
- package/test/universal-graph.js +1135 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.28.0
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
- Adds support for static URLs and static external frontend builds.
|
|
8
|
+
- Adds widget graph store, accessible in Admin UI.
|
|
9
|
+
- Support for the new `prettyUrls: true` option for @apostrophecms/file, which enables "pretty URLs" for PDFs and other items in the file library, in exchange for a small performance impact. Edit the slug field to adjust the pretty URL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- Fix a bug when rich text link open in new tab checkbox can't be cleared
|
|
15
|
+
- Ensures the `getOne` API can correctly retrieve documents that are not localized. Thanks to [Eduardo Correal](https://github.com/ecb34).
|
|
16
|
+
- Clicking a choice should dismiss the relationship suggestions dropdown. This regression was caused by part of the patch in release 4.27.1.
|
|
17
|
+
- lint on main
|
|
18
|
+
- Adds inner wrapper to area widget that creates a separate z-index context for the user, fixing widget/apostrophe UI conflicts
|
|
19
|
+
- Fixes a bug where Layout's mode erroneously switches state
|
|
20
|
+
- Fixes a bug where Layouts synthetic column styles were not reaching their children
|
|
21
|
+
- Fixes styles being sanitized as html (breaks quotes) and resolves duplicate styles on the page.
|
|
22
|
+
- Fix subtle bug in AposPermissionGrid that caused unrelated clicks to be "swallowed" due to a race condition at low network speeds
|
|
23
|
+
- Fixes two conditions where slow internet speed could cause input to lose focus before a selection can be registered.
|
|
24
|
+
|
|
25
|
+
### Changes
|
|
26
|
+
|
|
27
|
+
- Improve re-rendering UX while keeping the performance optimization
|
|
28
|
+
- raise the user's widget z-index context only when focused
|
|
29
|
+
- Hide add content buttons on rich text editing, like widget controls
|
|
30
|
+
- Refine in-context focus states for calmer UX
|
|
31
|
+
- Simplifies some in-context UI rendering checks
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
|
|
36
|
+
- This previously undisclosed security vulnerability allowed users who had compromised a password to perform actions in the CMS without 2FA. For sites not using 2FA (e.g. our @apostrophecms/login-totp module), this changes nothing. But for those using our TOTP module or similar, upgrading to this release is urgent. Thanks to 0xkakashi for reporting the issue and recommending a fix.
|
|
37
|
+
|
|
3
38
|
## 4.27.1 (2026-03-03)
|
|
4
39
|
|
|
5
40
|
### Fixes
|
package/index.js
CHANGED
|
@@ -479,6 +479,9 @@ async function apostrophe(options, telemetry, rootSpan) {
|
|
|
479
479
|
// Environment variable override
|
|
480
480
|
self.options.baseUrl = process.env.APOS_BASE_URL || self.options.baseUrl;
|
|
481
481
|
self.baseUrl = self.options.baseUrl;
|
|
482
|
+
self.options.staticBaseUrl = process.env.APOS_STATIC_BASE_URL ||
|
|
483
|
+
self.options.staticBaseUrl;
|
|
484
|
+
self.staticBaseUrl = self.options.staticBaseUrl;
|
|
482
485
|
self.prefix = self.options.prefix || '';
|
|
483
486
|
}
|
|
484
487
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Make a request for url and stream it to req.res,
|
|
2
|
+
// passing through relevant headers, without downloading
|
|
3
|
+
// the whole thing to disk. Site-relative URLs are
|
|
4
|
+
// resolved via req.baseUrl. Server-side errors are logged to
|
|
5
|
+
// error() which should accept multiple arguments
|
|
6
|
+
|
|
7
|
+
module.exports = async function(req, url, { error }) {
|
|
8
|
+
const res = req.res;
|
|
9
|
+
if (url.startsWith('/')) {
|
|
10
|
+
// Can't make a self-request without an absolute URL
|
|
11
|
+
url = `${req.baseUrl}${url}`;
|
|
12
|
+
}
|
|
13
|
+
let response;
|
|
14
|
+
try {
|
|
15
|
+
response = await fetch(url);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return send502(e);
|
|
18
|
+
}
|
|
19
|
+
for (const header of [ 'content-type', 'etag', 'last-modified', 'content-disposition', 'cache-control' ]) {
|
|
20
|
+
const result = response.headers.get(header);
|
|
21
|
+
if (result != null) {
|
|
22
|
+
res.header(header, result);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
res.status(response.status);
|
|
26
|
+
if (response.body == null) {
|
|
27
|
+
return res.end();
|
|
28
|
+
}
|
|
29
|
+
response.body.pipeTo(new WritableStream({
|
|
30
|
+
write(chunk) {
|
|
31
|
+
res.write(chunk);
|
|
32
|
+
},
|
|
33
|
+
close() {
|
|
34
|
+
res.end();
|
|
35
|
+
},
|
|
36
|
+
abort(reason) {
|
|
37
|
+
if (!res.headersSent) {
|
|
38
|
+
return send502(reason);
|
|
39
|
+
} else {
|
|
40
|
+
// Only way to signal failure after headers are sent
|
|
41
|
+
res.destroy();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
function send502(e) {
|
|
46
|
+
error(`Error fetching "ugly URL" ${url} to resolve pretty URL ${req.url}:`, e);
|
|
47
|
+
return res.status(502).send('upstream media error fetching data for pretty URL');
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -166,22 +166,13 @@ export default {
|
|
|
166
166
|
color: var(--a-text-primary);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
&__document-title {
|
|
170
|
-
margin-top: 1px;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
169
|
&__separator {
|
|
174
170
|
align-items: center;
|
|
175
|
-
margin-top: 1px;
|
|
176
171
|
padding: 0 7px;
|
|
177
172
|
}
|
|
178
173
|
|
|
179
|
-
&__document {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
:deep(.apos-context-menu__items) {
|
|
183
|
-
min-width: 150px;
|
|
184
|
-
}
|
|
174
|
+
&__document :deep(.apos-context-menu__items) {
|
|
175
|
+
min-width: 150px;
|
|
185
176
|
}
|
|
186
177
|
}
|
|
187
178
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import createApp from 'Modules/@apostrophecms/ui/lib/vue';
|
|
2
|
+
import createApp, { pinia } from 'Modules/@apostrophecms/ui/lib/vue';
|
|
3
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph.js';
|
|
3
4
|
import { nextTick } from 'vue';
|
|
4
5
|
|
|
5
6
|
export default function() {
|
|
@@ -24,6 +25,12 @@ export default function() {
|
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
apos.bus.$on('refreshed', function() {
|
|
28
|
+
// Re-instantiate the on-page widget graph before remounting areas.
|
|
29
|
+
// The normal mounted hooks will rebuild it from fresh data.
|
|
30
|
+
const graphStore = useWidgetGraphStore(pinia);
|
|
31
|
+
if (apos.adminBar?.contextId) {
|
|
32
|
+
graphStore.resetGraph(apos.adminBar.contextId);
|
|
33
|
+
}
|
|
27
34
|
createAreaAppsAndRunPlayersIfDone();
|
|
28
35
|
});
|
|
29
36
|
|
|
@@ -117,10 +124,18 @@ export default function() {
|
|
|
117
124
|
|
|
118
125
|
el.parentNode.replaceChild(apos.area.activeEditor.$el, el);
|
|
119
126
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
const rect = el.getBoundingClientRect();
|
|
128
|
+
const isInViewport = rect.bottom >= 0 &&
|
|
129
|
+
rect.top <= window.innerHeight;
|
|
130
|
+
|
|
131
|
+
if (isInViewport) {
|
|
132
|
+
mountApp();
|
|
133
|
+
} else {
|
|
134
|
+
observer = new IntersectionObserver(observed, {
|
|
135
|
+
rootMargin: '600px'
|
|
136
|
+
});
|
|
137
|
+
observer.observe(el);
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
function observed(entries) {
|
|
@@ -129,8 +144,14 @@ export default function() {
|
|
|
129
144
|
return;
|
|
130
145
|
}
|
|
131
146
|
if (created) {
|
|
147
|
+
observer.disconnect();
|
|
132
148
|
return;
|
|
133
149
|
}
|
|
150
|
+
mountApp();
|
|
151
|
+
observer.disconnect();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function mountApp() {
|
|
134
155
|
const app = createApp(component, {
|
|
135
156
|
options,
|
|
136
157
|
id: data._id,
|
|
@@ -142,10 +163,21 @@ export default function() {
|
|
|
142
163
|
parentOptions,
|
|
143
164
|
renderings
|
|
144
165
|
});
|
|
166
|
+
|
|
167
|
+
// Resolve graphKey: if this area is inside a modal that owns a
|
|
168
|
+
// graph (data-apos-graph-key), use that key. Otherwise fall back
|
|
169
|
+
// to the on-page contextId. This single DOM lookup bridges the
|
|
170
|
+
// provide/inject gap created by createApp.
|
|
171
|
+
const graphKey = el.closest('[data-apos-graph-key]')
|
|
172
|
+
?.getAttribute('data-apos-graph-key') || apos.adminBar?.contextId || null;
|
|
173
|
+
// Provide the resolved graphKey so every descendant component
|
|
174
|
+
// can simply inject('aposGraphKey') and get the correct value.
|
|
175
|
+
if (graphKey) {
|
|
176
|
+
app.provide('aposGraphKey', graphKey);
|
|
177
|
+
}
|
|
145
178
|
app.mount(el);
|
|
146
179
|
mountedApps.set(el, app);
|
|
147
180
|
created = true;
|
|
148
|
-
observer.disconnect();
|
|
149
181
|
}
|
|
150
182
|
}
|
|
151
183
|
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
v-click-outside-element="resetFocusedArea"
|
|
4
4
|
:data-apos-area="areaId"
|
|
5
5
|
class="apos-area"
|
|
6
|
-
:class="
|
|
6
|
+
:class="[
|
|
7
|
+
themeClass,
|
|
8
|
+
{
|
|
9
|
+
'apos-area--empty': next.length === 0
|
|
10
|
+
}
|
|
11
|
+
]"
|
|
7
12
|
@click="setFocusedArea(areaId, $event)"
|
|
8
13
|
>
|
|
9
14
|
<div
|
|
@@ -61,6 +66,12 @@
|
|
|
61
66
|
:disabled="field && field.readOnly"
|
|
62
67
|
:max-reached="maxReached"
|
|
63
68
|
:rendering="rendering(widget)"
|
|
69
|
+
:raised="raisedWidgets.has(widget._id)"
|
|
70
|
+
:style="{
|
|
71
|
+
'z-index': raisedWidgets.has(widget._id)
|
|
72
|
+
? next.length + 1
|
|
73
|
+
: null
|
|
74
|
+
}"
|
|
64
75
|
@up="up"
|
|
65
76
|
@down="down"
|
|
66
77
|
@remove="remove"
|
|
@@ -23,14 +23,13 @@
|
|
|
23
23
|
@keyup.enter="onKeyup"
|
|
24
24
|
>
|
|
25
25
|
<div
|
|
26
|
-
v-if="!breadcrumbDisabled"
|
|
27
26
|
ref="label"
|
|
28
27
|
class="apos-area-widget-controls apos-area-widget__label"
|
|
29
28
|
:class="labelsClasses"
|
|
30
29
|
>
|
|
31
30
|
<ol
|
|
32
31
|
class="apos-area-widget__breadcrumbs"
|
|
33
|
-
@click="
|
|
32
|
+
@click="clearSuppressionFlags"
|
|
34
33
|
>
|
|
35
34
|
<li
|
|
36
35
|
class="
|
|
@@ -92,7 +91,7 @@
|
|
|
92
91
|
/>
|
|
93
92
|
</div>
|
|
94
93
|
<div
|
|
95
|
-
v-if="!controlsDisabled"
|
|
94
|
+
v-if="!controlsDisabled && !maxReached && !isSuppressingAddContentButtons"
|
|
96
95
|
class="
|
|
97
96
|
apos-area-widget-controls
|
|
98
97
|
apos-area-widget-controls--add--top
|
|
@@ -143,43 +142,52 @@
|
|
|
143
142
|
@operation="onOperation"
|
|
144
143
|
/>
|
|
145
144
|
</div>
|
|
146
|
-
|
|
147
|
-
<!-- Still used for contextual editing components -->
|
|
148
|
-
<component
|
|
149
|
-
:is="widgetEditorComponent(widget.type)"
|
|
150
|
-
v-if="isContextual && !foreign"
|
|
151
|
-
:key="generation"
|
|
152
|
-
:class="adminContentDirectionClass"
|
|
153
|
-
:options="widgetOptions"
|
|
154
|
-
:type="widget.type"
|
|
155
|
-
:model-value="widget"
|
|
156
|
-
:meta="meta"
|
|
157
|
-
:doc-id="docId"
|
|
158
|
-
:focused="isFocused"
|
|
159
|
-
@update="$emit('update', $event)"
|
|
160
|
-
@suppress-widget-controls="isSuppressingWidgetControls = true"
|
|
161
|
-
/>
|
|
162
|
-
<component
|
|
163
|
-
:is="widgetComponent(widget.type)"
|
|
164
|
-
v-else
|
|
165
|
-
:id="widget._id"
|
|
166
|
-
:key="`${generation}-preview`"
|
|
167
|
-
:class="adminContentDirectionClass"
|
|
168
|
-
:options="widgetOptions"
|
|
169
|
-
:type="widget.type"
|
|
170
|
-
:area-field-id="fieldId"
|
|
171
|
-
:following-values="followingValuesWithParent"
|
|
172
|
-
:model-value="widget"
|
|
173
|
-
:value="widget"
|
|
174
|
-
:meta="meta"
|
|
175
|
-
:foreign="foreign"
|
|
176
|
-
:doc-id="docId"
|
|
177
|
-
:rendering="rendering"
|
|
178
|
-
@edit="$emit('edit', i);"
|
|
179
|
-
@update="$emit('update', $event);"
|
|
180
|
-
/>
|
|
181
145
|
<div
|
|
182
|
-
|
|
146
|
+
class="apos-area-widget-rendered-widget"
|
|
147
|
+
:style="{
|
|
148
|
+
'z-index': raised
|
|
149
|
+
? 0
|
|
150
|
+
: null
|
|
151
|
+
}"
|
|
152
|
+
>
|
|
153
|
+
<!-- Still used for contextual editing components -->
|
|
154
|
+
<component
|
|
155
|
+
:is="widgetEditorComponent(widget.type)"
|
|
156
|
+
v-if="isContextual && !foreign"
|
|
157
|
+
:key="generation"
|
|
158
|
+
:class="adminContentDirectionClass"
|
|
159
|
+
:options="widgetOptions"
|
|
160
|
+
:type="widget.type"
|
|
161
|
+
:model-value="widget"
|
|
162
|
+
:meta="meta"
|
|
163
|
+
:doc-id="docId"
|
|
164
|
+
:focused="isFocused"
|
|
165
|
+
@update="$emit('update', $event)"
|
|
166
|
+
@suppress-widget-controls="doSuppressWidgetControls"
|
|
167
|
+
@suppress-add-content-buttons="doSuppressAddContentButtons"
|
|
168
|
+
/>
|
|
169
|
+
<component
|
|
170
|
+
:is="widgetComponent(widget.type)"
|
|
171
|
+
v-else
|
|
172
|
+
:id="widget._id"
|
|
173
|
+
:key="`${generation}-preview`"
|
|
174
|
+
:class="adminContentDirectionClass"
|
|
175
|
+
:options="widgetOptions"
|
|
176
|
+
:type="widget.type"
|
|
177
|
+
:area-field-id="fieldId"
|
|
178
|
+
:following-values="followingValuesWithParent"
|
|
179
|
+
:model-value="widget"
|
|
180
|
+
:value="widget"
|
|
181
|
+
:meta="meta"
|
|
182
|
+
:foreign="foreign"
|
|
183
|
+
:doc-id="docId"
|
|
184
|
+
:rendering="rendering"
|
|
185
|
+
@edit="$emit('edit', i);"
|
|
186
|
+
@update="$emit('update', $event);"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<div
|
|
190
|
+
v-if="!controlsDisabled && !maxReached && !isSuppressingAddContentButtons"
|
|
183
191
|
class="
|
|
184
192
|
apos-area-widget-controls
|
|
185
193
|
apos-area-widget-controls--add
|
|
@@ -209,13 +217,25 @@
|
|
|
209
217
|
|
|
210
218
|
<script>
|
|
211
219
|
import { mapState, mapActions } from 'pinia';
|
|
220
|
+
import { unref } from 'vue';
|
|
212
221
|
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
|
|
213
222
|
import { useBreakpointPreviewStore } from 'Modules/@apostrophecms/ui/stores/breakpointPreview.js';
|
|
214
223
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal.js';
|
|
224
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph.js';
|
|
215
225
|
|
|
216
226
|
export default {
|
|
217
227
|
name: 'AposAreaWidget',
|
|
228
|
+
inject: {
|
|
229
|
+
aposGraphKey: {
|
|
230
|
+
from: 'aposGraphKey',
|
|
231
|
+
default: null
|
|
232
|
+
}
|
|
233
|
+
},
|
|
218
234
|
props: {
|
|
235
|
+
raised: {
|
|
236
|
+
type: Boolean,
|
|
237
|
+
default: false
|
|
238
|
+
},
|
|
219
239
|
docId: {
|
|
220
240
|
type: String,
|
|
221
241
|
required: false,
|
|
@@ -316,6 +336,7 @@ export default {
|
|
|
316
336
|
mounted: false, // hack around needing DOM to be rendered for computed classes
|
|
317
337
|
menuOpen: null,
|
|
318
338
|
isSuppressingWidgetControls: false,
|
|
339
|
+
isSuppressingAddContentButtons: false,
|
|
319
340
|
hasClickOutsideListener: false,
|
|
320
341
|
classes: {
|
|
321
342
|
show: 'apos-is-visible',
|
|
@@ -447,7 +468,7 @@ export default {
|
|
|
447
468
|
},
|
|
448
469
|
labelsClasses() {
|
|
449
470
|
return {
|
|
450
|
-
[this.classes.show]: this.isHovered
|
|
471
|
+
[this.classes.show]: (this.isHovered && !this.focusedWidget) || this.isFocused,
|
|
451
472
|
// Force LTR for breadcrumbs for now so that nested AreaWidgets
|
|
452
473
|
// behave properly.
|
|
453
474
|
'apos-ltr': true
|
|
@@ -455,7 +476,7 @@ export default {
|
|
|
455
476
|
},
|
|
456
477
|
addClasses() {
|
|
457
478
|
return {
|
|
458
|
-
[this.classes.show]: this.isHovered || this.isFocused,
|
|
479
|
+
[this.classes.show]: (this.isHovered && !this.focusedWidget) || this.isFocused,
|
|
459
480
|
[`${this.classes.open}--menu-${this.menuOpen}`]: !!this.menuOpen
|
|
460
481
|
};
|
|
461
482
|
},
|
|
@@ -493,6 +514,7 @@ export default {
|
|
|
493
514
|
} else {
|
|
494
515
|
this.menuOpen = null;
|
|
495
516
|
this.isSuppressingWidgetControls = false;
|
|
517
|
+
this.isSuppressingAddContentButtons = false;
|
|
496
518
|
this.removeClickOutsideListener();
|
|
497
519
|
}
|
|
498
520
|
// Helps get scroll tracking unstuck on new/modified widgets
|
|
@@ -515,11 +537,14 @@ export default {
|
|
|
515
537
|
// a 'focus my parent' plea
|
|
516
538
|
apos.bus.$on('widget-focus-parent', this.focusParent);
|
|
517
539
|
apos.bus.$on('context-menu-toggled', this.getFocusForMenu);
|
|
540
|
+
apos.bus.$on('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
518
541
|
|
|
519
542
|
this.breadcrumbs.$lastEl = this.$el;
|
|
520
543
|
|
|
521
544
|
this.getBreadcrumbs();
|
|
522
545
|
|
|
546
|
+
this.registerInGraph();
|
|
547
|
+
|
|
523
548
|
if (this.focusedWidget) {
|
|
524
549
|
// If another widget was in focus (because the user clicked the "add"
|
|
525
550
|
// menu, for example), and this widget was created, give the new widget
|
|
@@ -549,12 +574,50 @@ export default {
|
|
|
549
574
|
unmounted() {
|
|
550
575
|
// Remove the focus parent listener when unmounted
|
|
551
576
|
apos.bus.$off('widget-focus-parent', this.focusParent);
|
|
577
|
+
apos.bus.$off('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
552
578
|
window.removeEventListener('scroll', this.stickyControlsScroll);
|
|
553
579
|
window.removeEventListener('resize', this.stickyControlsResize);
|
|
580
|
+
this.unregisterFromGraph();
|
|
554
581
|
},
|
|
555
582
|
methods: {
|
|
583
|
+
clearSuppressionFlags() {
|
|
584
|
+
this.isSuppressingWidgetControls = false;
|
|
585
|
+
this.isSuppressingAddContentButtons = false;
|
|
586
|
+
},
|
|
556
587
|
...mapActions(useWidgetStore, [ 'setFocusedWidget', 'setHoveredWidget' ]),
|
|
557
588
|
...mapActions(useModalStore, [ 'getAdminContentDirectionClass' ]),
|
|
589
|
+
...mapActions(useWidgetGraphStore, {
|
|
590
|
+
storeRegisterWidget: 'registerWidget',
|
|
591
|
+
storeUnregisterWidget: 'unregisterWidget'
|
|
592
|
+
}),
|
|
593
|
+
registerInGraph() {
|
|
594
|
+
if (this.foreign) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const graphKey = unref(this.aposGraphKey);
|
|
598
|
+
if (!graphKey) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
this.storeRegisterWidget(graphKey, this.widget, {
|
|
602
|
+
areaId: this.areaId
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
unregisterFromGraph() {
|
|
606
|
+
if (this.foreign) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const graphKey = unref(this.aposGraphKey);
|
|
610
|
+
if (!graphKey) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
this.storeUnregisterWidget(graphKey, this.widget._id);
|
|
614
|
+
},
|
|
615
|
+
doSuppressWidgetControls() {
|
|
616
|
+
this.isSuppressingWidgetControls = true;
|
|
617
|
+
},
|
|
618
|
+
doSuppressAddContentButtons() {
|
|
619
|
+
this.isSuppressingAddContentButtons = true;
|
|
620
|
+
},
|
|
558
621
|
// Emits same actions as the native operations,
|
|
559
622
|
// e.g ('edit', { index }), ('remove', { index }), etc.
|
|
560
623
|
onOperation({ name, payload }) {
|
|
@@ -716,6 +779,9 @@ export default {
|
|
|
716
779
|
}
|
|
717
780
|
},
|
|
718
781
|
unfocus(event) {
|
|
782
|
+
if (event.target.closest('[data-apos-ignore-unfocus="true"]')) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
719
785
|
if (!this.$el.contains(event.target)) {
|
|
720
786
|
this.removeClickOutsideListener();
|
|
721
787
|
|
|
@@ -925,6 +991,10 @@ export default {
|
|
|
925
991
|
}
|
|
926
992
|
}
|
|
927
993
|
|
|
994
|
+
.apos-area-widget-rendered-widget {
|
|
995
|
+
position: relative;
|
|
996
|
+
}
|
|
997
|
+
|
|
928
998
|
.apos-area-widget-controls {
|
|
929
999
|
z-index: $z-index-widget-controls;
|
|
930
1000
|
position: absolute;
|
|
@@ -97,17 +97,29 @@ export default {
|
|
|
97
97
|
};
|
|
98
98
|
},
|
|
99
99
|
widgetPrimaryControls() {
|
|
100
|
+
const removeForSingleWidget = [ 'nudgeUp', 'nudgeDown' ];
|
|
100
101
|
// Custom widget operations displayed in the primary controls
|
|
101
|
-
return this.widgetPrimaryOperations
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
return this.widgetPrimaryOperations
|
|
103
|
+
.map(operation => {
|
|
104
|
+
const disabled = this.disabled || isOperationDisabled(operation, this.$props);
|
|
105
|
+
const tooltip = getOperationTooltip(operation, { disabled });
|
|
106
|
+
return {
|
|
107
|
+
...this.widgetDefaultControl,
|
|
108
|
+
...operation,
|
|
109
|
+
disabled,
|
|
110
|
+
tooltip
|
|
111
|
+
};
|
|
112
|
+
})
|
|
113
|
+
.filter(operation => {
|
|
114
|
+
if (
|
|
115
|
+
removeForSingleWidget.includes(operation.action) &&
|
|
116
|
+
this.first &&
|
|
117
|
+
this.last
|
|
118
|
+
) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
111
123
|
},
|
|
112
124
|
widgetSecondaryControls() {
|
|
113
125
|
const renderOperation = (operation) => {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { createId } from '@paralleldrive/cuid2';
|
|
2
|
+
import { unref } from 'vue';
|
|
2
3
|
import { mapState, mapActions } from 'pinia';
|
|
3
4
|
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
|
|
4
5
|
import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
|
|
5
6
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
|
|
6
7
|
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
|
|
8
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph';
|
|
7
9
|
import cloneWidget from 'Modules/@apostrophecms/area/lib/clone-widget.js';
|
|
8
10
|
import { klona } from 'klona';
|
|
9
11
|
|
|
10
12
|
export default {
|
|
11
13
|
mixins: [ AposThemeMixin ],
|
|
14
|
+
inject: {
|
|
15
|
+
aposGraphKey: {
|
|
16
|
+
from: 'aposGraphKey',
|
|
17
|
+
default: null
|
|
18
|
+
}
|
|
19
|
+
},
|
|
12
20
|
props: {
|
|
13
21
|
docId: {
|
|
14
22
|
type: String,
|
|
@@ -130,6 +138,35 @@ export default {
|
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
return this.next.findIndex(widget => widget._id === this.focusedWidget);
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Set of widget _ids (from `next`) that should have a raised z-index
|
|
144
|
+
* because they are, or contain, the currently focused widget.
|
|
145
|
+
* Computed once per focusedWidget / graph change; O(depth) ancestor
|
|
146
|
+
* walk + O(1) per-widget lookup in the template.
|
|
147
|
+
*/
|
|
148
|
+
raisedWidgets() {
|
|
149
|
+
const raised = new Set();
|
|
150
|
+
if (!this.focusedWidget) {
|
|
151
|
+
return raised;
|
|
152
|
+
}
|
|
153
|
+
const graphKey = unref(this.aposGraphKey);
|
|
154
|
+
if (!graphKey) {
|
|
155
|
+
// No graph — fall back to exact match only
|
|
156
|
+
if (this.next.some(w => w._id === this.focusedWidget)) {
|
|
157
|
+
raised.add(this.focusedWidget);
|
|
158
|
+
}
|
|
159
|
+
return raised;
|
|
160
|
+
}
|
|
161
|
+
const ancestors = this.storeGetAncestors(graphKey, this.focusedWidget);
|
|
162
|
+
const chain = new Set([ this.focusedWidget, ...ancestors ]);
|
|
163
|
+
|
|
164
|
+
for (const widget of this.next) {
|
|
165
|
+
if (chain.has(widget._id)) {
|
|
166
|
+
raised.add(widget._id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return raised;
|
|
133
170
|
}
|
|
134
171
|
},
|
|
135
172
|
watch: {
|
|
@@ -176,6 +213,9 @@ export default {
|
|
|
176
213
|
methods: {
|
|
177
214
|
...mapActions(useWidgetStore, [ 'setFocusedArea', 'setFocusedWidget' ]),
|
|
178
215
|
...mapActions(useModalStore, [ 'isOnTop' ]),
|
|
216
|
+
...mapActions(useWidgetGraphStore, {
|
|
217
|
+
storeGetAncestors: 'getAncestors'
|
|
218
|
+
}),
|
|
179
219
|
bindEventListeners() {
|
|
180
220
|
apos.bus.$on('area-updated', this.areaUpdatedHandler);
|
|
181
221
|
apos.bus.$on('command-menu-area-copy-widget', this.handleCopy);
|
|
@@ -1187,9 +1187,10 @@ module.exports = {
|
|
|
1187
1187
|
if (!self.shouldRefreshOnRestart()) {
|
|
1188
1188
|
return '';
|
|
1189
1189
|
}
|
|
1190
|
+
const prefix = self.apos.prefix || '';
|
|
1190
1191
|
return self.apos.template.safe(
|
|
1191
|
-
`<script data-apos-refresh-on-restart="${self.action}/restart-id" ` +
|
|
1192
|
-
`src="${self.action}/refresh-on-restart"></script>`
|
|
1192
|
+
`<script data-apos-refresh-on-restart="${prefix}${self.action}/restart-id" ` +
|
|
1193
|
+
`src="${prefix}${self.action}/refresh-on-restart"></script>`
|
|
1193
1194
|
);
|
|
1194
1195
|
},
|
|
1195
1196
|
// Return the URL of the release asset with the given path, taking into
|