@antelopejs/dms 0.5.0 → 0.5.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 +50 -0
- package/dist/implementations/dms/page.js +69 -22
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/settings/users/member-invite-form.d.ts +5 -0
- package/dist/pages/settings/users/member-invite-form.js +11 -2
- package/dist/pages/settings/users/member-invite-form.js.map +1 -1
- package/dist/pages/settings/users/members.d.ts +2 -0
- package/dist/pages/settings/users/members.js +11 -0
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/realtime/index.js +2 -0
- package/dist/realtime/index.js.map +1 -1
- package/dist/realtime/sessions.d.ts +3 -0
- package/dist/realtime/sessions.js +5 -0
- package/dist/realtime/sessions.js.map +1 -1
- package/dist/realtime/sse.d.ts +1 -1
- package/dist/realtime/sse.js +9 -3
- package/dist/realtime/sse.js.map +1 -1
- package/dist/routes/realtime.js +8 -3
- package/dist/routes/realtime.js.map +1 -1
- package/dist/test/helpers/reload-host.d.ts +7 -2
- package/dist/test/helpers/reload-host.js +23 -4
- package/dist/test/helpers/reload-host.js.map +1 -1
- package/dist/test/reload-host/index.js +27 -1
- package/dist/test/reload-host/index.js.map +1 -1
- package/dist/test/reload-host/package.json +1 -1
- package/dist/test/unit/implementations/dms/missing-category.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/missing-category.test.js +174 -0
- package/dist/test/unit/implementations/dms/missing-category.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/database-initialized-hook.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/database-initialized-hook.test.js +92 -0
- package/dist/test/unit/interfaces/dms/database-initialized-hook.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/invite-extension.test.js +59 -0
- package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js +2 -2
- package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-extension-ownership.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js +153 -0
- package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js.map +1 -0
- package/dist/test/unit/pages/settings/invite-availability.test.d.ts +1 -0
- package/dist/test/unit/pages/settings/invite-availability.test.js +112 -0
- package/dist/test/unit/pages/settings/invite-availability.test.js.map +1 -0
- package/dist/test/unit/realtime/stop-closes-streams.test.d.ts +1 -0
- package/dist/test/unit/realtime/stop-closes-streams.test.js +99 -0
- package/dist/test/unit/realtime/stop-closes-streams.test.js.map +1 -0
- package/dist/test/unit/skills/dms-dev-script.test.d.ts +1 -0
- package/dist/test/unit/skills/dms-dev-script.test.js +137 -0
- package/dist/test/unit/skills/dms-dev-script.test.js.map +1 -0
- package/dist/validation/auth.schema.d.ts +2 -2
- package/frontend-vue/dms.frontend.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +116 -44
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +18 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/plugins/shortcuts.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/types/shortcuts-aggregated.d.ts +8 -0
- package/frontend-vue/tests/realtime-reconnect.test.ts +215 -0
- package/frontend-vue/tests/shortcuts-aggregate.test.ts +41 -0
- package/frontend-vue/vitest.config.ts +4 -0
- package/package.json +4 -4
- package/skills/dms-dev/SKILL.md +28 -16
- package/skills/dms-dev/scripts/dms-dev.sh +275 -83
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_child_process_1 = require("node:child_process");
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const chai_1 = require("chai");
|
|
9
|
+
const PACKAGE_ROOT = (0, node_path_1.resolve)(__dirname, "..", "..", "..", "..");
|
|
10
|
+
const SCRIPT = (0, node_path_1.join)(PACKAGE_ROOT, "skills", "dms-dev", "scripts", "dms-dev.sh");
|
|
11
|
+
const SCRIPT_TIMEOUT_MS = 30_000;
|
|
12
|
+
const SUITE_TIMEOUT_MS = 90_000;
|
|
13
|
+
const STOP_GRACE_SECONDS = "1";
|
|
14
|
+
const READY_TIMEOUT_SECONDS = "15";
|
|
15
|
+
const PROJECT_PORTS = { first: 6110, second: 6120 };
|
|
16
|
+
const FAKE_BACKEND = `#!/usr/bin/env bash
|
|
17
|
+
mkdir -p .antelope
|
|
18
|
+
printf '{"pid": %s, "servers": {"api": {"endpoints": [{"protocol": "http", "host": "0.0.0.0", "port": %s}]}}}' "$$" "$FAKE_PORT" > .antelope/dev.json
|
|
19
|
+
sleep 300 &
|
|
20
|
+
echo "Server started, listening on http://localhost:$FAKE_PORT"
|
|
21
|
+
wait
|
|
22
|
+
`;
|
|
23
|
+
const FAKE_FRONTEND = `#!/usr/bin/env bash
|
|
24
|
+
trap '' TERM
|
|
25
|
+
sleep 300 &
|
|
26
|
+
echo "Server ready on http://localhost:$((FAKE_PORT + 1))"
|
|
27
|
+
wait
|
|
28
|
+
`;
|
|
29
|
+
const runProcess = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
30
|
+
function createProject(root, name, port) {
|
|
31
|
+
const dir = (0, node_path_1.join)(root, name);
|
|
32
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(dir, "bin"), { recursive: true });
|
|
33
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "package.json"), "{}");
|
|
34
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "bin", "antelope-runner"), FAKE_BACKEND);
|
|
35
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "bin", "ajs-dms"), FAKE_FRONTEND);
|
|
36
|
+
(0, node_fs_1.chmodSync)((0, node_path_1.join)(dir, "bin", "antelope-runner"), 0o755);
|
|
37
|
+
(0, node_fs_1.chmodSync)((0, node_path_1.join)(dir, "bin", "ajs-dms"), 0o755);
|
|
38
|
+
return { dir, port };
|
|
39
|
+
}
|
|
40
|
+
async function runScript(project, command) {
|
|
41
|
+
const env = {
|
|
42
|
+
...process.env,
|
|
43
|
+
FAKE_PORT: String(project.port),
|
|
44
|
+
DMS_BACK_DIR: project.dir,
|
|
45
|
+
DMS_BACK_CMD: (0, node_path_1.join)(project.dir, "bin", "antelope-runner"),
|
|
46
|
+
DMS_FRONT_CMD: `${(0, node_path_1.join)(project.dir, "bin", "ajs-dms")} dev`,
|
|
47
|
+
DMS_BACK_LOG: (0, node_path_1.join)(project.dir, "back.log"),
|
|
48
|
+
DMS_FRONT_LOG: (0, node_path_1.join)(project.dir, "front.log"),
|
|
49
|
+
DMS_DEV_STOP_GRACE: STOP_GRACE_SECONDS,
|
|
50
|
+
DMS_DEV_TIMEOUT: READY_TIMEOUT_SECONDS,
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
const { stdout, stderr } = await runProcess("bash", [SCRIPT, command], {
|
|
54
|
+
env,
|
|
55
|
+
timeout: SCRIPT_TIMEOUT_MS,
|
|
56
|
+
});
|
|
57
|
+
return { code: 0, output: stdout + stderr };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const failure = error;
|
|
61
|
+
return {
|
|
62
|
+
code: failure.code ?? 1,
|
|
63
|
+
output: `${failure.stdout ?? ""}${failure.stderr ?? ""}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function pidFile(project) {
|
|
68
|
+
return (0, node_path_1.join)(project.dir, ".antelope", "dms-dev.pid");
|
|
69
|
+
}
|
|
70
|
+
function recordedPids(project) {
|
|
71
|
+
return (0, node_fs_1.readFileSync)(pidFile(project), "utf8")
|
|
72
|
+
.trim()
|
|
73
|
+
.split("\n")
|
|
74
|
+
.map((line) => Number(line.split(" ")[1]));
|
|
75
|
+
}
|
|
76
|
+
function isAlive(pid) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
describe("[unit] skills/dms-dev script", function () {
|
|
86
|
+
this.timeout(SUITE_TIMEOUT_MS);
|
|
87
|
+
let root;
|
|
88
|
+
let first;
|
|
89
|
+
let second;
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "dms-dev-script-"));
|
|
92
|
+
first = createProject(root, "first", PROJECT_PORTS.first);
|
|
93
|
+
second = createProject(root, "second", PROJECT_PORTS.second);
|
|
94
|
+
});
|
|
95
|
+
afterEach(async () => {
|
|
96
|
+
await runScript(first, "stop");
|
|
97
|
+
await runScript(second, "stop");
|
|
98
|
+
(0, node_fs_1.rmSync)(root, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
it("reports the ports the servers announce", async () => {
|
|
101
|
+
const started = await runScript(first, "start");
|
|
102
|
+
(0, chai_1.expect)(started.code, started.output).to.equal(0);
|
|
103
|
+
(0, chai_1.expect)(started.output).to.contain(`backend ready on http://localhost:${PROJECT_PORTS.first}.`);
|
|
104
|
+
(0, chai_1.expect)(started.output).to.contain(`frontend ready on http://localhost:${PROJECT_PORTS.first + 1}.`);
|
|
105
|
+
});
|
|
106
|
+
it("stops only the processes of its own project", async () => {
|
|
107
|
+
await runScript(first, "start");
|
|
108
|
+
await runScript(second, "start");
|
|
109
|
+
const firstPids = recordedPids(first);
|
|
110
|
+
const secondPids = recordedPids(second);
|
|
111
|
+
const stopped = await runScript(first, "stop");
|
|
112
|
+
(0, chai_1.expect)(stopped.code, stopped.output).to.equal(0);
|
|
113
|
+
(0, chai_1.expect)(firstPids.filter(isAlive)).to.deep.equal([]);
|
|
114
|
+
(0, chai_1.expect)(secondPids.filter(isAlive)).to.deep.equal(secondPids);
|
|
115
|
+
(0, chai_1.expect)((0, node_fs_1.existsSync)(pidFile(first))).to.equal(false);
|
|
116
|
+
});
|
|
117
|
+
it("falls back to the project's directory when the pid file is gone", async () => {
|
|
118
|
+
await runScript(first, "start");
|
|
119
|
+
await runScript(second, "start");
|
|
120
|
+
const firstPids = recordedPids(first);
|
|
121
|
+
const secondPids = recordedPids(second);
|
|
122
|
+
(0, node_fs_1.rmSync)(pidFile(first));
|
|
123
|
+
const stopped = await runScript(first, "stop");
|
|
124
|
+
(0, chai_1.expect)(stopped.code, stopped.output).to.equal(0);
|
|
125
|
+
(0, chai_1.expect)(firstPids.filter(isAlive)).to.deep.equal([]);
|
|
126
|
+
(0, chai_1.expect)(secondPids.filter(isAlive)).to.deep.equal(secondPids);
|
|
127
|
+
});
|
|
128
|
+
it("fails fast when the server command dies right away", async () => {
|
|
129
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(first.dir, "bin", "antelope-runner"));
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
const started = await runScript(first, "start");
|
|
132
|
+
(0, chai_1.expect)(started.code).to.equal(1);
|
|
133
|
+
(0, chai_1.expect)(started.output).to.contain("exited before becoming ready");
|
|
134
|
+
(0, chai_1.expect)(Date.now() - startedAt).to.be.below(SCRIPT_TIMEOUT_MS / 2);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
//# sourceMappingURL=dms-dev-script.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dms-dev-script.test.js","sourceRoot":"","sources":["../../../../src/test/unit/skills/dms-dev-script.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,14 +42,14 @@ export declare const authSchema: {
|
|
|
42
42
|
code: string;
|
|
43
43
|
state: string;
|
|
44
44
|
state_cookie: string;
|
|
45
|
-
language?: string | undefined;
|
|
46
45
|
invite?: string | undefined;
|
|
46
|
+
language?: string | undefined;
|
|
47
47
|
}, {
|
|
48
48
|
code: string;
|
|
49
49
|
state: string;
|
|
50
50
|
state_cookie: string;
|
|
51
|
-
language?: string | undefined;
|
|
52
51
|
invite?: string | undefined;
|
|
52
|
+
language?: string | undefined;
|
|
53
53
|
}>;
|
|
54
54
|
refresh: z.ZodObject<{
|
|
55
55
|
token: z.ZodString;
|
|
@@ -2,6 +2,7 @@ import { defineAsyncComponent, type Component } from "vue";
|
|
|
2
2
|
import {
|
|
3
3
|
type DmsFrontendModule,
|
|
4
4
|
useDmsAppConfig,
|
|
5
|
+
useDmsRuntimeConfig,
|
|
5
6
|
} from "#dms/frontend-module";
|
|
6
7
|
import { defu } from "defu";
|
|
7
8
|
import appConfig from "./layers/dms-layout/app/app.config";
|
|
@@ -171,9 +172,9 @@ function registerMiddleware(
|
|
|
171
172
|
|
|
172
173
|
const frontendModule: DmsFrontendModule = {
|
|
173
174
|
setup(sdk) {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
|
|
175
|
+
const runtimeDms = (useDmsRuntimeConfig().public as DmsPublicOptions).dms;
|
|
176
|
+
if (runtimeDms && typeof window !== "undefined")
|
|
177
|
+
runtimeDms.baseURL = window.location.origin;
|
|
177
178
|
const runtimeAppConfig = useDmsAppConfig();
|
|
178
179
|
Object.assign(runtimeAppConfig, defu(runtimeAppConfig, appConfig));
|
|
179
180
|
registerPlugins(sdk);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isUnauthorizedStreamError,
|
|
3
|
+
openSseStream,
|
|
4
|
+
type SseStreamHandle,
|
|
5
|
+
} from "./useSseStream";
|
|
2
6
|
|
|
3
7
|
const REALTIME_USER_PATH = "/api/realtime/user";
|
|
4
8
|
const REALTIME_SUBSCRIBE_PATH_PREFIX = "/api/realtime/subscribe/";
|
|
@@ -117,57 +121,109 @@ const handleEvent =
|
|
|
117
121
|
}
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
interface ConnectionCallbacks {
|
|
125
|
+
onConnected: () => void;
|
|
126
|
+
onDisconnected: () => void;
|
|
127
|
+
refreshSession: () => Promise<boolean>;
|
|
128
|
+
redirectToAuth: () => Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface Reconnector {
|
|
132
|
+
isCancelled(): boolean;
|
|
133
|
+
schedule(open: () => void): void;
|
|
134
|
+
resetDelay(): void;
|
|
135
|
+
cancel(): void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const createReconnector = (): Reconnector => {
|
|
139
|
+
let isCancelled = false;
|
|
140
|
+
let delay = RECONNECT_INITIAL_DELAY_MS;
|
|
141
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
142
|
+
return {
|
|
143
|
+
isCancelled: () => isCancelled,
|
|
144
|
+
schedule: (open) => {
|
|
145
|
+
// A pending reconnect absorbs concurrent triggers: one stream per tab.
|
|
146
|
+
if (isCancelled || timer) return;
|
|
147
|
+
timer = setTimeout(() => {
|
|
148
|
+
timer = undefined;
|
|
149
|
+
open();
|
|
150
|
+
}, delay);
|
|
151
|
+
delay = Math.min(delay * RECONNECT_MULTIPLIER, RECONNECT_MAX_DELAY_MS);
|
|
152
|
+
},
|
|
153
|
+
resetDelay: () => {
|
|
154
|
+
delay = RECONNECT_INITIAL_DELAY_MS;
|
|
155
|
+
},
|
|
156
|
+
cancel: () => {
|
|
157
|
+
isCancelled = true;
|
|
158
|
+
if (timer) clearTimeout(timer);
|
|
159
|
+
timer = undefined;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const recoverFromError = (
|
|
165
|
+
error: unknown,
|
|
166
|
+
reconnect: () => void,
|
|
167
|
+
callbacks: ConnectionCallbacks,
|
|
168
|
+
): void => {
|
|
169
|
+
if (!isUnauthorizedStreamError(error)) {
|
|
170
|
+
reconnect();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
void callbacks.refreshSession().then((isRefreshed) => {
|
|
174
|
+
if (isRefreshed) {
|
|
175
|
+
reconnect();
|
|
176
|
+
} else {
|
|
177
|
+
void callbacks.redirectToAuth();
|
|
178
|
+
}
|
|
179
|
+
}, reconnect);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Keeps one user stream open until cancelled. The server ends the stream when
|
|
184
|
+
* it stops (a dev reload included), so a clean EOF reconnects just like an
|
|
185
|
+
* error does: the user stream has no business completion.
|
|
186
|
+
*/
|
|
120
187
|
const connectWithBackoff = (
|
|
121
188
|
url: string,
|
|
122
189
|
state: UserRealtimeState,
|
|
123
|
-
|
|
124
|
-
refreshSession: () => Promise<boolean>,
|
|
125
|
-
redirectToAuth: () => Promise<void>,
|
|
190
|
+
callbacks: ConnectionCallbacks,
|
|
126
191
|
): (() => void) => {
|
|
127
|
-
|
|
128
|
-
let delay = RECONNECT_INITIAL_DELAY_MS;
|
|
129
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
130
|
-
let activeHandle: { close(): void } | undefined;
|
|
192
|
+
const reconnector = createReconnector();
|
|
131
193
|
const dispatch = handleEvent(state);
|
|
194
|
+
let activeHandle: SseStreamHandle | undefined;
|
|
132
195
|
const open = () => {
|
|
133
|
-
if (
|
|
134
|
-
activeHandle
|
|
196
|
+
if (reconnector.isCancelled()) return;
|
|
197
|
+
activeHandle?.close();
|
|
198
|
+
const handle: SseStreamHandle = openSseStream({
|
|
135
199
|
url,
|
|
136
200
|
onEvent: (eventName, data) => {
|
|
137
|
-
if (
|
|
138
|
-
|
|
201
|
+
if (handle !== activeHandle) return;
|
|
202
|
+
reconnector.resetDelay();
|
|
139
203
|
dispatch(eventName, data);
|
|
140
|
-
if (eventName === HELLO_EVENT) onConnected();
|
|
204
|
+
if (eventName === HELLO_EVENT) callbacks.onConnected();
|
|
141
205
|
},
|
|
142
206
|
onError: (error) => {
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
delay * RECONNECT_MULTIPLIER,
|
|
150
|
-
RECONNECT_MAX_DELAY_MS,
|
|
151
|
-
);
|
|
152
|
-
};
|
|
153
|
-
if (isUnauthorizedStreamError(error)) {
|
|
154
|
-
void refreshSession().then((refreshed) => {
|
|
155
|
-
if (refreshed) {
|
|
156
|
-
scheduleReconnect();
|
|
157
|
-
} else {
|
|
158
|
-
void redirectToAuth();
|
|
159
|
-
}
|
|
160
|
-
}, scheduleReconnect);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
scheduleReconnect();
|
|
207
|
+
if (!retire(handle)) return;
|
|
208
|
+
recoverFromError(error, reconnect, callbacks);
|
|
209
|
+
},
|
|
210
|
+
onClose: (reason) => {
|
|
211
|
+
if (reason !== "eof" || !retire(handle)) return;
|
|
212
|
+
reconnect();
|
|
164
213
|
},
|
|
165
214
|
});
|
|
215
|
+
activeHandle = handle;
|
|
216
|
+
};
|
|
217
|
+
const reconnect = () => reconnector.schedule(open);
|
|
218
|
+
const retire = (handle: SseStreamHandle): boolean => {
|
|
219
|
+
if (handle !== activeHandle || reconnector.isCancelled()) return false;
|
|
220
|
+
activeHandle = undefined;
|
|
221
|
+
callbacks.onDisconnected();
|
|
222
|
+
return true;
|
|
166
223
|
};
|
|
167
224
|
open();
|
|
168
225
|
return () => {
|
|
169
|
-
|
|
170
|
-
if (timer) clearTimeout(timer);
|
|
226
|
+
reconnector.cancel();
|
|
171
227
|
activeHandle?.close();
|
|
172
228
|
activeHandle = undefined;
|
|
173
229
|
};
|
|
@@ -196,14 +252,29 @@ const reconcilePage = async (
|
|
|
196
252
|
baseUrl: string,
|
|
197
253
|
): Promise<void> => {
|
|
198
254
|
const desired = state.pageId.value ?? null;
|
|
255
|
+
const sessionId = state.sessionId.value;
|
|
199
256
|
if (state.appliedPageId.value === desired) return;
|
|
200
|
-
if (!
|
|
257
|
+
if (!sessionId) return;
|
|
201
258
|
await callPageEndpoint(state, desired, baseUrl);
|
|
259
|
+
// A reconnect during the call replaced the session: the page is applied to
|
|
260
|
+
// the dead one, and the new one still needs it.
|
|
261
|
+
if (state.sessionId.value !== sessionId) return;
|
|
202
262
|
if ((state.pageId.value ?? null) === desired) {
|
|
203
263
|
state.appliedPageId.value = desired;
|
|
204
264
|
}
|
|
205
265
|
};
|
|
206
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Forgets what the server held for the closed session. The next greeting
|
|
269
|
+
* brings a new session id, which re-applies the page topics here and lets the
|
|
270
|
+
* components holding presence re-acquire it.
|
|
271
|
+
*/
|
|
272
|
+
const resetSession = (state: UserRealtimeState): void => {
|
|
273
|
+
state.sessionId.value = undefined;
|
|
274
|
+
state.appliedPageId.value = null;
|
|
275
|
+
state.snapshots.clear();
|
|
276
|
+
};
|
|
277
|
+
|
|
207
278
|
const buildApi = (): UserRealtimeApi => {
|
|
208
279
|
const sessionId = useDmsState<string | undefined>(
|
|
209
280
|
SESSION_STATE_KEY,
|
|
@@ -232,19 +303,20 @@ const buildApi = (): UserRealtimeApi => {
|
|
|
232
303
|
(isLoggedIn) => {
|
|
233
304
|
state.cancelConnect?.();
|
|
234
305
|
state.cancelConnect = undefined;
|
|
235
|
-
state
|
|
306
|
+
resetSession(state);
|
|
236
307
|
state.menuTopics.value = [];
|
|
237
|
-
state.appliedPageId.value = null;
|
|
238
|
-
state.snapshots.clear();
|
|
239
308
|
if (!isLoggedIn) return;
|
|
240
309
|
state.cancelConnect = connectWithBackoff(
|
|
241
310
|
`${baseUrl}${REALTIME_USER_PATH}`,
|
|
242
311
|
state,
|
|
243
|
-
|
|
244
|
-
|
|
312
|
+
{
|
|
313
|
+
onConnected: () => {
|
|
314
|
+
void reconcilePage(state, baseUrl);
|
|
315
|
+
},
|
|
316
|
+
onDisconnected: () => resetSession(state),
|
|
317
|
+
refreshSession,
|
|
318
|
+
redirectToAuth,
|
|
245
319
|
},
|
|
246
|
-
refreshSession,
|
|
247
|
-
redirectToAuth,
|
|
248
320
|
);
|
|
249
321
|
},
|
|
250
322
|
{ immediate: true },
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { withoutTrailingSlash } from "ufo";
|
|
3
3
|
|
|
4
|
+
defineOptions({ inheritAttrs: false });
|
|
5
|
+
|
|
4
6
|
// Dev-only: created behind the flag so the key never reaches a production
|
|
5
7
|
// SSR payload.
|
|
6
8
|
const devReloading = import.meta.env.DEV ? useDevReloading() : ref(false);
|
|
@@ -244,15 +244,25 @@ const uiTableActions = computed(() => uiTableActionsVariant());
|
|
|
244
244
|
@click="tableSharedData!.emits('add')"
|
|
245
245
|
/>
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
<!-- A disabled button fires no pointer event: the tooltip hangs on a
|
|
248
|
+
wrapper, focusable so the reason also reaches keyboard users. -->
|
|
249
|
+
<UTooltip
|
|
248
250
|
v-for="(button, index) in customButtons"
|
|
249
251
|
:key="`custom-btn-${index}`"
|
|
250
|
-
:
|
|
251
|
-
:
|
|
252
|
-
|
|
253
|
-
:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
:text="button.disabledReason && processI18n(button.disabledReason)"
|
|
253
|
+
:disabled="!button.disabled || !button.disabledReason"
|
|
254
|
+
>
|
|
255
|
+
<span :tabindex="button.disabled ? 0 : undefined" class="inline-flex">
|
|
256
|
+
<UButton
|
|
257
|
+
:label="processI18n(button.label)"
|
|
258
|
+
:icon="button.icon"
|
|
259
|
+
:variant="button.variant"
|
|
260
|
+
:color="button.color"
|
|
261
|
+
:disabled="button.disabled"
|
|
262
|
+
size="sm"
|
|
263
|
+
@click="props.onCustomButton?.(button)"
|
|
264
|
+
/>
|
|
265
|
+
</span>
|
|
266
|
+
</UTooltip>
|
|
257
267
|
</div>
|
|
258
268
|
</template>
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -795,7 +795,19 @@ export const useTableRowActions = <T extends Data>(
|
|
|
795
795
|
}
|
|
796
796
|
};
|
|
797
797
|
|
|
798
|
+
// Reached for a disabled button only through a quick action, which presses
|
|
799
|
+
// it by id: the reason is told instead of the target being opened.
|
|
798
800
|
const handleCustomButton = (button: CustomButton) => {
|
|
801
|
+
if (button.disabled) {
|
|
802
|
+
toast.add({
|
|
803
|
+
color: Color.warning,
|
|
804
|
+
title: processI18n(button.label),
|
|
805
|
+
description: button.disabledReason
|
|
806
|
+
? processI18n(button.disabledReason)
|
|
807
|
+
: undefined,
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
799
811
|
handleActionTarget(button.target, button.label);
|
|
800
812
|
};
|
|
801
813
|
|
|
@@ -10,4 +10,8 @@ export interface CustomButton {
|
|
|
10
10
|
variant?: CustomButtonVariant;
|
|
11
11
|
color?: ColorValue;
|
|
12
12
|
target: ActionTarget;
|
|
13
|
+
/** Set by the server when the button cannot be pressed for this request. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Why the button is disabled, shown in its tooltip. May be an i18n key. */
|
|
16
|
+
disabledReason?: string;
|
|
13
17
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Generated by the renderer in the workspace: every module's
|
|
2
|
+
// `app/config/shortcuts-registry.ts`, spread in priority order.
|
|
3
|
+
declare module "#shortcuts-aggregated" {
|
|
4
|
+
import type { ComponentShortcuts } from "#dms-ui/app/types/shortcuts";
|
|
5
|
+
|
|
6
|
+
const aggregatedShortcuts: ComponentShortcuts[];
|
|
7
|
+
export default aggregatedShortcuts;
|
|
8
|
+
}
|