@terpjs/react-core 0.1.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 +190 -0
- package/package.json +44 -0
- package/src/AppShell.test.tsx +152 -0
- package/src/AppShell.tsx +554 -0
- package/src/Authorized.test.tsx +60 -0
- package/src/Authorized.tsx +21 -0
- package/src/Breadcrumbs.test.tsx +45 -0
- package/src/Breadcrumbs.tsx +110 -0
- package/src/ConfirmDialog.tsx +170 -0
- package/src/DetailPage.tsx +28 -0
- package/src/EmptyState.tsx +74 -0
- package/src/ErrorState.tsx +108 -0
- package/src/Field.test.tsx +53 -0
- package/src/Field.tsx +51 -0
- package/src/HubPage.test.tsx +108 -0
- package/src/HubPage.tsx +204 -0
- package/src/LoadingState.test.tsx +40 -0
- package/src/LoadingState.tsx +96 -0
- package/src/LoginView.test.tsx +57 -0
- package/src/LoginView.tsx +203 -0
- package/src/ModuleNav.test.tsx +96 -0
- package/src/ModuleNav.tsx +88 -0
- package/src/OverviewPage.tsx +26 -0
- package/src/Page.test.tsx +147 -0
- package/src/Page.tsx +158 -0
- package/src/PageActions.test.tsx +104 -0
- package/src/PageActions.tsx +72 -0
- package/src/ProfileView.test.tsx +112 -0
- package/src/ProfileView.tsx +84 -0
- package/src/RequireAuth.test.tsx +89 -0
- package/src/RequireAuth.tsx +22 -0
- package/src/ResourceList.test.tsx +176 -0
- package/src/ResourceList.tsx +123 -0
- package/src/TerpProvider.tsx +320 -0
- package/src/UserMenu.test.tsx +166 -0
- package/src/UserMenu.tsx +125 -0
- package/src/admin/AdminHub.tsx +108 -0
- package/src/admin/AuditLogAdmin.tsx +116 -0
- package/src/admin/GroupCreate.tsx +90 -0
- package/src/admin/GroupDetail.tsx +446 -0
- package/src/admin/GroupsAdmin.tsx +109 -0
- package/src/admin/UserCreate.tsx +115 -0
- package/src/admin/UserDetail.tsx +228 -0
- package/src/admin/UsersAdmin.tsx +111 -0
- package/src/admin/admin.test.tsx +537 -0
- package/src/admin/crumbs.tsx +14 -0
- package/src/admin/module.tsx +51 -0
- package/src/admin/roles.ts +19 -0
- package/src/bootstrap.test.tsx +67 -0
- package/src/bootstrap.tsx +270 -0
- package/src/capabilities.test.ts +24 -0
- package/src/capabilities.ts +26 -0
- package/src/createAuthClient.test.ts +176 -0
- package/src/createAuthClient.ts +105 -0
- package/src/dataview/DataView.test.tsx +392 -0
- package/src/dataview/DataView.tsx +467 -0
- package/src/dataview/DataViewCardList.tsx +189 -0
- package/src/dataview/DataViewColumnSettings.tsx +118 -0
- package/src/dataview/DataViewExpandableRow.tsx +67 -0
- package/src/dataview/DataViewPagination.tsx +113 -0
- package/src/dataview/DataViewRowActions.tsx +131 -0
- package/src/dataview/DataViewTable.tsx +359 -0
- package/src/dataview/DataViewToolbar.tsx +260 -0
- package/src/dataview/README.md +138 -0
- package/src/dataview/glyphs.tsx +175 -0
- package/src/dataview/hooks/hooks.test.tsx +240 -0
- package/src/dataview/hooks/useDataViewQuery.ts +72 -0
- package/src/dataview/hooks/useDataViewState.ts +310 -0
- package/src/dataview/hooks/useServerDataView.ts +154 -0
- package/src/dataview/hooks/useViewSearch.ts +68 -0
- package/src/dataview/index.ts +62 -0
- package/src/dataview/internal.tsx +96 -0
- package/src/dataview/repositories/HttpDataViewRepository.ts +110 -0
- package/src/dataview/repositories/InMemoryDataViewRepository.ts +145 -0
- package/src/dataview/repositories/repositories.test.ts +158 -0
- package/src/dataview/repositories/viewState.test.ts +90 -0
- package/src/dataview/repositories/viewState.ts +128 -0
- package/src/dataview/types.ts +249 -0
- package/src/errorMessages.test.tsx +83 -0
- package/src/errorMessages.tsx +79 -0
- package/src/feedback.test.tsx +167 -0
- package/src/files.test.tsx +142 -0
- package/src/files.tsx +174 -0
- package/src/icons.test.tsx +46 -0
- package/src/icons.tsx +533 -0
- package/src/index.ts +155 -0
- package/src/layout.test.tsx +72 -0
- package/src/layout.tsx +90 -0
- package/src/layoutContract.test.tsx +179 -0
- package/src/layoutContract.ts +137 -0
- package/src/locale.test.tsx +97 -0
- package/src/locale.tsx +246 -0
- package/src/nav.test.ts +21 -0
- package/src/nav.ts +13 -0
- package/src/pageMarker.ts +15 -0
- package/src/raw.d.ts +7 -0
- package/src/realtime-hook.test.tsx +226 -0
- package/src/realtime.test.ts +44 -0
- package/src/realtime.ts +307 -0
- package/src/refresh-session.test.tsx +114 -0
- package/src/revocation.test.tsx +81 -0
- package/src/router.test.tsx +307 -0
- package/src/router.tsx +222 -0
- package/src/sso.test.tsx +128 -0
- package/src/sso.ts +142 -0
- package/src/ssr.test.tsx +45 -0
- package/src/styles.test.ts +21 -0
- package/src/styles.ts +302 -0
- package/src/theme.test.tsx +74 -0
- package/src/theme.tsx +143 -0
- package/src/toast.test.tsx +94 -0
- package/src/toast.tsx +214 -0
- package/src/tokens.guard.test.ts +51 -0
- package/src/ui/Alert.test.tsx +19 -0
- package/src/ui/Alert.tsx +115 -0
- package/src/ui/Badge.test.tsx +14 -0
- package/src/ui/Badge.tsx +48 -0
- package/src/ui/Button.test.tsx +36 -0
- package/src/ui/Button.tsx +95 -0
- package/src/ui/Card.test.tsx +40 -0
- package/src/ui/Card.tsx +92 -0
- package/src/ui/Checkbox.test.tsx +17 -0
- package/src/ui/Checkbox.tsx +51 -0
- package/src/ui/Combobox.test.tsx +58 -0
- package/src/ui/Combobox.tsx +313 -0
- package/src/ui/DatePicker.test.tsx +60 -0
- package/src/ui/DatePicker.tsx +421 -0
- package/src/ui/Input.tsx +30 -0
- package/src/ui/Markdown.test.tsx +32 -0
- package/src/ui/Markdown.tsx +213 -0
- package/src/ui/Menu.test.tsx +85 -0
- package/src/ui/Menu.tsx +216 -0
- package/src/ui/Popover.tsx +218 -0
- package/src/ui/Radio.test.tsx +29 -0
- package/src/ui/Radio.tsx +127 -0
- package/src/ui/Select.tsx +40 -0
- package/src/ui/Switch.test.tsx +17 -0
- package/src/ui/Switch.tsx +53 -0
- package/src/ui/Tabs.test.tsx +29 -0
- package/src/ui/Tabs.tsx +128 -0
- package/src/ui/Textarea.tsx +27 -0
- package/src/ui/Tooltip.test.tsx +28 -0
- package/src/ui/Tooltip.tsx +67 -0
- package/src/ui/controlStyles.ts +9 -0
- package/src/uiText.test.tsx +93 -0
- package/src/uiText.tsx +342 -0
- package/src/unwrap.test.ts +67 -0
- package/src/unwrap.ts +101 -0
- package/src/useResource.test.tsx +118 -0
- package/src/useResource.ts +110 -0
- package/src/useTerpClient.test.ts +35 -0
- package/tsconfig.json +17 -0
- package/vite.config.ts +14 -0
- package/vitest.setup.ts +58 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ArrowDownGlyph, ArrowUpGlyph, ColumnsGlyph } from "./glyphs";
|
|
2
|
+
import { DataViewMenu, useDataViewText } from "./internal";
|
|
3
|
+
import type { DataViewColumn } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface DataViewColumnSettingsProps<T> {
|
|
6
|
+
/** The user columns in effective order (system columns are never listed here). */
|
|
7
|
+
columns: DataViewColumn<T>[];
|
|
8
|
+
columnVisibility: Record<string, boolean>;
|
|
9
|
+
onColumnVisibleChange: (columnId: string, visible: boolean) => void;
|
|
10
|
+
onMoveColumn: (columnId: string, direction: -1 | 1) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The "view options" menu: per-column show/hide checkboxes and up/down reordering.
|
|
15
|
+
* Only user columns appear — the pinned system columns (select/expand/actions) are
|
|
16
|
+
* never hideable or reorderable, and are skipped when computing reorder targets
|
|
17
|
+
* because this list simply does not contain them.
|
|
18
|
+
*/
|
|
19
|
+
export function DataViewColumnSettings<T>({
|
|
20
|
+
columns,
|
|
21
|
+
columnVisibility,
|
|
22
|
+
onColumnVisibleChange,
|
|
23
|
+
onMoveColumn,
|
|
24
|
+
}: DataViewColumnSettingsProps<T>) {
|
|
25
|
+
const { strings, resolve } = useDataViewText();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<DataViewMenu
|
|
29
|
+
trigger={
|
|
30
|
+
<>
|
|
31
|
+
<ColumnsGlyph />
|
|
32
|
+
<span style={{ fontSize: "var(--font-size-sm)" }}>{resolve(strings.viewOptions)}</span>
|
|
33
|
+
</>
|
|
34
|
+
}
|
|
35
|
+
triggerLabel={resolve(strings.viewOptions)}
|
|
36
|
+
>
|
|
37
|
+
{() => (
|
|
38
|
+
<div style={{ display: "grid", gap: "var(--space-1)" }}>
|
|
39
|
+
<div
|
|
40
|
+
style={{
|
|
41
|
+
padding: "var(--space-1) var(--space-2)",
|
|
42
|
+
fontSize: "var(--font-size-sm)",
|
|
43
|
+
fontWeight: "var(--font-weight-medium)" as never,
|
|
44
|
+
color: "var(--color-neutral-500)",
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{resolve(strings.columns)}
|
|
48
|
+
</div>
|
|
49
|
+
{columns.map((column, index) => {
|
|
50
|
+
const label = resolve(column.meta?.label ?? column.header);
|
|
51
|
+
const visible = columnVisibility[column.id] !== false;
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
key={column.id}
|
|
55
|
+
style={{
|
|
56
|
+
display: "flex",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
gap: "var(--space-2)",
|
|
59
|
+
padding: "var(--space-1) var(--space-2)",
|
|
60
|
+
borderRadius: "var(--radius-sm)",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<label
|
|
64
|
+
style={{
|
|
65
|
+
display: "flex",
|
|
66
|
+
alignItems: "center",
|
|
67
|
+
gap: "var(--space-2)",
|
|
68
|
+
flex: 1,
|
|
69
|
+
cursor: "pointer",
|
|
70
|
+
fontSize: "var(--font-size-sm)",
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<input
|
|
74
|
+
type="checkbox"
|
|
75
|
+
checked={visible}
|
|
76
|
+
onChange={(event) => onColumnVisibleChange(column.id, event.target.checked)}
|
|
77
|
+
aria-label={label}
|
|
78
|
+
/>
|
|
79
|
+
{label}
|
|
80
|
+
</label>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
aria-label={`${resolve(strings.moveUp)}: ${label}`}
|
|
84
|
+
disabled={index === 0}
|
|
85
|
+
onClick={() => onMoveColumn(column.id, -1)}
|
|
86
|
+
style={reorderButtonStyle(index === 0)}
|
|
87
|
+
>
|
|
88
|
+
<ArrowUpGlyph size={14} />
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
aria-label={`${resolve(strings.moveDown)}: ${label}`}
|
|
93
|
+
disabled={index === columns.length - 1}
|
|
94
|
+
onClick={() => onMoveColumn(column.id, 1)}
|
|
95
|
+
style={reorderButtonStyle(index === columns.length - 1)}
|
|
96
|
+
>
|
|
97
|
+
<ArrowDownGlyph size={14} />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</DataViewMenu>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function reorderButtonStyle(disabled: boolean) {
|
|
109
|
+
return {
|
|
110
|
+
display: "inline-flex",
|
|
111
|
+
padding: "var(--space-1)",
|
|
112
|
+
background: "transparent",
|
|
113
|
+
border: "none",
|
|
114
|
+
borderRadius: "var(--radius-sm)",
|
|
115
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
116
|
+
color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
|
|
117
|
+
} as const;
|
|
118
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { ChevronDownGlyph, ChevronRightGlyph } from "./glyphs";
|
|
4
|
+
import { useDataViewText } from "./internal";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The expand-toggle chevron rendered in the expand system column (and on cards).
|
|
8
|
+
* Stops click propagation so toggling never triggers row navigation.
|
|
9
|
+
*/
|
|
10
|
+
export function DataViewExpandToggle({
|
|
11
|
+
expanded,
|
|
12
|
+
onToggle,
|
|
13
|
+
}: {
|
|
14
|
+
expanded: boolean;
|
|
15
|
+
onToggle: () => void;
|
|
16
|
+
}) {
|
|
17
|
+
const { strings, resolve } = useDataViewText();
|
|
18
|
+
return (
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
aria-label={resolve(expanded ? strings.collapseRow : strings.expandRow)}
|
|
22
|
+
aria-expanded={expanded}
|
|
23
|
+
onClick={(event) => {
|
|
24
|
+
event.stopPropagation();
|
|
25
|
+
onToggle();
|
|
26
|
+
}}
|
|
27
|
+
style={{
|
|
28
|
+
display: "inline-flex",
|
|
29
|
+
padding: "var(--space-1)",
|
|
30
|
+
background: "transparent",
|
|
31
|
+
border: "none",
|
|
32
|
+
borderRadius: "var(--radius-sm)",
|
|
33
|
+
cursor: "pointer",
|
|
34
|
+
color: "var(--color-neutral-500)",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{expanded ? <ChevronDownGlyph /> : <ChevronRightGlyph />}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The full-width detail panel row rendered directly under an expanded row: one cell
|
|
44
|
+
* spanning every column, visually attached to its parent row.
|
|
45
|
+
*/
|
|
46
|
+
export function DataViewExpandableRow({
|
|
47
|
+
colSpan,
|
|
48
|
+
children,
|
|
49
|
+
}: {
|
|
50
|
+
colSpan: number;
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<tr>
|
|
55
|
+
<td
|
|
56
|
+
colSpan={colSpan}
|
|
57
|
+
style={{
|
|
58
|
+
padding: "var(--space-3) var(--space-4)",
|
|
59
|
+
background: "var(--color-neutral-50)",
|
|
60
|
+
borderBottom: "1px solid var(--color-neutral-200)",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
import { useDataViewText } from "./internal";
|
|
4
|
+
import {
|
|
5
|
+
PageFirstGlyph,
|
|
6
|
+
PageLastGlyph,
|
|
7
|
+
PageNextGlyph,
|
|
8
|
+
PagePrevGlyph,
|
|
9
|
+
} from "./glyphs";
|
|
10
|
+
import type { DataViewPaginationState } from "./hooks/useDataViewState";
|
|
11
|
+
|
|
12
|
+
export interface DataViewPaginationProps {
|
|
13
|
+
pagination: DataViewPaginationState;
|
|
14
|
+
totalCount: number;
|
|
15
|
+
onPaginationChange: (pagination: DataViewPaginationState) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pagerButtonStyle = (disabled: boolean): CSSProperties => ({
|
|
19
|
+
display: "inline-flex",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
minHeight: "2rem",
|
|
22
|
+
padding: "var(--space-1) var(--space-2)",
|
|
23
|
+
background: "var(--color-neutral-0)",
|
|
24
|
+
border: "1px solid var(--color-neutral-300)",
|
|
25
|
+
borderRadius: "var(--radius-md)",
|
|
26
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
27
|
+
color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The footer pagination bar: "X–Y of Z results", the current page / page count, and
|
|
32
|
+
* first / prev / next / last controls (disabled at bounds; page controls hidden when
|
|
33
|
+
* there is only one page).
|
|
34
|
+
*/
|
|
35
|
+
export function DataViewPagination({
|
|
36
|
+
pagination,
|
|
37
|
+
totalCount,
|
|
38
|
+
onPaginationChange,
|
|
39
|
+
}: DataViewPaginationProps) {
|
|
40
|
+
const { strings, format } = useDataViewText();
|
|
41
|
+
|
|
42
|
+
const pageCount = Math.max(1, Math.ceil(totalCount / pagination.pageSize));
|
|
43
|
+
const pageIndex = Math.min(pagination.pageIndex, pageCount - 1);
|
|
44
|
+
const from = totalCount === 0 ? 0 : pageIndex * pagination.pageSize + 1;
|
|
45
|
+
const to = Math.min(totalCount, (pageIndex + 1) * pagination.pageSize);
|
|
46
|
+
|
|
47
|
+
const goTo = (index: number) => onPaginationChange({ ...pagination, pageIndex: index });
|
|
48
|
+
const atFirst = pageIndex === 0;
|
|
49
|
+
const atLast = pageIndex >= pageCount - 1;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
display: "flex",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
gap: "var(--space-3)",
|
|
58
|
+
flexWrap: "wrap",
|
|
59
|
+
padding: "var(--space-2) var(--space-3)",
|
|
60
|
+
borderTop: "1px solid var(--color-neutral-200)",
|
|
61
|
+
fontSize: "var(--font-size-sm)",
|
|
62
|
+
color: "var(--color-neutral-500)",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<span>{format(strings.resultsRange, { from, to, total: totalCount })}</span>
|
|
66
|
+
{pageCount > 1 && (
|
|
67
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-2)" }}>
|
|
68
|
+
<span>{format(strings.pageOf, { page: pageIndex + 1, pages: pageCount })}</span>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
aria-label={format(strings.firstPage, {})}
|
|
72
|
+
disabled={atFirst}
|
|
73
|
+
onClick={() => goTo(0)}
|
|
74
|
+
data-terp="iconbutton"
|
|
75
|
+
style={pagerButtonStyle(atFirst)}
|
|
76
|
+
>
|
|
77
|
+
<PageFirstGlyph />
|
|
78
|
+
</button>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
aria-label={format(strings.previousPage, {})}
|
|
82
|
+
disabled={atFirst}
|
|
83
|
+
onClick={() => goTo(pageIndex - 1)}
|
|
84
|
+
data-terp="iconbutton"
|
|
85
|
+
style={pagerButtonStyle(atFirst)}
|
|
86
|
+
>
|
|
87
|
+
<PagePrevGlyph />
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
aria-label={format(strings.nextPage, {})}
|
|
92
|
+
disabled={atLast}
|
|
93
|
+
onClick={() => goTo(pageIndex + 1)}
|
|
94
|
+
data-terp="iconbutton"
|
|
95
|
+
style={pagerButtonStyle(atLast)}
|
|
96
|
+
>
|
|
97
|
+
<PageNextGlyph />
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label={format(strings.lastPage, {})}
|
|
102
|
+
disabled={atLast}
|
|
103
|
+
onClick={() => goTo(pageCount - 1)}
|
|
104
|
+
data-terp="iconbutton"
|
|
105
|
+
style={pagerButtonStyle(atLast)}
|
|
106
|
+
>
|
|
107
|
+
<PageLastGlyph />
|
|
108
|
+
</button>
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { MouseEvent, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { EllipsisGlyph } from "./glyphs";
|
|
4
|
+
import { DataViewMenu, DataViewMenuItem, useDataViewText } from "./internal";
|
|
5
|
+
import type { DataViewRowAction } from "./types";
|
|
6
|
+
import { resolveRowFlag } from "./types";
|
|
7
|
+
|
|
8
|
+
export type DataViewRowActionsLayout = "menu" | "inline";
|
|
9
|
+
|
|
10
|
+
export interface DataViewRowActionsProps<T> {
|
|
11
|
+
row: T;
|
|
12
|
+
actions: DataViewRowAction<T>[];
|
|
13
|
+
/** "menu" (default): ellipsis dropdown, `inline`-flagged actions beside it. */
|
|
14
|
+
layout: DataViewRowActionsLayout;
|
|
15
|
+
/** On mobile all standard actions collapse into the menu regardless of flags. */
|
|
16
|
+
isMobile: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function InlineActionButton<T>({ action, row }: { action: DataViewRowAction<T>; row: T }) {
|
|
20
|
+
const { resolve } = useDataViewText();
|
|
21
|
+
const disabled = resolveRowFlag(action.disabled, row);
|
|
22
|
+
const destructive = action.variant === "destructive";
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
onClick={() => action.onClick?.(row)}
|
|
28
|
+
style={{
|
|
29
|
+
font: "inherit",
|
|
30
|
+
fontSize: "var(--font-size-sm)",
|
|
31
|
+
display: "inline-flex",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
minHeight: "2rem",
|
|
34
|
+
gap: "var(--space-1)",
|
|
35
|
+
padding: "var(--space-1) var(--space-2)",
|
|
36
|
+
background: "transparent",
|
|
37
|
+
border: "1px solid var(--color-neutral-300)",
|
|
38
|
+
borderRadius: "var(--radius-md)",
|
|
39
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
40
|
+
color: disabled
|
|
41
|
+
? "var(--color-neutral-300)"
|
|
42
|
+
: destructive
|
|
43
|
+
? "var(--color-status-danger)"
|
|
44
|
+
: "var(--color-neutral-700)",
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{action.icon !== undefined && (
|
|
48
|
+
<span aria-hidden style={{ display: "inline-flex" }}>{action.icon}</span>
|
|
49
|
+
)}
|
|
50
|
+
{resolve(action.label)}
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The per-row actions control: renders custom controls inline always, standard actions
|
|
57
|
+
* either as buttons ("inline" layout / `inline` flag) or in an ellipsis menu. The whole
|
|
58
|
+
* cluster stops click propagation so actions never trigger row navigation.
|
|
59
|
+
*/
|
|
60
|
+
export function DataViewRowActions<T>({
|
|
61
|
+
row,
|
|
62
|
+
actions,
|
|
63
|
+
layout,
|
|
64
|
+
isMobile,
|
|
65
|
+
}: DataViewRowActionsProps<T>) {
|
|
66
|
+
const { strings, resolve } = useDataViewText();
|
|
67
|
+
|
|
68
|
+
const visible = actions.filter((action) => !resolveRowFlag(action.hidden, row));
|
|
69
|
+
if (visible.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Custom controls always render inline and own their interaction surface.
|
|
74
|
+
const custom = visible.filter((action) => action.render !== undefined);
|
|
75
|
+
const standard = visible.filter((action) => action.render === undefined);
|
|
76
|
+
const inline = isMobile
|
|
77
|
+
? []
|
|
78
|
+
: layout === "inline"
|
|
79
|
+
? standard
|
|
80
|
+
: standard.filter((action) => action.inline === true);
|
|
81
|
+
const menu = standard.filter((action) => !inline.includes(action));
|
|
82
|
+
|
|
83
|
+
const stop = (event: MouseEvent) => event.stopPropagation();
|
|
84
|
+
|
|
85
|
+
const renderCustom = (action: DataViewRowAction<T>, index: number): ReactNode => (
|
|
86
|
+
<span key={`custom-${index}`} style={{ display: "inline-flex" }}>
|
|
87
|
+
{action.render?.(row)}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<span
|
|
93
|
+
onClick={stop}
|
|
94
|
+
style={{
|
|
95
|
+
display: "inline-flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
justifyContent: "flex-end",
|
|
98
|
+
gap: "var(--space-1)",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{custom.map(renderCustom)}
|
|
102
|
+
{inline.map((action, index) => (
|
|
103
|
+
<InlineActionButton key={`inline-${index}`} action={action} row={row} />
|
|
104
|
+
))}
|
|
105
|
+
{menu.length > 0 && (
|
|
106
|
+
<DataViewMenu
|
|
107
|
+
trigger={<EllipsisGlyph />}
|
|
108
|
+
triggerLabel={resolve(strings.moreActions)}
|
|
109
|
+
>
|
|
110
|
+
{(close) => (
|
|
111
|
+
<>
|
|
112
|
+
{menu.map((action, index) => (
|
|
113
|
+
<DataViewMenuItem
|
|
114
|
+
key={index}
|
|
115
|
+
label={resolve(action.label)}
|
|
116
|
+
icon={action.icon}
|
|
117
|
+
destructive={action.variant === "destructive"}
|
|
118
|
+
disabled={resolveRowFlag(action.disabled, row)}
|
|
119
|
+
onSelect={() => {
|
|
120
|
+
close();
|
|
121
|
+
action.onClick?.(row);
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</DataViewMenu>
|
|
128
|
+
)}
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
}
|