@svadmin/ui 0.73.0 → 0.75.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 +53 -28
- package/dist/app.css +3733 -4119
- package/dist/app.theme.css +3733 -4119
- package/dist/component-registry.d.ts +14977 -0
- package/dist/component-registry.js +216 -0
- package/dist/components/AboutSettings.svelte +7 -1
- package/dist/components/AboutSettings.svelte.d.ts +4 -1
- package/dist/components/ActivityFeed.svelte +9 -3
- package/dist/components/ActivityFeed.svelte.d.ts +3 -0
- package/dist/components/AdminApp.svelte +24 -15
- package/dist/components/AdminApp.svelte.d.ts +6 -0
- package/dist/components/ApiSettings.svelte +41 -5
- package/dist/components/ApprovalCenter.svelte +283 -0
- package/dist/components/ApprovalCenter.svelte.d.ts +10 -0
- package/dist/components/AuditLogViewer.svelte +3 -1
- package/dist/components/AutoForm.svelte +33 -3
- package/dist/components/AutoForm.svelte.d.ts +1 -0
- package/dist/components/AutoTable.svelte +610 -47
- package/dist/components/AutoTable.svelte.d.ts +17 -1
- package/dist/components/BoundRecordDetailDrawer.svelte +9 -0
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
- package/dist/components/Breadcrumbs.svelte +11 -1
- package/dist/components/CancelTaskButton.svelte +12 -42
- package/dist/components/ColorPicker.svelte +73 -0
- package/dist/components/ColorPicker.svelte.d.ts +15 -0
- package/dist/components/ComboboxField.svelte +114 -27
- package/dist/components/ComboboxField.svelte.d.ts +1 -0
- package/dist/components/DashboardView.svelte +99 -0
- package/dist/components/DashboardView.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +10 -0
- package/dist/components/DatePicker.svelte.d.ts +6 -0
- package/dist/components/DateRangeInput.svelte +85 -5
- package/dist/components/DateRangeInput.svelte.d.ts +10 -0
- package/dist/components/DateRangePicker.svelte +10 -0
- package/dist/components/DateRangePicker.svelte.d.ts +6 -0
- package/dist/components/DateTimeInput.svelte +85 -16
- package/dist/components/DateTimeInput.svelte.d.ts +19 -11
- package/dist/components/DateTimePicker.svelte +10 -0
- package/dist/components/DateTimePicker.svelte.d.ts +6 -0
- package/dist/components/DevTools.svelte +34 -26
- package/dist/components/DevframePanel.svelte +123 -0
- package/dist/components/DevframePanel.svelte.d.ts +3 -0
- package/dist/components/ErrorSummary.svelte +52 -0
- package/dist/components/ErrorSummary.svelte.d.ts +14 -0
- package/dist/components/FieldRenderer.svelte +82 -81
- package/dist/components/FileUpload.svelte +214 -52
- package/dist/components/FileUpload.svelte.d.ts +11 -0
- package/dist/components/FilterBuilder.svelte +88 -66
- package/dist/components/FilterCollectionInput.svelte +80 -0
- package/dist/components/FilterCollectionInput.svelte.d.ts +10 -0
- package/dist/components/GanttChart.svelte +66 -14
- package/dist/components/GanttChart.svelte.d.ts +6 -0
- package/dist/components/Header.svelte +18 -2
- package/dist/components/ImportWizard.svelte +339 -17
- package/dist/components/ImportWizard.svelte.d.ts +11 -0
- package/dist/components/JsonSchemaForm.svelte +120 -52
- package/dist/components/JsonSchemaForm.svelte.d.ts +7 -1
- package/dist/components/KanbanBoard.svelte +195 -40
- package/dist/components/KanbanBoard.svelte.d.ts +9 -2
- package/dist/components/Layout.svelte +4 -2
- package/dist/components/LazyAutoForm.svelte +28 -0
- package/dist/components/LazyAutoForm.svelte.d.ts +8 -0
- package/dist/components/LazyAutoTable.svelte +28 -0
- package/dist/components/LazyAutoTable.svelte.d.ts +8 -0
- package/dist/components/LazyPage.svelte +3 -3
- package/dist/components/LazyPage.svelte.d.ts +2 -2
- package/dist/components/LazySettingsPage.svelte +28 -0
- package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
- package/dist/components/LazyShowPage.svelte +28 -0
- package/dist/components/LazyShowPage.svelte.d.ts +8 -0
- package/dist/components/ListPage.svelte +5 -1
- package/dist/components/ListPage.svelte.d.ts +3 -0
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/MultiTabKeepAlive.svelte +11 -6
- package/dist/components/NotificationsSettings.svelte +69 -2
- package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
- package/dist/components/PasswordInput.svelte +3 -3
- package/dist/components/PasswordInput.svelte.d.ts +2 -1
- package/dist/components/PermissionMatrix.svelte +20 -1
- package/dist/components/PivotExportButton.svelte +157 -0
- package/dist/components/PivotExportButton.svelte.d.ts +16 -0
- package/dist/components/PivotTable.svelte +229 -74
- package/dist/components/PivotTable.svelte.d.ts +19 -2
- package/dist/components/RadioGroup.svelte +121 -0
- package/dist/components/RadioGroup.svelte.d.ts +43 -0
- package/dist/components/RangeSlider.svelte +127 -0
- package/dist/components/RangeSlider.svelte.d.ts +22 -0
- package/dist/components/Rate.svelte +121 -0
- package/dist/components/Rate.svelte.d.ts +16 -0
- package/dist/components/RecordDetailDrawer.svelte +4 -2
- package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
- package/dist/components/RelationPicker.svelte +341 -0
- package/dist/components/RelationPicker.svelte.d.ts +26 -0
- package/dist/components/ResourceOperationsPage.svelte +23 -19
- package/dist/components/ResourceOperationsPage.svelte.d.ts +2 -2
- package/dist/components/RetryTaskButton.svelte +12 -42
- package/dist/components/SecuritySettings.svelte +29 -5
- package/dist/components/SegmentedControl.svelte +145 -0
- package/dist/components/SegmentedControl.svelte.d.ts +38 -0
- package/dist/components/SettingsPage.svelte +3 -1
- package/dist/components/SettingsPage.svelte.d.ts +1 -0
- package/dist/components/ShowPage.svelte +18 -3
- package/dist/components/ShowPage.svelte.d.ts +3 -0
- package/dist/components/Sidebar.svelte +2 -2
- package/dist/components/Slider.svelte +100 -0
- package/dist/components/Slider.svelte.d.ts +19 -0
- package/dist/components/SpreadsheetView.svelte +1213 -95
- package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
- package/dist/components/TagsInput.svelte +178 -0
- package/dist/components/TagsInput.svelte.d.ts +17 -0
- package/dist/components/TimePicker.svelte +10 -0
- package/dist/components/TimePicker.svelte.d.ts +6 -0
- package/dist/components/TooltipButton.svelte +3 -4
- package/dist/components/TreeSelect.svelte +267 -53
- package/dist/components/TreeSelect.svelte.d.ts +3 -0
- package/dist/components/TreeTable.svelte +484 -82
- package/dist/components/TreeTable.svelte.d.ts +24 -1
- package/dist/components/TwoFactorAuthPage.svelte +85 -48
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
- package/dist/components/VirtualTable.svelte +116 -20
- package/dist/components/VirtualTable.svelte.d.ts +5 -0
- package/dist/components/account/CompanyProfilePage.svelte +74 -38
- package/dist/components/account/GetStartedPage.svelte +2 -1
- package/dist/components/account/ImportMembersPage.svelte +22 -167
- package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
- package/dist/components/account/MemberDirectory.svelte +156 -0
- package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
- package/dist/components/account/MembersStarterPage.svelte +9 -55
- package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
- package/dist/components/account/SecurityLogPage.svelte +4 -1
- package/dist/components/account/SettingsPlainPage.svelte +7 -38
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
- package/dist/components/account/SettingsSidebarPage.svelte +7 -20
- package/dist/components/account/TeamMembersPage.svelte +4 -20
- package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
- package/dist/components/account/UserProfilePage.svelte +3 -25
- package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
- package/dist/components/buttons/ExportButton.svelte +56 -12
- package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
- package/dist/components/content/ContentPageHeader.svelte +1 -1
- package/dist/components/content/ContentPageShell.svelte +1 -1
- package/dist/components/content/MetricBlock.svelte +1 -1
- package/dist/components/content/PageToolbar.svelte +1 -1
- package/dist/components/content/ReferenceNotice.svelte +5 -0
- package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
- package/dist/components/content/SectionHeader.svelte +1 -1
- package/dist/components/content/SettingsFieldRow.svelte +1 -1
- package/dist/components/content/SettingsGroup.svelte +1 -1
- package/dist/components/content/StatusBadge.svelte +1 -1
- package/dist/components/content/WorkspaceLayout.svelte +1 -1
- package/dist/components/export-reference.d.ts +2 -0
- package/dist/components/export-reference.js +9 -0
- package/dist/components/fieldComponentMap.js +2 -5
- package/dist/components/fields/DateField.svelte +12 -13
- package/dist/components/fields/MarkdownField.svelte +14 -5
- package/dist/components/fields/date-display.d.ts +6 -0
- package/dist/components/fields/date-display.js +24 -0
- package/dist/components/network/TeamCrewTablePage.svelte +6 -1
- package/dist/components/network/UserCardsNFTPage.svelte +2 -0
- package/dist/components/profile/PublicProfilePage.svelte +2 -0
- package/dist/components/saved-list-views.d.ts +74 -1
- package/dist/components/saved-list-views.js +191 -3
- package/dist/components/table-contract.d.ts +12 -1
- package/dist/components/task-action.svelte.d.ts +13 -0
- package/dist/components/task-action.svelte.js +73 -0
- package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
- package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
- package/dist/components/ui/badge/badge-variants.d.ts +3 -2
- package/dist/components/ui/badge/badge-variants.js +2 -3
- package/dist/components/ui/badge/badge.svelte +3 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
- package/dist/components/ui/button/button-variants.d.ts +5 -4
- package/dist/components/ui/button/button-variants.js +7 -5
- package/dist/components/ui/checkbox/checkbox.svelte +5 -1
- package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -2
- package/dist/components/ui/input/input.svelte +7 -2
- package/dist/components/ui/input/input.svelte.d.ts +4 -1
- package/dist/components/ui/select/select.svelte +4 -1
- package/dist/components/ui/select/select.svelte.d.ts +2 -1
- package/dist/components/ui/textarea/textarea.svelte +2 -2
- package/dist/components.css +33 -2
- package/dist/date-input-policy.d.ts +4 -0
- package/dist/date-input-policy.js +35 -0
- package/dist/design-system-components.d.ts +100 -0
- package/dist/design-system-components.js +101 -0
- package/dist/design-system.js +1 -1
- package/dist/devtools-bridge.d.ts +80 -0
- package/dist/devtools-bridge.js +92 -0
- package/dist/editor-config.svelte.d.ts +4 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +23 -2
- package/dist/recipes.d.ts +960 -4
- package/dist/recipes.js +264 -4
- package/dist/tree-table-model.d.ts +24 -0
- package/dist/tree-table-model.js +111 -0
- package/dist/types.d.ts +4 -4
- package/package.json +22 -10
- package/dist/styled-system/css/conditions.js +0 -36
- package/dist/styled-system/css/css.d.ts +0 -22
- package/dist/styled-system/css/css.js +0 -45
- package/dist/styled-system/css/cva.d.ts +0 -6
- package/dist/styled-system/css/cva.js +0 -87
- package/dist/styled-system/css/cx.d.ts +0 -5
- package/dist/styled-system/css/cx.js +0 -15
- package/dist/styled-system/css/index.d.ts +0 -5
- package/dist/styled-system/css/index.js +0 -4
- package/dist/styled-system/css/sva.d.ts +0 -4
- package/dist/styled-system/css/sva.js +0 -46
- package/dist/styled-system/helpers.js +0 -316
- package/dist/styled-system/patterns/aspect-ratio.d.ts +0 -20
- package/dist/styled-system/patterns/aspect-ratio.js +0 -38
- package/dist/styled-system/patterns/bleed.d.ts +0 -21
- package/dist/styled-system/patterns/bleed.js +0 -24
- package/dist/styled-system/patterns/box.d.ts +0 -20
- package/dist/styled-system/patterns/box.js +0 -15
- package/dist/styled-system/patterns/center.d.ts +0 -20
- package/dist/styled-system/patterns/center.js +0 -21
- package/dist/styled-system/patterns/circle.d.ts +0 -20
- package/dist/styled-system/patterns/circle.js +0 -25
- package/dist/styled-system/patterns/container.d.ts +0 -20
- package/dist/styled-system/patterns/container.js +0 -21
- package/dist/styled-system/patterns/cq.d.ts +0 -21
- package/dist/styled-system/patterns/cq.js +0 -21
- package/dist/styled-system/patterns/divider.d.ts +0 -22
- package/dist/styled-system/patterns/divider.js +0 -25
- package/dist/styled-system/patterns/flex.d.ts +0 -26
- package/dist/styled-system/patterns/flex.js +0 -26
- package/dist/styled-system/patterns/float.d.ts +0 -23
- package/dist/styled-system/patterns/float.js +0 -52
- package/dist/styled-system/patterns/grid-item.d.ts +0 -25
- package/dist/styled-system/patterns/grid-item.js +0 -25
- package/dist/styled-system/patterns/grid.d.ts +0 -24
- package/dist/styled-system/patterns/grid.js +0 -27
- package/dist/styled-system/patterns/hstack.d.ts +0 -21
- package/dist/styled-system/patterns/hstack.js +0 -24
- package/dist/styled-system/patterns/index.d.ts +0 -21
- package/dist/styled-system/patterns/index.js +0 -20
- package/dist/styled-system/patterns/link-overlay.d.ts +0 -20
- package/dist/styled-system/patterns/link-overlay.js +0 -24
- package/dist/styled-system/patterns/spacer.d.ts +0 -20
- package/dist/styled-system/patterns/spacer.js +0 -25
- package/dist/styled-system/patterns/square.d.ts +0 -20
- package/dist/styled-system/patterns/square.js +0 -24
- package/dist/styled-system/patterns/stack.d.ts +0 -23
- package/dist/styled-system/patterns/stack.js +0 -24
- package/dist/styled-system/patterns/visually-hidden.d.ts +0 -20
- package/dist/styled-system/patterns/visually-hidden.js +0 -18
- package/dist/styled-system/patterns/vstack.d.ts +0 -21
- package/dist/styled-system/patterns/vstack.js +0 -24
- package/dist/styled-system/patterns/wrap.d.ts +0 -24
- package/dist/styled-system/patterns/wrap.js +0 -25
- package/dist/styled-system/recipes/content-header.d.ts +0 -31
- package/dist/styled-system/recipes/content-header.js +0 -65
- package/dist/styled-system/recipes/content-page.d.ts +0 -34
- package/dist/styled-system/recipes/content-page.js +0 -43
- package/dist/styled-system/recipes/create-recipe.js +0 -82
- package/dist/styled-system/recipes/index.d.ts +0 -17
- package/dist/styled-system/recipes/index.js +0 -16
- package/dist/styled-system/recipes/metric-block.d.ts +0 -34
- package/dist/styled-system/recipes/metric-block.js +0 -76
- package/dist/styled-system/recipes/product-list.d.ts +0 -31
- package/dist/styled-system/recipes/product-list.js +0 -73
- package/dist/styled-system/recipes/product-section.d.ts +0 -31
- package/dist/styled-system/recipes/product-section.js +0 -49
- package/dist/styled-system/recipes/product-settings-row.d.ts +0 -34
- package/dist/styled-system/recipes/product-settings-row.js +0 -58
- package/dist/styled-system/recipes/product-settings.d.ts +0 -31
- package/dist/styled-system/recipes/product-settings.js +0 -57
- package/dist/styled-system/recipes/product-status.d.ts +0 -34
- package/dist/styled-system/recipes/product-status.js +0 -45
- package/dist/styled-system/recipes/product-toolbar.d.ts +0 -31
- package/dist/styled-system/recipes/product-toolbar.js +0 -41
- package/dist/styled-system/recipes/product-workspace.d.ts +0 -34
- package/dist/styled-system/recipes/product-workspace.js +0 -58
- package/dist/styled-system/recipes/surface-metric.d.ts +0 -38
- package/dist/styled-system/recipes/surface-metric.js +0 -63
- package/dist/styled-system/recipes/surface-table.d.ts +0 -34
- package/dist/styled-system/recipes/surface-table.js +0 -62
- package/dist/styled-system/recipes/ui-badge.d.ts +0 -31
- package/dist/styled-system/recipes/ui-badge.js +0 -38
- package/dist/styled-system/recipes/ui-button.d.ts +0 -32
- package/dist/styled-system/recipes/ui-button.js +0 -43
- package/dist/styled-system/recipes/ui-input.d.ts +0 -31
- package/dist/styled-system/recipes/ui-input.js +0 -49
- package/dist/styled-system/recipes/ui-textarea.d.ts +0 -31
- package/dist/styled-system/recipes/ui-textarea.js +0 -24
- package/dist/styled-system/styles.css +0 -1257
- package/dist/styled-system/tokens/index.d.ts +0 -9
- package/dist/styled-system/tokens/index.js +0 -268
- package/dist/styled-system/tokens/tokens.d.ts +0 -36
- package/dist/styled-system/types/composition.d.ts +0 -227
- package/dist/styled-system/types/conditions.d.ts +0 -236
- package/dist/styled-system/types/csstype.d.ts +0 -22570
- package/dist/styled-system/types/index.d.ts +0 -6
- package/dist/styled-system/types/parts.d.ts +0 -8
- package/dist/styled-system/types/pattern.d.ts +0 -78
- package/dist/styled-system/types/prop-type.d.ts +0 -223
- package/dist/styled-system/types/recipe.d.ts +0 -181
- package/dist/styled-system/types/selectors.d.ts +0 -59
- package/dist/styled-system/types/static-css.d.ts +0 -56
- package/dist/styled-system/types/style-props.d.ts +0 -8088
- package/dist/styled-system/types/system-types.d.ts +0 -151
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onDestroy } from 'svelte';
|
|
2
|
+
import { onDestroy, untrack } from 'svelte';
|
|
3
3
|
import { X, Upload, RotateCw, Ban } from '@lucide/svelte';
|
|
4
4
|
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
5
6
|
|
|
6
7
|
export type UploadItemStatus = 'queued' | 'uploading' | 'success' | 'error' | 'cancelled';
|
|
7
8
|
|
|
@@ -9,15 +10,25 @@
|
|
|
9
10
|
id: string;
|
|
10
11
|
file: File;
|
|
11
12
|
status: UploadItemStatus;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
progress: number;
|
|
14
|
+
url?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
uploadId?: string;
|
|
17
|
+
cleanupStatus?: 'pending' | 'success' | 'error';
|
|
18
|
+
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
export interface UploadSession {
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
idempotencyKey: string;
|
|
23
|
+
setUploadId: (uploadId: string) => void;
|
|
24
|
+
onProgress: (progress: number) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UploadCancellation {
|
|
28
|
+
uploadId: string;
|
|
29
|
+
idempotencyKey: string;
|
|
30
|
+
reason: 'cancel' | 'remove' | 'replace' | 'scope-change' | 'unmount';
|
|
31
|
+
}
|
|
21
32
|
|
|
22
33
|
interface Props {
|
|
23
34
|
id?: string;
|
|
@@ -28,7 +39,8 @@
|
|
|
28
39
|
maxSize?: number;
|
|
29
40
|
disabled?: boolean;
|
|
30
41
|
required?: boolean;
|
|
31
|
-
upload?: (file: File, session: UploadSession) => Promise<{ url?: string } | undefined> | Promise<void>;
|
|
42
|
+
upload?: (file: File, session: UploadSession) => Promise<{ url?: string; uploadId?: string } | undefined> | Promise<void>;
|
|
43
|
+
cancelUpload?: (file: File, cancellation: UploadCancellation) => Promise<void>;
|
|
32
44
|
onChange?: (items: UploadItem[]) => void;
|
|
33
45
|
onReject?: (file: File, reason: string) => void;
|
|
34
46
|
class?: string;
|
|
@@ -44,20 +56,89 @@
|
|
|
44
56
|
disabled = false,
|
|
45
57
|
required = false,
|
|
46
58
|
upload,
|
|
59
|
+
cancelUpload,
|
|
47
60
|
onChange,
|
|
48
61
|
onReject,
|
|
49
62
|
class: className = '',
|
|
50
63
|
}: Props = $props();
|
|
51
64
|
|
|
65
|
+
const i18n = useTranslation();
|
|
52
66
|
let input: HTMLInputElement | undefined = $state();
|
|
53
67
|
let items = $state<UploadItem[]>([]);
|
|
68
|
+
let rejected = $state<{ name: string; reason: string }[]>([]);
|
|
54
69
|
let sequence = 0;
|
|
55
70
|
const controllers = new Map<string, AbortController>();
|
|
56
|
-
|
|
71
|
+
interface UploadAttempt {
|
|
72
|
+
controller: AbortController;
|
|
73
|
+
upload: NonNullable<Props['upload']>;
|
|
74
|
+
cancelUpload?: Props['cancelUpload'];
|
|
75
|
+
epoch: number;
|
|
76
|
+
file: File;
|
|
77
|
+
idempotencyKey: string;
|
|
78
|
+
uploadId?: string;
|
|
79
|
+
reason?: UploadCancellation['reason'];
|
|
80
|
+
cleanupStarted?: boolean;
|
|
81
|
+
settled?: boolean;
|
|
82
|
+
}
|
|
83
|
+
const attempts = new Map<string, UploadAttempt>();
|
|
84
|
+
const latestAttempts = new Map<string, UploadAttempt>();
|
|
85
|
+
let scopeEpoch = 0;
|
|
86
|
+
let observedUpload = untrack(() => upload);
|
|
87
|
+
let destroyed = false;
|
|
88
|
+
|
|
89
|
+
$effect.pre(() => {
|
|
90
|
+
if (upload === observedUpload) return;
|
|
91
|
+
observedUpload = upload;
|
|
92
|
+
scopeEpoch += 1;
|
|
93
|
+
untrack(() => {
|
|
94
|
+
const activeIds = [...controllers.keys()];
|
|
95
|
+
for (const id of activeIds) retire(id, 'scope-change');
|
|
96
|
+
const cancelledIds = new Set(activeIds);
|
|
97
|
+
if (cancelledIds.size > 0) {
|
|
98
|
+
items = items.map(item => cancelledIds.has(item.id) && item.status === 'uploading'
|
|
99
|
+
? { ...item, status: 'cancelled', error: 'Upload cancelled.' }
|
|
100
|
+
: item);
|
|
101
|
+
emitChange();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function retire(id: string, reason: UploadCancellation['reason'] = 'cancel'): void {
|
|
107
|
+
const attempt = attempts.get(id);
|
|
108
|
+
const controller = controllers.get(id);
|
|
109
|
+
controllers.delete(id);
|
|
110
|
+
attempts.delete(id);
|
|
111
|
+
if (attempt) attempt.reason = reason;
|
|
112
|
+
controller?.abort();
|
|
113
|
+
if (attempt) cleanupAttempt(id, attempt);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cleanupAttempt(id: string, attempt: UploadAttempt): void {
|
|
117
|
+
if (!attempt.reason || attempt.cleanupStarted) return;
|
|
118
|
+
if (attempt?.uploadId && attempt.cancelUpload) {
|
|
119
|
+
attempt.cleanupStarted = true;
|
|
120
|
+
const publish = (cleanupStatus: NonNullable<UploadItem['cleanupStatus']>) => {
|
|
121
|
+
if (!destroyed && scopeEpoch === attempt.epoch && upload === attempt.upload
|
|
122
|
+
&& latestAttempts.get(id) === attempt && items.some(item => item.id === id)) {
|
|
123
|
+
updateItem(id, { cleanupStatus });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
publish('pending');
|
|
127
|
+
const cancellation: UploadCancellation = {
|
|
128
|
+
uploadId: attempt.uploadId,
|
|
129
|
+
idempotencyKey: attempt.idempotencyKey,
|
|
130
|
+
reason: attempt.reason,
|
|
131
|
+
};
|
|
132
|
+
const cancel = attempt.cancelUpload;
|
|
133
|
+
void Promise.resolve().then(() => cancel(attempt.file, cancellation))
|
|
134
|
+
.then(() => publish('success'), () => publish('error'));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
57
138
|
onDestroy(() => {
|
|
58
|
-
|
|
59
|
-
for (const
|
|
60
|
-
|
|
139
|
+
destroyed = true;
|
|
140
|
+
for (const id of controllers.keys()) retire(id, 'unmount');
|
|
141
|
+
latestAttempts.clear();
|
|
61
142
|
});
|
|
62
143
|
|
|
63
144
|
function emitChange(): void {
|
|
@@ -86,61 +167,127 @@
|
|
|
86
167
|
return undefined;
|
|
87
168
|
}
|
|
88
169
|
|
|
89
|
-
|
|
90
|
-
|
|
170
|
+
const normalizedMaxFiles = $derived(
|
|
171
|
+
multiple ? (Number.isSafeInteger(maxFiles) && maxFiles > 0 ? maxFiles : 10) : 1,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
type UploadItemUpdate = Partial<Omit<UploadItem, 'error' | 'url' | 'uploadId' | 'cleanupStatus'>> & {
|
|
175
|
+
[K in 'error' | 'url' | 'uploadId' | 'cleanupStatus']?: UploadItem[K] | undefined;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function updateItem(id: string, update: UploadItemUpdate): void {
|
|
91
179
|
items = items.map(item => {
|
|
92
180
|
if (item.id !== id) return item;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
if (
|
|
181
|
+
const { error, url, uploadId, cleanupStatus, ...required } = update;
|
|
182
|
+
const next: UploadItem = { ...item, ...required };
|
|
183
|
+
if ('error' in update && error === undefined) delete next.error;
|
|
184
|
+
if ('url' in update && url === undefined) delete next.url;
|
|
185
|
+
if ('uploadId' in update && uploadId === undefined) delete next.uploadId;
|
|
186
|
+
if ('cleanupStatus' in update && cleanupStatus === undefined) delete next.cleanupStatus;
|
|
187
|
+
if (error !== undefined) next.error = error;
|
|
188
|
+
if (url !== undefined) next.url = url;
|
|
189
|
+
if (uploadId !== undefined) next.uploadId = uploadId;
|
|
190
|
+
if (cleanupStatus !== undefined) next.cleanupStatus = cleanupStatus;
|
|
96
191
|
return next;
|
|
97
192
|
});
|
|
98
193
|
emitChange();
|
|
99
194
|
}
|
|
100
195
|
|
|
101
196
|
async function process(item: UploadItem): Promise<void> {
|
|
102
|
-
if (!upload || disabled
|
|
197
|
+
if (destroyed || !upload || disabled) return;
|
|
198
|
+
if (controllers.has(item.id) || !items.some(candidate => candidate.id === item.id)) return;
|
|
103
199
|
const controller = new AbortController();
|
|
104
200
|
controllers.set(item.id, controller);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
201
|
+
const attempt: UploadAttempt = {
|
|
202
|
+
controller, upload, epoch: scopeEpoch, file: item.file,
|
|
203
|
+
cancelUpload,
|
|
204
|
+
idempotencyKey: crypto.randomUUID(),
|
|
205
|
+
};
|
|
206
|
+
attempts.set(item.id, attempt);
|
|
207
|
+
latestAttempts.set(item.id, attempt);
|
|
208
|
+
// 每次尝试由独立控制器持有;旧回执和 finally 不得影响新尝试。
|
|
209
|
+
const current = () => attempts.get(item.id) === attempt
|
|
210
|
+
&& controllers.get(item.id) === controller
|
|
211
|
+
&& scopeEpoch === attempt.epoch && upload === attempt.upload;
|
|
212
|
+
const updateCurrent = (update: UploadItemUpdate) => {
|
|
213
|
+
if (current() && !controller.signal.aborted) updateItem(item.id, update);
|
|
214
|
+
};
|
|
215
|
+
const register = (uploadId: string) => {
|
|
216
|
+
if (attempt.settled) return;
|
|
217
|
+
if (typeof uploadId !== 'string' || !uploadId.trim() || uploadId.length > 200
|
|
218
|
+
|| (attempt.uploadId !== undefined && attempt.uploadId !== uploadId)) throw new Error('Invalid upload session.');
|
|
219
|
+
attempt.uploadId = uploadId;
|
|
220
|
+
updateCurrent({ uploadId });
|
|
221
|
+
cleanupAttempt(item.id, attempt);
|
|
222
|
+
};
|
|
223
|
+
updateItem(item.id, {
|
|
224
|
+
status: 'uploading',
|
|
225
|
+
progress: 0,
|
|
226
|
+
error: undefined,
|
|
227
|
+
uploadId: undefined,
|
|
228
|
+
cleanupStatus: undefined,
|
|
229
|
+
});
|
|
108
230
|
try {
|
|
109
|
-
//
|
|
110
|
-
if (!current()) return;
|
|
111
|
-
const result = await upload(item.file, {
|
|
231
|
+
// 宿主回调可以同步卸载或替换组件;通知返回后必须重新确认本次请求仍有效。
|
|
232
|
+
if (destroyed || !current() || controller.signal.aborted) return;
|
|
233
|
+
const result = await attempt.upload(item.file, {
|
|
112
234
|
signal: controller.signal,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
},
|
|
235
|
+
idempotencyKey: attempt.idempotencyKey,
|
|
236
|
+
setUploadId: register,
|
|
237
|
+
onProgress: progress => updateCurrent({
|
|
238
|
+
progress: Number.isFinite(progress) ? Math.max(0, Math.min(100, Math.round(progress))) : 0,
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
if (result?.uploadId !== undefined) register(result.uploadId);
|
|
242
|
+
const uploadId = result?.uploadId ?? attempt.uploadId;
|
|
243
|
+
updateCurrent({
|
|
244
|
+
status: 'success',
|
|
245
|
+
progress: 100,
|
|
246
|
+
...(result?.url === undefined ? {} : { url: result.url }),
|
|
247
|
+
...(uploadId === undefined ? {} : { uploadId }),
|
|
118
248
|
});
|
|
119
|
-
if (current()) updateItem(item.id, { status: 'success', progress: 100,
|
|
120
|
-
...(result?.url === undefined ? {} : { url: result.url }) });
|
|
121
249
|
} catch (error) {
|
|
122
|
-
if (current())
|
|
123
|
-
|
|
250
|
+
if (!current()) return;
|
|
251
|
+
updateItem(item.id, {
|
|
252
|
+
status: controller.signal.aborted ? 'cancelled' : 'error',
|
|
253
|
+
error: controller.signal.aborted ? 'Upload cancelled.' : error instanceof Error ? error.message : 'Upload failed.',
|
|
254
|
+
});
|
|
124
255
|
} finally {
|
|
125
|
-
|
|
256
|
+
attempt.settled = true;
|
|
257
|
+
if (current()) {
|
|
258
|
+
controllers.delete(item.id);
|
|
259
|
+
attempts.delete(item.id);
|
|
260
|
+
}
|
|
126
261
|
}
|
|
127
262
|
}
|
|
128
263
|
|
|
129
264
|
function addFiles(selected: File[]): void {
|
|
130
|
-
if (
|
|
131
|
-
const available =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
265
|
+
if (destroyed || disabled) return;
|
|
266
|
+
const available = multiple
|
|
267
|
+
? Math.max(0, normalizedMaxFiles - items.length)
|
|
268
|
+
: 1;
|
|
269
|
+
rejected = [];
|
|
270
|
+
if (selected.length > available) {
|
|
271
|
+
for (const file of selected.slice(available)) {
|
|
272
|
+
if (destroyed) return;
|
|
273
|
+
rejected = [...rejected, { name: file.name, reason: 'Maximum file count exceeded.' }];
|
|
274
|
+
onReject?.(file, 'Maximum file count exceeded.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const file of selected.slice(0, multiple ? available : Math.min(available, 1))) {
|
|
278
|
+
if (destroyed) return;
|
|
135
279
|
const reason = validate(file);
|
|
136
280
|
if (reason) {
|
|
281
|
+
rejected = [...rejected, { name: file.name, reason }];
|
|
137
282
|
onReject?.(file, reason);
|
|
138
283
|
continue;
|
|
139
284
|
}
|
|
140
285
|
const item: UploadItem = { id: nextId(), file, status: 'queued', progress: 0 };
|
|
141
286
|
if (!multiple) {
|
|
142
|
-
for (const
|
|
143
|
-
|
|
287
|
+
for (const previous of items) {
|
|
288
|
+
retire(previous.id, 'replace');
|
|
289
|
+
latestAttempts.delete(previous.id);
|
|
290
|
+
}
|
|
144
291
|
}
|
|
145
292
|
items = multiple ? [...items, item] : [item];
|
|
146
293
|
emitChange();
|
|
@@ -152,21 +299,23 @@
|
|
|
152
299
|
const target = event.currentTarget;
|
|
153
300
|
if (!(target instanceof HTMLInputElement)) return;
|
|
154
301
|
addFiles(Array.from(target.files ?? []));
|
|
302
|
+
// 异步模式的文件由队列持有,清空输入以允许再次选择同名文件。
|
|
303
|
+
if (upload) target.value = '';
|
|
155
304
|
}
|
|
156
305
|
|
|
157
306
|
function remove(id: string): void {
|
|
158
|
-
if (disabled
|
|
159
|
-
|
|
160
|
-
|
|
307
|
+
if (disabled) return;
|
|
308
|
+
retire(id, 'remove');
|
|
309
|
+
latestAttempts.delete(id);
|
|
161
310
|
items = items.filter(item => item.id !== id);
|
|
311
|
+
if (input) input.value = '';
|
|
162
312
|
emitChange();
|
|
163
313
|
}
|
|
164
314
|
|
|
165
315
|
function cancel(id: string): void {
|
|
166
316
|
const controller = controllers.get(id);
|
|
167
317
|
if (!controller) return;
|
|
168
|
-
|
|
169
|
-
controllers.delete(id);
|
|
318
|
+
retire(id, 'cancel');
|
|
170
319
|
updateItem(id, { status: 'cancelled', error: 'Upload cancelled.' });
|
|
171
320
|
}
|
|
172
321
|
|
|
@@ -183,15 +332,15 @@
|
|
|
183
332
|
</script>
|
|
184
333
|
|
|
185
334
|
<div class={`svadmin-file-upload ${className}`}>
|
|
186
|
-
|
|
335
|
+
<input
|
|
187
336
|
bind:this={input}
|
|
188
337
|
type="file"
|
|
189
338
|
{id}
|
|
190
|
-
{name}
|
|
339
|
+
name={upload ? undefined : name}
|
|
191
340
|
{accept}
|
|
192
341
|
{multiple}
|
|
193
342
|
disabled={disabled}
|
|
194
|
-
{required}
|
|
343
|
+
required={upload ? false : required}
|
|
195
344
|
class="svadmin-file-upload-input"
|
|
196
345
|
onchange={handleInput}
|
|
197
346
|
/>
|
|
@@ -209,8 +358,19 @@
|
|
|
209
358
|
}}
|
|
210
359
|
>
|
|
211
360
|
<Upload aria-hidden="true" />
|
|
212
|
-
<span>
|
|
361
|
+
<span>{i18n.t('upload.selectOrDrop')}</span>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<div role="status" aria-live="polite">
|
|
365
|
+
{i18n.t('upload.selectedCount', { count: String(items.length), max: String(normalizedMaxFiles) })}
|
|
213
366
|
</div>
|
|
367
|
+
{#if rejected.length}
|
|
368
|
+
<ul aria-label={i18n.t('upload.rejectedFiles')}>
|
|
369
|
+
{#each rejected as rejection (rejection)}
|
|
370
|
+
<li>{rejection.name}: {rejection.reason}</li>
|
|
371
|
+
{/each}
|
|
372
|
+
</ul>
|
|
373
|
+
{/if}
|
|
214
374
|
|
|
215
375
|
{#if items.length}
|
|
216
376
|
<ul class="svadmin-file-upload-list" aria-label="Selected files">
|
|
@@ -233,6 +393,8 @@
|
|
|
233
393
|
<X aria-hidden="true" />
|
|
234
394
|
</Button>
|
|
235
395
|
{#if item.error}<span role="alert">{item.error}</span>{/if}
|
|
396
|
+
{#if item.cleanupStatus === 'pending'}<span role="status">{i18n.t('upload.cleanupPending')}</span>{/if}
|
|
397
|
+
{#if item.cleanupStatus === 'error'}<span role="alert">{i18n.t('upload.cleanupFailed')}</span>{/if}
|
|
236
398
|
</li>
|
|
237
399
|
{/each}
|
|
238
400
|
</ul>
|
|
@@ -6,11 +6,20 @@ export interface UploadItem {
|
|
|
6
6
|
progress: number;
|
|
7
7
|
url?: string;
|
|
8
8
|
error?: string;
|
|
9
|
+
uploadId?: string;
|
|
10
|
+
cleanupStatus?: 'pending' | 'success' | 'error';
|
|
9
11
|
}
|
|
10
12
|
export interface UploadSession {
|
|
11
13
|
signal: AbortSignal;
|
|
14
|
+
idempotencyKey: string;
|
|
15
|
+
setUploadId: (uploadId: string) => void;
|
|
12
16
|
onProgress: (progress: number) => void;
|
|
13
17
|
}
|
|
18
|
+
export interface UploadCancellation {
|
|
19
|
+
uploadId: string;
|
|
20
|
+
idempotencyKey: string;
|
|
21
|
+
reason: 'cancel' | 'remove' | 'replace' | 'scope-change' | 'unmount';
|
|
22
|
+
}
|
|
14
23
|
interface Props {
|
|
15
24
|
id?: string;
|
|
16
25
|
name?: string;
|
|
@@ -22,7 +31,9 @@ interface Props {
|
|
|
22
31
|
required?: boolean;
|
|
23
32
|
upload?: (file: File, session: UploadSession) => Promise<{
|
|
24
33
|
url?: string;
|
|
34
|
+
uploadId?: string;
|
|
25
35
|
} | undefined> | Promise<void>;
|
|
36
|
+
cancelUpload?: (file: File, cancellation: UploadCancellation) => Promise<void>;
|
|
26
37
|
onChange?: (items: UploadItem[]) => void;
|
|
27
38
|
onReject?: (file: File, reason: string) => void;
|
|
28
39
|
class?: string;
|
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
import { Select } from './ui/select/index.js';
|
|
8
8
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
9
9
|
import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
|
|
10
|
+
import { isNumericFilterField as isNumericField } from '@svadmin/core';
|
|
11
|
+
import FilterCollectionInput from './FilterCollectionInput.svelte';
|
|
10
12
|
import { readFilterTree, compileFilterTree, filterOperators, parseFilterInput, isCollectionOperator, isNullOperator,
|
|
11
13
|
FILTER_EDITOR_LIMITS, type FilterGroupNode, type FilterNode, type FilterRuleNode, type FilterEditorIssue, type FilterIssueCode } from './enterprise/filter-tree.js';
|
|
12
14
|
|
|
13
|
-
export interface FilterRuleItem {
|
|
15
|
+
export interface FilterRuleItem {
|
|
16
|
+
id: string;
|
|
17
|
+
field: string;
|
|
18
|
+
operator: CrudOperator;
|
|
19
|
+
value: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
interface Props {
|
|
15
23
|
fields?: FieldDefinition[];
|
|
16
24
|
filters?: Filter[];
|
|
@@ -27,13 +35,15 @@
|
|
|
27
35
|
const i18n = useTranslation();
|
|
28
36
|
const chinese = $derived(i18n.locale.startsWith('zh'));
|
|
29
37
|
const availableFields = $derived(fields.filter((field) => field.filterable !== false));
|
|
38
|
+
// 金额、评分等数字字段沿用 core 的输入语义;树编译器统一按 number 验证。
|
|
39
|
+
const editorFields = $derived(fields.map(field => isNumericField(field) ? { ...field, type: 'number' as const } : field));
|
|
30
40
|
let root = $state<FilterGroupNode>({ kind: 'group', id: 'root', operator: 'and', children: [], wrapped: false });
|
|
31
41
|
let loadIssues = $state<FilterEditorIssue[]>([]);
|
|
32
42
|
let attempted = $state(false);
|
|
33
43
|
let nextId = 0;
|
|
34
44
|
let lastInputFilters: Filter[] | undefined;
|
|
35
45
|
let preserved = $state.raw<ReadonlyMap<string, Filter>>(new Map());
|
|
36
|
-
const compilation = $derived(compileFilterTree({ ...root, operator: logicalOperator },
|
|
46
|
+
const compilation = $derived(compileFilterTree({ ...root, operator: logicalOperator }, editorFields, preserved));
|
|
37
47
|
const issues = $derived(loadIssues.length ? loadIssues : attempted && !compilation.ok ? compilation.issues : []);
|
|
38
48
|
function countNodes(node: FilterNode): number { return 1 + (node.kind === 'group' ? node.children.reduce((sum, child) => sum + countNodes(child), 0) : 0); }
|
|
39
49
|
const nodeCount = $derived(countNodes(root));
|
|
@@ -43,94 +53,75 @@
|
|
|
43
53
|
return node.kind === 'group' ? { operator: node.operator, value: node.children.map(snapshotNode) }
|
|
44
54
|
: { field: node.field, operator: node.operator, value: Array.isArray(node.value) ? [...node.value] : node.value };
|
|
45
55
|
}
|
|
56
|
+
|
|
46
57
|
function cannotEdit(node: FilterRuleNode): boolean {
|
|
47
|
-
return
|
|
48
|
-
|| !compileFilterTree({ kind: 'group', id: 'probe', operator: 'and', children: [{ ...node, readonly: false }] }, fields).ok;
|
|
58
|
+
return !compileFilterTree({ kind: 'group', id: 'probe', operator: 'and', children: [node] }, editorFields).ok;
|
|
49
59
|
}
|
|
50
60
|
function remember(node: FilterNode, saved: Map<string, Filter>): void {
|
|
51
61
|
saved.set(node.id, snapshotNode(node));
|
|
52
62
|
if (node.kind === 'group') node.children.forEach(child => remember(child, saved));
|
|
53
63
|
else node.readonly = cannotEdit(node);
|
|
54
64
|
}
|
|
55
|
-
|
|
56
|
-
if (node.kind === 'group') { node.children.forEach(refreshReadonly); return; }
|
|
57
|
-
const original = preserved.get(node.id);
|
|
58
|
-
node.readonly = original !== undefined && JSON.stringify(original) === JSON.stringify(snapshotNode(node)) && cannotEdit(node);
|
|
59
|
-
}
|
|
65
|
+
|
|
60
66
|
$effect(() => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const input = filters;
|
|
68
|
+
const metadata = editorFields;
|
|
69
|
+
untrack(() => {
|
|
70
|
+
if (input !== lastInputFilters) {
|
|
71
|
+
lastInputFilters = input;
|
|
72
|
+
const parsed = readFilterTree(input);
|
|
66
73
|
const saved = new Map<string, Filter>();
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
if (parsed.ok) {
|
|
75
|
+
root = parsed.root;
|
|
76
|
+
remember(root, saved);
|
|
77
|
+
loadIssues = [];
|
|
78
|
+
if (input.length) logicalOperator = root.operator;
|
|
79
|
+
} else {
|
|
80
|
+
root = { kind: 'group', id: 'root', operator: 'and', children: [], wrapped: false };
|
|
81
|
+
loadIssues = parsed.issues;
|
|
82
|
+
}
|
|
69
83
|
preserved = saved;
|
|
70
|
-
|
|
71
|
-
if (filters.length) logicalOperator = parsed.root.operator;
|
|
84
|
+
attempted = false;
|
|
72
85
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
// 元数据加载后只解锁已支持的历史条件;不能把失效草稿改成只读而绕过校验。
|
|
87
|
+
function unlock(node: FilterNode): void {
|
|
88
|
+
if (node.kind === 'group') node.children.forEach(unlock);
|
|
89
|
+
else if (node.readonly && compileFilterTree({ kind: 'group', id: 'probe', operator: 'and', children: [node] }, metadata).ok) node.readonly = false;
|
|
90
|
+
}
|
|
91
|
+
unlock(root);
|
|
76
92
|
}
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
untrack(() => { void metadata; refreshReadonly(root); });
|
|
80
|
-
}
|
|
93
|
+
});
|
|
81
94
|
});
|
|
82
|
-
function depthOf(target: FilterGroupNode, node: FilterGroupNode = root, depth = root.wrapped || logicalOperator === 'or' ? 1 : 0): number {
|
|
83
|
-
if (node === target) return depth;
|
|
84
|
-
for (const child of node.children) if (child.kind === 'group') {
|
|
85
|
-
const found = depthOf(target, child, depth + 1);
|
|
86
|
-
if (found >= 0) return found;
|
|
87
|
-
}
|
|
88
|
-
return -1;
|
|
89
|
-
}
|
|
90
95
|
|
|
91
96
|
function message(code: FilterIssueCode): string {
|
|
92
97
|
const labels: Record<FilterIssueCode, [string, string]> = {
|
|
93
98
|
'invalid-filter': ['条件结构不受支持,原条件未被修改', 'Unsupported filter structure; the original query is unchanged'],
|
|
94
99
|
limit: ['条件数量或深度超过限制', 'Filter count or depth exceeds the limit'],
|
|
95
|
-
'unknown-field': ['字段不可用于筛选', 'Field is unavailable for filtering'],
|
|
96
|
-
|
|
100
|
+
'unknown-field': ['字段不可用于筛选', 'Field is unavailable for filtering'],
|
|
101
|
+
operator: ['操作符不适用于此字段', 'Operator is not supported for this field'],
|
|
102
|
+
value: ['请输入有效的类型化值', 'Enter a valid typed value'],
|
|
97
103
|
'empty-group': ['条件组不能为空', 'A filter group cannot be empty'],
|
|
98
104
|
};
|
|
99
105
|
return labels[code][chinese ? 0 : 1];
|
|
100
106
|
}
|
|
101
107
|
function appendRule(group: FilterGroupNode): void {
|
|
102
|
-
if (disabled || loadIssues.length || full
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
105
|
-
group.children = [...group.children, { kind: 'rule', id: `draft-${++nextId}`, field:
|
|
106
|
-
operator: filterOperators(
|
|
108
|
+
if (disabled || loadIssues.length || full) return;
|
|
109
|
+
const field = availableFields[0];
|
|
110
|
+
if (!field) return;
|
|
111
|
+
group.children = [...group.children, { kind: 'rule', id: `draft-${++nextId}`, field: field.key,
|
|
112
|
+
operator: filterOperators(editorFields.find(item => item.key === field.key)).includes('contains') ? 'contains' : 'eq', value: undefined }];
|
|
107
113
|
}
|
|
114
|
+
|
|
108
115
|
function appendGroup(group: FilterGroupNode): void {
|
|
109
|
-
if (disabled || loadIssues.length || full
|
|
116
|
+
if (disabled || loadIssues.length || full) return;
|
|
110
117
|
group.children = [...group.children, { kind: 'group', id: `draft-${++nextId}`, operator: 'and', children: [] }];
|
|
111
118
|
}
|
|
119
|
+
|
|
112
120
|
function removeNode(group: FilterGroupNode, id: string): void {
|
|
113
121
|
if (disabled || loadIssues.length) return;
|
|
114
|
-
group.children = group.children.filter(
|
|
115
|
-
}
|
|
116
|
-
function changeField(rule: FilterRuleNode, key: string): void {
|
|
117
|
-
if (disabled || rule.readonly) return;
|
|
118
|
-
rule.field = key;
|
|
119
|
-
rule.operator = filterOperators(fields.find((field) => field.key === key)).includes('contains') ? 'contains' : 'eq';
|
|
120
|
-
rule.value = undefined;
|
|
121
|
-
}
|
|
122
|
-
function changeOperator(rule: FilterRuleNode, next: CrudOperator): void {
|
|
123
|
-
if (disabled || rule.readonly) return;
|
|
124
|
-
if (isNullOperator(next)) rule.value = null;
|
|
125
|
-
else if (isNullOperator(rule.operator) || isCollectionOperator(rule.operator) !== isCollectionOperator(next)) rule.value = undefined;
|
|
126
|
-
rule.operator = next;
|
|
127
|
-
}
|
|
128
|
-
function changeValue(rule: FilterRuleNode, raw: string): void {
|
|
129
|
-
if (disabled || rule.readonly) return;
|
|
130
|
-
try { rule.value = parseFilterInput(raw, fields.find((field) => field.key === rule.field), rule.operator); }
|
|
131
|
-
catch { rule.value = raw; }
|
|
122
|
+
group.children = group.children.filter(child => child.id !== id);
|
|
132
123
|
}
|
|
133
|
-
|
|
124
|
+
|
|
134
125
|
export function addRule(): void { appendRule(root); }
|
|
135
126
|
export function removeRule(index: number): void { const node = root.children[index]; if (node) removeNode(root, node.id); }
|
|
136
127
|
export function reset(): void {
|
|
@@ -138,21 +129,48 @@
|
|
|
138
129
|
root = { kind: 'group', id: 'root', operator: 'and', children: [], wrapped: false };
|
|
139
130
|
loadIssues = [];
|
|
140
131
|
preserved = new Map();
|
|
141
|
-
logicalOperator = 'and';
|
|
142
132
|
filters = [];
|
|
133
|
+
lastInputFilters = filters;
|
|
143
134
|
attempted = false;
|
|
144
135
|
onReset?.();
|
|
145
136
|
}
|
|
137
|
+
|
|
146
138
|
export function apply(): void {
|
|
147
139
|
if (disabled) return;
|
|
148
140
|
attempted = true;
|
|
149
141
|
if (loadIssues.length) { onInvalid?.(loadIssues); return; }
|
|
150
142
|
if (!compilation.ok) { onInvalid?.(compilation.issues); return; }
|
|
151
|
-
//
|
|
143
|
+
// 失败时不输出部分条件,避免改变查询含义。
|
|
152
144
|
filters = compilation.filters;
|
|
153
145
|
lastInputFilters = filters;
|
|
154
146
|
onApply?.(compilation.filters);
|
|
155
147
|
}
|
|
148
|
+
|
|
149
|
+
function changeField(rule: FilterRuleNode, key: string): void {
|
|
150
|
+
if (disabled || rule.readonly) return;
|
|
151
|
+
rule.field = key;
|
|
152
|
+
rule.operator = filterOperators(editorFields.find(field => field.key === key)).includes('contains') ? 'contains' : 'eq';
|
|
153
|
+
rule.value = undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function changeValue(rule: FilterRuleNode, raw: string): void {
|
|
157
|
+
if (disabled || rule.readonly) return;
|
|
158
|
+
try { rule.value = parseFilterInput(raw, editorFields.find(field => field.key === rule.field), rule.operator); }
|
|
159
|
+
catch { rule.value = raw; }
|
|
160
|
+
}
|
|
161
|
+
function valueText(value: unknown): string { return Array.isArray(value) ? JSON.stringify(value) : value === undefined || value === null ? '' : String(value); }
|
|
162
|
+
|
|
163
|
+
function changeOperator(rule: FilterRuleNode, operator: CrudOperator): void {
|
|
164
|
+
if (disabled || rule.readonly) return;
|
|
165
|
+
const previous = rule.operator;
|
|
166
|
+
rule.operator = operator;
|
|
167
|
+
if (isNullOperator(operator)) rule.value = null;
|
|
168
|
+
else if (operator === 'between' || operator === 'nbetween') {
|
|
169
|
+
if (previous !== 'between' && previous !== 'nbetween') rule.value = [null, null];
|
|
170
|
+
} else if (isCollectionOperator(operator)) {
|
|
171
|
+
if (previous !== 'in' && previous !== 'nin') rule.value = [null];
|
|
172
|
+
} else if (isCollectionOperator(previous) || isNullOperator(previous)) rule.value = undefined;
|
|
173
|
+
}
|
|
156
174
|
</script>
|
|
157
175
|
|
|
158
176
|
{#snippet groupEditor(group: FilterGroupNode, depth: number, top: boolean)}
|
|
@@ -185,7 +203,7 @@
|
|
|
185
203
|
</div>
|
|
186
204
|
{:else}
|
|
187
205
|
{@const field = availableFields.find((item) => item.key === node.field)}
|
|
188
|
-
{@const allowed = filterOperators(field)}
|
|
206
|
+
{@const allowed = filterOperators(editorFields.find(item => item.key === node.field))}
|
|
189
207
|
{@const collection = isCollectionOperator(node.operator)}
|
|
190
208
|
{@const invalid = issues.some((issue) => issue.path === node.id)}
|
|
191
209
|
<div class="filter-rule" data-testid="filter-builder-rule" data-filter-rule={node.id}>
|
|
@@ -210,6 +228,10 @@
|
|
|
210
228
|
<label for={`${uid}-${node.id}-value`}>{collection ? (chinese ? '值(JSON 数组)' : 'Value (JSON array)') : (chinese ? '值' : 'Value')}</label>
|
|
211
229
|
{#if isNullOperator(node.operator)}
|
|
212
230
|
<Input id={`${uid}-${node.id}-value`} value={chinese ? '无需填值' : 'No value required'} disabled />
|
|
231
|
+
{:else if collection && field}
|
|
232
|
+
<FilterCollectionInput {field} value={node.value}
|
|
233
|
+
range={node.operator === 'between' || node.operator === 'nbetween'}
|
|
234
|
+
onchange={(value) => { if (!disabled && !node.readonly) node.value = value; }} />
|
|
213
235
|
{:else if !collection && field?.options?.length}
|
|
214
236
|
<Select id={`${uid}-${node.id}-value`} aria-invalid={invalid}
|
|
215
237
|
value={field.options.findIndex((option) => option.value === node.value) < 0 ? '' : String(field.options.findIndex((option) => option.value === node.value))}
|
|
@@ -229,12 +251,12 @@
|
|
|
229
251
|
onchange={(event: Event) => { if (event.currentTarget instanceof HTMLSelectElement) changeValue(node, event.currentTarget.value); }}>
|
|
230
252
|
<option value="">{chinese ? '请选择' : 'Choose'}</option><option value="true">true</option><option value="false">false</option>
|
|
231
253
|
</Select>
|
|
232
|
-
{:else if !collection && field
|
|
254
|
+
{:else if !collection && isNumericField(field)}
|
|
233
255
|
<Input id={`${uid}-${node.id}-value`} type="number" step="any" aria-invalid={invalid} value={typeof node.value === 'number' ? node.value : undefined}
|
|
234
|
-
oninput={(event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
|
|
256
|
+
oninput={(event: Event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
|
|
235
257
|
{:else}
|
|
236
258
|
<Input id={`${uid}-${node.id}-value`} type="text" aria-invalid={invalid} value={valueText(node.value)} placeholder={collection ? '[1, 2]' : ''}
|
|
237
|
-
oninput={(event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
|
|
259
|
+
oninput={(event: Event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
|
|
238
260
|
{/if}
|
|
239
261
|
</div>
|
|
240
262
|
{/if}
|