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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(timeout 180 npx mocha:*)",
|
|
5
|
+
"Bash(timeout 600 npx mocha:*)",
|
|
6
|
+
"Bash(npm ls:*)",
|
|
7
|
+
"Bash(timeout 540 npx mocha:*)",
|
|
8
|
+
"Bash(echo:*)",
|
|
9
|
+
"Bash(timeout 10 node:*)",
|
|
10
|
+
"Bash(timeout 300 npx mocha:*)",
|
|
11
|
+
"Bash(timeout 60 npx mocha:*)",
|
|
12
|
+
"Bash(timeout 120 npx mocha:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 4.30.0
|
|
3
|
+
## 4.30.0
|
|
4
4
|
|
|
5
5
|
### Adds
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Layout widget gap is now controllable through the styles system, both site-wide via a global `layoutGap` preset and per widget via a `gap` styles field. A new `className` option allows additional CSS classes to be added to the widget grid container.
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
|
|
11
|
+
- Fixed layout widget not regaining full focus when switching back to Edit content mode.
|
|
12
|
+
- Fixed illegal HTML `id` attribute values generated by the admin UI.
|
|
13
|
+
- Fixed orderable table array items dragging the entire floating window.
|
|
14
|
+
- Fixed keyboard shortcuts for widget operations (copy, cut, paste, duplicate, remove) blocking the browser's native clipboard behavior when no widget was focused. Previously, selecting and copying text while logged in was intercepted unconditionally by the admin UI.
|
|
15
|
+
- Removed duplicate `<meta charset>` tag from `outerLayoutBase.html` and standardized charset to `utf-8`.
|
|
16
|
+
- Updated `apostrophe` and `oembetter` to remove oembed services that no longer support public access, eliminating them as a potential future XSS vector. New `minimumAllowlist` and `minimumEndpoints` options on `@apostrophecms/oembed` allow developers to prune the list further.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- **Password reset base URL requirement:** The password reset feature now refuses to operate unless `baseUrl` or `APOS_BASE_URL` is set, preventing a vulnerability where ApostropheCMS could be convinced to send emails with links to attacker-controlled sites. Only affects projects with `passwordReset: true` on the login module. Thanks to [SPIDY](https://github.com/Mujahidkhan525) for reporting.
|
|
21
|
+
- **XSS via full name field:** A malicious full name containing HTML was executed in the page title tooltip in the admin bar, posing an XSS risk to other users. All multi-user projects should update promptly. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
|
|
22
|
+
- **XSS via image widget link URL:** Users with editing privileges could trigger arbitrary JavaScript via a `javascript:` URL in the image widget's link URL field. A migration is included to strip any such URLs already in the database. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
|
|
23
|
+
- **SSRF via rich text HTML import:** The rich text widget's HTML import feature no longer fetches images from arbitrary hosts, which could be used to probe internal networks or exfiltrate internal images. Configure `imageImportAllowedHostnames` on `@apostrophecms/rich-text-widget` to opt in. Thanks to [Yiğit Şengezer](https://github.com/yigitsengezer) and [Sainithin0309](https://github.com/Sainithin0309) for reporting.
|
|
24
|
+
- **the xmp tag could be used to pass forbidden markup through sanitize-html**, even when xmp itself. This was fixed in `sanitize-html` and the dependency was bumped. Thanks to [Vincenzo Turturro](https://github.com/sushi-gif) for reporting the vulnerability.
|
|
25
|
+
- **the `linkHref` field of image widgets was an XSS vulnerability** because it did not use the `url` field type. This means that a user with editing privileges could potentially carry out XSS. In addition, we have updated the `launder` module to sanitize URLs more robustly for the `url` field type, and bumped that dependency. Also, a database migration is included to clean any XSS attacks that could be present in existing links. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
26
|
+
|
|
27
|
+
### Accessibility
|
|
28
|
+
|
|
29
|
+
- Corrected ARIA semantics on the top admin navigation bar.
|
|
30
|
+
- Improved the document context title (admin bar middle group) and the underlying `AposContextMenu` machinery.
|
|
31
|
+
- Improved the locale switcher (`AposLocalePicker`).
|
|
32
|
+
- The Recently Edited Documents tray icon now exposes its action via `aria-label`.
|
|
33
|
+
- Fixed `.apos-sr-only` so screen-reader-only content is correctly exposed to the accessibility tree.
|
|
34
|
+
- Icon-only context-utility buttons in the admin bar tray (e.g. the global settings cog) now expose their action via `aria-label`.
|
|
35
|
+
|
|
8
36
|
|
|
9
37
|
## 4.29.0 (2026-04-15)
|
|
10
38
|
|
package/eslint.config.js
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const mongo = require('@apostrophecms/emulate-mongo-3-driver');
|
|
2
|
+
const dns = require('dns');
|
|
3
|
+
|
|
4
|
+
// Connect to MongoDB, using the modern topology and parser, and
|
|
5
|
+
// a tolerant policy to successfully connect to "localhost" even if
|
|
6
|
+
// the first record returned by the resolver doesn't reach mongodb's
|
|
7
|
+
// bind address because localhost resolves first to ::1 (ipv6) and
|
|
8
|
+
// mongodb lists only on 127.0.0.1 (ipv4) by default. For broadest
|
|
9
|
+
// compatibility we don't assume we know this will happen, we try all the
|
|
10
|
+
// addresses that localhost actually resolves to and succeed with the
|
|
11
|
+
// first one that works.
|
|
12
|
+
|
|
13
|
+
module.exports = async (uri, options) => {
|
|
14
|
+
const connectOptions = {
|
|
15
|
+
useUnifiedTopology: true,
|
|
16
|
+
useNewUrlParser: true,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(uri);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
// Parse failed, e.g. old school replica set URI
|
|
24
|
+
// with commas, just let the mongo driver handle it
|
|
25
|
+
return mongo.MongoClient.connect(uri, connectOptions);
|
|
26
|
+
}
|
|
27
|
+
if (!parsed || (parsed.protocol !== 'mongodb:') || (parsed.hostname !== 'localhost')) {
|
|
28
|
+
return mongo.MongoClient.connect(parsed.toString(), connectOptions);
|
|
29
|
+
}
|
|
30
|
+
const records = await dns.promises.lookup('localhost', { all: true });
|
|
31
|
+
if (!records.length) {
|
|
32
|
+
// The computer that reaches this point has bigger problems 😅
|
|
33
|
+
throw new Error('Unable to resolve localhost to an IP address.');
|
|
34
|
+
}
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let failed = 0;
|
|
37
|
+
let succeeded = false;
|
|
38
|
+
records.forEach(attempt);
|
|
39
|
+
async function attempt(record) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = new URL(uri);
|
|
42
|
+
parsed.hostname = record.address;
|
|
43
|
+
const result = await mongo.MongoClient.connect(parsed.toString(), connectOptions);
|
|
44
|
+
if (!succeeded) {
|
|
45
|
+
succeeded = true;
|
|
46
|
+
resolve(result);
|
|
47
|
+
} else {
|
|
48
|
+
// We succeeded in reaching localhost at both ip4 and ip6,
|
|
49
|
+
// but we only need one of them to succeed
|
|
50
|
+
await result.close();
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
failed++;
|
|
54
|
+
if (failed === records.length) {
|
|
55
|
+
// None succeeded, so reject with the last error
|
|
56
|
+
// (which one we reject with doesn't really matter)
|
|
57
|
+
reject(e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<ol
|
|
3
|
-
class="apos-admin-bar__items"
|
|
4
|
-
role="menu"
|
|
5
|
-
>
|
|
2
|
+
<ol class="apos-admin-bar__items">
|
|
6
3
|
<li
|
|
7
4
|
v-if="pageTree"
|
|
8
5
|
class="apos-admin-bar__item"
|
|
@@ -12,7 +9,6 @@
|
|
|
12
9
|
label="apostrophe:pages"
|
|
13
10
|
class="apos-admin-bar__btn"
|
|
14
11
|
:modifiers="['no-motion']"
|
|
15
|
-
role="menuitem"
|
|
16
12
|
action-test-label="page-manager-button"
|
|
17
13
|
@click="emitEvent({ action: '@apostrophecms/page:manager' })"
|
|
18
14
|
/>
|
|
@@ -33,7 +29,6 @@
|
|
|
33
29
|
class: 'apos-admin-bar__btn',
|
|
34
30
|
type: 'subtle'
|
|
35
31
|
}"
|
|
36
|
-
role="menuitem"
|
|
37
32
|
@item-clicked="emitEvent"
|
|
38
33
|
/>
|
|
39
34
|
<Component
|
|
@@ -44,7 +39,6 @@
|
|
|
44
39
|
:modifiers="['no-motion']"
|
|
45
40
|
class="apos-admin-bar__btn"
|
|
46
41
|
:action-test-label="`${item.name}-manager-button`"
|
|
47
|
-
role="menuitem"
|
|
48
42
|
@click="emitEvent(item)"
|
|
49
43
|
/>
|
|
50
44
|
</li>
|
|
@@ -62,7 +56,6 @@
|
|
|
62
56
|
type: 'primary',
|
|
63
57
|
modifiers: ['round', 'no-motion']
|
|
64
58
|
}"
|
|
65
|
-
role="menuitem"
|
|
66
59
|
@item-clicked="emitEvent"
|
|
67
60
|
/>
|
|
68
61
|
</li>
|
|
@@ -89,6 +82,7 @@
|
|
|
89
82
|
:label="item.label"
|
|
90
83
|
:action="item.action"
|
|
91
84
|
:state="trayItemState[item.name] ? [ 'active' ] : []"
|
|
85
|
+
:attrs="trayItemAttrs(item)"
|
|
92
86
|
@click="emitEvent(item)"
|
|
93
87
|
/>
|
|
94
88
|
</template>
|
|
@@ -185,6 +179,29 @@ export default {
|
|
|
185
179
|
} else {
|
|
186
180
|
return item.options.tooltip;
|
|
187
181
|
}
|
|
182
|
+
},
|
|
183
|
+
// Tray utility buttons render icon-only, so the visible label
|
|
184
|
+
// (e.g. "Global Content") is sr-only and doesn't describe what the
|
|
185
|
+
// button does. Make them accessible by providing an aria-label based on
|
|
186
|
+
// the tooltip content.
|
|
187
|
+
trayItemAttrs(item) {
|
|
188
|
+
const tooltip = item.options?.tooltip;
|
|
189
|
+
let key = null;
|
|
190
|
+
if (item.options?.toggle) {
|
|
191
|
+
if (this.trayItemState[item.name] && tooltip?.deactivate) {
|
|
192
|
+
key = tooltip.deactivate;
|
|
193
|
+
} else if (tooltip?.activate) {
|
|
194
|
+
key = tooltip.activate;
|
|
195
|
+
}
|
|
196
|
+
} else if (typeof tooltip === 'string') {
|
|
197
|
+
key = tooltip;
|
|
198
|
+
} else if (tooltip && typeof tooltip.content === 'string') {
|
|
199
|
+
key = tooltip.content;
|
|
200
|
+
}
|
|
201
|
+
if (!key) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
return { 'aria-label': this.$t(key) };
|
|
188
205
|
}
|
|
189
206
|
}
|
|
190
207
|
};
|
package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
:label="screen.label"
|
|
15
15
|
:tooltip="$t(screen.label)"
|
|
16
16
|
:title="$t(screen.label)"
|
|
17
|
+
:attrs="shortcutAttrs(screen)"
|
|
17
18
|
:icon="screen.icon"
|
|
18
19
|
:icon-only="true"
|
|
19
20
|
type="subtle"
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
:active-item="mode"
|
|
37
38
|
:center-on-icon="true"
|
|
38
39
|
menu-placement="bottom-end"
|
|
40
|
+
:dialog-label="'apostrophe:breakpointPreviewSelectMenu'"
|
|
39
41
|
@item-clicked="selectBreakpoint"
|
|
40
42
|
/>
|
|
41
43
|
<Transition>
|
|
@@ -320,6 +322,13 @@ export default {
|
|
|
320
322
|
},
|
|
321
323
|
setShowDropdown() {
|
|
322
324
|
this.showDropdown = Object.values(this.screens).some(({ shortcut }) => !shortcut);
|
|
325
|
+
},
|
|
326
|
+
shortcutAttrs(screen) {
|
|
327
|
+
return {
|
|
328
|
+
'aria-label': this.$t('apostrophe:breakpointPreviewShortcut', {
|
|
329
|
+
breakpoint: this.$t(screen.label)
|
|
330
|
+
})
|
|
331
|
+
};
|
|
323
332
|
}
|
|
324
333
|
}
|
|
325
334
|
};
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
:disabled="hasCustomUi || isUnpublished"
|
|
34
34
|
:center-on-icon="true"
|
|
35
35
|
menu-placement="bottom-end"
|
|
36
|
+
:dialog-label="'apostrophe:publicationStatusMenu'"
|
|
37
|
+
:trigger-aria-label="draftTriggerAriaLabel"
|
|
36
38
|
@item-clicked="switchDraftMode"
|
|
37
39
|
/>
|
|
38
40
|
<AposLabel
|
|
@@ -56,6 +58,15 @@
|
|
|
56
58
|
<script>
|
|
57
59
|
import dayjs from 'dayjs';
|
|
58
60
|
|
|
61
|
+
function escapeHtml(s) {
|
|
62
|
+
return String(s)
|
|
63
|
+
.replace(/&/g, '&')
|
|
64
|
+
.replace(/</g, '<')
|
|
65
|
+
.replace(/>/g, '>')
|
|
66
|
+
.replace(/"/g, '"')
|
|
67
|
+
.replace(/'/g, ''');
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
export default {
|
|
60
71
|
name: 'TheAposContextTitle',
|
|
61
72
|
props: {
|
|
@@ -78,8 +89,8 @@ export default {
|
|
|
78
89
|
if (this.context.updatedBy) {
|
|
79
90
|
const editor = this.context.updatedBy;
|
|
80
91
|
editorLabel = '';
|
|
81
|
-
editorLabel += editor.title ? `${editor.title} ` : '';
|
|
82
|
-
editorLabel += editor.username ? `(${editor.username})` : '';
|
|
92
|
+
editorLabel += editor.title ? `${escapeHtml(editor.title)} ` : '';
|
|
93
|
+
editorLabel += editor.username ? `(${escapeHtml(editor.username)})` : '';
|
|
83
94
|
}
|
|
84
95
|
return editorLabel;
|
|
85
96
|
},
|
|
@@ -91,6 +102,13 @@ export default {
|
|
|
91
102
|
type: 'quiet'
|
|
92
103
|
};
|
|
93
104
|
},
|
|
105
|
+
draftTriggerAriaLabel() {
|
|
106
|
+
return this.$t('apostrophe:publicationStatusTrigger', {
|
|
107
|
+
status: this.$t(
|
|
108
|
+
this.draftMode === 'draft' ? 'apostrophe:draft' : 'apostrophe:published'
|
|
109
|
+
)
|
|
110
|
+
});
|
|
111
|
+
},
|
|
94
112
|
isUnpublished() {
|
|
95
113
|
return !this.context.lastPublishedAt;
|
|
96
114
|
},
|
|
@@ -19,7 +19,8 @@ module.exports = {
|
|
|
19
19
|
action: {
|
|
20
20
|
type: 'command-menu-area-cut-widget'
|
|
21
21
|
},
|
|
22
|
-
shortcut: 'Ctrl+X Meta+X'
|
|
22
|
+
shortcut: 'Ctrl+X Meta+X',
|
|
23
|
+
requireWidgetFocus: true
|
|
23
24
|
},
|
|
24
25
|
[`${self.__meta.name}:copy-widget`]: {
|
|
25
26
|
type: 'item',
|
|
@@ -27,7 +28,8 @@ module.exports = {
|
|
|
27
28
|
action: {
|
|
28
29
|
type: 'command-menu-area-copy-widget'
|
|
29
30
|
},
|
|
30
|
-
shortcut: 'Ctrl+C Meta+C'
|
|
31
|
+
shortcut: 'Ctrl+C Meta+C',
|
|
32
|
+
requireWidgetFocus: true
|
|
31
33
|
},
|
|
32
34
|
[`${self.__meta.name}:paste-widget`]: {
|
|
33
35
|
type: 'item',
|
|
@@ -35,7 +37,8 @@ module.exports = {
|
|
|
35
37
|
action: {
|
|
36
38
|
type: 'command-menu-area-paste-widget'
|
|
37
39
|
},
|
|
38
|
-
shortcut: 'Ctrl+V Meta+V'
|
|
40
|
+
shortcut: 'Ctrl+V Meta+V',
|
|
41
|
+
requireWidgetFocus: true
|
|
39
42
|
},
|
|
40
43
|
[`${self.__meta.name}:duplicate-widget`]: {
|
|
41
44
|
type: 'item',
|
|
@@ -43,7 +46,8 @@ module.exports = {
|
|
|
43
46
|
action: {
|
|
44
47
|
type: 'command-menu-area-duplicate-widget'
|
|
45
48
|
},
|
|
46
|
-
shortcut: 'Ctrl+Shift+D Meta+Shift+D'
|
|
49
|
+
shortcut: 'Ctrl+Shift+D Meta+Shift+D',
|
|
50
|
+
requireWidgetFocus: true
|
|
47
51
|
},
|
|
48
52
|
[`${self.__meta.name}:remove-widget`]: {
|
|
49
53
|
type: 'item',
|
|
@@ -51,7 +55,8 @@ module.exports = {
|
|
|
51
55
|
action: {
|
|
52
56
|
type: 'command-menu-area-remove-widget'
|
|
53
57
|
},
|
|
54
|
-
shortcut: 'Backspace'
|
|
58
|
+
shortcut: 'Backspace',
|
|
59
|
+
requireWidgetFocus: true
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
modal: {
|
|
@@ -536,6 +536,7 @@ export default {
|
|
|
536
536
|
apos.bus.$on('widget-focus-parent', this.focusParent);
|
|
537
537
|
apos.bus.$on('context-menu-toggled', this.getFocusForMenu);
|
|
538
538
|
apos.bus.$on('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
539
|
+
apos.bus.$on('clear-focused-widget-control-suppression', this.clearSuppressionFlags);
|
|
539
540
|
|
|
540
541
|
this.breadcrumbs.$lastEl = this.$el;
|
|
541
542
|
|
|
@@ -573,6 +574,7 @@ export default {
|
|
|
573
574
|
// Remove the focus parent listener when unmounted
|
|
574
575
|
apos.bus.$off('widget-focus-parent', this.focusParent);
|
|
575
576
|
apos.bus.$off('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
577
|
+
apos.bus.$off('clear-focused-widget-control-suppression', this.clearSuppressionFlags);
|
|
576
578
|
window.removeEventListener('scroll', this.stickyControlsScroll);
|
|
577
579
|
window.removeEventListener('resize', this.stickyControlsResize);
|
|
578
580
|
this.unregisterFromGraph();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { mapActions, mapState } from 'pinia';
|
|
11
11
|
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
|
|
12
12
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
|
|
13
|
+
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
|
|
13
14
|
|
|
14
15
|
export default {
|
|
15
16
|
name: 'TheAposCommandMenu',
|
|
@@ -50,7 +51,13 @@ export default {
|
|
|
50
51
|
.flatMap(command => {
|
|
51
52
|
return command.shortcut
|
|
52
53
|
.split(' ')
|
|
53
|
-
.map(shortcut => [
|
|
54
|
+
.map(shortcut => [
|
|
55
|
+
shortcut.toUpperCase(),
|
|
56
|
+
{
|
|
57
|
+
...command.action,
|
|
58
|
+
requireWidgetFocus: command.requireWidgetFocus || false
|
|
59
|
+
}
|
|
60
|
+
]);
|
|
54
61
|
});
|
|
55
62
|
})
|
|
56
63
|
);
|
|
@@ -111,6 +118,9 @@ export default {
|
|
|
111
118
|
? keys.slice('SHIFT+'.length)
|
|
112
119
|
: keys];
|
|
113
120
|
if (action) {
|
|
121
|
+
if (action.requireWidgetFocus && !useWidgetStore().focusedWidget) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
114
124
|
event.preventDefault();
|
|
115
125
|
apos.bus.$emit(action.type, action.payload);
|
|
116
126
|
return;
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
//
|
|
5
5
|
// ### `uri`
|
|
6
6
|
//
|
|
7
|
-
// The
|
|
8
|
-
// and the postgres documentation.
|
|
7
|
+
// The MongoDB connection URI. See the [MongoDB URI documentation](https://docs.mongodb.com/manual/reference/connection-string/).
|
|
9
8
|
//
|
|
10
9
|
// ### `connect`
|
|
11
10
|
//
|
|
12
|
-
// If present, this object is passed on as options to
|
|
11
|
+
// If present, this object is passed on as options to MongoDB's "connect"
|
|
13
12
|
// method, along with the uri. See the [MongoDB connect settings documentation](http://mongodb.github.io/node-mongodb-native/2.2/reference/connecting/connection-settings/).
|
|
14
13
|
//
|
|
15
14
|
// By default, Apostrophe sets options to retry lost connections forever,
|
|
@@ -21,16 +20,9 @@
|
|
|
21
20
|
//
|
|
22
21
|
// ### `client`
|
|
23
22
|
//
|
|
24
|
-
// An existing MongoDB
|
|
23
|
+
// An existing MongoDB connection (MongoClient) object. If present, it is used
|
|
25
24
|
// and `uri`, `host`, `connect`, etc. are ignored.
|
|
26
25
|
//
|
|
27
|
-
// ### `adapters`
|
|
28
|
-
//
|
|
29
|
-
// An array of adapters, each of which must provide `name`, `connect(uri, options)`,
|
|
30
|
-
// and `protocols` properties. `name` may be used to override a core adapter,
|
|
31
|
-
// such as `postgres` or `mongodb`. `connect` must resolve to a client object
|
|
32
|
-
// supporting a sufficient subset of the mongodb API.
|
|
33
|
-
//
|
|
34
26
|
// ### `versionCheck`
|
|
35
27
|
//
|
|
36
28
|
// If `true`, check to make sure the database does not belong to an
|
|
@@ -57,15 +49,15 @@
|
|
|
57
49
|
// in your project. However you may find it easier to just use the
|
|
58
50
|
// `client` option.
|
|
59
51
|
|
|
60
|
-
const
|
|
61
|
-
const escapeHost = require('../../../lib/escape-host
|
|
52
|
+
const mongodbConnect = require('../../../lib/mongodb-connect');
|
|
53
|
+
const escapeHost = require('../../../lib/escape-host');
|
|
62
54
|
|
|
63
55
|
module.exports = {
|
|
64
56
|
options: {
|
|
65
57
|
versionCheck: true
|
|
66
58
|
},
|
|
67
59
|
async init(self) {
|
|
68
|
-
await self.
|
|
60
|
+
await self.connectToMongo();
|
|
69
61
|
await self.versionCheck();
|
|
70
62
|
},
|
|
71
63
|
handlers(self) {
|
|
@@ -89,12 +81,14 @@ module.exports = {
|
|
|
89
81
|
},
|
|
90
82
|
methods(self) {
|
|
91
83
|
return {
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
84
|
+
// Open the database connection. Always uses MongoClient with its
|
|
85
|
+
// sensible defaults. Builds a URI if necessary, so we can call it
|
|
86
|
+
// in a consistent way.
|
|
87
|
+
//
|
|
88
|
+
// One default we override: if the connection is lost, we keep
|
|
89
|
+
// attempting to reconnect forever. This is the most sensible behavior
|
|
90
|
+
// for a persistent process that requires MongoDB in order to operate.
|
|
91
|
+
async connectToMongo() {
|
|
98
92
|
if (self.options.client) {
|
|
99
93
|
// Reuse a single client connection http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#db
|
|
100
94
|
self.apos.dbClient = self.options.client;
|
|
@@ -102,67 +96,32 @@ module.exports = {
|
|
|
102
96
|
self.connectionReused = true;
|
|
103
97
|
return;
|
|
104
98
|
}
|
|
105
|
-
let uri;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
uri = viaEnv;
|
|
99
|
+
let uri = 'mongodb://';
|
|
100
|
+
if (process.env.APOS_MONGODB_URI) {
|
|
101
|
+
uri = process.env.APOS_MONGODB_URI;
|
|
109
102
|
} else if (self.options.uri) {
|
|
110
103
|
uri = self.options.uri;
|
|
111
104
|
} else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
if (self.options.user) {
|
|
106
|
+
uri += self.options.user + ':' + self.options.password + '@';
|
|
107
|
+
}
|
|
108
|
+
if (!self.options.host) {
|
|
109
|
+
self.options.host = 'localhost';
|
|
110
|
+
}
|
|
111
|
+
if (!self.options.port) {
|
|
112
|
+
self.options.port = 27017;
|
|
116
113
|
}
|
|
117
114
|
if (!self.options.name) {
|
|
118
115
|
self.options.name = self.apos.shortName;
|
|
119
116
|
}
|
|
120
|
-
|
|
121
|
-
const path = require('path');
|
|
122
|
-
uri = `sqlite://${path.resolve(self.apos.rootDir, 'data', self.options.name + '.sqlite')}`;
|
|
123
|
-
} else {
|
|
124
|
-
const credentials = self.options.user
|
|
125
|
-
? encodeURIComponent(self.options.user) + ':' + encodeURIComponent(self.options.password) + '@'
|
|
126
|
-
: '';
|
|
127
|
-
if (adapter === 'mongodb') {
|
|
128
|
-
if (!self.options.host) {
|
|
129
|
-
self.options.host = 'localhost';
|
|
130
|
-
}
|
|
131
|
-
if (!self.options.port) {
|
|
132
|
-
self.options.port = 27017;
|
|
133
|
-
}
|
|
134
|
-
uri = 'mongodb://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
|
|
135
|
-
} else {
|
|
136
|
-
// postgres or multipostgres
|
|
137
|
-
if (!self.options.host) {
|
|
138
|
-
self.options.host = 'localhost';
|
|
139
|
-
}
|
|
140
|
-
if (!self.options.port) {
|
|
141
|
-
self.options.port = 5432;
|
|
142
|
-
}
|
|
143
|
-
uri = adapter + '://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
117
|
+
uri += escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
|
|
146
118
|
}
|
|
147
119
|
|
|
148
|
-
self.apos.dbClient = await
|
|
149
|
-
...self.options.connect,
|
|
150
|
-
adapters: self.options.adapters
|
|
151
|
-
});
|
|
120
|
+
self.apos.dbClient = await mongodbConnect(uri, self.options.connect);
|
|
152
121
|
self.uri = uri;
|
|
153
122
|
// Automatically uses the db name in the connection string
|
|
154
123
|
self.apos.db = self.apos.dbClient.db();
|
|
155
124
|
},
|
|
156
|
-
// Connect to a database using the appropriate adapter based on the URI protocol.
|
|
157
|
-
// Returns a client object compatible with the MongoDB driver interface.
|
|
158
|
-
// This method has no side effects — it does not set apos.db or apos.dbClient.
|
|
159
|
-
// It can be used to make temporary connections, e.g. for dropping a test database.
|
|
160
|
-
async connectToAdapter(uri, options) {
|
|
161
|
-
return dbConnect(uri, {
|
|
162
|
-
...options,
|
|
163
|
-
adapters: self.options.adapters
|
|
164
|
-
});
|
|
165
|
-
},
|
|
166
125
|
async versionCheck() {
|
|
167
126
|
if (!self.options.versionCheck) {
|
|
168
127
|
return;
|
|
@@ -2,7 +2,7 @@ const _ = require('lodash');
|
|
|
2
2
|
const qs = require('qs');
|
|
3
3
|
const fetch = require('node-fetch');
|
|
4
4
|
const tough = require('tough-cookie');
|
|
5
|
-
const escapeHost = require('../../../lib/escape-host
|
|
5
|
+
const escapeHost = require('../../../lib/escape-host');
|
|
6
6
|
const util = require('util');
|
|
7
7
|
|
|
8
8
|
module.exports = {
|
|
@@ -89,6 +89,8 @@
|
|
|
89
89
|
"breakpointPreviewExit": "Exit",
|
|
90
90
|
"breakpointPreviewMobile": "Mobile",
|
|
91
91
|
"breakpointPreviewSelect": "Select Breakpoint",
|
|
92
|
+
"breakpointPreviewSelectMenu": "Breakpoint preview menu",
|
|
93
|
+
"breakpointPreviewShortcut": "Preview at {{ breakpoint }} breakpoint",
|
|
92
94
|
"breakpointPreviewTablet": "Tablet",
|
|
93
95
|
"browse": "Browse",
|
|
94
96
|
"browseDocType": "Browse {{ type }}",
|
|
@@ -387,6 +389,7 @@
|
|
|
387
389
|
"mediaUploadViaDrop": "Drop ’em when you’re ready",
|
|
388
390
|
"mediaUploadViaExplorer": "Or click to open the file explorer",
|
|
389
391
|
"mergeCells": "Merge Cells",
|
|
392
|
+
"menu": "Menu",
|
|
390
393
|
"minLabel": "Min:",
|
|
391
394
|
"minSize": "Min size of {{ width }}x{{ height }}",
|
|
392
395
|
"minimumSize": "Minimum size of {{ width }} x {{ height }} px",
|
|
@@ -475,6 +478,8 @@
|
|
|
475
478
|
"publishBeforeUsingTooltip": "Publish this content before using it in a relationship",
|
|
476
479
|
"publishType": "Publish {{ type }}",
|
|
477
480
|
"published": "Published",
|
|
481
|
+
"publicationStatusMenu": "Publication status",
|
|
482
|
+
"publicationStatusTrigger": "Publication status: {{ status }}. Change publication status.",
|
|
478
483
|
"publishingBatchConfirmation": "Are you sure you want to publish {{ count }} {{ type }}?",
|
|
479
484
|
"publishingBatchConfirmationButton": "Yes, publish content.",
|
|
480
485
|
"rawCssAndJs": "Raw CSS and JS",
|
|
@@ -490,6 +495,7 @@
|
|
|
490
495
|
"recentlyEditedActionSubmitted": "Submitted",
|
|
491
496
|
"recentlyEditedCurrentUser": "Me ({{ user }})",
|
|
492
497
|
"recentlyEditedDocuments": "Recently edited documents",
|
|
498
|
+
"recentlyEditedManagerOpen": "Open recently edited documents manager",
|
|
493
499
|
"recentlyEditedEditedBy": "Edited by",
|
|
494
500
|
"recentlyEditedClearAllFilters": "Clear all filters",
|
|
495
501
|
"recentlyEditedClearSearch": "Clear search",
|
|
@@ -643,6 +649,8 @@
|
|
|
643
649
|
"styleGradientAngle": "Angle",
|
|
644
650
|
"styleGradientEnd": "End Color",
|
|
645
651
|
"styleGradientStart": "Start Color",
|
|
652
|
+
"styleLayoutGap": "Layout Gap",
|
|
653
|
+
"styleLayoutGapHelp": "Sets the spacing between columns inside layout sections across the site.",
|
|
646
654
|
"styleLeft": "Left",
|
|
647
655
|
"styleMargin": "Margin",
|
|
648
656
|
"styleOverlayColor": "Overlay Color",
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
// in the same language as the website content.
|
|
30
30
|
// Example: `defaultAdminLocale: 'fr'`.
|
|
31
31
|
//
|
|
32
|
-
// ### `encoding`
|
|
33
|
-
//
|
|
34
|
-
// Defaults to `'utf-8'`. You almost certainly do not want to change this.
|
|
35
|
-
//
|
|
36
32
|
// ### `slugDirection`
|
|
37
33
|
//
|
|
38
34
|
// Controls the default `direction` value of slug schema. Can be `ltr`, `rtl` or
|
|
@@ -81,8 +77,6 @@ module.exports = {
|
|
|
81
77
|
},
|
|
82
78
|
// If true, slugifying will strip accents from Latin characters
|
|
83
79
|
stripUrlAccents: false,
|
|
84
|
-
// You almost certainly do not want to change this
|
|
85
|
-
encoding: 'utf-8',
|
|
86
80
|
slugDirection: 'ltr'
|
|
87
81
|
},
|
|
88
82
|
async init(self) {
|
|
@@ -166,7 +160,6 @@ module.exports = {
|
|
|
166
160
|
await self.i18next.init(i18nextOptions);
|
|
167
161
|
self.addInitialResources();
|
|
168
162
|
self.enableBrowserData();
|
|
169
|
-
self.encoding = self.options.encoding;
|
|
170
163
|
},
|
|
171
164
|
handlers(self) {
|
|
172
165
|
return {
|
|
@@ -1369,7 +1362,7 @@ module.exports = {
|
|
|
1369
1362
|
helpers(self) {
|
|
1370
1363
|
return {
|
|
1371
1364
|
encoding() {
|
|
1372
|
-
return
|
|
1365
|
+
return 'utf-8';
|
|
1373
1366
|
}
|
|
1374
1367
|
};
|
|
1375
1368
|
}
|