@panneau/actions 4.1.0 → 5.0.0-alpha.1

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 (45) hide show
  1. package/README.md +75 -0
  2. package/dist/ActionRunner.css +84 -0
  3. package/dist/ActionRunner.module.js +34 -0
  4. package/dist/ActionRunner.module.js.map +1 -0
  5. package/dist/ActionRunnerProvider.d.ts +36 -0
  6. package/dist/ActionRunnerProvider.d.ts.map +1 -0
  7. package/dist/ActionRunnerProvider.js +245 -0
  8. package/dist/ActionRunnerProvider.js.map +1 -0
  9. package/dist/ActionRuns.d.ts +12 -0
  10. package/dist/ActionRuns.d.ts.map +1 -0
  11. package/dist/ActionRuns.js +128 -0
  12. package/dist/ActionRuns.js.map +1 -0
  13. package/dist/ParametersDialog.d.ts +19 -0
  14. package/dist/ParametersDialog.d.ts.map +1 -0
  15. package/dist/ParametersDialog.js +119 -0
  16. package/dist/ParametersDialog.js.map +1 -0
  17. package/dist/ResultsDialog.d.ts +10 -0
  18. package/dist/ResultsDialog.d.ts.map +1 -0
  19. package/dist/ResultsDialog.js +130 -0
  20. package/dist/ResultsDialog.js.map +1 -0
  21. package/dist/context.d.ts +10 -0
  22. package/dist/context.d.ts.map +1 -0
  23. package/dist/context.js +7 -0
  24. package/dist/context.js.map +1 -0
  25. package/dist/describe.d.ts +8 -0
  26. package/dist/describe.d.ts.map +1 -0
  27. package/dist/describe.js +32 -0
  28. package/dist/describe.js.map +1 -0
  29. package/dist/index.d.ts +9 -10
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -74
  32. package/dist/messages.d.ts +2 -0
  33. package/dist/messages.d.ts.map +1 -0
  34. package/dist/messages.js +1 -0
  35. package/dist/request.d.ts +15 -0
  36. package/dist/request.d.ts.map +1 -0
  37. package/dist/request.js +50 -0
  38. package/dist/request.js.map +1 -0
  39. package/dist/types.d.ts +62 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/useActionRunner.d.ts +21 -0
  42. package/dist/useActionRunner.d.ts.map +1 -0
  43. package/dist/useActionRunner.js +34 -0
  44. package/dist/useActionRunner.js.map +1 -0
  45. package/package.json +29 -50
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @panneau/actions
2
+
3
+ Runs the actions of lists and forms: parameters in a dialog, the action through a data adapter, its result, and queued runs. Why it is its own package: [decision 0023](../../docs/decisions/0023-action-runner.md).
4
+
5
+ ```tsx
6
+ import { useQueryClient } from '@tanstack/react-query';
7
+
8
+ import { ActionRunnerProvider, ActionRuns, useActionRunner } from '@panneau/actions';
9
+ import { createRestAdapter } from '@panneau/data';
10
+ import { invalidateActionResult } from '@panneau/data/query';
11
+ import { ListView } from '@panneau/lists';
12
+ import { PanneauRoot } from '@panneau/ui';
13
+
14
+ const adapter = createRestAdapter({ baseUrl: '/panneau/api' });
15
+
16
+ function App() {
17
+ const client = useQueryClient();
18
+ return (
19
+ <PanneauRoot>
20
+ <ActionRunnerProvider
21
+ adapter={adapter}
22
+ onResult={(result, { resource, target }) =>
23
+ invalidateActionResult(client, resource, result, target.selection)
24
+ }
25
+ onNavigate={(to) => navigate(typeof to === 'string' ? to : routeUrl(to))}
26
+ >
27
+ <Pages />
28
+ <ActionRuns />
29
+ </ActionRunnerProvider>
30
+ </PanneauRoot>
31
+ );
32
+ }
33
+
34
+ function Pages() {
35
+ const runAction = useActionRunner('pages');
36
+ return (
37
+ <ListView
38
+ list={list}
39
+ items={items}
40
+ columns={columns}
41
+ actions={actions}
42
+ onAction={runAction}
43
+ />
44
+ );
45
+ }
46
+ ```
47
+
48
+ | Export | Content |
49
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `@panneau/actions` | `ActionRunnerProvider`, `useActionRunner()`, `ActionRuns` and `useActionRuns()`, the dialogs (`ParametersDialog`, `ResultsDialog`), and their helpers |
51
+
52
+ ## What the runner does
53
+
54
+ `useActionRunner(resource)` returns a function that takes a list's `ListActionEvent` (or `{ action, scope: 'item', item }` for the item of a form). Lists confirm the action first, when it asks for it; then the runner:
55
+
56
+ - **`server` actions:**
57
+ 1. asks for the parameters (`fields`, in their `layout`) in a dialog, with the forms of `@panneau/forms`: client validation, then the `422` errors of the action endpoint on the fields;
58
+ 2. runs the action through the adapter, with the item or the selection (`ids`, or the list query with exceptions), or nothing for the resource;
59
+ 3. shows the result in a toast: its `message`, the counts of what didn't succeed, and a "Details" button that lists those items with their message or reason;
60
+ 4. calls `onResult` (refresh what it changed), opens the `download`, and goes to the `redirect`.
61
+ - **Queued runs** (`202`): a toast says that the action runs in the background, the runner checks the run every `pollInterval` until it ends, then shows its result as above. `ActionRuns` lists the runs, with their progress, a button to cancel them (when the adapter has `cancelActionRun()`), and one to dismiss them once they have ended.
62
+ - **`link` actions** go to their `url` (with `{id}`) or `route`, through `onNavigate`.
63
+ - **`client` actions** render the handler that their `component` names: one of `clientActions`, or of the plugins of `PanneauProvider` (`definePlugin({ actions: { 'share-dialog': ShareDialog } })`). It gets `ClientActionProps`, and ends with `onDone(result?)` or `onCancel()`.
64
+
65
+ The function resolves with what happened (`{ status: 'done', result }`, `{ status: 'queued', run }` or `{ status: 'navigated', to }`), with `false` when the user cancelled the parameters (a list then keeps its selection), and rejects when the action failed (a list then shows it in a toast).
66
+
67
+ ## The adapter
68
+
69
+ The runner only needs the action methods of a `DataAdapter`: `runAction()`, `getActionRun()` and, to cancel runs, `cancelActionRun()`. `createRestAdapter()` of `@panneau/data` has them. A product with its own API implements those three, and errors with a `problem` for validation errors (`422`). The runner doesn't need TanStack Query.
70
+
71
+ ## Where to put it
72
+
73
+ Inside `PanneauRoot` (dialogs and toasts render there), and above the router of an app, so that it follows runs from page to page. It only knows the runs that it started.
74
+
75
+ The package layout, scripts and API reports are explained in [`docs/packages.md`](../../docs/packages.md).
@@ -0,0 +1,84 @@
1
+ .panneau-parameters-i1oaps {
2
+ margin: 0;
3
+ }
4
+
5
+ .panneau-summary-tEkW4i {
6
+ margin: 0 0 var(--panneau-space-3);
7
+ color: var(--panneau-color-text-muted);
8
+ }
9
+
10
+ .panneau-results-vlBNo6 {
11
+ display: grid;
12
+ gap: var(--panneau-space-2);
13
+ margin: 0;
14
+ padding: 0;
15
+ list-style: none;
16
+ }
17
+
18
+ .panneau-result-XImXXf {
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ align-items: center;
22
+ gap: var(--panneau-space-2);
23
+ padding: var(--panneau-space-2) 0;
24
+ border-bottom: var(--panneau-border-width) solid var(--panneau-color-border);
25
+ }
26
+
27
+ .panneau-resultItem-52NJiR {
28
+ font-weight: var(--panneau-font-weight-medium);
29
+ }
30
+
31
+ .panneau-resultReason-p8tmL2 {
32
+ flex: 1 1 12rem;
33
+ color: var(--panneau-color-text-muted);
34
+ }
35
+
36
+ .panneau-runs-jyhVY9 {
37
+ display: grid;
38
+ gap: var(--panneau-space-3);
39
+ margin: 0;
40
+ padding: 0;
41
+ list-style: none;
42
+ }
43
+
44
+ .panneau-run-WVS47T {
45
+ display: grid;
46
+ grid-template-columns: 1fr auto;
47
+ align-items: center;
48
+ gap: var(--panneau-space-1) var(--panneau-space-2);
49
+ }
50
+
51
+ .panneau-runLabel-fBJsOE {
52
+ font-weight: var(--panneau-font-weight-medium);
53
+ }
54
+
55
+ .panneau-runStatus-sbRCO7 {
56
+ font-size: var(--panneau-font-size-sm);
57
+ color: var(--panneau-color-text-muted);
58
+ }
59
+
60
+ .panneau-progress-iJJ5Sw {
61
+ grid-column: 1 / -1;
62
+ display: grid;
63
+ gap: var(--panneau-space-1);
64
+ }
65
+
66
+ .panneau-track-QAzi12 {
67
+ height: 0.375rem;
68
+ overflow: hidden;
69
+ background: var(--panneau-color-surface-sunken);
70
+ border-radius: var(--panneau-radius-full);
71
+ }
72
+
73
+ .panneau-fill-IPxBGq {
74
+ height: 100%;
75
+ background: var(--panneau-color-accent);
76
+ border-radius: inherit;
77
+ transition: width var(--panneau-duration) ease-out;
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce) {
81
+ .panneau-fill-IPxBGq {
82
+ transition: none;
83
+ }
84
+ }
@@ -0,0 +1,34 @@
1
+ import "./ActionRunner.css";
2
+ //#region src/ActionRunner.module.css
3
+ var parameters = "panneau-parameters-i1oaps";
4
+ var summary = "panneau-summary-tEkW4i";
5
+ var results = "panneau-results-vlBNo6";
6
+ var result = "panneau-result-XImXXf";
7
+ var resultItem = "panneau-resultItem-52NJiR";
8
+ var resultReason = "panneau-resultReason-p8tmL2";
9
+ var runs = "panneau-runs-jyhVY9";
10
+ var run = "panneau-run-WVS47T";
11
+ var runLabel = "panneau-runLabel-fBJsOE";
12
+ var runStatus = "panneau-runStatus-sbRCO7";
13
+ var progress = "panneau-progress-iJJ5Sw";
14
+ var track = "panneau-track-QAzi12";
15
+ var fill = "panneau-fill-IPxBGq";
16
+ var ActionRunner_module_default = {
17
+ parameters,
18
+ summary,
19
+ results,
20
+ result,
21
+ resultItem,
22
+ resultReason,
23
+ runs,
24
+ run,
25
+ runLabel,
26
+ runStatus,
27
+ progress,
28
+ track,
29
+ fill
30
+ };
31
+ //#endregion
32
+ export { ActionRunner_module_default as default, fill, parameters, progress, result, resultItem, resultReason, results, run, runLabel, runStatus, runs, summary, track };
33
+
34
+ //# sourceMappingURL=ActionRunner.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionRunner.module.js","names":[],"sources":["../src/ActionRunner.module.css"],"sourcesContent":[".parameters {\n margin: 0;\n}\n\n.summary {\n margin: 0 0 var(--panneau-space-3);\n color: var(--panneau-color-text-muted);\n}\n\n.results {\n display: grid;\n gap: var(--panneau-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.result {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--panneau-space-2);\n padding: var(--panneau-space-2) 0;\n border-bottom: var(--panneau-border-width) solid var(--panneau-color-border);\n}\n\n.resultItem {\n font-weight: var(--panneau-font-weight-medium);\n}\n\n.resultReason {\n flex: 1 1 12rem;\n color: var(--panneau-color-text-muted);\n}\n\n.runs {\n display: grid;\n gap: var(--panneau-space-3);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.run {\n display: grid;\n grid-template-columns: 1fr auto;\n align-items: center;\n gap: var(--panneau-space-1) var(--panneau-space-2);\n}\n\n.runLabel {\n font-weight: var(--panneau-font-weight-medium);\n}\n\n.runStatus {\n font-size: var(--panneau-font-size-sm);\n color: var(--panneau-color-text-muted);\n}\n\n.progress {\n grid-column: 1 / -1;\n display: grid;\n gap: var(--panneau-space-1);\n}\n\n.track {\n height: 0.375rem;\n overflow: hidden;\n background: var(--panneau-color-surface-sunken);\n border-radius: var(--panneau-radius-full);\n}\n\n.fill {\n height: 100%;\n background: var(--panneau-color-accent);\n border-radius: inherit;\n transition: width var(--panneau-duration) ease-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fill {\n transition: none;\n }\n}\n"],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ActionResult, RouteReference } from '@panneau/protocol';
3
+ import type { ActionAdapter, ActionDownload, ActionResultContext, ClientActionHandler } from './types';
4
+ export interface ActionRunnerProviderProps {
5
+ /** Runs the actions: a `DataAdapter`, or any object with its action methods. */
6
+ adapter: ActionAdapter;
7
+ children?: ReactNode;
8
+ /**
9
+ * Called with every result: of an action that finished at once, and of a queued run when it
10
+ * finishes. Refresh what it changed here. With `@panneau/data/query`:
11
+ * `invalidateActionResult(client, resource, result, target.selection)`.
12
+ */
13
+ onResult?: (result: ActionResult, context: ActionResultContext) => unknown;
14
+ /**
15
+ * Goes to the `redirect` of a result, or to the URL or route of a `link` action. Defaults to
16
+ * `window.location.assign()` for URLs; routes need it.
17
+ */
18
+ onNavigate?: (to: string | RouteReference) => void;
19
+ /** Opens the `download` of a result. Defaults to a link to its URL, clicked. */
20
+ onDownload?: (download: ActionDownload) => void;
21
+ /** Handlers of `client` actions by their `component`, over those of the plugins. */
22
+ clientActions?: Readonly<Record<string, ClientActionHandler>>;
23
+ /** Milliseconds between two checks of a queued run. Defaults to 2000. */
24
+ pollInterval?: number;
25
+ /** Locales of localized parameters. Defaults to the definition's. */
26
+ locales?: readonly string[];
27
+ }
28
+ /**
29
+ * Runs the actions that lists and forms hand over ([decision 0023]): it asks for their parameters
30
+ * in a dialog, runs them through the adapter, shows their result (a toast, and the items that
31
+ * didn't succeed), follows queued runs until they end, refreshes through `onResult`, follows
32
+ * redirects and opens downloads. Put it inside `PanneauRoot`, and above the router of an app, so
33
+ * that runs are followed from page to page. `useActionRunner(resource)` runs an action.
34
+ */
35
+ export declare function ActionRunnerProvider({ adapter, children, onResult, onNavigate, onDownload, clientActions, pollInterval, locales, }: ActionRunnerProviderProps): import("react").JSX.Element;
36
+ //# sourceMappingURL=ActionRunnerProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionRunnerProvider.d.ts","sourceRoot":"","sources":["../src/ActionRunnerProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA+C,MAAM,OAAO,CAAC;AAGpF,OAAO,KAAK,EAAE,YAAY,EAAa,cAAc,EAAE,MAAM,mBAAmB,CAAC;AASjF,OAAO,KAAK,EACR,aAAa,EACb,cAAc,EAEd,mBAAmB,EAEnB,mBAAmB,EAEtB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,yBAAyB;IACtC,gFAAgF;IAChF,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC3E;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,KAAK,IAAI,CAAC;IACnD,gFAAgF;IAChF,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,oFAAoF;IACpF,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAC9D,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAmCD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,UAAuB,EACvB,UAAyB,EACzB,aAAa,EACb,YAAmB,EACnB,OAAO,GACV,EAAE,yBAAyB,+BA+O3B"}
@@ -0,0 +1,245 @@
1
+ import { messages } from "./messages.js";
2
+ import { ParametersDialog } from "./ParametersDialog.js";
3
+ import { buildActionRequest, getLinkTarget, isFinished } from "./request.js";
4
+ import { describeResult } from "./describe.js";
5
+ import { ResultsDialog } from "./ResultsDialog.js";
6
+ import { ActionRunnerContext } from "./context.js";
7
+ import { useEffect, useEffectEvent, useRef, useState } from "react";
8
+ import { usePlugins } from "@panneau/core";
9
+ import { usePanneauIntl, useToast } from "@panneau/ui";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/ActionRunnerProvider.tsx
12
+ function navigateTo(to) {
13
+ if (typeof to === "string") window.location.assign(to);
14
+ else console.warn(`[panneau] An action goes to the route "${to.route}": give ActionRunnerProvider an onNavigate.`);
15
+ }
16
+ function openDownload(download) {
17
+ const link = document.createElement("a");
18
+ link.href = download.url;
19
+ if (download.filename !== void 0) link.download = download.filename;
20
+ link.rel = "noopener";
21
+ link.hidden = true;
22
+ document.body.append(link);
23
+ link.click();
24
+ link.remove();
25
+ }
26
+ /**
27
+ * Runs the actions that lists and forms hand over ([decision 0023]): it asks for their parameters
28
+ * in a dialog, runs them through the adapter, shows their result (a toast, and the items that
29
+ * didn't succeed), follows queued runs until they end, refreshes through `onResult`, follows
30
+ * redirects and opens downloads. Put it inside `PanneauRoot`, and above the router of an app, so
31
+ * that runs are followed from page to page. `useActionRunner(resource)` runs an action.
32
+ */
33
+ function ActionRunnerProvider({ adapter, children, onResult, onNavigate = navigateTo, onDownload = openDownload, clientActions, pollInterval = 2e3, locales }) {
34
+ const intl = usePanneauIntl();
35
+ const toasts = useToast();
36
+ const plugins = usePlugins();
37
+ const [parameters, setParameters] = useState(null);
38
+ const [client, setClient] = useState(null);
39
+ const [details, setDetails] = useState(null);
40
+ const [runs, setRuns] = useState([]);
41
+ const notified = useRef(/* @__PURE__ */ new Set());
42
+ const finish = async (resource, target, result) => {
43
+ await onResult?.(result, {
44
+ resource,
45
+ target
46
+ });
47
+ const content = describeResult(result, target.action.label, intl);
48
+ const hasDetails = (result.results ?? []).length > 0;
49
+ const download = result.download ?? null;
50
+ if (download !== null) onDownload(download);
51
+ toasts.show({
52
+ ...content,
53
+ ...hasDetails ? { action: {
54
+ label: intl.formatMessage(messages.details),
55
+ onAction: () => setDetails({
56
+ label: target.action.label,
57
+ result
58
+ })
59
+ } } : download !== null ? { action: {
60
+ label: intl.formatMessage(messages.download),
61
+ onAction: () => onDownload(download)
62
+ } } : {}
63
+ });
64
+ const redirect = result.redirect ?? null;
65
+ if (redirect !== null) onNavigate(redirect);
66
+ };
67
+ const follow = (resource_0, target_0, run) => {
68
+ setRuns((current) => [...current.filter((entry) => entry.run.id !== run.id), {
69
+ run,
70
+ resource: resource_0,
71
+ target: target_0
72
+ }]);
73
+ toasts.show({
74
+ title: intl.formatMessage(messages.queued, { action: target_0.action.label }),
75
+ description: intl.formatMessage(messages.queuedDescription),
76
+ tone: "info"
77
+ });
78
+ };
79
+ const ended = async (entry_0) => {
80
+ const { run: run_0, resource: resource_1, target: target_1 } = entry_0;
81
+ if (notified.current.has(run_0.id)) return;
82
+ notified.current.add(run_0.id);
83
+ if (run_0.result !== void 0) await finish(resource_1, target_1, run_0.result);
84
+ else if (run_0.status === "cancelled") toasts.show({ title: intl.formatMessage(messages.cancelled, { action: target_1.action.label }) });
85
+ else if (run_0.status === "failed") toasts.show({
86
+ title: intl.formatMessage(messages.failed, { action: target_1.action.label }),
87
+ tone: "danger"
88
+ });
89
+ else toasts.show({
90
+ title: intl.formatMessage(messages.done, { action: target_1.action.label }),
91
+ tone: "success"
92
+ });
93
+ };
94
+ const execute = async (resource_2, target_2, values = {}) => {
95
+ const response = await adapter.runAction(resource_2, target_2.action.id, buildActionRequest(target_2, values));
96
+ if (response.run !== void 0) {
97
+ follow(resource_2, target_2, response.run);
98
+ return {
99
+ status: "queued",
100
+ run: response.run
101
+ };
102
+ }
103
+ await finish(resource_2, target_2, response.result);
104
+ return {
105
+ status: "done",
106
+ result: response.result
107
+ };
108
+ };
109
+ const run_1 = (resource_3, target_3) => {
110
+ const { action } = target_3;
111
+ if (action.kind === "link") {
112
+ const to = getLinkTarget(target_3);
113
+ if (to === null) return Promise.reject(/* @__PURE__ */ new Error(`The action "${action.id}" has no URL or route.`));
114
+ onNavigate(to);
115
+ return Promise.resolve({
116
+ status: "navigated",
117
+ to
118
+ });
119
+ }
120
+ if (action.kind === "client") {
121
+ const name = action.component ?? "";
122
+ const Handler = (clientActions !== void 0 && Object.hasOwn(clientActions, name) ? clientActions[name] : void 0) ?? plugins.actions.get(name);
123
+ if (Handler === void 0) return Promise.reject(/* @__PURE__ */ new Error(`No handler for the client action "${action.id}" (${name}).`));
124
+ return new Promise((resolve) => setClient({
125
+ resource: resource_3,
126
+ target: target_3,
127
+ resolve,
128
+ Handler
129
+ }));
130
+ }
131
+ if ((action.fields ?? []).length > 0) return new Promise((resolve_0) => setParameters({
132
+ resource: resource_3,
133
+ target: target_3,
134
+ resolve: resolve_0
135
+ }));
136
+ return execute(resource_3, target_3);
137
+ };
138
+ const cancelRun = async (id) => {
139
+ if (adapter.cancelActionRun === void 0) throw new Error("The adapter cannot cancel runs.");
140
+ await adapter.cancelActionRun(id);
141
+ const updated = await adapter.getActionRun(id);
142
+ setRuns((current_0) => current_0.map((entry_1) => entry_1.run.id === id ? {
143
+ ...entry_1,
144
+ run: updated
145
+ } : entry_1));
146
+ const entry_2 = runs.find((candidate) => candidate.run.id === id);
147
+ if (entry_2 !== void 0 && isFinished(updated.status)) await ended({
148
+ ...entry_2,
149
+ run: updated
150
+ });
151
+ };
152
+ const dismissRun = (id_0) => setRuns((current_1) => current_1.filter((entry_3) => entry_3.run.id !== id_0));
153
+ const onRunEnded = useEffectEvent((entry_4) => ended(entry_4));
154
+ useEffect(() => {
155
+ const active = runs.filter((entry_5) => !isFinished(entry_5.run.status));
156
+ if (active.length === 0) return;
157
+ let stopped = false;
158
+ const timer = setTimeout(() => {
159
+ Promise.all(active.map(async (entry_6) => {
160
+ try {
161
+ const next = await adapter.getActionRun(entry_6.run.id);
162
+ if (stopped) return;
163
+ setRuns((current_3) => current_3.map((candidate_0) => candidate_0.run.id === next.id ? {
164
+ ...candidate_0,
165
+ run: next
166
+ } : candidate_0));
167
+ if (isFinished(next.status)) await onRunEnded({
168
+ ...entry_6,
169
+ run: next
170
+ });
171
+ } catch {
172
+ if (!stopped) setRuns((current_2) => [...current_2]);
173
+ }
174
+ }));
175
+ }, pollInterval);
176
+ return () => {
177
+ stopped = true;
178
+ clearTimeout(timer);
179
+ };
180
+ }, [
181
+ runs,
182
+ adapter,
183
+ pollInterval
184
+ ]);
185
+ const value = {
186
+ adapter,
187
+ run: run_1,
188
+ runs,
189
+ cancelRun,
190
+ dismissRun
191
+ };
192
+ const ClientHandler = client?.Handler;
193
+ return /* @__PURE__ */ jsxs(ActionRunnerContext, {
194
+ value,
195
+ children: [
196
+ children,
197
+ parameters !== null ? /* @__PURE__ */ jsx(ParametersDialog, {
198
+ action: parameters.target.action,
199
+ count: parameters.target.count,
200
+ locales,
201
+ onSubmit: (values_0) => execute(parameters.resource, parameters.target, values_0),
202
+ onDone: (outcome) => {
203
+ setParameters(null);
204
+ parameters.resolve(outcome);
205
+ },
206
+ onCancel: () => {
207
+ setParameters(null);
208
+ parameters.resolve(false);
209
+ }
210
+ }) : null,
211
+ client !== null && ClientHandler !== void 0 ? /* @__PURE__ */ jsx(ClientHandler, {
212
+ action: client.target.action,
213
+ resource: client.resource,
214
+ target: client.target,
215
+ onDone: (result_0) => {
216
+ setClient(null);
217
+ if (result_0 === void 0) {
218
+ client.resolve({
219
+ status: "done",
220
+ result: {}
221
+ });
222
+ return;
223
+ }
224
+ finish(client.resource, client.target, result_0).then(() => client.resolve({
225
+ status: "done",
226
+ result: result_0
227
+ }));
228
+ },
229
+ onCancel: () => {
230
+ setClient(null);
231
+ client.resolve(false);
232
+ }
233
+ }) : null,
234
+ details !== null ? /* @__PURE__ */ jsx(ResultsDialog, {
235
+ label: details.label,
236
+ result: details.result,
237
+ onClose: () => setDetails(null)
238
+ }) : null
239
+ ]
240
+ });
241
+ }
242
+ //#endregion
243
+ export { ActionRunnerProvider };
244
+
245
+ //# sourceMappingURL=ActionRunnerProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionRunnerProvider.js","names":["resource","target","entry","run","resolve","current","id","candidate","values","result"],"sources":["../src/ActionRunnerProvider.tsx"],"sourcesContent":["import { type ReactNode, useEffect, useEffectEvent, useRef, useState } from 'react';\n\nimport { usePlugins } from '@panneau/core';\nimport type { ActionResult, ActionRun, RouteReference } from '@panneau/protocol';\nimport { usePanneauIntl, useToast } from '@panneau/ui';\n\nimport { ParametersDialog } from './ParametersDialog';\nimport { ResultsDialog } from './ResultsDialog';\nimport { ActionRunnerContext, type ActionRunnerContextValue } from './context';\nimport { describeResult } from './describe';\nimport { messages } from './messages';\nimport { buildActionRequest, getLinkTarget, isFinished } from './request';\nimport type {\n ActionAdapter,\n ActionDownload,\n ActionOutcome,\n ActionResultContext,\n ActionTarget,\n ClientActionHandler,\n FollowedRun,\n} from './types';\n\nexport interface ActionRunnerProviderProps {\n /** Runs the actions: a `DataAdapter`, or any object with its action methods. */\n adapter: ActionAdapter;\n children?: ReactNode;\n /**\n * Called with every result: of an action that finished at once, and of a queued run when it\n * finishes. Refresh what it changed here. With `@panneau/data/query`:\n * `invalidateActionResult(client, resource, result, target.selection)`.\n */\n onResult?: (result: ActionResult, context: ActionResultContext) => unknown;\n /**\n * Goes to the `redirect` of a result, or to the URL or route of a `link` action. Defaults to\n * `window.location.assign()` for URLs; routes need it.\n */\n onNavigate?: (to: string | RouteReference) => void;\n /** Opens the `download` of a result. Defaults to a link to its URL, clicked. */\n onDownload?: (download: ActionDownload) => void;\n /** Handlers of `client` actions by their `component`, over those of the plugins. */\n clientActions?: Readonly<Record<string, ClientActionHandler>>;\n /** Milliseconds between two checks of a queued run. Defaults to 2000. */\n pollInterval?: number;\n /** Locales of localized parameters. Defaults to the definition's. */\n locales?: readonly string[];\n}\n\ninterface Pending {\n resource: string;\n target: ActionTarget;\n resolve: (outcome: ActionOutcome | false) => void;\n}\n\ninterface ClientPending extends Pending {\n Handler: ClientActionHandler;\n}\n\nfunction navigateTo(to: string | RouteReference) {\n if (typeof to === 'string') {\n window.location.assign(to);\n } else {\n console.warn(\n `[panneau] An action goes to the route \"${to.route}\": give ActionRunnerProvider an onNavigate.`,\n );\n }\n}\n\nfunction openDownload(download: ActionDownload) {\n const link = document.createElement('a');\n link.href = download.url;\n if (download.filename !== undefined) {\n link.download = download.filename;\n }\n link.rel = 'noopener';\n link.hidden = true;\n document.body.append(link);\n link.click();\n link.remove();\n}\n\n/**\n * Runs the actions that lists and forms hand over ([decision 0023]): it asks for their parameters\n * in a dialog, runs them through the adapter, shows their result (a toast, and the items that\n * didn't succeed), follows queued runs until they end, refreshes through `onResult`, follows\n * redirects and opens downloads. Put it inside `PanneauRoot`, and above the router of an app, so\n * that runs are followed from page to page. `useActionRunner(resource)` runs an action.\n */\nexport function ActionRunnerProvider({\n adapter,\n children,\n onResult,\n onNavigate = navigateTo,\n onDownload = openDownload,\n clientActions,\n pollInterval = 2000,\n locales,\n}: ActionRunnerProviderProps) {\n const intl = usePanneauIntl();\n const toasts = useToast();\n const plugins = usePlugins();\n const [parameters, setParameters] = useState<Pending | null>(null);\n const [client, setClient] = useState<ClientPending | null>(null);\n const [details, setDetails] = useState<{ label: string; result: ActionResult } | null>(null);\n const [runs, setRuns] = useState<readonly FollowedRun[]>([]);\n // Runs whose end was already shown, so that a late poll doesn't show it twice.\n const notified = useRef(new Set<string>());\n\n const finish = async (resource: string, target: ActionTarget, result: ActionResult) => {\n await onResult?.(result, { resource, target });\n const content = describeResult(result, target.action.label, intl);\n const hasDetails = (result.results ?? []).length > 0;\n const download = result.download ?? null;\n if (download !== null) {\n onDownload(download);\n }\n toasts.show({\n ...content,\n ...(hasDetails\n ? {\n action: {\n label: intl.formatMessage(messages.details),\n onAction: () => setDetails({ label: target.action.label, result }),\n },\n }\n : download !== null\n ? {\n action: {\n label: intl.formatMessage(messages.download),\n onAction: () => onDownload(download),\n },\n }\n : {}),\n });\n const redirect = result.redirect ?? null;\n if (redirect !== null) {\n onNavigate(redirect);\n }\n };\n\n const follow = (resource: string, target: ActionTarget, run: ActionRun) => {\n setRuns((current) => [\n ...current.filter((entry) => entry.run.id !== run.id),\n { run, resource, target },\n ]);\n toasts.show({\n title: intl.formatMessage(messages.queued, { action: target.action.label }),\n description: intl.formatMessage(messages.queuedDescription),\n tone: 'info',\n });\n };\n\n const ended = async (entry: FollowedRun) => {\n const { run, resource, target } = entry;\n if (notified.current.has(run.id)) {\n return;\n }\n notified.current.add(run.id);\n if (run.result !== undefined) {\n await finish(resource, target, run.result);\n } else if (run.status === 'cancelled') {\n toasts.show({\n title: intl.formatMessage(messages.cancelled, { action: target.action.label }),\n });\n } else if (run.status === 'failed') {\n toasts.show({\n title: intl.formatMessage(messages.failed, { action: target.action.label }),\n tone: 'danger',\n });\n } else {\n toasts.show({\n title: intl.formatMessage(messages.done, { action: target.action.label }),\n tone: 'success',\n });\n }\n };\n\n const execute = async (\n resource: string,\n target: ActionTarget,\n values: Record<string, unknown> = {},\n ): Promise<ActionOutcome> => {\n const response = await adapter.runAction(\n resource,\n target.action.id,\n buildActionRequest(target, values),\n );\n if (response.run !== undefined) {\n follow(resource, target, response.run);\n return { status: 'queued', run: response.run };\n }\n await finish(resource, target, response.result);\n return { status: 'done', result: response.result };\n };\n\n const run = (resource: string, target: ActionTarget): Promise<ActionOutcome | false> => {\n const { action } = target;\n if (action.kind === 'link') {\n const to = getLinkTarget(target);\n if (to === null) {\n return Promise.reject(new Error(`The action \"${action.id}\" has no URL or route.`));\n }\n onNavigate(to);\n return Promise.resolve({ status: 'navigated', to });\n }\n if (action.kind === 'client') {\n const name = action.component ?? '';\n const Handler =\n (clientActions !== undefined && Object.hasOwn(clientActions, name)\n ? clientActions[name]\n : undefined) ?? (plugins.actions.get(name) as ClientActionHandler | undefined);\n if (Handler === undefined) {\n return Promise.reject(\n new Error(`No handler for the client action \"${action.id}\" (${name}).`),\n );\n }\n return new Promise((resolve) => setClient({ resource, target, resolve, Handler }));\n }\n if ((action.fields ?? []).length > 0) {\n return new Promise((resolve) => setParameters({ resource, target, resolve }));\n }\n return execute(resource, target);\n };\n\n const cancelRun = async (id: string) => {\n if (adapter.cancelActionRun === undefined) {\n throw new Error('The adapter cannot cancel runs.');\n }\n await adapter.cancelActionRun(id);\n const updated = await adapter.getActionRun(id);\n setRuns((current) =>\n current.map((entry) => (entry.run.id === id ? { ...entry, run: updated } : entry)),\n );\n const entry = runs.find((candidate) => candidate.run.id === id);\n if (entry !== undefined && isFinished(updated.status)) {\n await ended({ ...entry, run: updated });\n }\n };\n\n const dismissRun = (id: string) =>\n setRuns((current) => current.filter((entry) => entry.run.id !== id));\n\n const onRunEnded = useEffectEvent((entry: FollowedRun) => ended(entry));\n\n // Checks the unfinished runs, one interval after the last change of the runs.\n useEffect(() => {\n const active = runs.filter((entry) => !isFinished(entry.run.status));\n if (active.length === 0) {\n return undefined;\n }\n let stopped = false;\n const timer = setTimeout(() => {\n void Promise.all(\n active.map(async (entry) => {\n try {\n const next = await adapter.getActionRun(entry.run.id);\n if (stopped) {\n return;\n }\n setRuns((current) =>\n current.map((candidate) =>\n candidate.run.id === next.id\n ? { ...candidate, run: next }\n : candidate,\n ),\n );\n if (isFinished(next.status)) {\n await onRunEnded({ ...entry, run: next });\n }\n } catch {\n // Checked again at the next interval.\n if (!stopped) {\n setRuns((current) => [...current]);\n }\n }\n }),\n );\n }, pollInterval);\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n }, [runs, adapter, pollInterval]);\n\n const value: ActionRunnerContextValue = { adapter, run, runs, cancelRun, dismissRun };\n\n const ClientHandler = client?.Handler;\n return (\n <ActionRunnerContext value={value}>\n {children}\n {parameters !== null ? (\n <ParametersDialog\n action={parameters.target.action}\n count={parameters.target.count}\n locales={locales}\n onSubmit={(values) => execute(parameters.resource, parameters.target, values)}\n onDone={(outcome) => {\n setParameters(null);\n parameters.resolve(outcome);\n }}\n onCancel={() => {\n setParameters(null);\n parameters.resolve(false);\n }}\n />\n ) : null}\n {client !== null && ClientHandler !== undefined ? (\n <ClientHandler\n action={client.target.action}\n resource={client.resource}\n target={client.target}\n onDone={(result) => {\n setClient(null);\n if (result === undefined) {\n client.resolve({ status: 'done', result: {} });\n return;\n }\n void finish(client.resource, client.target, result).then(() =>\n client.resolve({ status: 'done', result }),\n );\n }}\n onCancel={() => {\n setClient(null);\n client.resolve(false);\n }}\n />\n ) : null}\n {details !== null ? (\n <ResultsDialog\n label={details.label}\n result={details.result}\n onClose={() => setDetails(null)}\n />\n ) : null}\n </ActionRunnerContext>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export interface ActionRunsProps {
2
+ /** Accessible name of the list. Defaults to "Actions in progress". */
3
+ 'aria-label'?: string;
4
+ className?: string;
5
+ }
6
+ /**
7
+ * The queued runs that the closest `ActionRunnerProvider` follows: their status and progress, a
8
+ * button to cancel those that haven't finished (when the adapter can), and one to dismiss those
9
+ * that have. For a notification area. Renders nothing without runs.
10
+ */
11
+ export declare function ActionRuns({ 'aria-label': ariaLabel, className }: ActionRunsProps): import("react").JSX.Element | null;
12
+ //# sourceMappingURL=ActionRuns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionRuns.d.ts","sourceRoot":"","sources":["../src/ActionRuns.tsx"],"names":[],"mappings":"AAqBA,MAAM,WAAW,eAAe;IAC5B,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,eAAe,sCA8EjF"}
@@ -0,0 +1,128 @@
1
+ import ActionRunner_module_default from "./ActionRunner.module.js";
2
+ import { messages } from "./messages.js";
3
+ import { isFinished } from "./request.js";
4
+ import { ActionRunnerContext } from "./context.js";
5
+ import { use } from "react";
6
+ import { Button, IconButton, usePanneauIntl } from "@panneau/ui";
7
+ import { c } from "react/compiler-runtime";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { X } from "lucide-react";
10
+ import { ProgressBar } from "react-aria-components";
11
+ //#region src/ActionRuns.tsx
12
+ var STATUS_MESSAGES = {
13
+ queued: messages.runQueued,
14
+ running: messages.runRunning,
15
+ succeeded: messages.runSucceeded,
16
+ partial: messages.runPartial,
17
+ failed: messages.runFailed,
18
+ cancelled: messages.runCancelled
19
+ };
20
+ /**
21
+ * The queued runs that the closest `ActionRunnerProvider` follows: their status and progress, a
22
+ * button to cancel those that haven't finished (when the adapter can), and one to dismiss those
23
+ * that have. For a notification area. Renders nothing without runs.
24
+ */
25
+ function ActionRuns(t0) {
26
+ const $ = c(13);
27
+ const { "aria-label": ariaLabel, className } = t0;
28
+ const intl = usePanneauIntl();
29
+ const context = use(ActionRunnerContext);
30
+ if (context === null || context.runs.length === 0) return null;
31
+ const canCancel = context.adapter.cancelActionRun !== void 0;
32
+ let t1;
33
+ if ($[0] !== ariaLabel || $[1] !== intl) {
34
+ t1 = ariaLabel ?? intl.formatMessage(messages.runs);
35
+ $[0] = ariaLabel;
36
+ $[1] = intl;
37
+ $[2] = t1;
38
+ } else t1 = $[2];
39
+ let t2;
40
+ if ($[3] !== className) {
41
+ t2 = [ActionRunner_module_default.runs, className].filter(Boolean);
42
+ $[3] = className;
43
+ $[4] = t2;
44
+ } else t2 = $[4];
45
+ const t3 = t2.join(" ");
46
+ let t4;
47
+ if ($[5] !== canCancel || $[6] !== context || $[7] !== intl) {
48
+ t4 = context.runs.map((t5) => {
49
+ const { run, target } = t5;
50
+ const label = target.action.label;
51
+ const finished = isFinished(run.status);
52
+ const status = intl.formatMessage(STATUS_MESSAGES[run.status]);
53
+ return /* @__PURE__ */ jsxs("li", {
54
+ className: ActionRunner_module_default.run,
55
+ children: [
56
+ /* @__PURE__ */ jsx("span", {
57
+ className: ActionRunner_module_default.runLabel,
58
+ children: label
59
+ }),
60
+ finished ? /* @__PURE__ */ jsx(IconButton, {
61
+ icon: X,
62
+ size: "sm",
63
+ label: intl.formatMessage(messages.dismissRun, { action: label }),
64
+ onPress: () => context.dismissRun(run.id)
65
+ }) : canCancel ? /* @__PURE__ */ jsx(Button, {
66
+ size: "sm",
67
+ variant: "ghost",
68
+ "aria-label": intl.formatMessage(messages.cancelRun, { action: label }),
69
+ onPress: () => {
70
+ context.cancelRun(run.id).catch(_temp);
71
+ },
72
+ children: intl.formatMessage(messages.cancel)
73
+ }) : /* @__PURE__ */ jsx("span", {}),
74
+ finished ? /* @__PURE__ */ jsx("span", {
75
+ className: ActionRunner_module_default.runStatus,
76
+ children: status
77
+ }) : /* @__PURE__ */ jsx(ProgressBar, {
78
+ "aria-label": label,
79
+ className: ActionRunner_module_default.progress,
80
+ value: run.progress,
81
+ maxValue: run.total ?? void 0,
82
+ isIndeterminate: run.total === null || run.total === 0,
83
+ valueLabel: run.total !== null && run.total > 0 ? intl.formatMessage(messages.progress, {
84
+ progress: run.progress,
85
+ total: run.total
86
+ }) : status,
87
+ children: _temp2
88
+ })
89
+ ]
90
+ }, run.id);
91
+ });
92
+ $[5] = canCancel;
93
+ $[6] = context;
94
+ $[7] = intl;
95
+ $[8] = t4;
96
+ } else t4 = $[8];
97
+ let t5;
98
+ if ($[9] !== t1 || $[10] !== t3 || $[11] !== t4) {
99
+ t5 = /* @__PURE__ */ jsx("ul", {
100
+ "aria-label": t1,
101
+ className: t3,
102
+ children: t4
103
+ });
104
+ $[9] = t1;
105
+ $[10] = t3;
106
+ $[11] = t4;
107
+ $[12] = t5;
108
+ } else t5 = $[12];
109
+ return t5;
110
+ }
111
+ function _temp2(t0) {
112
+ const { percentage, valueText } = t0;
113
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
114
+ className: ActionRunner_module_default.runStatus,
115
+ children: valueText
116
+ }), /* @__PURE__ */ jsx("span", {
117
+ className: ActionRunner_module_default.track,
118
+ children: /* @__PURE__ */ jsx("span", {
119
+ className: ActionRunner_module_default.fill,
120
+ style: { width: `${percentage ?? 100}%` }
121
+ })
122
+ })] });
123
+ }
124
+ function _temp() {}
125
+ //#endregion
126
+ export { ActionRuns };
127
+
128
+ //# sourceMappingURL=ActionRuns.js.map