@haneinfo/frontend-instances 0.6.0 → 0.7.0
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/README.md +117 -29
- package/agent-guide.md +12 -0
- package/dist/attachments-client.d.ts +7 -2
- package/dist/attachments-client.d.ts.map +1 -1
- package/dist/attachments-client.js +47 -11
- package/dist/attachments-client.js.map +1 -1
- package/dist/client.d.ts +10 -75
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -463
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +94 -4
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -98
- package/dist/contracts.js.map +1 -1
- package/dist/dashboard.d.ts +41 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +72 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/detail-client.d.ts +55 -0
- package/dist/detail-client.d.ts.map +1 -0
- package/dist/detail-client.js +173 -0
- package/dist/detail-client.js.map +1 -0
- package/dist/documents-client.d.ts +2 -1
- package/dist/documents-client.d.ts.map +1 -1
- package/dist/documents-client.js +13 -9
- package/dist/documents-client.js.map +1 -1
- package/dist/draft-client.d.ts +8 -2
- package/dist/draft-client.d.ts.map +1 -1
- package/dist/draft-client.js +71 -53
- package/dist/draft-client.js.map +1 -1
- package/dist/filter-editor.d.ts +18 -0
- package/dist/filter-editor.d.ts.map +1 -0
- package/dist/filter-editor.js +145 -0
- package/dist/filter-editor.js.map +1 -0
- package/dist/format.d.ts +152 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +402 -0
- package/dist/format.js.map +1 -0
- package/dist/import-client.d.ts +31 -0
- package/dist/import-client.d.ts.map +1 -0
- package/dist/import-client.js +179 -0
- package/dist/import-client.js.map +1 -0
- package/dist/instance-management-client.d.ts +20 -6
- package/dist/instance-management-client.d.ts.map +1 -1
- package/dist/instance-management-client.js +90 -67
- package/dist/instance-management-client.js.map +1 -1
- package/dist/labels.d.ts +10 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +40 -0
- package/dist/labels.js.map +1 -0
- package/dist/list-client.d.ts +39 -0
- package/dist/list-client.d.ts.map +1 -0
- package/dist/list-client.js +402 -0
- package/dist/list-client.js.map +1 -0
- package/dist/list-query.d.ts +41 -0
- package/dist/list-query.d.ts.map +1 -0
- package/dist/list-query.js +171 -0
- package/dist/list-query.js.map +1 -0
- package/dist/management-client.d.ts +6 -2
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +113 -84
- package/dist/management-client.js.map +1 -1
- package/dist/messages.d.ts +4 -4
- package/dist/messages.js +4 -4
- package/dist/messages.js.map +1 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +265 -50
- package/dist/server.js.map +1 -1
- package/dist/timeline.d.ts +24 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +189 -0
- package/dist/timeline.js.map +1 -0
- package/dist/ui-messages.d.ts +284 -0
- package/dist/ui-messages.d.ts.map +1 -0
- package/dist/ui-messages.js +372 -0
- package/dist/ui-messages.js.map +1 -0
- package/dist/ui.d.ts +100 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +169 -0
- package/dist/ui.js.map +1 -0
- package/package.json +4 -2
package/dist/labels.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Labels, type Translation } from './contracts.js';
|
|
2
|
+
import { type DisplayLocale } from './format.js';
|
|
3
|
+
/** The package's complete English or Traditional Chinese catalog for every label key. */
|
|
4
|
+
export declare function defaultLabels(locale?: DisplayLocale | string): Labels;
|
|
5
|
+
/**
|
|
6
|
+
* The host's translations with the package defaults behind them: a key the host does not translate
|
|
7
|
+
* (missing, empty or echoed back as the key itself) uses the default for the locale.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveLabels(t: Translation, locale?: DisplayLocale | string): Labels;
|
|
10
|
+
//# sourceMappingURL=labels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,MAAM,EAAa,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEzF,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAgBhE,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,MAAM,GAAE,aAAa,GAAG,MAAgB,GAAG,MAAM,CAG9E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,MAAM,CAarF"}
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { attachmentMessages } from './attachment-messages.js';
|
|
2
|
+
import { labelKeys } from './contracts.js';
|
|
3
|
+
import { documentMessages } from './document-messages.js';
|
|
4
|
+
import { displayLocale } from './format.js';
|
|
5
|
+
import { instanceManagementMessages } from './instance-management-messages.js';
|
|
6
|
+
import { errorMessages, managementMessages } from './messages.js';
|
|
7
|
+
import { coreMessages, uiMessages } from './ui-messages.js';
|
|
8
|
+
const sources = [
|
|
9
|
+
coreMessages,
|
|
10
|
+
uiMessages,
|
|
11
|
+
instanceManagementMessages,
|
|
12
|
+
documentMessages,
|
|
13
|
+
attachmentMessages,
|
|
14
|
+
errorMessages,
|
|
15
|
+
managementMessages,
|
|
16
|
+
];
|
|
17
|
+
const tuples = Object.assign({}, ...sources);
|
|
18
|
+
/** The package's complete English or Traditional Chinese catalog for every label key. */
|
|
19
|
+
export function defaultLabels(locale = 'zh-TW') {
|
|
20
|
+
const index = displayLocale(locale) === 'en' ? 0 : 1;
|
|
21
|
+
return Object.fromEntries(labelKeys.map((key) => [key, tuples[key]?.[index] ?? key]));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The host's translations with the package defaults behind them: a key the host does not translate
|
|
25
|
+
* (missing, empty or echoed back as the key itself) uses the default for the locale.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveLabels(t, locale) {
|
|
28
|
+
const defaults = defaultLabels(locale);
|
|
29
|
+
return Object.fromEntries(labelKeys.map((key) => {
|
|
30
|
+
let value;
|
|
31
|
+
try {
|
|
32
|
+
value = t(key);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
value = undefined;
|
|
36
|
+
}
|
|
37
|
+
return [key, typeof value === 'string' && value && value !== key ? value : defaults[key]];
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAA8B,SAAS,EAAoB,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAsB,aAAa,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,OAAO,GAAG;IACd,YAAY;IACZ,UAAU;IACV,0BAA0B;IAC1B,gBAAgB;IAChB,kBAAkB;IAClB,aAAa;IACb,kBAAkB;CACV,CAAC;AACX,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,OAAO,CAAgD,CAAC;AAE5F,yFAAyF;AACzF,MAAM,UAAU,aAAa,CAAC,SAAiC,OAAO;IACpE,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAW,CAAC;AAClG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,CAAc,EAAE,MAA+B;IAC3E,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC,WAAW,CACvB,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACpB,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACH,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,GAAG,SAAS,CAAC;QACpB,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5F,CAAC,CAAC,CACO,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type FormField, type InstanceAction, type InstanceQuery, type Labels, type LinkComponent, type ListResult, type QueryField, type SavedView, type StateOption } from './contracts.js';
|
|
2
|
+
export interface ListClientProps {
|
|
3
|
+
fields: QueryField[];
|
|
4
|
+
initial: ListResult;
|
|
5
|
+
initialQuery: InstanceQuery;
|
|
6
|
+
initialTagsAvailable?: boolean;
|
|
7
|
+
views: SavedView[];
|
|
8
|
+
machineCode: string;
|
|
9
|
+
basePath: string;
|
|
10
|
+
labels: Labels;
|
|
11
|
+
action: InstanceAction;
|
|
12
|
+
locale?: string;
|
|
13
|
+
timeZone?: string;
|
|
14
|
+
/** All states of the machine (label, tone) for the state chips; may be empty. */
|
|
15
|
+
states?: StateOption[];
|
|
16
|
+
/** The subject Form's fields, to render option labels in cells. */
|
|
17
|
+
formFields?: FormField[];
|
|
18
|
+
personField?: string;
|
|
19
|
+
showCreate?: boolean;
|
|
20
|
+
LinkComponent?: LinkComponent;
|
|
21
|
+
}
|
|
22
|
+
export declare function ListClient(props: ListClientProps): import("react").JSX.Element;
|
|
23
|
+
/** Starts a draft and opens it; the backend decides whether the caller may create one. */
|
|
24
|
+
export declare function CreateRecordButton({ machineCode, basePath, labels: t, action, variant, initialData, }: {
|
|
25
|
+
machineCode: string;
|
|
26
|
+
/** Initial draft data, e.g. from blankDraftData. */
|
|
27
|
+
initialData?: Record<string, unknown>;
|
|
28
|
+
basePath: string;
|
|
29
|
+
labels: Labels;
|
|
30
|
+
action: InstanceAction;
|
|
31
|
+
variant?: 'default' | 'outline';
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
export declare function ShareForm({ view, labels: t, disabled, onShare, }: {
|
|
34
|
+
view: SavedView;
|
|
35
|
+
labels: Labels;
|
|
36
|
+
disabled: boolean;
|
|
37
|
+
onShare: (visibility: SavedView['visibility'], roles: string[]) => void;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=list-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-client.d.ts","sourceRoot":"","sources":["../src/list-client.tsx"],"names":[],"mappings":"AAqDA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,aAAa,EAElB,KAAK,MAAM,EACX,KAAK,aAAa,EAElB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAC;AAqCxB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,EAAE,UAAU,CAAC;IACpB,YAAY,EAAE,aAAa,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,mEAAmE;IACnE,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,+BA4xBhD;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,QAAQ,EACR,MAAM,EAAE,CAAC,EACT,MAAM,EACN,OAAmB,EACnB,WAAW,GACZ,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CACjC,+BAyBA;AAyhBD,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,MAAM,EAAE,CAAC,EACT,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACzE,+BAgDA"}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The record list (draft 202610-a8d3 point 7): toolbar with exact-match search, state chips, saved
|
|
5
|
+
* views and a filter drawer; removable applied-filter chips; a table on wide screens and cards on
|
|
6
|
+
* narrow ones; skeleton rows while loading, two empty states, pagination and a "More" menu.
|
|
7
|
+
*/
|
|
8
|
+
import { Button, Checkbox, cn, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@haneinfo/frontend-shell';
|
|
9
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, FileDown, FileSpreadsheet, FileUp, Inbox, MoreHorizontal, Plus, Search, SearchX, SlidersHorizontal, Trash2, X, } from 'lucide-react';
|
|
10
|
+
import NextLink from 'next/link';
|
|
11
|
+
import { useId, useMemo, useRef, useState } from 'react';
|
|
12
|
+
import { call, ErrorMessage, useTask } from './action-client.js';
|
|
13
|
+
import { FilterEditor, labelOf, prepareFilter } from './filter-editor.js';
|
|
14
|
+
import { formatDateTime, formatFieldValue, formatNumber, fromZonedInput } from './format.js';
|
|
15
|
+
import { JobStatus } from './import-client.js';
|
|
16
|
+
import { queryWithTagsFallback, TagPicker } from './instance-management-client.js';
|
|
17
|
+
import { blankDraftData, composeFilter, conditionCount, defaultColumns, emptyGroup, personFieldOf, queryIdentity, searchFieldsOf, splitFilter, } from './list-query.js';
|
|
18
|
+
import { dateRange, listHref } from './query.js';
|
|
19
|
+
import { Chip, DateTimeText, EmptyState, ErrorState, fillText, NativeSelect, Skeleton, StateBadge, Tag, ToneDot, touch, } from './ui.js';
|
|
20
|
+
const LIMITS = [10, 25, 50, 100, 200, 500];
|
|
21
|
+
const inaccessible = (error) => error &&
|
|
22
|
+
['unauthorized', 'forbidden', 'notFound', 'FORBIDDEN', 'INSTANCE_NOT_FOUND'].includes(error.key);
|
|
23
|
+
export function ListClient(props) {
|
|
24
|
+
const { fields, initial, initialQuery, initialTagsAvailable = true, machineCode, basePath, labels: t, action, locale = 'zh-TW', timeZone = 'Asia/Taipei', states = [], formFields = [], showCreate = false, LinkComponent = NextLink, } = props;
|
|
25
|
+
const task = useTask();
|
|
26
|
+
const searchId = useId();
|
|
27
|
+
// The numbered data field repeats $number, so the search box names it only once.
|
|
28
|
+
const searchFields = useMemo(() => searchFieldsOf(fields).filter((field) => !formFields.some((form) => form.numbered && (form.id ?? form.key) === field.dataKey)), [fields, formFields]);
|
|
29
|
+
const columnsDefault = useMemo(() => defaultColumns(fields, props.personField), [fields, props.personField]);
|
|
30
|
+
const person = personFieldOf(fields, props.personField);
|
|
31
|
+
const [query, setQuery] = useState(initialQuery);
|
|
32
|
+
const [result, setResult] = useState(initial);
|
|
33
|
+
const [tagsAvailable, setTagsAvailable] = useState(initialTagsAvailable);
|
|
34
|
+
const [views, setViews] = useState(props.views);
|
|
35
|
+
const parts = splitFilter(query.filter, searchFields);
|
|
36
|
+
const [searchText, setSearchText] = useState(parts.search);
|
|
37
|
+
const [drawer, setDrawer] = useState(false);
|
|
38
|
+
const [saveOpen, setSaveOpen] = useState(false);
|
|
39
|
+
const [job, setJob] = useState();
|
|
40
|
+
const [announcement, setAnnouncement] = useState('');
|
|
41
|
+
const knownStates = useMemo(() => {
|
|
42
|
+
const map = new Map(states.map((state) => [state.key, state]));
|
|
43
|
+
for (const item of result.items)
|
|
44
|
+
if (!map.has(item.state) && item.stateLabel)
|
|
45
|
+
map.set(item.state, {
|
|
46
|
+
key: item.state,
|
|
47
|
+
label: item.stateLabel,
|
|
48
|
+
tone: item.stateTone ?? 'neutral',
|
|
49
|
+
});
|
|
50
|
+
return map;
|
|
51
|
+
}, [states, result.items]);
|
|
52
|
+
if (inaccessible(task.error))
|
|
53
|
+
return (_jsx(ErrorState, { title: task.error?.key === 'unauthorized' ? t.signInTitle : t.forbiddenTitle, description: t[task.error?.key ?? 'requestFailed'] }));
|
|
54
|
+
const apply = (next, message = t.listUpdated) => task.run(async () => {
|
|
55
|
+
const prepared = {
|
|
56
|
+
...next,
|
|
57
|
+
columns: next.columns ?? columnsDefault,
|
|
58
|
+
filter: next.filter ? prepareFilter(next.filter, fields) : undefined,
|
|
59
|
+
};
|
|
60
|
+
if (!prepared.filter)
|
|
61
|
+
delete prepared.filter;
|
|
62
|
+
const response = await queryWithTagsFallback(action, machineCode, prepared);
|
|
63
|
+
const applied = { ...response, query: { ...response.query, columns: next.columns } };
|
|
64
|
+
if (!applied.query.columns)
|
|
65
|
+
delete applied.query.columns;
|
|
66
|
+
if (!applied.available)
|
|
67
|
+
setTagsAvailable(false);
|
|
68
|
+
setResult(applied.result);
|
|
69
|
+
setQuery(applied.query);
|
|
70
|
+
setSearchText(splitFilter(applied.query.filter, searchFields).search);
|
|
71
|
+
setAnnouncement(`${message} ${fillText(t.resultCount, { count: formatNumber(applied.result.total, locale) })}`);
|
|
72
|
+
try {
|
|
73
|
+
window.history.replaceState(window.history.state, '', listHref(basePath, applied.query));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* URL sync is a convenience only. */
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const withParts = (change, base = query) => {
|
|
80
|
+
const filter = composeFilter({ ...splitFilter(base.filter, searchFields), ...change }, searchFields);
|
|
81
|
+
const { filter: _old, ...rest } = base;
|
|
82
|
+
return { ...rest, ...(filter ? { filter } : {}), offset: 0 };
|
|
83
|
+
};
|
|
84
|
+
const columns = (query.columns ?? columnsDefault).filter((id) => id !== '$number');
|
|
85
|
+
const visibleColumns = result.columns.filter((id) => id !== '$number');
|
|
86
|
+
const limit = result.limit || query.limit || 50;
|
|
87
|
+
const page = Math.floor(result.offset / limit) + 1;
|
|
88
|
+
const pages = Math.max(1, Math.ceil(result.total / limit));
|
|
89
|
+
const archived = query.archived ?? 'EXCLUDE';
|
|
90
|
+
const filtered = !!query.filter || !!query.tags?.length || archived !== 'EXCLUDE';
|
|
91
|
+
const currentView = views.find((view) => queryIdentity(view.query) === queryIdentity(query));
|
|
92
|
+
const fieldById = (id) => fields.find((field) => field.id === id);
|
|
93
|
+
const formFieldOf = (id) => {
|
|
94
|
+
const key = fieldById(id)?.dataKey;
|
|
95
|
+
return key ? formFields.find((field) => (field.id ?? field.key) === key) : undefined;
|
|
96
|
+
};
|
|
97
|
+
const stateOf = (item) => {
|
|
98
|
+
const known = knownStates.get(item.state);
|
|
99
|
+
return {
|
|
100
|
+
label: item.stateLabel ?? known?.label ?? item.state,
|
|
101
|
+
tone: item.stateTone ?? known?.tone,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const cell = (item, id) => {
|
|
105
|
+
if (id === '$state') {
|
|
106
|
+
const state = stateOf(item);
|
|
107
|
+
return _jsx(StateBadge, { label: state.label, tone: state.tone });
|
|
108
|
+
}
|
|
109
|
+
if (id === '$submitterId')
|
|
110
|
+
return item.submitterName ?? (item.submitterId ? t.unknownPerson : '—');
|
|
111
|
+
if (id === '$createdAt') {
|
|
112
|
+
const text = formatDateTime(item.createdAt, { locale, timeZone });
|
|
113
|
+
return text ? (_jsx(DateTimeText, { iso: new Date(item.createdAt).toISOString(), text: text, full: formatDateTime(item.createdAt, { locale, timeZone, full: true }) ?? text })) : ('—');
|
|
114
|
+
}
|
|
115
|
+
const field = fieldById(id);
|
|
116
|
+
const display = formatFieldValue(formFieldOf(id) ?? { type: field?.type ?? 'text' }, item.values[id], { locale, timeZone });
|
|
117
|
+
if (display.kind === 'empty')
|
|
118
|
+
return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
119
|
+
if (display.kind === 'date' || display.kind === 'datetime')
|
|
120
|
+
return _jsx(DateTimeText, { iso: display.iso, text: display.text, full: display.full });
|
|
121
|
+
return (_jsx("span", { className: "line-clamp-2 [overflow-wrap:anywhere]", title: display.text, children: display.text }));
|
|
122
|
+
};
|
|
123
|
+
const title = (item) => item.number ?? t.untitledDraft;
|
|
124
|
+
const href = (item) => `${basePath}/${encodeURIComponent(item.id)}`;
|
|
125
|
+
const tags = (item) => item.tags?.length ? (_jsxs("span", { className: "flex flex-wrap gap-1", children: [item.tags.slice(0, 2).map((tag) => (_jsx(Tag, { children: tag }, tag))), item.tags.length > 2 && (_jsx("span", { className: "text-muted-foreground text-xs leading-5", title: item.tags.slice(2).join('、'), children: fillText(t.moreTags, { count: item.tags.length - 2 }) }))] })) : null;
|
|
126
|
+
const sortOf = (id) => query.sort?.find((entry) => entry.fieldId === id)?.direction;
|
|
127
|
+
const toggleSort = (id) => {
|
|
128
|
+
const current = query.sort?.[0]?.fieldId === id ? query.sort[0].direction : undefined;
|
|
129
|
+
const sort = current === 'asc'
|
|
130
|
+
? [{ fieldId: id, direction: 'desc' }]
|
|
131
|
+
: current === 'desc'
|
|
132
|
+
? []
|
|
133
|
+
: [{ fieldId: id, direction: 'asc' }];
|
|
134
|
+
void apply({ ...query, sort, offset: 0 });
|
|
135
|
+
};
|
|
136
|
+
const toggleState = (key) => void apply(withParts({
|
|
137
|
+
states: parts.states.includes(key)
|
|
138
|
+
? parts.states.filter((state) => state !== key)
|
|
139
|
+
: [...parts.states, key],
|
|
140
|
+
}));
|
|
141
|
+
const exportAs = (format) => void task.run(async () => {
|
|
142
|
+
setJob(await call(action, { kind: 'export', machineCode, query, format }));
|
|
143
|
+
setAnnouncement(t.exportStarted);
|
|
144
|
+
});
|
|
145
|
+
const chips = [];
|
|
146
|
+
if (parts.search)
|
|
147
|
+
chips.push({
|
|
148
|
+
key: 'search',
|
|
149
|
+
label: fillText(t.chipSearch, { value: parts.search }),
|
|
150
|
+
remove: () => void apply(withParts({ search: '' })),
|
|
151
|
+
});
|
|
152
|
+
if (parts.mine)
|
|
153
|
+
chips.push({
|
|
154
|
+
key: 'mine',
|
|
155
|
+
label: t.chipMine,
|
|
156
|
+
remove: () => void apply(withParts({ mine: false })),
|
|
157
|
+
});
|
|
158
|
+
for (const key of parts.states)
|
|
159
|
+
chips.push({
|
|
160
|
+
key: `state:${key}`,
|
|
161
|
+
label: fillText(t.chipState, { value: knownStates.get(key)?.label ?? t.unknownState }),
|
|
162
|
+
remove: () => toggleState(key),
|
|
163
|
+
});
|
|
164
|
+
for (const tag of query.tags ?? [])
|
|
165
|
+
chips.push({
|
|
166
|
+
key: `tag:${tag}`,
|
|
167
|
+
label: fillText(t.chipTag, { value: tag }),
|
|
168
|
+
remove: () => {
|
|
169
|
+
const rest = (query.tags ?? []).filter((value) => value !== tag);
|
|
170
|
+
void apply({ ...query, tags: rest.length ? rest : undefined, offset: 0 });
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (archived !== 'EXCLUDE')
|
|
174
|
+
chips.push({
|
|
175
|
+
key: 'archived',
|
|
176
|
+
label: t[archived],
|
|
177
|
+
remove: () => void apply({ ...query, archived: 'EXCLUDE', offset: 0 }),
|
|
178
|
+
});
|
|
179
|
+
if (parts.advanced.expressions.length)
|
|
180
|
+
chips.push({
|
|
181
|
+
key: 'advanced',
|
|
182
|
+
label: fillText(t.chipConditions, { count: conditionCount(parts.advanced) }),
|
|
183
|
+
remove: () => void apply(withParts({ advanced: emptyGroup() })),
|
|
184
|
+
});
|
|
185
|
+
const drawerCount = (parts.advanced.expressions.length ? 1 : 0) +
|
|
186
|
+
(query.tags?.length ? 1 : 0) +
|
|
187
|
+
(archived !== 'EXCLUDE' ? 1 : 0);
|
|
188
|
+
const clearAll = () => {
|
|
189
|
+
const { filter: _f, tags: _t, tagMatch: _m, ...rest } = query;
|
|
190
|
+
setSearchText('');
|
|
191
|
+
void apply({ ...rest, archived: 'EXCLUDE', offset: 0 });
|
|
192
|
+
};
|
|
193
|
+
const searchPlaceholder = fillText(t.searchPlaceholder, {
|
|
194
|
+
fields: searchFields
|
|
195
|
+
.map((field) => labelOf(field, t))
|
|
196
|
+
.join(locale.startsWith('en') ? ', ' : '、'),
|
|
197
|
+
});
|
|
198
|
+
const columnLabel = (id) => labelOf(fieldById(id) ?? { id, label: id }, t);
|
|
199
|
+
const from = result.total ? result.offset + 1 : 0;
|
|
200
|
+
const to = Math.min(result.offset + result.items.length, result.total);
|
|
201
|
+
return (
|
|
202
|
+
// Table or cards follow the width of the list itself (sidebar open or not), not the viewport.
|
|
203
|
+
_jsxs("div", { className: "@container flex min-w-0 flex-col gap-4", "aria-busy": task.pending, children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx("h1", { className: "font-bold text-2xl tracking-tight", children: t.listTitle }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [showCreate && (_jsx(CreateRecordButton, { initialData: blankDraftData(formFields), machineCode: machineCode, basePath: basePath, labels: t, action: action })), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: touch, children: [_jsx(MoreHorizontal, { "aria-hidden": "true", "data-icon": "inline-start" }), t.moreMenu, _jsx(ChevronDown, { "aria-hidden": "true", "data-icon": "inline-end" })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [_jsx(DropdownMenuItem, { asChild: true, className: "max-md:min-h-11", children: _jsxs(LinkComponent, { href: `${basePath}/import`, children: [_jsx(FileUp, { "aria-hidden": "true" }), t.importTitle] }) }), _jsxs(DropdownMenuItem, { className: "max-md:min-h-11", disabled: task.pending, onSelect: () => exportAs('csv'), children: [_jsx(FileDown, { "aria-hidden": "true" }), t.exportCsv] }), _jsxs(DropdownMenuItem, { className: "max-md:min-h-11", disabled: task.pending, onSelect: () => exportAs('xlsx'), children: [_jsx(FileSpreadsheet, { "aria-hidden": "true" }), t.exportXlsx] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "max-md:min-h-11", children: _jsxs(LinkComponent, { href: `${basePath}/deleted`, children: [_jsx(Trash2, { "aria-hidden": "true" }), t.deletedLink] }) })] })] })] })] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-2 @3xl:flex-row @3xl:items-center", children: [searchFields.length > 0 && (
|
|
204
|
+
// biome-ignore lint/a11y/useSemanticElements: <search> is not yet a React DOM element.
|
|
205
|
+
_jsxs("form", { role: "search", className: "relative min-w-0 flex-1", onSubmit: (event) => {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
void apply(withParts({ search: searchText }));
|
|
208
|
+
}, children: [_jsx("label", { htmlFor: searchId, className: "sr-only", children: t.searchLabel }), _jsx(Search, { "aria-hidden": "true", className: "pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { id: searchId, type: "search", value: searchText, placeholder: searchPlaceholder, maxLength: 200, enterKeyHint: "search", onChange: (event) => setSearchText(event.target.value), className: "h-9 pl-9 max-md:h-11 sm:pr-20" }), _jsx(Button, { type: "submit", size: "sm", variant: "secondary", disabled: task.pending, className: "absolute top-1/2 right-1 -translate-y-1/2 max-sm:hidden max-md:h-9", children: t.searchSubmit })] })), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: cn('max-w-64 justify-between', touch), children: [_jsx("span", { className: "truncate", children: fillText(t.currentView, { name: currentView?.name ?? t.defaultView }) }), _jsx(ChevronDown, { "aria-hidden": "true", "data-icon": "inline-end" })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-64", children: [_jsx(DropdownMenuLabel, { children: t.savedViews }), _jsxs(DropdownMenuRadioGroup, { value: currentView?.id ?? (filtered ? '' : '__default'), onValueChange: (value) => {
|
|
209
|
+
if (value === '__default') {
|
|
210
|
+
setSearchText('');
|
|
211
|
+
void apply({ columns: query.columns, limit: query.limit, offset: 0 });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const view = views.find((candidate) => candidate.id === value);
|
|
215
|
+
if (view)
|
|
216
|
+
void apply({ ...view.query, offset: 0 });
|
|
217
|
+
}, children: [_jsx(DropdownMenuRadioItem, { value: "__default", className: "max-md:min-h-11", children: t.defaultView }), views.map((view) => (_jsx(DropdownMenuRadioItem, { value: view.id, className: "max-md:min-h-11", children: _jsx("span", { className: "min-w-0 truncate", children: view.name }) }, view.id)))] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "max-md:min-h-11", onSelect: () => setSaveOpen(true), children: [_jsx(Plus, { "aria-hidden": "true" }), t.saveCurrentView] })] })] }), _jsxs(Button, { variant: "outline", className: touch, onClick: () => setDrawer(true), "aria-haspopup": "dialog", children: [_jsx(SlidersHorizontal, { "aria-hidden": "true", "data-icon": "inline-start" }), t.filters, drawerCount > 0 && (_jsx("span", { className: "ml-1 rounded-full bg-primary px-1.5 text-primary-foreground text-xs tabular-nums leading-5", children: drawerCount }))] })] })] }), knownStates.size > 0 && (_jsxs("fieldset", { className: "min-w-0", children: [_jsx("legend", { className: "sr-only", children: t.stateFilterLabel }), _jsx("div", { className: "-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 [scrollbar-width:thin] sm:flex-wrap sm:overflow-visible", children: [...knownStates.values()].map((state) => {
|
|
218
|
+
const pressed = parts.states.includes(state.key);
|
|
219
|
+
return (_jsxs("button", { type: "button", "aria-pressed": pressed, disabled: task.pending, onClick: () => toggleState(state.key), className: cn('inline-flex h-8 shrink-0 items-center gap-2 rounded-full border px-3 text-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50 max-md:h-11 motion-safe:transition-colors', pressed
|
|
220
|
+
? 'border-foreground bg-foreground text-background'
|
|
221
|
+
: 'bg-background text-foreground hover:bg-muted'), children: [_jsx(ToneDot, { tone: state.tone }), state.label] }, state.key));
|
|
222
|
+
}) })] })), chips.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [chips.map((chip) => (_jsx(Chip, { removeLabel: fillText(t.removeFilter, { name: chip.label }), disabled: task.pending, onRemove: chip.remove, children: chip.label }, chip.key))), _jsx(Button, { variant: "link", size: "sm", className: touch, disabled: task.pending, onClick: clearAll, children: t.clearAll })] }))] }), _jsx("p", { role: "status", "aria-live": "polite", className: "sr-only", children: task.pending ? t.loading : announcement }), _jsx(ErrorMessage, { error: task.error, labels: t }), job && (_jsx(JobStatus, { initial: job, labels: t, action: action, downloadBase: `${basePath}/download` }, job.id)), _jsxs("section", { "aria-label": t.listTitle, className: "flex min-w-0 flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-center justify-between gap-2 text-muted-foreground text-sm", children: _jsx("span", { children: result.total
|
|
223
|
+
? fillText(t.resultRange, {
|
|
224
|
+
from: formatNumber(from, locale),
|
|
225
|
+
to: formatNumber(to, locale),
|
|
226
|
+
total: formatNumber(result.total, locale),
|
|
227
|
+
})
|
|
228
|
+
: fillText(t.resultCount, { count: 0 }) }) }), !task.pending && !result.items.length ? (filtered ? (_jsx(EmptyState, { icon: _jsx(SearchX, {}), title: t.noMatchTitle, description: t.noMatchHint, action: _jsx(Button, { variant: "outline", className: touch, onClick: clearAll, children: t.clearAll }) })) : (_jsx(EmptyState, { icon: _jsx(Inbox, {}), title: t.emptyTitle, description: t.emptyHint, action: showCreate ? (_jsx(CreateRecordButton, { initialData: blankDraftData(formFields), machineCode: machineCode, basePath: basePath, labels: t, action: action })) : undefined }))) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "hidden max-h-[calc(100dvh-14rem)] min-h-64 overflow-auto rounded-xl ring-1 ring-foreground/10 @3xl:block", children: _jsxs("table", { className: "w-full border-separate border-spacing-0 text-left text-sm", children: [_jsx("thead", { children: _jsxs("tr", { children: [['$number', ...visibleColumns].map((id) => {
|
|
229
|
+
const field = fieldById(id);
|
|
230
|
+
const direction = sortOf(id);
|
|
231
|
+
return (_jsx("th", { scope: "col", "aria-sort": direction === 'asc'
|
|
232
|
+
? 'ascending'
|
|
233
|
+
: direction === 'desc'
|
|
234
|
+
? 'descending'
|
|
235
|
+
: undefined, className: "sticky top-0 z-10 whitespace-nowrap border-b bg-muted px-3 py-2 font-medium", children: field?.sortable ? (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-sm outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50", disabled: task.pending, onClick: () => toggleSort(id), "aria-label": fillText(t.sortBy, { field: columnLabel(id) }), children: [columnLabel(id), direction === 'asc' ? (_jsx(ArrowUp, { "aria-hidden": "true", className: "size-3.5" })) : direction === 'desc' ? (_jsx(ArrowDown, { "aria-hidden": "true", className: "size-3.5" })) : (_jsx(ArrowUpDown, { "aria-hidden": "true", className: "size-3.5 text-muted-foreground" }))] })) : (columnLabel(id)) }, id));
|
|
236
|
+
}), tagsAvailable && (_jsx("th", { scope: "col", className: "sticky top-0 z-10 border-b bg-muted px-3 py-2 font-medium", children: t.tags }))] }) }), _jsx("tbody", { children: task.pending
|
|
237
|
+
? Array.from({ length: Math.min(8, Math.max(3, result.items.length)) }, (_, row) => (
|
|
238
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: placeholder rows.
|
|
239
|
+
_jsx("tr", { children: ['$number', ...visibleColumns, ...(tagsAvailable ? ['tags'] : [])].map((id) => (_jsx("td", { className: "border-b px-3 py-3", children: _jsx(Skeleton, { className: "h-4 w-24" }) }, id))) }, row)))
|
|
240
|
+
: result.items.map((item) => (_jsxs("tr", { className: "group cursor-pointer align-middle outline-ring hover:bg-muted/50 has-[a:focus-visible]:bg-muted/50 has-[a:focus-visible]:outline-2", onClick: (event) => {
|
|
241
|
+
if (event.target.closest('a,button'))
|
|
242
|
+
return;
|
|
243
|
+
event.currentTarget.querySelector('a')?.click();
|
|
244
|
+
}, children: [_jsx("td", { className: "whitespace-nowrap border-b px-3 py-3", children: _jsxs("span", { className: "flex flex-col items-start gap-1", children: [_jsx(LinkComponent, { href: href(item), className: cn('font-medium underline-offset-4 outline-none hover:underline focus-visible:underline', !item.number && 'text-muted-foreground'), children: title(item) }), item.archived && _jsx(Tag, { children: t.archived })] }) }), visibleColumns.map((id) => (_jsx("td", { className: cn('max-w-72 border-b px-3 py-3', (id === '$createdAt' || id === '$state') && 'whitespace-nowrap'), children: cell(item, id) }, id))), tagsAvailable && (_jsx("td", { className: "min-w-40 max-w-56 border-b px-3 py-3", children: tags(item) }))] }, item.id))) })] }) }), _jsx("ul", { className: "flex flex-col gap-2 @3xl:hidden", children: task.pending
|
|
245
|
+
? [0, 1, 2].map((row) => (_jsxs("li", { className: "flex flex-col gap-2 rounded-xl p-4 ring-1 ring-foreground/10", children: [_jsx(Skeleton, { className: "h-4 w-32" }), _jsx(Skeleton, { className: "h-4 w-48" })] }, row)))
|
|
246
|
+
: result.items.map((item) => {
|
|
247
|
+
const state = stateOf(item);
|
|
248
|
+
const secondary = visibleColumns.filter((id) => id !== '$state').slice(0, 3);
|
|
249
|
+
return (_jsx("li", { children: _jsxs(LinkComponent, { href: href(item), className: "flex min-w-0 flex-col gap-2 rounded-xl bg-card p-4 text-card-foreground outline-none ring-1 ring-foreground/10 hover:bg-muted/40 focus-visible:ring-3 focus-visible:ring-ring/50", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-start justify-between gap-2", children: [_jsx("span", { className: cn('min-w-0 font-medium [overflow-wrap:anywhere]', !item.number && 'text-muted-foreground'), children: title(item) }), _jsx(StateBadge, { label: state.label, tone: state.tone, className: "shrink-0" })] }), secondary.length > 0 && (_jsx("span", { className: "flex min-w-0 flex-col gap-1 text-sm", children: secondary.map((id) => (_jsxs("span", { className: "flex min-w-0 gap-2", children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: columnLabel(id) }), _jsx("span", { className: "min-w-0", children: cell(item, id) })] }, id))) })), (!!item.archived || !!item.tags?.length) && (_jsxs("span", { className: "flex flex-wrap items-center gap-1", children: [item.archived && _jsx(Tag, { children: t.archived }), tags(item)] }))] }) }, item.id));
|
|
250
|
+
}) })] })), result.total > 0 && (_jsxs("nav", { "aria-label": t.pagination, className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: t.limit }), _jsx(NativeSelect, { "aria-label": t.limit, value: String(limit), disabled: task.pending, onChange: (event) => void apply({ ...query, limit: Number(event.target.value), offset: 0 }), children: [...new Set([...LIMITS, limit])]
|
|
251
|
+
.sort((a, b) => a - b)
|
|
252
|
+
.map((value) => (_jsx("option", { value: value, children: value }, value))) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", className: touch, disabled: task.pending || page <= 1, onClick: () => void apply({ ...query, offset: Math.max(0, result.offset - limit) }), children: [_jsx(ChevronLeft, { "aria-hidden": "true", "data-icon": "inline-start" }), t.previous] }), _jsx("span", { className: "text-sm tabular-nums", children: fillText(t.pageOf, { page, pages }) }), _jsxs(Button, { variant: "outline", className: touch, disabled: task.pending || page >= pages, onClick: () => void apply({ ...query, offset: result.offset + limit }), children: [t.next, _jsx(ChevronRight, { "aria-hidden": "true", "data-icon": "inline-end" })] })] })] }))] }), _jsx(FilterDrawer, { open: drawer, onOpenChange: setDrawer, fields: fields, query: query, parts: parts, columnsDefault: columnsDefault, columns: columns, tagsAvailable: tagsAvailable, views: views, labels: t, action: action, timeZone: timeZone, pending: task.pending, person: person, onApply: (next) => {
|
|
253
|
+
setDrawer(false);
|
|
254
|
+
void apply(next);
|
|
255
|
+
}, onShare: (view, visibility, roles) => void task.run(async () => {
|
|
256
|
+
await call(action, { kind: 'shareView', id: view.id, visibility, roles });
|
|
257
|
+
setViews(views.map((v) => (v.id === view.id ? { ...v, visibility, roles } : v)));
|
|
258
|
+
setAnnouncement(t.sharingSaved);
|
|
259
|
+
}), searchFields: searchFields }), _jsx(SaveViewDialog, { open: saveOpen, onOpenChange: setSaveOpen, labels: t, pending: task.pending, onSave: (name) => void task.run(async () => {
|
|
260
|
+
const view = await call(action, {
|
|
261
|
+
kind: 'saveView',
|
|
262
|
+
machineCode,
|
|
263
|
+
name,
|
|
264
|
+
query,
|
|
265
|
+
});
|
|
266
|
+
setViews([...views, view]);
|
|
267
|
+
setSaveOpen(false);
|
|
268
|
+
setAnnouncement(t.viewSaved);
|
|
269
|
+
}) })] }));
|
|
270
|
+
}
|
|
271
|
+
/** Starts a draft and opens it; the backend decides whether the caller may create one. */
|
|
272
|
+
export function CreateRecordButton({ machineCode, basePath, labels: t, action, variant = 'default', initialData, }) {
|
|
273
|
+
const task = useTask();
|
|
274
|
+
return (_jsxs("span", { className: "inline-flex flex-col items-start gap-1", children: [_jsxs(Button, { variant: variant, className: touch, disabled: task.pending, onClick: () => void task.run(async () => {
|
|
275
|
+
const created = await call(action, {
|
|
276
|
+
kind: 'create',
|
|
277
|
+
machineCode,
|
|
278
|
+
...(initialData && Object.keys(initialData).length ? { data: initialData } : {}),
|
|
279
|
+
});
|
|
280
|
+
window.location.assign(`${basePath}/${encodeURIComponent(created.id)}`);
|
|
281
|
+
}), children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), task.pending ? t.creating : t.createRecord] }), _jsx(ErrorMessage, { error: task.error, labels: t })] }));
|
|
282
|
+
}
|
|
283
|
+
function SaveViewDialog({ open, onOpenChange, labels: t, pending, onSave, }) {
|
|
284
|
+
const [name, setName] = useState('');
|
|
285
|
+
const id = useId();
|
|
286
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { closeLabel: t.dialogClose, className: "sm:max-w-md", children: _jsxs("form", { className: "grid gap-4", onSubmit: (event) => {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
if (name.trim())
|
|
289
|
+
onSave(name.trim());
|
|
290
|
+
}, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t.saveView }), _jsx(DialogDescription, { children: t.groupViewsHint })] }), _jsxs("label", { htmlFor: id, className: "grid gap-1.5 text-sm", children: [_jsx("span", { className: "font-medium", children: t.viewName }), _jsx(Input, { id: id, required: true, maxLength: 200, value: name, onChange: (e) => setName(e.target.value), className: "h-9 max-md:h-11" })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", className: touch, onClick: () => onOpenChange(false), children: t.cancel }), _jsx(Button, { type: "submit", className: touch, disabled: pending || !name.trim(), children: t.saveView })] })] }) }) }));
|
|
291
|
+
}
|
|
292
|
+
function DrawerGroup({ title, hint, children, }) {
|
|
293
|
+
const id = useId();
|
|
294
|
+
return (_jsxs("section", { "aria-labelledby": id, className: "flex min-w-0 flex-col gap-3 border-b px-4 py-4 last:border-b-0 sm:px-5", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("h3", { id: id, className: "font-medium text-sm", children: title }), hint && _jsx("p", { className: "text-muted-foreground text-xs", children: hint })] }), children] }));
|
|
295
|
+
}
|
|
296
|
+
function FilterDrawer({ open, onOpenChange, fields, query, parts, columnsDefault, columns, tagsAvailable, views, labels: t, action, timeZone, pending, person, searchFields, onApply, onShare, }) {
|
|
297
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsx(SheetContent, { side: "right", showCloseButton: false, className: "gap-0 p-0 data-[side=right]:w-full data-[side=right]:sm:max-w-lg", children: open && (_jsx(DrawerBody, { fields,
|
|
298
|
+
query,
|
|
299
|
+
parts,
|
|
300
|
+
columnsDefault,
|
|
301
|
+
columns,
|
|
302
|
+
tagsAvailable,
|
|
303
|
+
views,
|
|
304
|
+
labels: t,
|
|
305
|
+
action,
|
|
306
|
+
timeZone,
|
|
307
|
+
pending,
|
|
308
|
+
person,
|
|
309
|
+
searchFields,
|
|
310
|
+
onApply,
|
|
311
|
+
onShare, onClose: () => onOpenChange(false) }, JSON.stringify(query))) }) }));
|
|
312
|
+
}
|
|
313
|
+
function DrawerBody({ fields, query, parts, columnsDefault, columns: initialColumns, tagsAvailable, views, labels: t, action, timeZone, pending, searchFields, onApply, onShare, onClose, }) {
|
|
314
|
+
const [archived, setArchived] = useState(query.archived ?? 'EXCLUDE');
|
|
315
|
+
const [tags, setTags] = useState(query.tags ?? []);
|
|
316
|
+
const [tagMatch, setTagMatch] = useState(query.tagMatch ?? 'ALL');
|
|
317
|
+
const [advanced, setAdvanced] = useState(parts.advanced);
|
|
318
|
+
const [sort, setSort] = useState(query.sort ?? []);
|
|
319
|
+
const [columns, setColumns] = useState(initialColumns);
|
|
320
|
+
const [error, setError] = useState();
|
|
321
|
+
const dateFields = fields.filter((f) => f.filterable && ['date', 'datetime'].includes(f.type));
|
|
322
|
+
const [rangeField, setRangeField] = useState(dateFields[0]?.id ?? '');
|
|
323
|
+
const [from, setFrom] = useState('');
|
|
324
|
+
const [to, setTo] = useState('');
|
|
325
|
+
const rangeId = useId();
|
|
326
|
+
const sortable = fields.filter((f) => f.sortable);
|
|
327
|
+
const filterable = fields.filter((f) => f.filterable);
|
|
328
|
+
const rangeType = fields.find((f) => f.id === rangeField)?.type === 'datetime' ? 'datetime-local' : 'date';
|
|
329
|
+
const toggleable = fields.filter((field) => field.id !== '$number');
|
|
330
|
+
const submit = () => {
|
|
331
|
+
if (!columns.length) {
|
|
332
|
+
setError({ key: 'invalidInput' });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const group = advanced;
|
|
337
|
+
prepareFilter(group, fields);
|
|
338
|
+
const filter = composeFilter({ ...parts, advanced: group }, searchFields);
|
|
339
|
+
const { filter: _f, tags: _t, tagMatch: _m, ...rest } = query;
|
|
340
|
+
const sameColumns = columns.length === columnsDefault.filter((id) => id !== '$number').length &&
|
|
341
|
+
columns.every((id) => columnsDefault.includes(id));
|
|
342
|
+
onApply({
|
|
343
|
+
...rest,
|
|
344
|
+
...(filter ? { filter } : {}),
|
|
345
|
+
archived,
|
|
346
|
+
sort,
|
|
347
|
+
columns: sameColumns ? undefined : ['$number', ...columns],
|
|
348
|
+
offset: 0,
|
|
349
|
+
...(tagsAvailable && tags.length ? { tags, tagMatch } : {}),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
setError({ key: 'invalidInput' });
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
return (_jsxs("form", { className: "flex h-full min-h-0 flex-col", onSubmit: (event) => {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
submit();
|
|
359
|
+
}, children: [_jsxs(SheetHeader, { className: "flex-row items-start justify-between gap-2 border-b px-4 py-4 sm:px-5", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx(SheetTitle, { children: t.filtersTitle }), _jsx(SheetDescription, { children: t.filtersDescription })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", className: touch, "aria-label": t.filterDrawerClose, onClick: onClose, children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [_jsx(DrawerGroup, { title: t.groupArchive, hint: t.groupArchiveHint, children: _jsx("div", { role: "radiogroup", "aria-label": t.groupArchive, className: "grid gap-1 sm:grid-cols-3", children: ['EXCLUDE', 'INCLUDE', 'ONLY'].map((value) => (_jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-sm has-[:checked]:border-foreground has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50 max-md:min-h-11", children: [_jsx("input", { type: "radio", name: "archived", value: value, checked: archived === value, onChange: () => setArchived(value), className: "size-4 accent-primary" }), t[value]] }, value))) }) }), tagsAvailable && (_jsxs(DrawerGroup, { title: t.groupTags, hint: t.groupTagsHint, children: [_jsx(TagPicker, { hideLabel: true, label: t.tagFilter, labels: t, action: action, disabled: pending || tags.length >= 10, onPick: (added) => setTags([...new Set([...tags, ...added])].slice(0, 10)) }), tags.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: tags.map((tag) => (_jsx(Chip, { removeLabel: fillText(t.removeFilter, { name: tag }), onRemove: () => setTags(tags.filter((v) => v !== tag)), children: tag }, tag))) })), tags.length > 1 && (_jsxs(NativeSelect, { "aria-label": t.tagFilter, value: tagMatch, onChange: (e) => setTagMatch(e.target.value), children: [_jsx("option", { value: "ALL", children: t.tagAll }), _jsx("option", { value: "ANY", children: t.tagAny })] }))] })), _jsx(DrawerGroup, { title: t.groupConditions, hint: t.groupConditionsHint, children: _jsx(FilterEditor, { condition: advanced, fields: filterable, labels: t, timeZone: timeZone, onChange: setAdvanced }) }), dateFields.length > 0 && (_jsxs(DrawerGroup, { title: t.groupDates, hint: t.groupDatesHint, children: [_jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1 text-sm sm:col-span-2", children: [_jsx("label", { htmlFor: `${rangeId}-field`, className: "text-muted-foreground", children: t.field }), _jsx(NativeSelect, { id: `${rangeId}-field`, value: rangeField, onChange: (e) => {
|
|
360
|
+
setRangeField(e.target.value);
|
|
361
|
+
setFrom('');
|
|
362
|
+
setTo('');
|
|
363
|
+
}, children: dateFields.map((f) => (_jsx("option", { value: f.id, children: labelOf(f, t) }, f.id))) })] }), _jsxs("label", { htmlFor: `${rangeId}-from`, className: "grid gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t.rangeFrom }), _jsx(Input, { id: `${rangeId}-from`, type: rangeType, value: from, className: "h-9 max-md:h-11", onChange: (e) => setFrom(e.target.value) })] }), _jsxs("label", { htmlFor: `${rangeId}-to`, className: "grid gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t.rangeTo }), _jsx(Input, { id: `${rangeId}-to`, type: rangeType, value: to, min: from, className: "h-9 max-md:h-11", onChange: (e) => setTo(e.target.value) })] })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: cn('self-start', touch), disabled: !from || !to || from > to, onClick: () => {
|
|
364
|
+
const field = fields.find((f) => f.id === rangeField);
|
|
365
|
+
if (!field)
|
|
366
|
+
return;
|
|
367
|
+
const zoned = (value) => rangeType === 'datetime-local'
|
|
368
|
+
? (fromZonedInput(value, timeZone) ?? value)
|
|
369
|
+
: value;
|
|
370
|
+
const range = dateRange(field, zoned(from), zoned(to));
|
|
371
|
+
const group = advanced;
|
|
372
|
+
setAdvanced({
|
|
373
|
+
...group,
|
|
374
|
+
operator: group.expressions.length ? group.operator : 'AND',
|
|
375
|
+
expressions: [...group.expressions, range],
|
|
376
|
+
});
|
|
377
|
+
setFrom('');
|
|
378
|
+
setTo('');
|
|
379
|
+
}, children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), t.addRange] })] })), _jsxs(DrawerGroup, { title: t.groupSort, hint: t.groupSortHint, children: [sort.map((entry, index) => (_jsxs("div", { className: "grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,10rem)_auto]", children: [_jsx(NativeSelect, { "aria-label": `${t.sort}: ${t.field}`, value: entry.fieldId, onChange: (e) => setSort(sort.map((s, i) => (i === index ? { ...s, fieldId: e.target.value } : s))), children: sortable.map((f) => (_jsx("option", { value: f.id, disabled: f.id !== entry.fieldId && sort.some((s) => s.fieldId === f.id), children: labelOf(f, t) }, f.id))) }), _jsxs(NativeSelect, { "aria-label": t.sort, value: entry.direction, onChange: (e) => setSort(sort.map((s, i) => i === index ? { ...s, direction: e.target.value } : s)), children: [_jsx("option", { value: "asc", children: t.asc }), _jsx("option", { value: "desc", children: t.desc })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", className: touch, "aria-label": t.remove, onClick: () => setSort(sort.filter((_, i) => i !== index)), children: _jsx(Trash2, { "aria-hidden": "true" }) })] }, entry.fieldId))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: cn('self-start', touch), disabled: sort.length >= 8 || !sortable.some((f) => !sort.some((s) => s.fieldId === f.id)), onClick: () => {
|
|
380
|
+
const f = sortable.find((f) => !sort.some((s) => s.fieldId === f.id));
|
|
381
|
+
if (f)
|
|
382
|
+
setSort([...sort, { fieldId: f.id, direction: 'asc' }]);
|
|
383
|
+
}, children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), t.addSort] })] }), _jsxs(DrawerGroup, { title: t.groupColumns, hint: t.groupColumnsHint, children: [_jsx("div", { className: "grid gap-1 sm:grid-cols-2", children: toggleable.map((f) => (_jsxs("span", { className: "flex items-center gap-2 rounded-md px-1 py-1.5 text-sm max-md:min-h-11", children: [_jsx(Checkbox, { id: `${rangeId}-column-${f.id}`, checked: columns.includes(f.id), onCheckedChange: (checked) => setColumns(checked === true ? [...columns, f.id] : columns.filter((c) => c !== f.id)) }), _jsx("label", { htmlFor: `${rangeId}-column-${f.id}`, className: "cursor-pointer", children: labelOf(f, t) })] }, f.id))) }), !columns.length && _jsx("p", { className: "text-destructive text-sm", children: t.columnsRequired })] }), views.some((view) => view.canShare) && (_jsx(DrawerGroup, { title: t.groupViews, hint: t.groupViewsHint, children: _jsx("ul", { className: "flex flex-col gap-3", children: views
|
|
384
|
+
.filter((view) => view.canShare)
|
|
385
|
+
.map((view) => (_jsxs("li", { className: "flex flex-col gap-2 rounded-lg border p-3", children: [_jsx("span", { className: "font-medium text-sm [overflow-wrap:anywhere]", children: view.name }), _jsx(ShareForm, { view: view, labels: t, disabled: pending, onShare: (visibility, roles) => onShare(view, visibility, roles) })] }, view.id))) }) }))] }), _jsxs("div", { className: "flex flex-col gap-2 border-t bg-muted/30 px-4 py-3 sm:px-5", children: [_jsx(ErrorMessage, { error: error, labels: t }), _jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", className: touch, onClick: () => {
|
|
386
|
+
setArchived('EXCLUDE');
|
|
387
|
+
setTags([]);
|
|
388
|
+
setAdvanced(emptyGroup());
|
|
389
|
+
setSort([]);
|
|
390
|
+
setColumns(columnsDefault.filter((id) => id !== '$number'));
|
|
391
|
+
}, children: t.resetFilters }), _jsx(Button, { type: "submit", className: touch, disabled: pending || !columns.length, children: t.apply })] })] })] }));
|
|
392
|
+
}
|
|
393
|
+
export function ShareForm({ view, labels: t, disabled, onShare, }) {
|
|
394
|
+
const [visibility, setVisibility] = useState(view.visibility);
|
|
395
|
+
const [roles, setRoles] = useState(view.roles.join(', '));
|
|
396
|
+
const ref = useRef(null);
|
|
397
|
+
return (_jsxs("div", { ref: ref, className: "flex flex-wrap items-end gap-2", children: [_jsx(NativeSelect, { "aria-label": t.share, disabled: disabled, value: visibility, onChange: (e) => setVisibility(e.target.value), className: "w-full sm:w-auto", children: ['PRIVATE', 'ALL', 'ROLES'].map((value) => (_jsx("option", { value: value, children: t[value] }, value))) }), visibility === 'ROLES' && (_jsx(Input, { "aria-label": t.roles, placeholder: t.roles, value: roles, onChange: (e) => setRoles(e.target.value), className: "h-9 min-w-0 flex-1 max-md:h-11" })), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: touch, disabled: disabled || (visibility === 'ROLES' && !roles.trim()), onClick: () => onShare(visibility, roles
|
|
398
|
+
.split(',')
|
|
399
|
+
.map((r) => r.trim())
|
|
400
|
+
.filter(Boolean)), children: t.share })] }));
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=list-client.js.map
|