@haneinfo/frontend-instances 0.5.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.
Files changed (91) hide show
  1. package/README.md +117 -5
  2. package/agent-guide.md +12 -0
  3. package/dist/attachments-client.d.ts +7 -2
  4. package/dist/attachments-client.d.ts.map +1 -1
  5. package/dist/attachments-client.js +47 -11
  6. package/dist/attachments-client.js.map +1 -1
  7. package/dist/client.d.ts +11 -68
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +11 -443
  10. package/dist/client.js.map +1 -1
  11. package/dist/contracts.d.ts +202 -4
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js +5 -98
  14. package/dist/contracts.js.map +1 -1
  15. package/dist/dashboard.d.ts +41 -0
  16. package/dist/dashboard.d.ts.map +1 -0
  17. package/dist/dashboard.js +72 -0
  18. package/dist/dashboard.js.map +1 -0
  19. package/dist/detail-client.d.ts +55 -0
  20. package/dist/detail-client.d.ts.map +1 -0
  21. package/dist/detail-client.js +173 -0
  22. package/dist/detail-client.js.map +1 -0
  23. package/dist/documents-client.d.ts +2 -1
  24. package/dist/documents-client.d.ts.map +1 -1
  25. package/dist/documents-client.js +13 -9
  26. package/dist/documents-client.js.map +1 -1
  27. package/dist/draft-client.d.ts +30 -0
  28. package/dist/draft-client.d.ts.map +1 -0
  29. package/dist/draft-client.js +236 -0
  30. package/dist/draft-client.js.map +1 -0
  31. package/dist/draft-session.d.ts +48 -0
  32. package/dist/draft-session.d.ts.map +1 -0
  33. package/dist/draft-session.js +267 -0
  34. package/dist/draft-session.js.map +1 -0
  35. package/dist/filter-editor.d.ts +18 -0
  36. package/dist/filter-editor.d.ts.map +1 -0
  37. package/dist/filter-editor.js +145 -0
  38. package/dist/filter-editor.js.map +1 -0
  39. package/dist/format.d.ts +152 -0
  40. package/dist/format.d.ts.map +1 -0
  41. package/dist/format.js +402 -0
  42. package/dist/format.js.map +1 -0
  43. package/dist/import-client.d.ts +31 -0
  44. package/dist/import-client.d.ts.map +1 -0
  45. package/dist/import-client.js +179 -0
  46. package/dist/import-client.js.map +1 -0
  47. package/dist/instance-management-client.d.ts +64 -0
  48. package/dist/instance-management-client.d.ts.map +1 -0
  49. package/dist/instance-management-client.js +216 -0
  50. package/dist/instance-management-client.js.map +1 -0
  51. package/dist/instance-management-messages.d.ts +51 -0
  52. package/dist/instance-management-messages.d.ts.map +1 -0
  53. package/dist/instance-management-messages.js +60 -0
  54. package/dist/instance-management-messages.js.map +1 -0
  55. package/dist/labels.d.ts +10 -0
  56. package/dist/labels.d.ts.map +1 -0
  57. package/dist/labels.js +40 -0
  58. package/dist/labels.js.map +1 -0
  59. package/dist/list-client.d.ts +39 -0
  60. package/dist/list-client.d.ts.map +1 -0
  61. package/dist/list-client.js +402 -0
  62. package/dist/list-client.js.map +1 -0
  63. package/dist/list-query.d.ts +41 -0
  64. package/dist/list-query.d.ts.map +1 -0
  65. package/dist/list-query.js +171 -0
  66. package/dist/list-query.js.map +1 -0
  67. package/dist/management-client.d.ts +6 -2
  68. package/dist/management-client.d.ts.map +1 -1
  69. package/dist/management-client.js +113 -84
  70. package/dist/management-client.js.map +1 -1
  71. package/dist/messages.d.ts +17 -4
  72. package/dist/messages.d.ts.map +1 -1
  73. package/dist/messages.js +59 -4
  74. package/dist/messages.js.map +1 -1
  75. package/dist/server.d.ts +32 -1
  76. package/dist/server.d.ts.map +1 -1
  77. package/dist/server.js +308 -44
  78. package/dist/server.js.map +1 -1
  79. package/dist/timeline.d.ts +24 -0
  80. package/dist/timeline.d.ts.map +1 -0
  81. package/dist/timeline.js +189 -0
  82. package/dist/timeline.js.map +1 -0
  83. package/dist/ui-messages.d.ts +284 -0
  84. package/dist/ui-messages.d.ts.map +1 -0
  85. package/dist/ui-messages.js +372 -0
  86. package/dist/ui-messages.js.map +1 -0
  87. package/dist/ui.d.ts +100 -0
  88. package/dist/ui.d.ts.map +1 -0
  89. package/dist/ui.js +169 -0
  90. package/dist/ui.js.map +1 -0
  91. package/package.json +4 -2
@@ -0,0 +1,60 @@
1
+ export const instanceManagementMessages = {
2
+ tags: ['Tags', '標籤'],
3
+ tagInput: ['Add tags (comma or Enter separated)', '新增標籤(逗號或 Enter 分隔)'],
4
+ addTags: ['Add tags', '新增標籤'],
5
+ removeTag: ['Remove tag', '移除標籤'],
6
+ tagFilter: ['Filter by tags', '標籤篩選'],
7
+ tagAll: ['Match all', '全部符合'],
8
+ tagAny: ['Match any', '任一符合'],
9
+ noSuggestions: ['No suggestions', '沒有建議標籤'],
10
+ archived: ['Archived', '已封存'],
11
+ archiveFilter: ['Archive filter', '封存選項'],
12
+ EXCLUDE: ['Exclude archived', '不含封存'],
13
+ INCLUDE: ['Include archived', '含封存'],
14
+ ONLY: ['Only archived', '只看封存'],
15
+ archive: ['Archive', '封存'],
16
+ unarchive: ['Unarchive', '還原封存'],
17
+ deleteInstance: ['Delete draft', '刪除草稿'],
18
+ deleteHint: [
19
+ 'Deleted drafts are kept for 30 days and can be restored from Deleted records.',
20
+ '刪除後保存 30 天,期間可在已刪除頁還原。',
21
+ ],
22
+ confirmDelete: ['Confirm deletion', '確認刪除'],
23
+ cancel: ['Cancel', '取消'],
24
+ deletedTitle: ['Deleted records', '已刪除'],
25
+ deletedEmpty: ['No deleted drafts are available to restore.', '沒有可還原的已刪除草稿。'],
26
+ deletedBy: ['Deleted by', '刪除者'],
27
+ deletedAt: ['Deleted at', '刪除時間'],
28
+ retentionDays: ['Remaining retention days', '剩餘保存天數'],
29
+ restore: ['Restore', '還原'],
30
+ draftTitle: ['Draft editor', '草稿編輯器'],
31
+ draftHint: [
32
+ 'Changes are saved automatically. This editor only saves draft data.',
33
+ '變更會自動儲存,此編輯器只儲存草稿資料。',
34
+ ],
35
+ requiredLater: ['Required on submission', '送出時必填'],
36
+ readOnly: ['Read only', '唯讀'],
37
+ saving: ['Saving…', '儲存中…'],
38
+ savedAt: ['Saved at', '已儲存於'],
39
+ unsaved: ['You have unsaved changes. Leave this page?', '還有未儲存的內容,確定要離開此頁?'],
40
+ saveFailed: ['Changes are kept locally. Retry saving.', '內容仍保留在此頁,請重試儲存。'],
41
+ rateRetry: [
42
+ 'Saving is rate limited. Retrying after this many seconds:',
43
+ '儲存次數過於頻繁,將於下列秒數後重試:',
44
+ ],
45
+ conflictTitle: ['Resolve field conflicts', '處理欄位衝突'],
46
+ currentValue: ['Current value', '目前的值'],
47
+ mineValue: ['Your value', '你的值'],
48
+ keepMine: ['Keep mine', '保留我的'],
49
+ keepCurrent: ['Use current', '採用目前的'],
50
+ allMine: ['Keep all mine', '全部保留我的'],
51
+ allCurrent: ['Use all current', '全部採用目前的'],
52
+ resolveConflicts: ['Apply choices', '套用選擇'],
53
+ valueMissing: ['No value', '未填值'],
54
+ valueTruncated: ['Value omitted because of its size', '值過大,未在衝突回應中提供'],
55
+ changedByOthers: ['Updated by another editor', '已更新其他人的變更'],
56
+ yes: ['Yes', '是'],
57
+ no: ['No', '否'],
58
+ };
59
+ export const instanceManagementKeys = Object.keys(instanceManagementMessages);
60
+ //# sourceMappingURL=instance-management-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-management-messages.js","sourceRoot":"","sources":["../src/instance-management-messages.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,qCAAqC,EAAE,oBAAoB,CAAC;IACvE,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,CAAC,gBAAgB,EAAE,MAAM,CAAC;IACrC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,CAAC,gBAAgB,EAAE,QAAQ,CAAC;IAC3C,QAAQ,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC;IAC7B,aAAa,EAAE,CAAC,gBAAgB,EAAE,MAAM,CAAC;IACzC,OAAO,EAAE,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,kBAAkB,EAAE,KAAK,CAAC;IACpC,IAAI,EAAE,CAAC,eAAe,EAAE,MAAM,CAAC;IAC/B,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC;IAC1B,SAAS,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,CAAC,cAAc,EAAE,MAAM,CAAC;IACxC,UAAU,EAAE;QACV,+EAA+E;QAC/E,wBAAwB;KACzB;IACD,aAAa,EAAE,CAAC,kBAAkB,EAAE,MAAM,CAAC;IAC3C,MAAM,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,YAAY,EAAE,CAAC,iBAAiB,EAAE,KAAK,CAAC;IACxC,YAAY,EAAE,CAAC,6CAA6C,EAAE,cAAc,CAAC;IAC7E,SAAS,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC;IAChC,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,CAAC,0BAA0B,EAAE,QAAQ,CAAC;IACrD,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC;IAC1B,UAAU,EAAE,CAAC,cAAc,EAAE,OAAO,CAAC;IACrC,SAAS,EAAE;QACT,qEAAqE;QACrE,sBAAsB;KACvB;IACD,aAAa,EAAE,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAClD,QAAQ,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC;IAC7B,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,OAAO,EAAE,CAAC,4CAA4C,EAAE,mBAAmB,CAAC;IAC5E,UAAU,EAAE,CAAC,yCAAyC,EAAE,iBAAiB,CAAC;IAC1E,SAAS,EAAE;QACT,2DAA2D;QAC3D,qBAAqB;KACtB;IACD,aAAa,EAAE,CAAC,yBAAyB,EAAE,QAAQ,CAAC;IACpD,YAAY,EAAE,CAAC,eAAe,EAAE,MAAM,CAAC;IACvC,SAAS,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC;IAChC,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC;IAC/B,WAAW,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC;IACrC,OAAO,EAAE,CAAC,eAAe,EAAE,QAAQ,CAAC;IACpC,UAAU,EAAE,CAAC,iBAAiB,EAAE,SAAS,CAAC;IAC1C,gBAAgB,EAAE,CAAC,eAAe,EAAE,MAAM,CAAC;IAC3C,YAAY,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC;IACjC,cAAc,EAAE,CAAC,mCAAmC,EAAE,eAAe,CAAC;IACtE,eAAe,EAAE,CAAC,2BAA2B,EAAE,WAAW,CAAC;IAC3D,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC;IACjB,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC;CACP,CAAC;AACX,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,IAAI,CAC/C,0BAA0B,CACoB,CAAC"}
@@ -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