@flowdular/sdk 0.3.0 → 0.3.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/.ai/platform-capabilities.md +4 -2
- package/.ai/policies/capabilities.yaml +30 -3
- package/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
- package/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
- package/.ai/references/catalog/module.json +11 -1
- package/.ai/references/catalog/package.json +2 -2
- package/.ai/references/catalog/spec/module.yaml +25 -4
- package/.ai/references/catalog/src/agent/tools.ts +19 -10
- package/.ai/references/catalog/src/api/endpoints.ts +150 -10
- package/.ai/references/catalog/src/api/list-cursor.ts +83 -0
- package/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
- package/.ai/references/catalog/src/client/api.ts +124 -36
- package/.ai/references/catalog/src/client/contribution.tsrx +5 -0
- package/.ai/references/catalog/src/client/state.ts +169 -3
- package/.ai/references/catalog/src/domain/lists.ts +7 -0
- package/.ai/references/catalog/src/domain/types.ts +20 -0
- package/.ai/references/catalog/src/platform.ts +20 -0
- package/.ai/references/catalog/src/services/catalog-service.ts +143 -8
- package/.ai/references/catalog/src/services/database-repository.ts +104 -17
- package/.ai/references/catalog/src/services/item-export.ts +81 -0
- package/.ai/references/catalog/src/services/migration.ts +27 -1
- package/.ai/references/catalog/src/services/repository.ts +31 -2
- package/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
- package/.ai/references/catalog/tests/client-state.test.ts +124 -0
- package/.ai/references/catalog/tests/endpoints.test.ts +269 -0
- package/.ai/references/catalog/tests/export.test.ts +134 -0
- package/.ai/references/catalog/tests/idempotency.test.ts +15 -14
- package/.ai/references/catalog/tests/list.test.ts +217 -0
- package/.ai/references/catalog/tests/migrations.test.ts +58 -2
- package/.ai/references/catalog/tests/module.test.ts +2 -1
- package/.ai/references/catalog/tests/support/database.ts +14 -0
- package/.ai/references/catalog/translations/en.json +35 -4
- package/.ai/references/catalog/translations/pl.json +35 -4
- package/.ai/references/catalog.provenance.json +34 -26
- package/.ai/skills/cli-extension/SKILL.md +1 -1
- package/.ai/skills/deploy-operate/SKILL.md +7 -2
- package/README.md +1 -1
- package/assets/flowdular-banner.webp +0 -0
- package/modules/access/module.json +1 -1
- package/modules/access/spec/module.yaml +1 -1
- package/modules/agents/module.json +1 -1
- package/modules/agents/spec/module.yaml +1 -1
- package/modules/approvals/migrations/0005_approvals_grant_audit.down.sql +4 -0
- package/modules/approvals/migrations/0005_approvals_grant_audit.up.sql +20 -0
- package/modules/approvals/module.json +2 -2
- package/modules/approvals/package.json +1 -1
- package/modules/approvals/spec/module.yaml +12 -3
- package/modules/approvals/src/domain/capability.ts +12 -0
- package/modules/approvals/src/domain/grant.ts +69 -0
- package/modules/approvals/src/domain/types.ts +14 -0
- package/modules/approvals/src/index.ts +9 -0
- package/modules/approvals/src/platform.ts +8 -0
- package/modules/approvals/src/server/runtime.ts +4 -0
- package/modules/approvals/src/services/approvals-service.ts +80 -0
- package/modules/approvals/src/services/database-repository.ts +65 -6
- package/modules/approvals/src/services/migration.ts +34 -0
- package/modules/approvals/src/services/repository.ts +8 -0
- package/modules/audit/module.json +1 -1
- package/modules/audit/spec/module.yaml +1 -1
- package/modules/auth/module.json +2 -2
- package/modules/auth/package.json +1 -1
- package/modules/auth/spec/module.yaml +2 -2
- package/modules/auth/src/middleware/authentication.ts +5 -1
- package/modules/auth/src/services/auth-service.ts +26 -0
- package/modules/auth/src/services/database-repository.ts +9 -0
- package/modules/auth/src/services/repository.ts +2 -0
- package/modules/automations/module.json +1 -1
- package/modules/automations/spec/module.yaml +1 -1
- package/modules/connectors/migrations/0003_connectors_rotation_inventory.down.sql +2 -0
- package/modules/connectors/migrations/0003_connectors_rotation_inventory.up.sql +19 -0
- package/modules/connectors/module.json +8 -4
- package/modules/connectors/package.json +2 -1
- package/modules/connectors/spec/module.yaml +3 -2
- package/modules/connectors/src/cli/commands.json +17 -0
- package/modules/connectors/src/cli/index.ts +126 -0
- package/modules/connectors/src/services/credential-rotation.ts +221 -0
- package/modules/connectors/src/services/credential-vault.ts +6 -0
- package/modules/connectors/src/services/migration.ts +36 -0
- package/modules/directory/module.json +1 -1
- package/modules/directory/spec/module.yaml +1 -1
- package/modules/documents/migrations/0003_documents_rotation_inventory.down.sql +2 -0
- package/modules/documents/migrations/0003_documents_rotation_inventory.up.sql +18 -0
- package/modules/documents/module.json +8 -4
- package/modules/documents/package.json +2 -1
- package/modules/documents/spec/module.yaml +3 -2
- package/modules/documents/src/cli/commands.json +17 -0
- package/modules/documents/src/cli/index.ts +145 -0
- package/modules/documents/src/services/database-repository.ts +15 -4
- package/modules/documents/src/services/documents-service.ts +13 -9
- package/modules/documents/src/services/migration.ts +35 -0
- package/modules/documents/src/services/repository.ts +12 -2
- package/modules/documents/src/services/storage-rotation.ts +157 -0
- package/modules/exports/migrations/0003_exports_rotation_inventory.down.sql +1 -0
- package/modules/exports/migrations/0003_exports_rotation_inventory.up.sql +9 -0
- package/modules/exports/module.json +8 -4
- package/modules/exports/package.json +2 -1
- package/modules/exports/spec/module.yaml +3 -2
- package/modules/exports/src/cli/commands.json +17 -0
- package/modules/exports/src/cli/index.ts +145 -0
- package/modules/exports/src/server/index.ts +0 -1
- package/modules/exports/src/services/data-classes.ts +16 -13
- package/modules/exports/src/services/database-repository.ts +30 -32
- package/modules/exports/src/services/migration.ts +27 -0
- package/modules/exports/src/services/repository.ts +9 -10
- package/modules/exports/src/services/storage-rotation.ts +138 -0
- package/modules/import/module.json +1 -1
- package/modules/import/spec/module.yaml +1 -1
- package/modules/metering/module.json +1 -1
- package/modules/metering/spec/module.yaml +1 -1
- package/modules/notifications/module.json +1 -1
- package/modules/notifications/spec/module.yaml +1 -1
- package/modules/reports/module.json +1 -1
- package/modules/reports/spec/module.yaml +1 -1
- package/modules/sandbox/module.json +1 -1
- package/modules/sandbox/spec/module.yaml +1 -1
- package/modules/search/module.json +1 -1
- package/modules/search/spec/module.yaml +1 -1
- package/modules/system/migrations/0001_system_module_activations.down.sql +2 -0
- package/modules/system/migrations/0001_system_module_activations.up.sql +17 -0
- package/modules/system/module.json +3 -2
- package/modules/system/package.json +2 -2
- package/modules/system/spec/module.yaml +98 -1
- package/modules/system/src/client/ModulesView.tsrx +101 -8
- package/modules/system/src/client/api.ts +52 -11
- package/modules/system/src/client/module-columns.tsrx +32 -6
- package/modules/system/src/client/state.ts +26 -0
- package/modules/system/src/domain/modules.ts +93 -0
- package/modules/system/src/index.ts +13 -0
- package/modules/system/src/platform.ts +41 -1
- package/modules/system/src/server/capability.ts +6 -0
- package/modules/system/src/server/endpoints.ts +125 -5
- package/modules/system/src/server/index.ts +26 -0
- package/modules/system/src/server/module-catalog.ts +19 -0
- package/modules/system/src/server/runtime.ts +102 -0
- package/modules/system/src/services/database-repository.ts +91 -0
- package/modules/system/src/services/migration.ts +39 -0
- package/modules/system/src/services/module-activation-service.ts +245 -0
- package/modules/system/src/services/repository.ts +17 -0
- package/modules/system/translations/en.json +17 -1
- package/modules/system/translations/pl.json +17 -1
- package/modules/users/module.json +2 -2
- package/modules/users/package.json +1 -1
- package/modules/users/spec/module.yaml +6 -9
- package/modules/users/src/client/UsersView.tsrx +27 -13
- package/modules/users/src/client/member-columns.tsrx +13 -4
- package/modules/users/src/client/state.ts +16 -0
- package/modules/users/src/services/users-service.ts +3 -0
- package/modules/users/translations/en.json +2 -1
- package/modules/users/translations/pl.json +2 -1
- package/modules/workflows/module.json +2 -2
- package/modules/workflows/package.json +1 -1
- package/modules/workflows/spec/module.yaml +2 -2
- package/modules/workflows/src/services/database-repository.ts +93 -53
- package/package.json +1 -1
- package/packages/client/package.json +1 -0
- package/packages/client/src/ApplicationShell.tsrx +46 -5
- package/packages/client/src/index.ts +5 -0
- package/packages/client/src/shell/modules.ts +49 -0
- package/packages/client/src/shell/types.ts +3 -0
- package/packages/contracts/src/index.ts +11 -1
- package/packages/database/src/backup.ts +1 -0
- package/packages/database/src/migrations.ts +7 -0
- package/packages/harness/src/runtime.ts +169 -10
- package/packages/harness/src/tool-adapters.ts +6 -13
- package/packages/kernel/src/approval-grant.ts +310 -0
- package/packages/kernel/src/index.ts +20 -0
- package/packages/server/src/endpoint.ts +17 -1
- package/packages/server/src/index.ts +10 -0
- package/packages/server/src/module-activation.ts +69 -0
- package/packages/storage/src/envelope.ts +70 -21
- package/packages/storage/src/index.ts +7 -1
- package/packages/storage/src/port.ts +12 -1
- package/packages/storage/src/reseal.ts +128 -0
- package/assets/flowdular-banner.png +0 -0
|
@@ -4,41 +4,82 @@ import {
|
|
|
4
4
|
Button,
|
|
5
5
|
ConfirmDialog,
|
|
6
6
|
Drawer,
|
|
7
|
+
EmptyState,
|
|
7
8
|
Filters,
|
|
8
9
|
Icon,
|
|
9
10
|
Kpi,
|
|
10
11
|
PageHeader,
|
|
12
|
+
Pagination,
|
|
11
13
|
SearchField,
|
|
14
|
+
Select,
|
|
12
15
|
TableCard,
|
|
13
16
|
Tag,
|
|
17
|
+
ToastHost,
|
|
18
|
+
toasts,
|
|
14
19
|
type TableColumn,
|
|
20
|
+
type TableSort,
|
|
15
21
|
} from '@flowdular/sdk/ui';
|
|
16
22
|
import { activeLocale, t } from '@flowdular/sdk/client';
|
|
17
23
|
import { useValue } from 'segment-state';
|
|
18
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
CatalogBulkOutcome,
|
|
26
|
+
CatalogItem,
|
|
27
|
+
CatalogItemKind,
|
|
28
|
+
} from '../domain/types.ts';
|
|
29
|
+
import { CATALOG_ITEMS_EXPORT_LIST_ID } from '../domain/lists.ts';
|
|
19
30
|
import {
|
|
31
|
+
ApiError,
|
|
20
32
|
archiveCatalogItem,
|
|
33
|
+
archiveCatalogItems,
|
|
21
34
|
createCatalogItem,
|
|
22
35
|
deleteCatalogItem,
|
|
23
36
|
loadCatalogItemHistory,
|
|
24
37
|
loadCatalogItems,
|
|
25
38
|
restoreCatalogItem,
|
|
39
|
+
restoreCatalogItems,
|
|
40
|
+
startListExport,
|
|
26
41
|
updateCatalogItem,
|
|
27
42
|
} from './api.ts';
|
|
28
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
bulkOutcomeCounts,
|
|
45
|
+
bulkTargets,
|
|
46
|
+
catalogSort,
|
|
47
|
+
createCatalogClientState,
|
|
48
|
+
DEFAULT_CATALOG_SORTING,
|
|
49
|
+
DEFAULT_STATUS_FILTER,
|
|
50
|
+
loadedListing,
|
|
51
|
+
pageCursor,
|
|
52
|
+
replaceItem,
|
|
53
|
+
resetPageCursors,
|
|
54
|
+
searchPending,
|
|
55
|
+
type CatalogKindFilter,
|
|
56
|
+
type CatalogStatusFilter,
|
|
57
|
+
type Listing,
|
|
58
|
+
} from './state.ts';
|
|
29
59
|
import { CatalogItemForm } from './CatalogItemForm.tsrx';
|
|
30
60
|
import { CatalogHistoryDrawer } from './CatalogHistoryDrawer.tsrx';
|
|
31
61
|
|
|
32
62
|
export interface CatalogViewProps {
|
|
33
63
|
readonly csrfToken: string;
|
|
34
64
|
readonly canManage: boolean;
|
|
65
|
+
/** Whether the principal may start an export job; exports.core owns it. */
|
|
66
|
+
readonly canExport: boolean;
|
|
35
67
|
}
|
|
36
68
|
|
|
69
|
+
const PAGE_SIZES = [25, 50, 100, 200];
|
|
70
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
71
|
+
/** The widget reads one page at the ceiling and says "more" past it. */
|
|
72
|
+
const WIDGET_PAGE = 200;
|
|
73
|
+
|
|
37
74
|
function formValue(form: FormData, key: string): string {
|
|
38
75
|
const value = form.get(key);
|
|
39
76
|
return typeof value === 'string' ? value : '';
|
|
40
77
|
}
|
|
41
78
|
|
|
79
|
+
function message(error: unknown, fallback: string): string {
|
|
80
|
+
return error instanceof Error ? error.message : fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
function price(minor: number, currency: string): string {
|
|
43
84
|
try {
|
|
44
85
|
return new Intl.NumberFormat(activeLocale(), {
|
|
@@ -50,21 +91,36 @@ function price(minor: number, currency: string): string {
|
|
|
50
91
|
}
|
|
51
92
|
}
|
|
52
93
|
|
|
94
|
+
function updatedAt(value: number): string {
|
|
95
|
+
return new Intl.DateTimeFormat(activeLocale(), {
|
|
96
|
+
dateStyle: 'medium',
|
|
97
|
+
timeStyle: 'short',
|
|
98
|
+
}).format(new Date(value));
|
|
99
|
+
}
|
|
100
|
+
|
|
53
101
|
function columns(): readonly TableColumn<CatalogItem>[] {
|
|
54
102
|
return [
|
|
55
103
|
{
|
|
56
|
-
key: '
|
|
57
|
-
header: t('catalog.table.column.
|
|
58
|
-
width: '
|
|
104
|
+
key: 'name',
|
|
105
|
+
header: t('catalog.table.column.name'),
|
|
106
|
+
width: '28%',
|
|
107
|
+
value: (item) => item.name,
|
|
59
108
|
cell: (item) => <span class="ui-cell">
|
|
60
109
|
<b>{item.name}</b>
|
|
61
|
-
<small
|
|
110
|
+
<small>{item.unit}</small>
|
|
62
111
|
</span>,
|
|
63
112
|
},
|
|
113
|
+
{
|
|
114
|
+
key: 'sku',
|
|
115
|
+
header: t('catalog.table.column.sku'),
|
|
116
|
+
width: '16%',
|
|
117
|
+
value: (item) => item.sku,
|
|
118
|
+
cell: (item) => <span class="ui-mono">{item.sku}</span>,
|
|
119
|
+
},
|
|
64
120
|
{
|
|
65
121
|
key: 'kind',
|
|
66
122
|
header: t('catalog.table.column.kind'),
|
|
67
|
-
width: '
|
|
123
|
+
width: '12%',
|
|
68
124
|
cell: (item) =>
|
|
69
125
|
<Tag tone={item.kind === 'product' ? 'info' : 'neutral'}>{t(
|
|
70
126
|
'catalog.kind.' + item.kind,
|
|
@@ -73,14 +129,21 @@ function columns(): readonly TableColumn<CatalogItem>[] {
|
|
|
73
129
|
{
|
|
74
130
|
key: 'price',
|
|
75
131
|
header: t('catalog.table.column.price'),
|
|
76
|
-
width: '
|
|
132
|
+
width: '14%',
|
|
77
133
|
numeric: true,
|
|
78
134
|
cell: (item) => price(item.basePriceMinor, item.currency),
|
|
79
135
|
},
|
|
136
|
+
{
|
|
137
|
+
key: 'updatedAt',
|
|
138
|
+
header: t('catalog.table.column.updated'),
|
|
139
|
+
width: '18%',
|
|
140
|
+
value: (item) => item.updatedAt,
|
|
141
|
+
cell: (item) => updatedAt(item.updatedAt),
|
|
142
|
+
},
|
|
80
143
|
{
|
|
81
144
|
key: 'status',
|
|
82
145
|
header: t('catalog.table.column.status'),
|
|
83
|
-
width: '
|
|
146
|
+
width: '12%',
|
|
84
147
|
cell: (item) =>
|
|
85
148
|
<Tag tone={item.status === 'active' ? 'success' : 'neutral'} dot>{t(
|
|
86
149
|
'catalog.status.' + item.status,
|
|
@@ -91,13 +154,22 @@ function columns(): readonly TableColumn<CatalogItem>[] {
|
|
|
91
154
|
|
|
92
155
|
export function CatalogView(props: CatalogViewProps) @{
|
|
93
156
|
const catalogState = useMemo(() => createCatalogClientState(), []);
|
|
157
|
+
/* Answers arrive in any order; only the latest request may write the page. */
|
|
158
|
+
const requests = useMemo(() => ({ sequence: 0 }), []);
|
|
94
159
|
const [items] = useValue(catalogState.state.items);
|
|
95
160
|
const [status, setStatus] = useValue(catalogState.state.status);
|
|
96
161
|
const [error, setError] = useValue(catalogState.state.error);
|
|
97
162
|
const [query, setQuery] = useValue(catalogState.state.query);
|
|
98
|
-
const [
|
|
99
|
-
|
|
163
|
+
const [appliedQuery] = useValue(catalogState.state.appliedQuery);
|
|
164
|
+
const [kindFilter, setKindFilter] = useValue(catalogState.state.kindFilter);
|
|
165
|
+
const [statusFilter, setStatusFilter] = useValue(
|
|
166
|
+
catalogState.state.statusFilter,
|
|
100
167
|
);
|
|
168
|
+
const [sorting, setSorting] = useValue(catalogState.state.sorting);
|
|
169
|
+
const [pageIndex] = useValue(catalogState.state.pageIndex);
|
|
170
|
+
const [pageSize, setPageSize] = useValue(catalogState.state.pageSize);
|
|
171
|
+
const [cursors] = useValue(catalogState.state.cursors);
|
|
172
|
+
const [nextCursor] = useValue(catalogState.state.nextCursor);
|
|
101
173
|
const [filtersOpen, setFiltersOpen] = useValue(
|
|
102
174
|
catalogState.state.filtersOpen,
|
|
103
175
|
);
|
|
@@ -124,40 +196,145 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
124
196
|
const [historyError, setHistoryError] = useValue(
|
|
125
197
|
catalogState.state.historyError,
|
|
126
198
|
);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
199
|
+
const [selectedIds, setSelectedIds] = useValue(
|
|
200
|
+
catalogState.state.selectedIds,
|
|
201
|
+
);
|
|
202
|
+
const [confirmArchiveMany, setConfirmArchiveMany] = useValue(
|
|
203
|
+
catalogState.state.confirmArchiveMany,
|
|
204
|
+
);
|
|
205
|
+
const [exportStarted, setExportStarted] = useValue(
|
|
206
|
+
catalogState.state.exportStarted,
|
|
134
207
|
);
|
|
135
|
-
const
|
|
208
|
+
const term = query.trim();
|
|
209
|
+
const hasMore = nextCursor !== null;
|
|
210
|
+
const archivable = bulkTargets(items, selectedIds, 'active');
|
|
211
|
+
const restorable = bulkTargets(items, selectedIds, 'archived');
|
|
212
|
+
const filterCount =
|
|
213
|
+
(kindFilter === '' ? 0 : 1) +
|
|
214
|
+
(statusFilter === DEFAULT_STATUS_FILTER ? 0 : 1);
|
|
215
|
+
const listing = (): Listing => ({
|
|
216
|
+
pageIndex,
|
|
217
|
+
pageSize,
|
|
218
|
+
sorting,
|
|
219
|
+
query: appliedQuery,
|
|
220
|
+
kind: kindFilter,
|
|
221
|
+
status: statusFilter,
|
|
222
|
+
cursors,
|
|
223
|
+
});
|
|
224
|
+
/* The search timer fires after later renders; it reads the listing those
|
|
225
|
+
renders left here rather than the one the keystroke saw. */
|
|
226
|
+
const latest = useMemo(() => ({ listing: listing() }), []);
|
|
227
|
+
latest.listing = listing();
|
|
228
|
+
/* A changed order, filter or page size is a new listing, whose cursors
|
|
229
|
+
name positions in the old one. */
|
|
230
|
+
const firstPage = (changes: Partial<Listing>): Listing => ({
|
|
231
|
+
...listing(),
|
|
232
|
+
...changes,
|
|
233
|
+
pageIndex: 0,
|
|
234
|
+
cursors: resetPageCursors(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const failLoad = (loadError: unknown) => {
|
|
238
|
+
setError(message(loadError, t('catalog.error.load')));
|
|
239
|
+
setStatus(
|
|
240
|
+
loadError instanceof ApiError && loadError.status === 403
|
|
241
|
+
? 'denied'
|
|
242
|
+
: 'error',
|
|
243
|
+
);
|
|
244
|
+
};
|
|
136
245
|
|
|
137
|
-
|
|
246
|
+
/* The server sorts, narrows and cuts the page; the listing travels as an
|
|
247
|
+
argument because the click that changes it and the load it starts happen
|
|
248
|
+
in one render, where the state hooks still read the old values. */
|
|
249
|
+
const load = async (next: Listing) => {
|
|
250
|
+
const cursor = pageCursor(next.cursors, next.pageIndex);
|
|
251
|
+
if (cursor === undefined) return;
|
|
252
|
+
const sequence = requests.sequence + 1;
|
|
253
|
+
requests.sequence = sequence;
|
|
138
254
|
setStatus('loading');
|
|
139
255
|
setError('');
|
|
140
256
|
try {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
257
|
+
const { sort, direction } = catalogSort(next.sorting);
|
|
258
|
+
const page = await loadCatalogItems({
|
|
259
|
+
sort,
|
|
260
|
+
direction,
|
|
261
|
+
limit: next.pageSize,
|
|
262
|
+
query: next.query,
|
|
263
|
+
kind: next.kind,
|
|
264
|
+
status: next.status,
|
|
265
|
+
cursor,
|
|
266
|
+
});
|
|
267
|
+
if (sequence !== requests.sequence) return;
|
|
268
|
+
const loaded = loadedListing(next, page);
|
|
269
|
+
catalogState.store.act((transaction) => {
|
|
270
|
+
transaction.set(catalogState.state.items, loaded.items);
|
|
271
|
+
transaction.set(catalogState.state.appliedQuery, loaded.appliedQuery);
|
|
272
|
+
transaction.set(catalogState.state.pageIndex, loaded.pageIndex);
|
|
273
|
+
transaction.set(catalogState.state.cursors, loaded.cursors);
|
|
274
|
+
transaction.set(catalogState.state.nextCursor, loaded.nextCursor);
|
|
275
|
+
transaction.set(catalogState.state.selectedIds, loaded.selectedIds);
|
|
276
|
+
transaction.set(catalogState.state.status, 'idle');
|
|
277
|
+
}, 'catalog/loaded');
|
|
146
278
|
} catch (loadError) {
|
|
147
|
-
|
|
148
|
-
loadError instanceof Error
|
|
149
|
-
? loadError.message
|
|
150
|
-
: t('catalog.error.load'),
|
|
151
|
-
);
|
|
152
|
-
} finally {
|
|
153
|
-
setStatus('idle');
|
|
279
|
+
if (sequence === requests.sequence) failLoad(loadError);
|
|
154
280
|
}
|
|
155
281
|
};
|
|
156
282
|
|
|
283
|
+
const refresh = () => {
|
|
284
|
+
setExportStarted(false);
|
|
285
|
+
return load(firstPage({}));
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const openPage = (index: number) => {
|
|
289
|
+
if (index !== pageIndex) void load({ ...listing(), pageIndex: index });
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const changeSorting = (sorts: readonly TableSort[]) => {
|
|
293
|
+
const next =
|
|
294
|
+
sorts.length === 0 ? DEFAULT_CATALOG_SORTING : sorts;
|
|
295
|
+
setSorting(next);
|
|
296
|
+
void load(firstPage({ sorting: next }));
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const changeKindFilter = (value: string) => {
|
|
300
|
+
const next: CatalogKindFilter =
|
|
301
|
+
value === 'product' || value === 'service' ? value : '';
|
|
302
|
+
setKindFilter(next);
|
|
303
|
+
void load(firstPage({ kind: next }));
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const changeStatusFilter = (value: string) => {
|
|
307
|
+
const next: CatalogStatusFilter =
|
|
308
|
+
value === 'active' || value === 'archived' ? value : '';
|
|
309
|
+
setStatusFilter(next);
|
|
310
|
+
void load(firstPage({ status: next }));
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const changePageSize = (size: number) => {
|
|
314
|
+
setPageSize(size);
|
|
315
|
+
void load(firstPage({ pageSize: size }));
|
|
316
|
+
};
|
|
317
|
+
|
|
157
318
|
useEffect(() => {
|
|
158
319
|
void refresh();
|
|
159
320
|
}, []);
|
|
160
321
|
|
|
322
|
+
/* The term is the server's filter, so typing it starts a new listing rather
|
|
323
|
+
than narrowing the page on screen: one listing per pause in the typing. */
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (!searchPending(query, appliedQuery)) return;
|
|
326
|
+
const timer = setTimeout(
|
|
327
|
+
() => void load({
|
|
328
|
+
...latest.listing,
|
|
329
|
+
query: term,
|
|
330
|
+
pageIndex: 0,
|
|
331
|
+
cursors: resetPageCursors(),
|
|
332
|
+
}),
|
|
333
|
+
SEARCH_DEBOUNCE_MS,
|
|
334
|
+
);
|
|
335
|
+
return () => clearTimeout(timer);
|
|
336
|
+
}, [query]);
|
|
337
|
+
|
|
161
338
|
const openForm = (item: CatalogItem | null) => {
|
|
162
339
|
setEditingItem(item);
|
|
163
340
|
setError('');
|
|
@@ -175,9 +352,9 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
175
352
|
catalogState.store.act(
|
|
176
353
|
(transaction) => transaction.set(
|
|
177
354
|
catalogState.state.items,
|
|
178
|
-
items
|
|
355
|
+
replaceItem(items, item),
|
|
179
356
|
),
|
|
180
|
-
'catalog/
|
|
357
|
+
'catalog/item-updated',
|
|
181
358
|
);
|
|
182
359
|
setEditingItem(item);
|
|
183
360
|
};
|
|
@@ -196,34 +373,32 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
196
373
|
basePriceMinor: Number(formValue(form, 'basePriceMinor')),
|
|
197
374
|
currency: formValue(form, 'currency'),
|
|
198
375
|
};
|
|
199
|
-
|
|
200
|
-
|
|
376
|
+
if (editingItem) {
|
|
377
|
+
replace(
|
|
378
|
+
await updateCatalogItem(
|
|
201
379
|
{ id: editingItem.id, ...mutable },
|
|
202
380
|
props.csrfToken,
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
editingItem ? 'catalog/updated' : 'catalog/created',
|
|
218
|
-
);
|
|
219
|
-
closeForm();
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
closeForm();
|
|
384
|
+
setStatus('idle');
|
|
385
|
+
} else {
|
|
386
|
+
await createCatalogItem(
|
|
387
|
+
{ sku: formValue(form, 'sku'), ...mutable },
|
|
388
|
+
props.csrfToken,
|
|
389
|
+
);
|
|
390
|
+
closeForm();
|
|
391
|
+
/* The new row belongs wherever the order puts it; the first page
|
|
392
|
+
of the listing shows it or the page it is on does. */
|
|
393
|
+
await load(firstPage({}));
|
|
394
|
+
}
|
|
220
395
|
} catch (saveError) {
|
|
221
396
|
setError(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
397
|
+
message(
|
|
398
|
+
saveError,
|
|
399
|
+
t(editingItem ? 'catalog.error.update' : 'catalog.error.create'),
|
|
400
|
+
),
|
|
225
401
|
);
|
|
226
|
-
} finally {
|
|
227
402
|
setStatus('idle');
|
|
228
403
|
}
|
|
229
404
|
};
|
|
@@ -238,13 +413,11 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
238
413
|
? await archiveCatalogItem(editingItem.id, props.csrfToken)
|
|
239
414
|
: await restoreCatalogItem(editingItem.id, props.csrfToken);
|
|
240
415
|
replace(item);
|
|
416
|
+
setStatus('idle');
|
|
417
|
+
/* Under a status filter the row no longer belongs on this page. */
|
|
418
|
+
if (statusFilter !== '') await load(listing());
|
|
241
419
|
} catch (lifecycleError) {
|
|
242
|
-
setError(
|
|
243
|
-
lifecycleError instanceof Error
|
|
244
|
-
? lifecycleError.message
|
|
245
|
-
: t('catalog.error.lifecycle'),
|
|
246
|
-
);
|
|
247
|
-
} finally {
|
|
420
|
+
setError(message(lifecycleError, t('catalog.error.lifecycle')));
|
|
248
421
|
setStatus('idle');
|
|
249
422
|
}
|
|
250
423
|
};
|
|
@@ -255,22 +428,68 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
255
428
|
setError('');
|
|
256
429
|
try {
|
|
257
430
|
await deleteCatalogItem(editingItem.id, props.csrfToken);
|
|
258
|
-
catalogState.store.act(
|
|
259
|
-
(transaction) => transaction.set(
|
|
260
|
-
catalogState.state.items,
|
|
261
|
-
items.filter((item) => item.id !== editingItem.id),
|
|
262
|
-
),
|
|
263
|
-
'catalog/deleted',
|
|
264
|
-
);
|
|
265
431
|
setDeleteConfirmOpen(false);
|
|
266
432
|
closeForm();
|
|
433
|
+
await load(listing());
|
|
267
434
|
} catch (deleteError) {
|
|
268
|
-
setError(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
435
|
+
setError(message(deleteError, t('catalog.error.delete')));
|
|
436
|
+
setStatus('idle');
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/* Both bulk actions go the same way: the qualifying selected ids on screen,
|
|
441
|
+
one outcome per id, then the listing is read again and the counts are
|
|
442
|
+
told. A refused or missing id is a count, never a failure of the call. */
|
|
443
|
+
const applyMany = async (
|
|
444
|
+
ids: readonly string[],
|
|
445
|
+
run: (ids: readonly string[]) => Promise<readonly CatalogBulkOutcome[]>,
|
|
446
|
+
done: string,
|
|
447
|
+
fallback: string,
|
|
448
|
+
) => {
|
|
449
|
+
if (ids.length === 0) return;
|
|
450
|
+
setStatus('submitting');
|
|
451
|
+
setError('');
|
|
452
|
+
try {
|
|
453
|
+
const outcomes = await run(ids);
|
|
454
|
+
await load(listing());
|
|
455
|
+
toasts.success(t(done, bulkOutcomeCounts(outcomes)));
|
|
456
|
+
} catch (bulkError) {
|
|
457
|
+
setError(message(bulkError, t(fallback)));
|
|
458
|
+
setStatus('idle');
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const archiveMany = () => {
|
|
463
|
+
setConfirmArchiveMany(false);
|
|
464
|
+
void applyMany(
|
|
465
|
+
archivable,
|
|
466
|
+
(ids) => archiveCatalogItems(ids, props.csrfToken),
|
|
467
|
+
'catalog.notice.archivedMany',
|
|
468
|
+
'catalog.error.archiveMany',
|
|
469
|
+
);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const restoreMany = () => {
|
|
473
|
+
void applyMany(
|
|
474
|
+
restorable,
|
|
475
|
+
(ids) => restoreCatalogItems(ids, props.csrfToken),
|
|
476
|
+
'catalog.notice.restoredMany',
|
|
477
|
+
'catalog.error.restoreMany',
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/* exports.core answers as soon as the job is recorded, not when the file is
|
|
482
|
+
written, so the screen reports that it started and points at the screen
|
|
483
|
+
that hands it over. */
|
|
484
|
+
const startExport = async () => {
|
|
485
|
+
setStatus('submitting');
|
|
486
|
+
setError('');
|
|
487
|
+
try {
|
|
488
|
+
await startListExport(CATALOG_ITEMS_EXPORT_LIST_ID, props.csrfToken);
|
|
489
|
+
setExportStarted(true);
|
|
490
|
+
setStatus('idle');
|
|
491
|
+
} catch (exportError) {
|
|
492
|
+
setError(message(exportError, t('catalog.export.error')));
|
|
274
493
|
setStatus('idle');
|
|
275
494
|
}
|
|
276
495
|
};
|
|
@@ -294,11 +513,7 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
294
513
|
'catalog/history-loaded',
|
|
295
514
|
);
|
|
296
515
|
} catch (historyFailure) {
|
|
297
|
-
setHistoryError(
|
|
298
|
-
historyFailure instanceof Error
|
|
299
|
-
? historyFailure.message
|
|
300
|
-
: t('catalog.error.history'),
|
|
301
|
-
);
|
|
516
|
+
setHistoryError(message(historyFailure, t('catalog.error.history')));
|
|
302
517
|
} finally {
|
|
303
518
|
setHistoryLoading(false);
|
|
304
519
|
}
|
|
@@ -314,85 +529,189 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
314
529
|
<Icon name="refresh" size={14} />
|
|
315
530
|
{t('catalog.page.action.refresh')}
|
|
316
531
|
</Button>
|
|
532
|
+
@if (props.canExport) {
|
|
533
|
+
<Button
|
|
534
|
+
size="sm"
|
|
535
|
+
disabled={status === 'denied' || status === 'submitting'}
|
|
536
|
+
onClick={() => void startExport()}
|
|
537
|
+
>
|
|
538
|
+
<Icon name="download" size={14} />
|
|
539
|
+
{t('catalog.export.action')}
|
|
540
|
+
</Button>
|
|
541
|
+
}
|
|
317
542
|
@if (props.canManage) {
|
|
318
|
-
<Button
|
|
543
|
+
<Button
|
|
544
|
+
size="sm"
|
|
545
|
+
variant="primary"
|
|
546
|
+
disabled={status === 'denied'}
|
|
547
|
+
onClick={() => openForm(null)}
|
|
548
|
+
>
|
|
319
549
|
<Icon name="plus" size={14} />
|
|
320
550
|
{t('catalog.page.action.new')}
|
|
321
551
|
</Button>
|
|
322
552
|
}
|
|
323
553
|
</PageHeader>
|
|
324
|
-
@if (
|
|
325
|
-
<
|
|
554
|
+
@if (status === 'denied') {
|
|
555
|
+
<section class="ui-card">
|
|
556
|
+
<EmptyState icon="shield" title={t('catalog.denied.title')}>{t(
|
|
557
|
+
'catalog.denied.description',
|
|
558
|
+
)}</EmptyState>
|
|
559
|
+
</section>
|
|
560
|
+
} @else {
|
|
561
|
+
<>
|
|
562
|
+
@if (error && !formOpen) {
|
|
563
|
+
<Alert>{error}</Alert>
|
|
564
|
+
}
|
|
565
|
+
@if (exportStarted) {
|
|
566
|
+
<Alert tone="info">
|
|
567
|
+
{t('catalog.export.started')}
|
|
568
|
+
{' '}
|
|
569
|
+
<a href="/exports">{t('catalog.export.link')}</a>
|
|
570
|
+
</Alert>
|
|
571
|
+
}
|
|
572
|
+
<TableCard
|
|
573
|
+
title={t('catalog.table.title')}
|
|
574
|
+
count={t('catalog.table.count.page', { count: items.length })}
|
|
575
|
+
search={<SearchField
|
|
576
|
+
value={query}
|
|
577
|
+
placeholder={t('catalog.table.search.placeholder')}
|
|
578
|
+
label={t('catalog.table.search.label')}
|
|
579
|
+
onInput={setQuery}
|
|
580
|
+
/>}
|
|
581
|
+
filters={<Filters
|
|
582
|
+
open={filtersOpen}
|
|
583
|
+
onToggle={() => setFiltersOpen(!filtersOpen)}
|
|
584
|
+
activeCount={filterCount}
|
|
585
|
+
label={t('catalog.filters.label')}
|
|
586
|
+
>
|
|
587
|
+
<Select
|
|
588
|
+
id="catalog-kind-filter"
|
|
589
|
+
label={t('catalog.filters.kind')}
|
|
590
|
+
value={kindFilter}
|
|
591
|
+
options={[
|
|
592
|
+
{ value: '', label: t('catalog.filters.allKinds') },
|
|
593
|
+
{ value: 'product', label: t('catalog.kind.product') },
|
|
594
|
+
{ value: 'service', label: t('catalog.kind.service') },
|
|
595
|
+
]}
|
|
596
|
+
onChange={changeKindFilter}
|
|
597
|
+
/>
|
|
598
|
+
<Select
|
|
599
|
+
id="catalog-status-filter"
|
|
600
|
+
label={t('catalog.filters.status')}
|
|
601
|
+
value={statusFilter}
|
|
602
|
+
options={[
|
|
603
|
+
{ value: '', label: t('catalog.filters.allStatuses') },
|
|
604
|
+
{ value: 'active', label: t('catalog.status.active') },
|
|
605
|
+
{ value: 'archived', label: t('catalog.status.archived') },
|
|
606
|
+
]}
|
|
607
|
+
onChange={changeStatusFilter}
|
|
608
|
+
/>
|
|
609
|
+
</Filters>}
|
|
610
|
+
caption={t('catalog.table.caption')}
|
|
611
|
+
mode="server"
|
|
612
|
+
columns={columns()}
|
|
613
|
+
rows={items}
|
|
614
|
+
rowKey={(item) => item.id}
|
|
615
|
+
sorting
|
|
616
|
+
sortingState={sorting}
|
|
617
|
+
onSortingChange={changeSorting}
|
|
618
|
+
pagination={{
|
|
619
|
+
pageIndex,
|
|
620
|
+
pageSize,
|
|
621
|
+
onPageChange: openPage,
|
|
622
|
+
hasMore,
|
|
623
|
+
}}
|
|
624
|
+
after={<Pagination
|
|
625
|
+
pageIndex={pageIndex}
|
|
626
|
+
pageSize={pageSize}
|
|
627
|
+
hasMore={hasMore}
|
|
628
|
+
pageSizes={PAGE_SIZES}
|
|
629
|
+
onPageChange={openPage}
|
|
630
|
+
onPageSizeChange={changePageSize}
|
|
631
|
+
label={t('catalog.pagination.label')}
|
|
632
|
+
previousLabel={t('catalog.pagination.previous')}
|
|
633
|
+
nextLabel={t('catalog.pagination.next')}
|
|
634
|
+
pageSizeLabel={t('catalog.pagination.size')}
|
|
635
|
+
summary={(page) => t('catalog.pagination.summary', {
|
|
636
|
+
page: page.pageIndex + 1,
|
|
637
|
+
from: page.from,
|
|
638
|
+
})}
|
|
639
|
+
/>}
|
|
640
|
+
status={status === 'loading' ? 'loading' : 'idle'}
|
|
641
|
+
loadingLabel={t('catalog.table.loading')}
|
|
642
|
+
filtered={appliedQuery !== '' || filterCount > 0}
|
|
643
|
+
empty={{
|
|
644
|
+
icon: 'catalog',
|
|
645
|
+
title: t('catalog.table.empty.title'),
|
|
646
|
+
hint: t('catalog.table.empty.hint'),
|
|
647
|
+
}}
|
|
648
|
+
emptyFiltered={{
|
|
649
|
+
icon: 'catalog',
|
|
650
|
+
title: t('catalog.table.emptyFiltered.title'),
|
|
651
|
+
hint: t('catalog.table.emptyFiltered.hint'),
|
|
652
|
+
}}
|
|
653
|
+
selection={props.canManage
|
|
654
|
+
? {
|
|
655
|
+
selectedKeys: selectedIds,
|
|
656
|
+
onSelectionChange: setSelectedIds,
|
|
657
|
+
label: t('catalog.selection.label'),
|
|
658
|
+
rowLabel: (item) => t('catalog.selection.rowLabel', {
|
|
659
|
+
name: item.name,
|
|
660
|
+
}),
|
|
661
|
+
clearLabel: t('catalog.selection.clear'),
|
|
662
|
+
}
|
|
663
|
+
: undefined}
|
|
664
|
+
bulkActions={props.canManage
|
|
665
|
+
? [
|
|
666
|
+
{
|
|
667
|
+
id: 'archive-selected',
|
|
668
|
+
label: t('catalog.action.archiveSelected'),
|
|
669
|
+
icon: 'x',
|
|
670
|
+
tone: 'danger',
|
|
671
|
+
disabled: archivable.length === 0 || status === 'submitting',
|
|
672
|
+
reason: archivable.length === 0
|
|
673
|
+
? t('catalog.action.archiveRefused')
|
|
674
|
+
: undefined,
|
|
675
|
+
onSelect: () => setConfirmArchiveMany(true),
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: 'restore-selected',
|
|
679
|
+
label: t('catalog.action.restoreSelected'),
|
|
680
|
+
icon: 'refresh',
|
|
681
|
+
disabled: restorable.length === 0 || status === 'submitting',
|
|
682
|
+
reason: restorable.length === 0
|
|
683
|
+
? t('catalog.action.restoreRefused')
|
|
684
|
+
: undefined,
|
|
685
|
+
onSelect: restoreMany,
|
|
686
|
+
},
|
|
687
|
+
]
|
|
688
|
+
: undefined}
|
|
689
|
+
selectionSummary={(count) => t('catalog.selection.summary', {
|
|
690
|
+
count,
|
|
691
|
+
})}
|
|
692
|
+
actionsLabel={t('catalog.table.column.actions')}
|
|
693
|
+
actionsWidth="280px"
|
|
694
|
+
actions={(item) => [
|
|
695
|
+
{
|
|
696
|
+
id: 'history',
|
|
697
|
+
label: t('catalog.action.history'),
|
|
698
|
+
icon: 'activity',
|
|
699
|
+
onSelect: () => void openHistory(item),
|
|
700
|
+
},
|
|
701
|
+
...(props.canManage
|
|
702
|
+
? [
|
|
703
|
+
{
|
|
704
|
+
id: 'edit',
|
|
705
|
+
label: t('catalog.action.edit'),
|
|
706
|
+
icon: 'file-text',
|
|
707
|
+
onSelect: () => openForm(item),
|
|
708
|
+
},
|
|
709
|
+
]
|
|
710
|
+
: []),
|
|
711
|
+
]}
|
|
712
|
+
/>
|
|
713
|
+
</>
|
|
326
714
|
}
|
|
327
|
-
<TableCard
|
|
328
|
-
title={t('catalog.table.title')}
|
|
329
|
-
count={t(
|
|
330
|
-
items.length === 1
|
|
331
|
-
? 'catalog.table.count.one'
|
|
332
|
-
: 'catalog.table.count.other',
|
|
333
|
-
{ count: items.length },
|
|
334
|
-
)}
|
|
335
|
-
search={<SearchField
|
|
336
|
-
value={query}
|
|
337
|
-
placeholder={t('catalog.table.search.placeholder')}
|
|
338
|
-
label={t('catalog.table.search.label')}
|
|
339
|
-
onInput={setQuery}
|
|
340
|
-
/>}
|
|
341
|
-
filters={<Filters
|
|
342
|
-
open={filtersOpen}
|
|
343
|
-
onToggle={() => setFiltersOpen(!filtersOpen)}
|
|
344
|
-
activeCount={includeArchived ? 1 : 0}
|
|
345
|
-
label={t('catalog.filters.label')}
|
|
346
|
-
>
|
|
347
|
-
<label class="ui-checkbox">
|
|
348
|
-
<input
|
|
349
|
-
type="checkbox"
|
|
350
|
-
checked={includeArchived}
|
|
351
|
-
onChange={(event: Event) => setIncludeArchived(
|
|
352
|
-
(event.currentTarget as HTMLInputElement).checked,
|
|
353
|
-
)}
|
|
354
|
-
/>
|
|
355
|
-
<span>{t('catalog.filters.includeArchived')}</span>
|
|
356
|
-
</label>
|
|
357
|
-
</Filters>}
|
|
358
|
-
caption={t('catalog.table.caption')}
|
|
359
|
-
columns={columns()}
|
|
360
|
-
rows={visible}
|
|
361
|
-
rowKey={(item) => item.id}
|
|
362
|
-
status={status === 'loading' && items.length === 0 ? 'loading' : 'idle'}
|
|
363
|
-
loadingLabel={t('catalog.table.loading')}
|
|
364
|
-
filtered={filtered}
|
|
365
|
-
empty={{
|
|
366
|
-
icon: 'catalog',
|
|
367
|
-
title: t('catalog.table.empty.title'),
|
|
368
|
-
hint: t('catalog.table.empty.hint'),
|
|
369
|
-
}}
|
|
370
|
-
emptyFiltered={{
|
|
371
|
-
icon: 'catalog',
|
|
372
|
-
title: t('catalog.table.emptyFiltered.title'),
|
|
373
|
-
hint: t('catalog.table.emptyFiltered.hint'),
|
|
374
|
-
}}
|
|
375
|
-
actionsLabel={t('catalog.table.column.actions')}
|
|
376
|
-
actionsWidth="280px"
|
|
377
|
-
actions={(item) => [
|
|
378
|
-
{
|
|
379
|
-
id: 'history',
|
|
380
|
-
label: t('catalog.action.history'),
|
|
381
|
-
icon: 'activity',
|
|
382
|
-
onSelect: () => void openHistory(item),
|
|
383
|
-
},
|
|
384
|
-
...(props.canManage
|
|
385
|
-
? [
|
|
386
|
-
{
|
|
387
|
-
id: 'edit',
|
|
388
|
-
label: t('catalog.action.edit'),
|
|
389
|
-
icon: 'file-text',
|
|
390
|
-
onSelect: () => openForm(item),
|
|
391
|
-
},
|
|
392
|
-
]
|
|
393
|
-
: []),
|
|
394
|
-
]}
|
|
395
|
-
/>
|
|
396
715
|
<Drawer
|
|
397
716
|
open={formOpen}
|
|
398
717
|
title={t(
|
|
@@ -445,27 +764,54 @@ export function CatalogView(props: CatalogViewProps) @{
|
|
|
445
764
|
>{t('catalog.delete.description', {
|
|
446
765
|
name: editingItem?.name ?? '',
|
|
447
766
|
})}</ConfirmDialog>
|
|
767
|
+
<ConfirmDialog
|
|
768
|
+
open={confirmArchiveMany}
|
|
769
|
+
title={t('catalog.archiveMany.title')}
|
|
770
|
+
confirmLabel={t('catalog.action.archiveSelected')}
|
|
771
|
+
cancelLabel={t('catalog.form.cancel')}
|
|
772
|
+
busy={status === 'submitting'}
|
|
773
|
+
onConfirm={archiveMany}
|
|
774
|
+
onCancel={() => setConfirmArchiveMany(false)}
|
|
775
|
+
>{t('catalog.archiveMany.description', {
|
|
776
|
+
count: archivable.length,
|
|
777
|
+
})}</ConfirmDialog>
|
|
778
|
+
<ToastHost
|
|
779
|
+
label={t('catalog.common.messages')}
|
|
780
|
+
closeLabel={t('catalog.action.close')}
|
|
781
|
+
/>
|
|
448
782
|
</div>
|
|
449
783
|
}
|
|
450
784
|
|
|
451
785
|
export function CatalogDashboardWidget() @{
|
|
452
786
|
const widgetState = useMemo(() => createCatalogClientState(), []);
|
|
453
787
|
const [items] = useValue(widgetState.state.items);
|
|
788
|
+
const [nextCursor] = useValue(widgetState.state.nextCursor);
|
|
454
789
|
const [status, setStatus] = useValue(widgetState.state.status);
|
|
455
790
|
useEffect(() => {
|
|
456
791
|
setStatus('loading');
|
|
457
|
-
void loadCatalogItems(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
792
|
+
void loadCatalogItems({
|
|
793
|
+
sort: 'name',
|
|
794
|
+
direction: 'asc',
|
|
795
|
+
limit: WIDGET_PAGE,
|
|
796
|
+
query: '',
|
|
797
|
+
kind: '',
|
|
798
|
+
status: 'active',
|
|
799
|
+
cursor: null,
|
|
800
|
+
}).then((page) => {
|
|
801
|
+
widgetState.store.act((transaction) => {
|
|
802
|
+
transaction.set(widgetState.state.items, page.items);
|
|
803
|
+
transaction.set(widgetState.state.nextCursor, page.page.nextCursor);
|
|
804
|
+
}, 'catalog/widget-loaded');
|
|
462
805
|
}).catch(() => undefined).finally(() => setStatus('idle'));
|
|
463
806
|
}, []);
|
|
807
|
+
const count = new Intl.NumberFormat(activeLocale()).format(items.length);
|
|
464
808
|
<Kpi
|
|
465
809
|
label={t('catalog.widget.label')}
|
|
466
810
|
value={status === 'loading'
|
|
467
811
|
? '…'
|
|
468
|
-
:
|
|
812
|
+
: nextCursor === null
|
|
813
|
+
? count
|
|
814
|
+
: count + '+'}
|
|
469
815
|
note={t('catalog.widget.note')}
|
|
470
816
|
href="/catalog"
|
|
471
817
|
linkLabel={t('catalog.widget.link')}
|