@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/implementations/dms/page.js +69 -22
  3. package/dist/implementations/dms/page.js.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/pages/settings/users/member-invite-form.d.ts +5 -0
  7. package/dist/pages/settings/users/member-invite-form.js +11 -2
  8. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  9. package/dist/pages/settings/users/members.d.ts +2 -0
  10. package/dist/pages/settings/users/members.js +11 -0
  11. package/dist/pages/settings/users/members.js.map +1 -1
  12. package/dist/realtime/index.js +2 -0
  13. package/dist/realtime/index.js.map +1 -1
  14. package/dist/realtime/sessions.d.ts +3 -0
  15. package/dist/realtime/sessions.js +5 -0
  16. package/dist/realtime/sessions.js.map +1 -1
  17. package/dist/realtime/sse.d.ts +1 -1
  18. package/dist/realtime/sse.js +9 -3
  19. package/dist/realtime/sse.js.map +1 -1
  20. package/dist/routes/realtime.js +8 -3
  21. package/dist/routes/realtime.js.map +1 -1
  22. package/dist/test/helpers/reload-host.d.ts +7 -2
  23. package/dist/test/helpers/reload-host.js +23 -4
  24. package/dist/test/helpers/reload-host.js.map +1 -1
  25. package/dist/test/reload-host/index.js +27 -1
  26. package/dist/test/reload-host/index.js.map +1 -1
  27. package/dist/test/reload-host/package.json +1 -1
  28. package/dist/test/unit/implementations/dms/missing-category.test.d.ts +1 -0
  29. package/dist/test/unit/implementations/dms/missing-category.test.js +174 -0
  30. package/dist/test/unit/implementations/dms/missing-category.test.js.map +1 -0
  31. package/dist/test/unit/interfaces/dms/database-initialized-hook.test.d.ts +1 -0
  32. package/dist/test/unit/interfaces/dms/database-initialized-hook.test.js +92 -0
  33. package/dist/test/unit/interfaces/dms/database-initialized-hook.test.js.map +1 -0
  34. package/dist/test/unit/interfaces/dms/invite-extension.test.js +59 -0
  35. package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
  36. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js +2 -2
  37. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js.map +1 -1
  38. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.d.ts +1 -0
  39. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js +153 -0
  40. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js.map +1 -0
  41. package/dist/test/unit/pages/settings/invite-availability.test.d.ts +1 -0
  42. package/dist/test/unit/pages/settings/invite-availability.test.js +112 -0
  43. package/dist/test/unit/pages/settings/invite-availability.test.js.map +1 -0
  44. package/dist/test/unit/realtime/stop-closes-streams.test.d.ts +1 -0
  45. package/dist/test/unit/realtime/stop-closes-streams.test.js +99 -0
  46. package/dist/test/unit/realtime/stop-closes-streams.test.js.map +1 -0
  47. package/dist/test/unit/skills/dms-dev-script.test.d.ts +1 -0
  48. package/dist/test/unit/skills/dms-dev-script.test.js +137 -0
  49. package/dist/test/unit/skills/dms-dev-script.test.js.map +1 -0
  50. package/dist/validation/auth.schema.d.ts +2 -2
  51. package/frontend-vue/dms.frontend.ts +4 -3
  52. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +116 -44
  53. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +2 -0
  54. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +18 -8
  55. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +12 -0
  56. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +4 -0
  57. package/frontend-vue/layers/dms-ui/app/plugins/shortcuts.ts +1 -1
  58. package/frontend-vue/layers/dms-ui/app/types/shortcuts-aggregated.d.ts +8 -0
  59. package/frontend-vue/tests/realtime-reconnect.test.ts +215 -0
  60. package/frontend-vue/tests/shortcuts-aggregate.test.ts +41 -0
  61. package/frontend-vue/vitest.config.ts +4 -0
  62. package/package.json +4 -4
  63. package/skills/dms-dev/SKILL.md +28 -16
  64. 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 publicOptions = sdk.options.public as DmsPublicOptions;
175
- if (publicOptions.dms && typeof window !== "undefined")
176
- publicOptions.dms.baseURL = window.location.origin;
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 { isUnauthorizedStreamError, openSseStream } from "./useSseStream";
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
- onConnected: () => void,
124
- refreshSession: () => Promise<boolean>,
125
- redirectToAuth: () => Promise<void>,
190
+ callbacks: ConnectionCallbacks,
126
191
  ): (() => void) => {
127
- let cancelled = false;
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 (cancelled) return;
134
- activeHandle = openSseStream({
196
+ if (reconnector.isCancelled()) return;
197
+ activeHandle?.close();
198
+ const handle: SseStreamHandle = openSseStream({
135
199
  url,
136
200
  onEvent: (eventName, data) => {
137
- if (cancelled) return;
138
- delay = RECONNECT_INITIAL_DELAY_MS;
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 (cancelled) return;
144
- state.appliedPageId.value = null;
145
- const scheduleReconnect = () => {
146
- if (cancelled) return;
147
- timer = setTimeout(open, delay);
148
- delay = Math.min(
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
- cancelled = true;
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 (!state.sessionId.value) return;
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.sessionId.value = undefined;
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
- void reconcilePage(state, baseUrl);
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
- <UButton
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
- :label="processI18n(button.label)"
251
- :icon="button.icon"
252
- :variant="button.variant"
253
- :color="button.color"
254
- size="sm"
255
- @click="props.onCustomButton?.(button)"
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>
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import aggregatedShortcuts from "../config/shortcuts-registry";
1
+ import aggregatedShortcuts from "#shortcuts-aggregated";
2
2
  export default defineDmsPlugin(() => {
3
3
  const { registerShortcut } = useShortcutRegistry();
4
4
 
@@ -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
+ }