@synopackageland/templates 0.42.0 → 0.42.2
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 +12 -0
- package/package.json +1 -1
- package/templates/_shared/AGENTS.md +6 -0
- package/templates/binary/dsm-package.yaml +4 -1
- package/templates/native-go/README.md +4 -1
- package/templates/native-go/dsm-package.yaml +4 -1
- package/templates/native-go/i18n/glossary.json +4 -4
- package/templates/native-go/i18n/messages/de.json +1 -1
- package/templates/native-go/i18n/messages/en.json +1 -1
- package/templates/native-go/i18n/messages/es.json +1 -1
- package/templates/native-go/i18n/messages/fr.json +1 -1
- package/templates/native-go/i18n/messages/it.json +1 -1
- package/templates/native-go/i18n/messages/ja.json +1 -1
- package/templates/native-go/i18n/messages/ko.json +1 -1
- package/templates/native-go/i18n/messages/zh-CN.json +1 -1
- package/templates/native-go/i18n/messages/zh-TW.json +1 -1
- package/templates/native-go/ui/app.css +2 -1
- package/templates/native-go/ui/app.js +8 -4
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +6 -3
- package/templates/native-go-jobs/frontend/test/background-shell.test.ts +18 -0
- package/templates/native-go-vue/README.md +12 -5
- package/templates/native-go-vue/frontend/DESIGN.md +7 -3
- package/templates/native-go-vue/frontend/src/App.vue +7 -4
- package/templates/native-go-vue/frontend/src/i18n/de.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/en.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/es.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/fr.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/it.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/ja.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/ko.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +4 -7
- package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +4 -7
- package/templates/native-go-vue/frontend/test/apps.test.ts +49 -7
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +6 -4
- package/templates/native-node/dsm-package.yaml +4 -1
- package/templates/native-node-vue/README.md +11 -3
- package/templates/native-python/dsm-package.yaml +4 -1
- package/templates/native-python-vue/README.md +11 -3
- package/templates/native-rust/dsm-package.yaml +4 -1
- package/templates/native-rust-vue/README.md +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.42.2
|
|
4
|
+
|
|
5
|
+
- New Apps get no header Refresh. The Vue templates leave `AppShell`'s `#actions` empty (Sign out only with standalone login), so the shell draws no bar above the page; the plain UI's title row drops its Refresh button
|
|
6
|
+
- Data stays fresh without it: reopening the window reads the status, panels and portal again, the Vue status page reads again each time it is shown, and a failed read offers Retry beside its error
|
|
7
|
+
- Delegate templates keep their Retry inside the capability's own error Callout
|
|
8
|
+
- READMEs, `DESIGN.md` and `AGENTS.md` say `#actions` is for real page actions (such as Add), never a Refresh
|
|
9
|
+
- Drop the unused `refresh`, `greeting`, `save` and `saved` messages; error texts no longer ask the user to refresh
|
|
10
|
+
|
|
11
|
+
## 0.42.1
|
|
12
|
+
|
|
13
|
+
- The commented `backup` example in the native and binary `dsm-package.yaml` says online backup reads SQLite in place, and shows the optional `exclude` list of data-folder globs left out of the archive (SDK 0.53.35, CLI 0.53.13)
|
|
14
|
+
|
|
3
15
|
## 0.42.0
|
|
4
16
|
|
|
5
17
|
- Scaffold pnpm projects with pinned package managers and reproducible lockfiles
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/templates",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.2",
|
|
4
4
|
"description": "Project templates for synopkgland init: native-go, native-python, native-node and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,12 @@ Only a confirmed refusal (signed out, expired session, not an administrator)
|
|
|
17
17
|
shows the sign-in page or a `@synopackageland/ui` Dialog; loading text inside a
|
|
18
18
|
section is fine.
|
|
19
19
|
|
|
20
|
+
Do not add a header Refresh. Re-read data when the window is shown again
|
|
21
|
+
(`ctx.reopen`) and when a page is shown; a failed read offers Retry beside its
|
|
22
|
+
error. `AppShell`'s `#actions` is for real page actions (such as Add); with
|
|
23
|
+
nothing in it the shell draws no bar. A section that needs a manual re-read
|
|
24
|
+
gets its own action in that section.
|
|
25
|
+
|
|
20
26
|
Everything below holds for every native backend language; the backend
|
|
21
27
|
sections at the end name each language's files, types and test commands.
|
|
22
28
|
|
|
@@ -44,9 +44,12 @@ ui: {dir: ui, launch: new-page}
|
|
|
44
44
|
# generated scripts/backup/ as root: a fixed script that only switches to the
|
|
45
45
|
# package account. See `synopkgland skill show backup`.
|
|
46
46
|
# backup:
|
|
47
|
-
# online: false # true keeps the App running; SQLite is
|
|
47
|
+
# online: false # true keeps the App running; SQLite is read in
|
|
48
|
+
# # place. Other files are read as they are: a file
|
|
49
|
+
# # being written meanwhile may be inconsistent
|
|
48
50
|
# data_version: "1.0" # "major.minor"; a restore refuses newer data
|
|
49
51
|
# hook: bin/backup-hook # optional packaged freeze/thaw executable
|
|
52
|
+
# exclude: [tmp] # optional data-folder globs left out (scratch data)
|
|
50
53
|
# To opt out, use only:
|
|
51
54
|
# backup:
|
|
52
55
|
# hyper_backup: false
|
|
@@ -205,7 +205,10 @@ the new version; `synopkgland check` refuses mismatched versions.
|
|
|
205
205
|
`ui/app.js` must register `window.DsmPackageApps[<package>].mount(root, ctx)`
|
|
206
206
|
(the template shows how, using `document.currentScript.dataset.package`).
|
|
207
207
|
`ctx.call(method, params)` is the only transport. `ctx.reopen(fn)` runs `fn`
|
|
208
|
-
whenever DSM shows the window again and returns an idempotent unsubscribe function.
|
|
208
|
+
whenever DSM shows the window again and returns an idempotent unsubscribe function.
|
|
209
|
+
The template re-reads its status there instead of showing a Refresh button; a
|
|
210
|
+
failed read shows Retry beside its error. Keep header buttons for real page
|
|
211
|
+
actions, such as Add. Scope your CSS under your own root class:
|
|
209
212
|
every DSM app shares one page.
|
|
210
213
|
|
|
211
214
|
Plain JS and Vue receive the same `ctx.host` adapter. `ctx.host.platform()`
|
|
@@ -78,9 +78,12 @@ capabilities: {}
|
|
|
78
78
|
# generated scripts/backup/ as root: a fixed script that only switches to the
|
|
79
79
|
# package account. See `synopkgland skill show backup`.
|
|
80
80
|
# backup:
|
|
81
|
-
# online: false # true keeps the App running; SQLite is
|
|
81
|
+
# online: false # true keeps the App running; SQLite is read in
|
|
82
|
+
# # place. Other files are read as they are: a file
|
|
83
|
+
# # being written meanwhile may be inconsistent
|
|
82
84
|
# data_version: "1.0" # "major.minor"; a restore refuses newer data
|
|
83
85
|
# hook: bin/backup-hook # optional packaged freeze/thaw executable
|
|
86
|
+
# exclude: [tmp] # optional data-folder globs left out (scratch data)
|
|
84
87
|
# To opt out, use only:
|
|
85
88
|
# backup:
|
|
86
89
|
# hyper_backup: false
|
|
@@ -163,9 +163,7 @@
|
|
|
163
163
|
"es": "Actualizar",
|
|
164
164
|
"it": "Aggiorna"
|
|
165
165
|
},
|
|
166
|
-
"keys": [
|
|
167
|
-
"app.refresh"
|
|
168
|
-
]
|
|
166
|
+
"keys": []
|
|
169
167
|
},
|
|
170
168
|
{
|
|
171
169
|
"id": "retry",
|
|
@@ -181,7 +179,9 @@
|
|
|
181
179
|
"es": "Volver a intentar",
|
|
182
180
|
"it": "Riprova"
|
|
183
181
|
},
|
|
184
|
-
"keys": [
|
|
182
|
+
"keys": [
|
|
183
|
+
"app.retry"
|
|
184
|
+
]
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"id": "back",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"app.
|
|
2
|
+
"app.retry": "Neuer Versuch",
|
|
3
3
|
"app.title": "Es funktioniert",
|
|
4
4
|
"error.admin_required": "Dieses Benutzerkonto verfügt nicht über die erforderlichen Berechtigungen, um diese Aktion durchzuführen",
|
|
5
5
|
"error.app_privilege_denied": "Dieses Benutzerkonto verfügt nicht über die erforderlichen Berechtigungen, um diese Aktion durchzuführen",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"app.
|
|
2
|
+
"app.retry": "Retry",
|
|
3
3
|
"app.title": "It works",
|
|
4
4
|
"error.admin_required": "Sorry, this user account has not been assigned proper privileges to perform this action",
|
|
5
5
|
"error.app_privilege_denied": "Sorry, this user account has not been assigned proper privileges to perform this action",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"app.
|
|
2
|
+
"app.retry": "Volver a intentar",
|
|
3
3
|
"app.title": "Funciona",
|
|
4
4
|
"error.admin_required": "A esta cuenta de usuario no se le han asignado los privilegios adecuados para realizar esta acción",
|
|
5
5
|
"error.app_privilege_denied": "A esta cuenta de usuario no se le han asignado los privilegios adecuados para realizar esta acción",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"app.
|
|
2
|
+
"app.retry": "Réessayer",
|
|
3
3
|
"app.title": "Ça fonctionne",
|
|
4
4
|
"error.admin_required": "Désolé, ce compte utilisateur ne dispose pas des droits nécessaires pour accomplir cette action",
|
|
5
5
|
"error.app_privilege_denied": "Désolé, ce compte utilisateur ne dispose pas des droits nécessaires pour accomplir cette action",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"app.
|
|
2
|
+
"app.retry": "Riprova",
|
|
3
3
|
"app.title": "Funziona",
|
|
4
4
|
"error.admin_required": "Siamo spiacenti, a questo account utente non sono stati assegnati privilegi appropriati per eseguire questa azione",
|
|
5
5
|
"error.app_privilege_denied": "Siamo spiacenti, a questo account utente non sono stati assegnati privilegi appropriati per eseguire questa azione",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
background: var(--ui-surface);
|
|
35
35
|
}
|
|
36
36
|
.dsmapp *, .dsmapp *::before, .dsmapp *::after { box-sizing: inherit; }
|
|
37
|
-
.dsmapp-header { display: flex; align-items: center; justify-content: space-between; gap: var(--ui-space-3); }
|
|
38
37
|
.dsmapp h1 { margin: 0 0 var(--ui-space-1); font-size: var(--ui-font-size-title); font-weight: 600; }
|
|
39
38
|
.dsmapp-secondary {
|
|
40
39
|
height: var(--ui-control-md); padding: 0 var(--ui-space-3); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);
|
|
@@ -55,6 +54,8 @@
|
|
|
55
54
|
.dsmapp-form button:disabled { opacity: 0.6; cursor: default; }
|
|
56
55
|
.dsmapp-note { margin: 0; color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); }
|
|
57
56
|
.dsmapp-note[hidden] { display: none; }
|
|
57
|
+
.dsmapp-message-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-3); }
|
|
58
|
+
.dsmapp-message-row .dsmapp-message { flex: 1 1 auto; }
|
|
58
59
|
.dsmapp-message { min-height: var(--ui-line-height); color: var(--ui-danger-text); }
|
|
59
60
|
.dsmapp-facts { display: grid; grid-template-columns: fit-content(40%) minmax(0, 1fr); gap: var(--ui-space-1) var(--ui-space-4); margin: 0; }
|
|
60
61
|
.dsmapp-facts dt { color: var(--ui-text-secondary); }
|
|
@@ -36,14 +36,16 @@
|
|
|
36
36
|
function mount(root, ctx) {
|
|
37
37
|
var t = ctx.t;
|
|
38
38
|
var status = el("dl", { class: "dsmapp-facts" });
|
|
39
|
-
var refreshButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.refresh") });
|
|
40
39
|
var message = el("p", { class: "dsmapp-message", role: "status" });
|
|
40
|
+
// Shown only after a failed read; the App re-reads on reopen, so no header Refresh.
|
|
41
|
+
var retryButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.retry") });
|
|
42
|
+
retryButton.hidden = true;
|
|
41
43
|
var portal = ctx.portal ? el("section", { class: "dsmapp-portal" }) : null;
|
|
42
44
|
|
|
43
45
|
root.classList.add("dsmapp");
|
|
44
46
|
root.replaceChildren(
|
|
45
|
-
el("
|
|
46
|
-
message,
|
|
47
|
+
el("h1", { text: t("app.title") }),
|
|
48
|
+
el("div", { class: "dsmapp-message-row" }, [message, retryButton]),
|
|
47
49
|
el("h2", { text: t("status.heading") }),
|
|
48
50
|
status
|
|
49
51
|
);
|
|
@@ -54,10 +56,12 @@
|
|
|
54
56
|
var code = (error && error.code) || "internal";
|
|
55
57
|
var text = t("error." + code);
|
|
56
58
|
message.textContent = text === "error." + code ? (error && error.message) || t("error.internal") : text;
|
|
59
|
+
retryButton.hidden = false;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function refresh() {
|
|
60
63
|
message.textContent = "";
|
|
64
|
+
retryButton.hidden = true;
|
|
61
65
|
ctx.call("app.status").then(function (data) {
|
|
62
66
|
status.replaceChildren();
|
|
63
67
|
[[t("status.user"), data.admin ? t("status.userAdmin", { user: data.user }) : data.user],
|
|
@@ -73,7 +77,7 @@
|
|
|
73
77
|
}).catch(showError);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
retryButton.addEventListener("click", refresh);
|
|
77
81
|
|
|
78
82
|
ctx.reopen(refresh);
|
|
79
83
|
refresh();
|
|
@@ -102,7 +102,8 @@ test('the app mount forwards ctx.executor to administrator setup only',async()=>
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
})
|
|
105
|
-
|
|
105
|
+
// No header Refresh: reopening the App reloads the save destinations.
|
|
106
|
+
test('reopening the App reloads the save destinations',async()=>{
|
|
106
107
|
const call=vi.fn(async(method:string)=>{
|
|
107
108
|
if(method==='app.status')return {package:'DelegateApp',version:'1',user:'alice',admin:false}
|
|
108
109
|
if(method==='delegate.status')return {state:'ready',disabled:false,lifetime:'until-revoked',destinations:['save']}
|
|
@@ -112,11 +113,13 @@ test('the header Refresh reloads the save destinations',async()=>{
|
|
|
112
113
|
throw {code:'unknown_method'}
|
|
113
114
|
})
|
|
114
115
|
const root=document.createElement('div');document.body.append(root)
|
|
115
|
-
const
|
|
116
|
+
const reopen:Array<()=>void>=[]
|
|
117
|
+
const app=mount(root,dsmWindow({call,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}}))
|
|
116
118
|
cleanup.push(()=>{app.unmount();root.remove()});await settle()
|
|
117
119
|
const reads=()=>call.mock.calls.filter(([method])=>method==='delegate.destination').length
|
|
118
120
|
const before=reads();expect(before).toBeGreaterThan(0)
|
|
119
|
-
|
|
121
|
+
expect([...root.querySelectorAll('button')].some(n=>n.textContent?.trim()==='Refresh')).toBe(false)
|
|
122
|
+
reopen.forEach(fn=>fn());await settle()
|
|
120
123
|
expect(reads()).toBeGreaterThan(before)
|
|
121
124
|
})
|
|
122
125
|
test('the panels show a skeleton while the delegate status loads',async()=>{
|
|
@@ -17,3 +17,21 @@ test('a jobs project opens its shell without starting a job',async()=>{
|
|
|
17
17
|
expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(false)
|
|
18
18
|
}finally{app.unmount();root.remove()}
|
|
19
19
|
})
|
|
20
|
+
// No header Refresh: showing the status page again reads its status again.
|
|
21
|
+
test('returning to the status page reads its status again',async()=>{
|
|
22
|
+
const call=vi.fn(async(method:string)=>{
|
|
23
|
+
if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
|
|
24
|
+
throw {code:'unknown_method'}
|
|
25
|
+
})
|
|
26
|
+
const root=document.createElement('div');document.body.append(root)
|
|
27
|
+
const app=mount(root,dsmWindow({call}))
|
|
28
|
+
try{
|
|
29
|
+
await settle()
|
|
30
|
+
const reads=()=>call.mock.calls.filter(([method])=>method==='app.status').length
|
|
31
|
+
const item=(name:string)=>[...root.querySelectorAll<HTMLElement>('button')].find(n=>n.textContent?.trim()===name)!
|
|
32
|
+
item('Background jobs').click();await settle()
|
|
33
|
+
const before=reads()
|
|
34
|
+
item('Status').click();await settle()
|
|
35
|
+
expect(reads()).toBeGreaterThan(before)
|
|
36
|
+
}finally{app.unmount();root.remove()}
|
|
37
|
+
})
|
|
@@ -42,8 +42,7 @@ pnpm test
|
|
|
42
42
|
pnpm run build
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
The base app reads package status
|
|
46
|
-
`ctx.call`. Each window has its own Vue mount, request state and host
|
|
45
|
+
The base app reads package status through `ctx.call`. Each window has its own Vue mount, request state and host
|
|
47
46
|
subscriptions; unmount disposes subscriptions and ignores late responses.
|
|
48
47
|
|
|
49
48
|
The build emits `app.module.js`, `app.css` and any lazy chunks. The entry
|
|
@@ -94,7 +93,7 @@ SDK's light default in place.
|
|
|
94
93
|
|
|
95
94
|
Use the installed `@synopackageland/ui` and its declared compatible patch range.
|
|
96
95
|
`App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
|
|
97
|
-
`pages` from `shellPages`,
|
|
96
|
+
`pages` from `shellPages`, real page actions in `#actions` and the pages in its
|
|
98
97
|
default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
|
|
99
98
|
`#actions` in a bar above the page and scrolls only the content; a settings
|
|
100
99
|
page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
|
|
@@ -109,6 +108,14 @@ device-width viewport. Its full-page sidebar names the App with
|
|
|
109
108
|
shows the signed-in DSM account and its role from `ctx.user`. See the ui's
|
|
110
109
|
`docs/adaptive-layout.md`.
|
|
111
110
|
|
|
111
|
+
The template puts no Refresh in `#actions`, and with nothing there `AppShell`
|
|
112
|
+
draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
|
|
113
|
+
the status, the panels and the portal again when the window is shown again,
|
|
114
|
+
and showing the status page again reads it again. A failed read offers Retry
|
|
115
|
+
beside its error. Sign out appears in `#actions` only when standalone login is
|
|
116
|
+
on. Keep `#actions` for real page actions, such as Add; a section that needs a
|
|
117
|
+
manual re-read gets its own action in that section, not a header Refresh.
|
|
118
|
+
|
|
112
119
|
The frontend SDK and UI use patch ranges; the lockfile records the installed
|
|
113
120
|
versions. Update the lockfile deliberately when taking a newer patch.
|
|
114
121
|
|
|
@@ -120,8 +127,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
|
|
|
120
127
|
even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
|
|
121
128
|
with Back keep their title bar. `header-actions` share one "…" menu; only an
|
|
122
129
|
entry marked `primary: true`, or a sole entry with an icon, becomes a separate
|
|
123
|
-
bar button. The template's `#actions`
|
|
124
|
-
workaround is needed.
|
|
130
|
+
bar button. The template's `#actions` holds only real page actions; no title or
|
|
131
|
+
empty-bar workaround is needed.
|
|
125
132
|
|
|
126
133
|
`AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
|
|
127
134
|
a full page as well, bounded at `height: 100%`) with the UI locale from
|
|
@@ -30,8 +30,8 @@ Use `syno-pkg-land-ui` directly in package scripts; keep AI guidance version-ind
|
|
|
30
30
|
## Overview
|
|
31
31
|
|
|
32
32
|
The template runs inside a DSM package window. `AppShell` provides status and
|
|
33
|
-
optional background navigation. The base page shows package status
|
|
34
|
-
|
|
33
|
+
optional background navigation. The base page shows package status; the root
|
|
34
|
+
overlay adds its existing fixed-operation authorization gate.
|
|
35
35
|
|
|
36
36
|
Use the installed `@synopackageland/ui` controls and `themes/dsm7-embedded.css`.
|
|
37
37
|
The theme owns colors, typography, spacing and dialogs. Keep per-app modal scope,
|
|
@@ -39,7 +39,11 @@ a unique Vue ID prefix and vertical scrolling at the DSM minimum window size.
|
|
|
39
39
|
In a DSM window, Dialogs may leave the window (`dialog-bounds="viewport"`) and a
|
|
40
40
|
press on one brings the window to the front; below 600px they are bottom sheets.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
There is no header Refresh: reopening the window, or showing the status page
|
|
43
|
+
again, reads it again, and a failed read offers Retry beside its error. The
|
|
44
|
+
shell's `#actions` holds only real page actions (Sign out with standalone
|
|
45
|
+
login); empty, the shell draws no bar. Reads expose busy state and localized
|
|
46
|
+
errors.
|
|
43
47
|
Each mount owns its state; unmount unsubscribes from host events and ignores late
|
|
44
48
|
responses. Backend route policies decide access.
|
|
45
49
|
|
|
@@ -46,13 +46,15 @@ async function refresh() {
|
|
|
46
46
|
} catch(e) {if(!disposed && !(early && login.enabled.value)) error.value = message(e)}
|
|
47
47
|
finally {if(!disposed) {busy.value=false; if(again) {again=false; void refresh()}}}
|
|
48
48
|
}
|
|
49
|
-
//
|
|
49
|
+
// No header Refresh: reopening the App, or showing its status page again, reads
|
|
50
|
+
// the status, the panels below and the portal again.
|
|
50
51
|
const panels = ref<{refresh(): unknown}>()
|
|
51
52
|
const portalReads = ref(0)
|
|
52
53
|
const portalKey = computed(() => `${app.value?.user}:${portalReads.value}`)
|
|
53
54
|
function refreshAll() {
|
|
54
55
|
void refresh(); void panels.value?.refresh(); portalReads.value++
|
|
55
56
|
}
|
|
57
|
+
watch(page, shown => {if(shown==='status') refreshAll()})
|
|
56
58
|
watch(login.subject, (subject, previous) => {
|
|
57
59
|
if(!subject) {app.value=undefined;error.value=''}
|
|
58
60
|
else if((!previous || previous.user!==subject.user)) void refresh()
|
|
@@ -61,7 +63,7 @@ async function signOut() {
|
|
|
61
63
|
try {await login.logout()} catch(e) {if(!disposed) error.value=message(e)}
|
|
62
64
|
}
|
|
63
65
|
onMounted(()=>{
|
|
64
|
-
unsubscribe = props.ctx.reopen(()=>{if(!disposed)
|
|
66
|
+
unsubscribe = props.ctx.reopen(()=>{if(!disposed) refreshAll()})
|
|
65
67
|
// The session check runs in the background; only a confirmed sign-in need replaces the App.
|
|
66
68
|
void login.start()
|
|
67
69
|
void refresh()
|
|
@@ -72,10 +74,11 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose()})
|
|
|
72
74
|
<template>
|
|
73
75
|
<AppShell app-root v-model="page" :ctx="shellCtx" :locale="ctx.locale" :pages="pages" :denied="signIn" sidebar-storage-key="__PACKAGE__">
|
|
74
76
|
<template #denied><LoginPage :session="login" :locale="ctx.locale" /></template>
|
|
75
|
-
|
|
77
|
+
<!-- #actions holds real page actions (such as Add), never a Refresh. Without one the shell draws no bar. -->
|
|
78
|
+
<template v-if="login.enabled.value" #actions><Button variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
|
|
76
79
|
<ExecutorGate :ctx="ctx" :admin="ctx.user?.isAdmin ?? !!app?.admin">
|
|
77
80
|
<div class="integration-app" :aria-busy="busy" :aria-label="copy.settings">
|
|
78
|
-
<Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span></Callout>
|
|
81
|
+
<Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span><template #actions><Button variant="secondary" :disabled="busy" @click="refreshAll">{{ copy.retry }}</Button></template></Callout>
|
|
79
82
|
<div data-page="status" v-show="page==='status'">
|
|
80
83
|
<SettingsSection :title="copy.statusTitle" :collapsible="false" :loading="busy && !app">
|
|
81
84
|
<div v-if="app" data-app-status>
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "Neuer Versuch",
|
|
3
3
|
"signOut": "Abmelden",
|
|
4
4
|
"settings": "Paketeinstellungen",
|
|
5
5
|
"actionRequired": "Aktion erforderlich",
|
|
6
6
|
"statusTitle": "Paketstatus",
|
|
7
|
-
"statusError": "Status nicht verfügbar
|
|
8
|
-
"greeting": "Begrüßung",
|
|
9
|
-
"save": "Speichern",
|
|
10
|
-
"saved": "Begrüßung gespeichert",
|
|
7
|
+
"statusError": "Status nicht verfügbar",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "Ihre DSM-Sitzung ist abgelaufen. Melden Sie sich erneut an",
|
|
13
10
|
"admin_required": "Diese Aktion muss ein DSM-Administrator ausführen",
|
|
14
11
|
"app_privilege_denied": "Die DSM-Anwendungsberechtigungen erlauben diesem Konto nicht, diese App zu verwenden",
|
|
15
|
-
"auth_unavailable": "Die DSM-Authentifizierung ist nicht verfügbar.
|
|
16
|
-
"request_failed": "Anfrage fehlgeschlagen ({code}).
|
|
12
|
+
"auth_unavailable": "Die DSM-Authentifizierung ist nicht verfügbar. Versuchen Sie es später erneut",
|
|
13
|
+
"request_failed": "Anfrage fehlgeschlagen ({code}). Versuchen Sie es erneut"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "Paketname",
|
|
19
16
|
"user": "Benutzer",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "Retry",
|
|
3
3
|
"signOut": "Sign out",
|
|
4
4
|
"settings": "Package settings",
|
|
5
5
|
"actionRequired": "Action required",
|
|
6
6
|
"statusTitle": "Package status",
|
|
7
|
-
"statusError": "Unable to load status
|
|
8
|
-
"greeting": "Greeting",
|
|
9
|
-
"save": "Save",
|
|
10
|
-
"saved": "Greeting saved",
|
|
7
|
+
"statusError": "Unable to load status",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "Your DSM session expired. Sign in again",
|
|
13
10
|
"admin_required": "A DSM administrator must perform this action",
|
|
14
11
|
"app_privilege_denied": "DSM Application Privileges do not allow this account to use this app",
|
|
15
|
-
"auth_unavailable": "DSM authentication is unavailable.
|
|
16
|
-
"request_failed": "Request failed ({code}).
|
|
12
|
+
"auth_unavailable": "DSM authentication is unavailable. Try again later",
|
|
13
|
+
"request_failed": "Request failed ({code}). Try again"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "Package name",
|
|
19
16
|
"user": "User",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "Volver a intentar",
|
|
3
3
|
"signOut": "Cerrar sesión",
|
|
4
4
|
"settings": "Configuración del paquete",
|
|
5
5
|
"actionRequired": "Acción necesaria",
|
|
6
6
|
"statusTitle": "Estado del paquete",
|
|
7
|
-
"statusError": "Estado no disponible
|
|
8
|
-
"greeting": "Saludo",
|
|
9
|
-
"save": "Guardar",
|
|
10
|
-
"saved": "Saludo guardado",
|
|
7
|
+
"statusError": "Estado no disponible",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "Su sesión de DSM ha caducado. Vuelva a iniciar sesión",
|
|
13
10
|
"admin_required": "Un administrador de DSM debe realizar esta acción",
|
|
14
11
|
"app_privilege_denied": "Los privilegios de aplicación de DSM no permiten que esta cuenta use esta aplicación",
|
|
15
|
-
"auth_unavailable": "La autenticación de DSM no está disponible.
|
|
16
|
-
"request_failed": "La solicitud falló ({code}).
|
|
12
|
+
"auth_unavailable": "La autenticación de DSM no está disponible. Inténtelo de nuevo más tarde",
|
|
13
|
+
"request_failed": "La solicitud falló ({code}). Inténtelo de nuevo"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "Nombre del paquete",
|
|
19
16
|
"user": "Usuario",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "Réessayer",
|
|
3
3
|
"signOut": "Se déconnecter",
|
|
4
4
|
"settings": "Paramètres du paquet",
|
|
5
5
|
"actionRequired": "Action requise",
|
|
6
6
|
"statusTitle": "État du paquet",
|
|
7
|
-
"statusError": "État indisponible
|
|
8
|
-
"greeting": "Message d’accueil",
|
|
9
|
-
"save": "Sauvegarder",
|
|
10
|
-
"saved": "Message d’accueil enregistré",
|
|
7
|
+
"statusError": "État indisponible",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "Votre session DSM a expiré. Reconnectez-vous",
|
|
13
10
|
"admin_required": "Un administrateur DSM doit effectuer cette action",
|
|
14
11
|
"app_privilege_denied": "Les privilèges d’application DSM n’autorisent pas ce compte à utiliser cette application",
|
|
15
|
-
"auth_unavailable": "L’authentification DSM n’est pas disponible.
|
|
16
|
-
"request_failed": "La requête a échoué ({code}).
|
|
12
|
+
"auth_unavailable": "L’authentification DSM n’est pas disponible. Réessayez plus tard",
|
|
13
|
+
"request_failed": "La requête a échoué ({code}). Réessayez"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "Nom du paquet",
|
|
19
16
|
"user": "Utilisateur",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "Riprova",
|
|
3
3
|
"signOut": "Disconnetti",
|
|
4
4
|
"settings": "Impostazioni del pacchetto",
|
|
5
5
|
"actionRequired": "Azione richiesta",
|
|
6
6
|
"statusTitle": "Stato del pacchetto",
|
|
7
|
-
"statusError": "Stato non disponibile
|
|
8
|
-
"greeting": "Saluto",
|
|
9
|
-
"save": "Salva",
|
|
10
|
-
"saved": "Saluto salvato",
|
|
7
|
+
"statusError": "Stato non disponibile",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "La sessione DSM è scaduta. Accedere di nuovo",
|
|
13
10
|
"admin_required": "Questa azione deve essere eseguita da un amministratore DSM",
|
|
14
11
|
"app_privilege_denied": "I privilegi delle applicazioni DSM non consentono a questo account di usare questa app",
|
|
15
|
-
"auth_unavailable": "L’autenticazione DSM non è disponibile.
|
|
16
|
-
"request_failed": "Richiesta non riuscita ({code}).
|
|
12
|
+
"auth_unavailable": "L’autenticazione DSM non è disponibile. Riprovare più tardi",
|
|
13
|
+
"request_failed": "Richiesta non riuscita ({code}). Riprovare"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "Nome pacchetto",
|
|
19
16
|
"user": "Utente",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "再試行",
|
|
3
3
|
"signOut": "サインアウト",
|
|
4
4
|
"settings": "パッケージ設定",
|
|
5
5
|
"actionRequired": "対応が必要です",
|
|
6
6
|
"statusTitle": "パッケージの状態",
|
|
7
|
-
"statusError": "
|
|
8
|
-
"greeting": "あいさつ",
|
|
9
|
-
"save": "保存",
|
|
10
|
-
"saved": "あいさつを保存しました",
|
|
7
|
+
"statusError": "ステータスを読み込めません",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "DSM セッションの有効期限が切れました。もう一度サインインしてください",
|
|
13
10
|
"admin_required": "この操作は DSM 管理者が行う必要があります",
|
|
14
11
|
"app_privilege_denied": "DSM のアプリケーション権限により、このアカウントはこのアプリを使用できません",
|
|
15
|
-
"auth_unavailable": "DSM
|
|
16
|
-
"request_failed": "リクエストに失敗しました({code}
|
|
12
|
+
"auth_unavailable": "DSM 認証を利用できません。しばらくしてから、もう一度お試しください",
|
|
13
|
+
"request_failed": "リクエストに失敗しました({code})。もう一度お試しください"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "パッケージ名",
|
|
19
16
|
"user": "ユーザー",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "재시도",
|
|
3
3
|
"signOut": "로그아웃",
|
|
4
4
|
"settings": "패키지 설정",
|
|
5
5
|
"actionRequired": "조치 필요",
|
|
6
6
|
"statusTitle": "패키지 상태",
|
|
7
|
-
"statusError": "상태를 불러올 수
|
|
8
|
-
"greeting": "인사말",
|
|
9
|
-
"save": "저장",
|
|
10
|
-
"saved": "인사말이 저장되었습니다",
|
|
7
|
+
"statusError": "상태를 불러올 수 없습니다",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "DSM 세션이 만료되었습니다. 다시 로그인하십시오",
|
|
13
10
|
"admin_required": "이 작업은 DSM 관리자가 수행해야 합니다",
|
|
14
11
|
"app_privilege_denied": "DSM 응용 프로그램 권한에서 이 계정의 앱 사용이 허용되지 않습니다",
|
|
15
|
-
"auth_unavailable": "DSM 인증을 사용할 수 없습니다.
|
|
16
|
-
"request_failed": "요청이 실패했습니다({code}).
|
|
12
|
+
"auth_unavailable": "DSM 인증을 사용할 수 없습니다. 잠시 후 다시 시도하십시오",
|
|
13
|
+
"request_failed": "요청이 실패했습니다({code}). 다시 시도하십시오"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "패키지 이름",
|
|
19
16
|
"user": "사용자",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "重试",
|
|
3
3
|
"signOut": "注销",
|
|
4
4
|
"settings": "套件设置",
|
|
5
5
|
"actionRequired": "需要处理",
|
|
6
6
|
"statusTitle": "套件状态",
|
|
7
|
-
"statusError": "
|
|
8
|
-
"greeting": "问候语",
|
|
9
|
-
"save": "保存",
|
|
10
|
-
"saved": "问候语已保存",
|
|
7
|
+
"statusError": "无法加载状态",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "DSM 登录已过期,请重新登录",
|
|
13
10
|
"admin_required": "此操作必须由 DSM 管理员执行",
|
|
14
11
|
"app_privilege_denied": "DSM 应用程序权限不允许此账户使用此 App",
|
|
15
|
-
"auth_unavailable": "DSM
|
|
16
|
-
"request_failed": "请求失败({code}
|
|
12
|
+
"auth_unavailable": "DSM 验证暂时不可用,请稍后重试",
|
|
13
|
+
"request_failed": "请求失败({code}),请重试"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "套件名称",
|
|
19
16
|
"user": "用户帐号",
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"retry": "重試",
|
|
3
3
|
"signOut": "登出",
|
|
4
4
|
"settings": "套件設定",
|
|
5
5
|
"actionRequired": "需要處理",
|
|
6
6
|
"statusTitle": "套件狀態",
|
|
7
|
-
"statusError": "
|
|
8
|
-
"greeting": "問候語",
|
|
9
|
-
"save": "儲存",
|
|
10
|
-
"saved": "問候語已儲存",
|
|
7
|
+
"statusError": "無法載入狀態",
|
|
11
8
|
"errors": {
|
|
12
9
|
"session_required": "DSM 登入已逾時,請重新登入",
|
|
13
10
|
"admin_required": "此操作須由 DSM 管理員執行",
|
|
14
11
|
"app_privilege_denied": "DSM 應用程式權限不允許此帳號使用這個 App",
|
|
15
|
-
"auth_unavailable": "DSM
|
|
16
|
-
"request_failed": "要求失敗({code}
|
|
12
|
+
"auth_unavailable": "DSM 驗證目前無法使用,請稍後再試",
|
|
13
|
+
"request_failed": "要求失敗({code}),請再試一次"
|
|
17
14
|
},
|
|
18
15
|
"packageName": "套件名稱",
|
|
19
16
|
"user": "使用者帳號",
|
|
@@ -47,13 +47,26 @@ test('a DSM window stays light when the system prefers dark',async()=>{
|
|
|
47
47
|
expect(f.root.textContent).toContain('Light')
|
|
48
48
|
}finally{f.close();Object.defineProperty(window,'matchMedia',{configurable:true,writable:true,value:query})}
|
|
49
49
|
})
|
|
50
|
-
test('failed status loading offers a
|
|
50
|
+
test('failed status loading offers a Retry beside the error and releases the mounting class',async()=>{
|
|
51
51
|
const f=fixture('Failure')
|
|
52
52
|
try{
|
|
53
|
-
await settle();f.
|
|
53
|
+
await settle();f.app.unmount()
|
|
54
|
+
let fail=true
|
|
55
|
+
f.call.mockImplementation(async(method:string)=>{
|
|
56
|
+
if(method!=='app.status')throw {code:'unknown_method'}
|
|
57
|
+
if(fail){fail=false;throw {code:'auth_unavailable'}}
|
|
58
|
+
return {package:'Failure',version:'1',user:'admin',admin:true}
|
|
59
|
+
})
|
|
60
|
+
const app=mount(f.root,f.ctx);await settle()
|
|
54
61
|
expect(f.root.textContent).toContain('DSM authentication is unavailable')
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
const retry=[...f.root.querySelectorAll<HTMLButtonElement>('[data-ui-callout-actions] button')].find(n=>n.textContent==='Retry')!
|
|
63
|
+
expect(retry.disabled).toBe(false)
|
|
64
|
+
const reads=f.call.mock.calls.filter(([method])=>method==='app.status').length
|
|
65
|
+
retry.click();await settle()
|
|
66
|
+
expect(f.call.mock.calls.filter(([method])=>method==='app.status').length).toBeGreaterThan(reads)
|
|
67
|
+
expect(f.root.textContent).not.toContain('DSM authentication is unavailable')
|
|
68
|
+
expect(f.root.querySelector('[data-app-status]')).not.toBeNull()
|
|
69
|
+
app.unmount();expect(f.root.classList.contains('dsmapp-vue-root')).toBe(false)
|
|
57
70
|
}finally{f.close()}
|
|
58
71
|
})
|
|
59
72
|
test('a portal app shows the Application Portal section on the status page',async()=>{
|
|
@@ -70,7 +83,7 @@ test('a portal app shows the Application Portal section on the status page',asyn
|
|
|
70
83
|
app.unmount()
|
|
71
84
|
}finally{without.close();f.root.remove()}
|
|
72
85
|
})
|
|
73
|
-
test('the
|
|
86
|
+
test('reopening the App reads the status, portal and executor status again',async()=>{
|
|
74
87
|
const f=fixture('Portal')
|
|
75
88
|
try{
|
|
76
89
|
await settle();f.app.unmount()
|
|
@@ -79,13 +92,42 @@ test('the header Refresh reads the portal and executor status again',async()=>{
|
|
|
79
92
|
f.ctx.portal={messages:{title:'Application Portal',link:'Direct link:'},status:portal,apply:vi.fn(),disable:vi.fn(),openControlPanel:vi.fn(),mountPanel:vi.fn()}
|
|
80
93
|
f.ctx.executor={status:executor,authorize:vi.fn(),control:vi.fn()} as unknown as Context['executor']
|
|
81
94
|
const app=mount(f.root,f.ctx);await settle()
|
|
82
|
-
const
|
|
83
|
-
|
|
95
|
+
const reads=()=>f.call.mock.calls.filter(([method])=>method==='app.status').length
|
|
96
|
+
const [statusReads,portalReads,executorReads]=[reads(),portal.mock.calls.length,executor.mock.calls.length]
|
|
97
|
+
f.reopen.forEach(fn=>fn());await settle()
|
|
98
|
+
expect(reads()).toBeGreaterThan(statusReads)
|
|
84
99
|
expect(portal.mock.calls.length).toBeGreaterThan(portalReads)
|
|
85
100
|
expect(executor.mock.calls.length).toBeGreaterThan(executorReads)
|
|
86
101
|
app.unmount()
|
|
87
102
|
}finally{f.root.remove()}
|
|
88
103
|
})
|
|
104
|
+
// The template puts no Refresh in the shell's #actions: with nothing there the
|
|
105
|
+
// shell draws no bar above the page. Sign out shows only with standalone login.
|
|
106
|
+
test('the status page has no header bar and no Refresh button',async()=>{
|
|
107
|
+
const f=fixture('Bare')
|
|
108
|
+
try{
|
|
109
|
+
await settle()
|
|
110
|
+
expect([...f.root.querySelectorAll('button')].some(n=>n.textContent?.trim()==='Refresh')).toBe(false)
|
|
111
|
+
const bar=f.root.querySelector<HTMLElement>('[data-app-page-bar]')
|
|
112
|
+
expect(bar===null||bar.hidden).toBe(true)
|
|
113
|
+
}finally{f.close()}
|
|
114
|
+
})
|
|
115
|
+
test('with standalone login the bar holds only Sign out',async()=>{
|
|
116
|
+
const call=vi.fn(async(method:string)=>{
|
|
117
|
+
if(method==='identity.status')return {enabled:true}
|
|
118
|
+
if(method==='identity.session')return {user:'alice',uid:'1',expiresAt:new Date(Date.now()+3600_000).toISOString()}
|
|
119
|
+
if(method==='app.status')return {package:'Login',version:'1',user:'alice',admin:false}
|
|
120
|
+
throw {code:'unknown_method'}
|
|
121
|
+
})
|
|
122
|
+
const root=document.createElement('div');document.body.append(root)
|
|
123
|
+
const app=mount(root,dsmWindow({call}))
|
|
124
|
+
try{
|
|
125
|
+
await settle()
|
|
126
|
+
const bar=root.querySelector<HTMLElement>('[data-app-page-bar]')!
|
|
127
|
+
expect(bar.hidden).toBe(false)
|
|
128
|
+
expect([...bar.querySelectorAll('button')].map(n=>n.textContent?.trim())).toEqual(['Sign out'])
|
|
129
|
+
}finally{app.unmount();root.remove()}
|
|
130
|
+
})
|
|
89
131
|
test('the app renders and loads status while the session check is still pending',async()=>{
|
|
90
132
|
const call=vi.fn(async(method:string)=>{
|
|
91
133
|
if(method==='identity.status')return new Promise(()=>{})
|
|
@@ -8,8 +8,10 @@ import {appCopy} from '../src/i18n'
|
|
|
8
8
|
import {identityCopy} from '@synopackageland/sdk/vue'
|
|
9
9
|
async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
|
|
10
10
|
const buttons=(root:HTMLElement)=>[...root.querySelectorAll('button')].map(b=>b.textContent?.trim())
|
|
11
|
+
const reopen:Array<()=>void>=[]
|
|
11
12
|
function start(locale:string,call:(method:string)=>Promise<unknown>){
|
|
12
|
-
|
|
13
|
+
reopen.length=0
|
|
14
|
+
const ctx=dsmWindow({locale,call:vi.fn(call) as never,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}})
|
|
13
15
|
const root=document.createElement('div');document.body.append(root)
|
|
14
16
|
const app=mount(root,ctx)
|
|
15
17
|
return {root,close(){app.unmount();root.remove()}}
|
|
@@ -23,12 +25,12 @@ test('the status page and its errors follow the DSM language',async()=>{
|
|
|
23
25
|
})
|
|
24
26
|
try{
|
|
25
27
|
await settle()
|
|
26
|
-
expect(buttons(root)).toContain(
|
|
28
|
+
expect(buttons(root)).not.toContain('Aktualisieren')
|
|
27
29
|
expect(root.textContent).toContain(copy.statusTitle)
|
|
28
30
|
expect(root.textContent).toContain(copy.packageName)
|
|
29
|
-
|
|
30
|
-
denied=true;[...root.querySelectorAll('button')].find(button=>button.textContent?.trim()===copy.refresh)!.click();await settle()
|
|
31
|
+
denied=true;reopen.forEach(fn=>fn());await settle()
|
|
31
32
|
expect(root.textContent).toContain(copy.errors.admin_required)
|
|
33
|
+
expect(buttons(root)).toContain(copy.retry)
|
|
32
34
|
expect(root.querySelector('[aria-label]')?.getAttribute('aria-label')).not.toBe('Package settings')
|
|
33
35
|
}finally{close()}
|
|
34
36
|
})
|
|
@@ -87,9 +87,12 @@ capabilities: {}
|
|
|
87
87
|
# generated scripts/backup/ as root: a fixed script that only switches to the
|
|
88
88
|
# package account. See `synopkgland skill show backup`.
|
|
89
89
|
# backup:
|
|
90
|
-
# online: false # true keeps the App running; SQLite is
|
|
90
|
+
# online: false # true keeps the App running; SQLite is read in
|
|
91
|
+
# # place. Other files are read as they are: a file
|
|
92
|
+
# # being written meanwhile may be inconsistent
|
|
91
93
|
# data_version: "1.0" # "major.minor"; a restore refuses newer data
|
|
92
94
|
# hook: bin/backup-hook # optional packaged freeze/thaw executable
|
|
95
|
+
# exclude: [tmp] # optional data-folder globs left out (scratch data)
|
|
93
96
|
# To opt out, use only:
|
|
94
97
|
# backup:
|
|
95
98
|
# hyper_backup: false
|
|
@@ -45,7 +45,7 @@ bridge App's runtime serves.
|
|
|
45
45
|
|
|
46
46
|
Use the installed `@synopackageland/ui` and its declared compatible patch range.
|
|
47
47
|
`App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
|
|
48
|
-
`pages` from `shellPages`,
|
|
48
|
+
`pages` from `shellPages`, real page actions in `#actions` and the pages in its
|
|
49
49
|
default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
|
|
50
50
|
`#actions` in a bar above the page and scrolls only the content; a settings
|
|
51
51
|
page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
|
|
@@ -60,6 +60,14 @@ device-width viewport. Its full-page sidebar names the App with
|
|
|
60
60
|
shows the signed-in DSM account and its role from `ctx.user`. See the ui's
|
|
61
61
|
`docs/adaptive-layout.md`.
|
|
62
62
|
|
|
63
|
+
The template puts no Refresh in `#actions`, and with nothing there `AppShell`
|
|
64
|
+
draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
|
|
65
|
+
the status, the panels and the portal again when the window is shown again,
|
|
66
|
+
and showing the status page again reads it again. A failed read offers Retry
|
|
67
|
+
beside its error. Sign out appears in `#actions` only when standalone login is
|
|
68
|
+
on. Keep `#actions` for real page actions, such as Add; a section that needs a
|
|
69
|
+
manual re-read gets its own action in that section, not a header Refresh.
|
|
70
|
+
|
|
63
71
|
The frontend SDK and UI use patch ranges; the lockfile records the installed
|
|
64
72
|
versions. Update the lockfile deliberately when taking a newer patch.
|
|
65
73
|
|
|
@@ -71,8 +79,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
|
|
|
71
79
|
even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
|
|
72
80
|
with Back keep their title bar. `header-actions` share one "…" menu; only an
|
|
73
81
|
entry marked `primary: true`, or a sole entry with an icon, becomes a separate
|
|
74
|
-
bar button. The template's `#actions`
|
|
75
|
-
workaround is needed.
|
|
82
|
+
bar button. The template's `#actions` holds only real page actions; no title or
|
|
83
|
+
empty-bar workaround is needed.
|
|
76
84
|
|
|
77
85
|
`AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
|
|
78
86
|
a full page as well, bounded at `height: 100%`) with the UI locale from
|
|
@@ -84,9 +84,12 @@ capabilities: {}
|
|
|
84
84
|
# generated scripts/backup/ as root: a fixed script that only switches to the
|
|
85
85
|
# package account. See `synopkgland skill show backup`.
|
|
86
86
|
# backup:
|
|
87
|
-
# online: false # true keeps the App running; SQLite is
|
|
87
|
+
# online: false # true keeps the App running; SQLite is read in
|
|
88
|
+
# # place. Other files are read as they are: a file
|
|
89
|
+
# # being written meanwhile may be inconsistent
|
|
88
90
|
# data_version: "1.0" # "major.minor"; a restore refuses newer data
|
|
89
91
|
# hook: bin/backup-hook # optional packaged freeze/thaw executable
|
|
92
|
+
# exclude: [tmp] # optional data-folder globs left out (scratch data)
|
|
90
93
|
# To opt out, use only:
|
|
91
94
|
# backup:
|
|
92
95
|
# hyper_backup: false
|
|
@@ -45,7 +45,7 @@ bridge App's runtime serves.
|
|
|
45
45
|
|
|
46
46
|
Use the installed `@synopackageland/ui` and its declared compatible patch range.
|
|
47
47
|
`App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
|
|
48
|
-
`pages` from `shellPages`,
|
|
48
|
+
`pages` from `shellPages`, real page actions in `#actions` and the pages in its
|
|
49
49
|
default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
|
|
50
50
|
`#actions` in a bar above the page and scrolls only the content; a settings
|
|
51
51
|
page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
|
|
@@ -60,6 +60,14 @@ device-width viewport. Its full-page sidebar names the App with
|
|
|
60
60
|
shows the signed-in DSM account and its role from `ctx.user`. See the ui's
|
|
61
61
|
`docs/adaptive-layout.md`.
|
|
62
62
|
|
|
63
|
+
The template puts no Refresh in `#actions`, and with nothing there `AppShell`
|
|
64
|
+
draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
|
|
65
|
+
the status, the panels and the portal again when the window is shown again,
|
|
66
|
+
and showing the status page again reads it again. A failed read offers Retry
|
|
67
|
+
beside its error. Sign out appears in `#actions` only when standalone login is
|
|
68
|
+
on. Keep `#actions` for real page actions, such as Add; a section that needs a
|
|
69
|
+
manual re-read gets its own action in that section, not a header Refresh.
|
|
70
|
+
|
|
63
71
|
The frontend SDK and UI use patch ranges; the lockfile records the installed
|
|
64
72
|
versions. Update the lockfile deliberately when taking a newer patch.
|
|
65
73
|
|
|
@@ -71,8 +79,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
|
|
|
71
79
|
even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
|
|
72
80
|
with Back keep their title bar. `header-actions` share one "…" menu; only an
|
|
73
81
|
entry marked `primary: true`, or a sole entry with an icon, becomes a separate
|
|
74
|
-
bar button. The template's `#actions`
|
|
75
|
-
workaround is needed.
|
|
82
|
+
bar button. The template's `#actions` holds only real page actions; no title or
|
|
83
|
+
empty-bar workaround is needed.
|
|
76
84
|
|
|
77
85
|
`AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
|
|
78
86
|
a full page as well, bounded at `height: 100%`) with the UI locale from
|
|
@@ -85,9 +85,12 @@ capabilities: {}
|
|
|
85
85
|
# generated scripts/backup/ as root: a fixed script that only switches to the
|
|
86
86
|
# package account. See `synopkgland skill show backup`.
|
|
87
87
|
# backup:
|
|
88
|
-
# online: false # true keeps the App running; SQLite is
|
|
88
|
+
# online: false # true keeps the App running; SQLite is read in
|
|
89
|
+
# # place. Other files are read as they are: a file
|
|
90
|
+
# # being written meanwhile may be inconsistent
|
|
89
91
|
# data_version: "1.0" # "major.minor"; a restore refuses newer data
|
|
90
92
|
# hook: bin/backup-hook # optional packaged freeze/thaw executable
|
|
93
|
+
# exclude: [tmp] # optional data-folder globs left out (scratch data)
|
|
91
94
|
# To opt out, use only:
|
|
92
95
|
# backup:
|
|
93
96
|
# hyper_backup: false
|
|
@@ -45,7 +45,7 @@ bridge App's runtime serves.
|
|
|
45
45
|
|
|
46
46
|
Use the installed `@synopackageland/ui` and its declared compatible patch range.
|
|
47
47
|
`App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
|
|
48
|
-
`pages` from `shellPages`,
|
|
48
|
+
`pages` from `shellPages`, real page actions in `#actions` and the pages in its
|
|
49
49
|
default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
|
|
50
50
|
`#actions` in a bar above the page and scrolls only the content; a settings
|
|
51
51
|
page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
|
|
@@ -60,6 +60,14 @@ device-width viewport. Its full-page sidebar names the App with
|
|
|
60
60
|
shows the signed-in DSM account and its role from `ctx.user`. See the ui's
|
|
61
61
|
`docs/adaptive-layout.md`.
|
|
62
62
|
|
|
63
|
+
The template puts no Refresh in `#actions`, and with nothing there `AppShell`
|
|
64
|
+
draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
|
|
65
|
+
the status, the panels and the portal again when the window is shown again,
|
|
66
|
+
and showing the status page again reads it again. A failed read offers Retry
|
|
67
|
+
beside its error. Sign out appears in `#actions` only when standalone login is
|
|
68
|
+
on. Keep `#actions` for real page actions, such as Add; a section that needs a
|
|
69
|
+
manual re-read gets its own action in that section, not a header Refresh.
|
|
70
|
+
|
|
63
71
|
The frontend SDK and UI use patch ranges; the lockfile records the installed
|
|
64
72
|
versions. Update the lockfile deliberately when taking a newer patch.
|
|
65
73
|
|
|
@@ -71,8 +79,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
|
|
|
71
79
|
even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
|
|
72
80
|
with Back keep their title bar. `header-actions` share one "…" menu; only an
|
|
73
81
|
entry marked `primary: true`, or a sole entry with an icon, becomes a separate
|
|
74
|
-
bar button. The template's `#actions`
|
|
75
|
-
workaround is needed.
|
|
82
|
+
bar button. The template's `#actions` holds only real page actions; no title or
|
|
83
|
+
empty-bar workaround is needed.
|
|
76
84
|
|
|
77
85
|
`AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
|
|
78
86
|
a full page as well, bounded at `height: 100%`) with the UI locale from
|