@evenicanpm/portal-editor-ui 1.5.0 → 1.6.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/package.json +5 -4
- package/src/editor.tsx +34 -0
- package/src/features/editor/components/column-list/column-card/column-card-options.tsx +48 -0
- package/src/features/editor/components/column-list/column-card/column-card-title.tsx +13 -0
- package/src/features/editor/components/column-list/column-card/column-card.tsx +134 -0
- package/src/features/editor/components/column-list/column-card/drag-handle.tsx +32 -0
- package/src/features/editor/components/column-list/column-card/editable-label.tsx +132 -0
- package/src/features/editor/components/column-list/column-card/index.ts +1 -0
- package/src/features/editor/components/column-list/column-list-section.tsx +49 -0
- package/src/features/editor/components/column-list/column-list-skeleton.tsx +49 -0
- package/src/features/editor/components/column-list/column-list-state.ts +92 -0
- package/src/features/editor/components/column-list/column-list-title.tsx +18 -0
- package/src/features/editor/components/column-list/column-list.tsx +212 -0
- package/src/features/editor/components/column-list/index.ts +1 -0
- package/src/features/editor/components/editor-dms-card.tsx +79 -0
- package/src/features/editor/components/editor-tabs.tsx +42 -0
- package/src/features/editor/components/editor.tsx +62 -0
- package/src/features/editor/index.ts +1 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evenicanpm/portal-editor-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
".": "./src/index.ts"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
19
20
|
],
|
|
20
21
|
"types": "./dist/index.d.ts",
|
|
21
22
|
"devDependencies": {
|
|
@@ -40,8 +41,8 @@
|
|
|
40
41
|
"@dnd-kit/core": "^6.3.1",
|
|
41
42
|
"@dnd-kit/sortable": "^10.0.0",
|
|
42
43
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
|
-
"@evenicanpm/portal-types-schemas": "^1.
|
|
44
|
+
"@evenicanpm/portal-types-schemas": "^1.6.0",
|
|
44
45
|
"@tanstack/react-query": "^5.90.11"
|
|
45
46
|
},
|
|
46
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "9b18fe6294cc0e57332ec8703825a609ac7b73b2"
|
|
47
48
|
}
|
package/src/editor.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import { Editor } from "./features/editor/components/editor";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Main Entry point properties into portal package
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
type PortalProps = {};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The main **e4Portal** entry point.
|
|
13
|
+
* * This is a React Functional Component that provides the core context
|
|
14
|
+
* and layout for the application. It is required to wrap all components
|
|
15
|
+
* that use the library's hooks or sub-components.
|
|
16
|
+
*
|
|
17
|
+
*
|
|
18
|
+
* @param props - The properties passed to the Portal component.
|
|
19
|
+
* @returns A React Element rendering the main application structure.
|
|
20
|
+
*/
|
|
21
|
+
const Portal = ({ children }: PropsWithChildren<PortalProps>) => {
|
|
22
|
+
return <>{children}</>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* # Feature: Editor
|
|
28
|
+
* This is the editor feature component.
|
|
29
|
+
* @params props
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
Portal.Editor = Editor;
|
|
33
|
+
|
|
34
|
+
export { Portal, type PortalProps };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Checkbox, FormControlLabel, Stack } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
export type ColumnCardOptionsValue = {
|
|
4
|
+
isSortable: boolean;
|
|
5
|
+
isSearchable: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ColumnCardOptionsProps = {
|
|
9
|
+
value: ColumnCardOptionsValue;
|
|
10
|
+
onChange: (patch: Partial<ColumnCardOptionsValue>) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function ColumnCardOptions({
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
}: Readonly<ColumnCardOptionsProps>) {
|
|
17
|
+
return (
|
|
18
|
+
<Stack
|
|
19
|
+
direction="row"
|
|
20
|
+
spacing={2}
|
|
21
|
+
alignItems="center"
|
|
22
|
+
justifyContent={{ xs: "center", lg: "flex-end" }}
|
|
23
|
+
sx={{ width: { xs: "100%", sm: "auto" } }}
|
|
24
|
+
>
|
|
25
|
+
<FormControlLabel
|
|
26
|
+
sx={{ m: 0 }}
|
|
27
|
+
control={
|
|
28
|
+
<Checkbox
|
|
29
|
+
checked={value.isSortable}
|
|
30
|
+
onChange={(e) => onChange({ isSortable: e.target.checked })}
|
|
31
|
+
/>
|
|
32
|
+
}
|
|
33
|
+
label="sortable"
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<FormControlLabel
|
|
37
|
+
sx={{ m: 0 }}
|
|
38
|
+
control={
|
|
39
|
+
<Checkbox
|
|
40
|
+
checked={value.isSearchable}
|
|
41
|
+
onChange={(e) => onChange({ isSearchable: e.target.checked })}
|
|
42
|
+
/>
|
|
43
|
+
}
|
|
44
|
+
label="searchable"
|
|
45
|
+
/>
|
|
46
|
+
</Stack>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Typography } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
export type ColumnCardTitleProps = {
|
|
4
|
+
value: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function ColumnCardTitle({ value }: Readonly<ColumnCardTitleProps>) {
|
|
8
|
+
return (
|
|
9
|
+
<Typography fontWeight={700} noWrap>
|
|
10
|
+
{value}
|
|
11
|
+
</Typography>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
2
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
3
|
+
import type { Column } from "@evenicanpm/portal-types-schemas";
|
|
4
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
5
|
+
import { Box, Card, CardContent, IconButton, Stack } from "@mui/material";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { ColumnCardOptions } from "./column-card-options";
|
|
8
|
+
import { ColumnCardTitle } from "./column-card-title";
|
|
9
|
+
import { DragHandle } from "./drag-handle";
|
|
10
|
+
import { EditableLabel } from "./editable-label";
|
|
11
|
+
|
|
12
|
+
export type ColumnCardProps = {
|
|
13
|
+
field: Column;
|
|
14
|
+
onChange: (id: string, patch: Partial<Column>) => void;
|
|
15
|
+
onRemove: (id: string) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function ColumnCard({
|
|
19
|
+
field,
|
|
20
|
+
onChange,
|
|
21
|
+
onRemove,
|
|
22
|
+
}: Readonly<ColumnCardProps>) {
|
|
23
|
+
const [isEditingLabel, setIsEditingLabel] = React.useState(false);
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
attributes,
|
|
27
|
+
listeners,
|
|
28
|
+
setNodeRef,
|
|
29
|
+
transform,
|
|
30
|
+
transition,
|
|
31
|
+
isDragging,
|
|
32
|
+
} = useSortable({ id: field.Data, disabled: isEditingLabel });
|
|
33
|
+
|
|
34
|
+
const style: React.CSSProperties = {
|
|
35
|
+
transform: CSS.Transform.toString(transform),
|
|
36
|
+
transition,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Card
|
|
41
|
+
ref={setNodeRef}
|
|
42
|
+
style={style}
|
|
43
|
+
variant="outlined"
|
|
44
|
+
sx={{
|
|
45
|
+
mb: 2,
|
|
46
|
+
height: { sm: 130, md: 100 },
|
|
47
|
+
borderRadius: 2,
|
|
48
|
+
bgcolor: isDragging ? "grey.100" : "background.paper",
|
|
49
|
+
boxShadow: isDragging ? 3 : 0,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<CardContent sx={{ py: 2, "&:last-child": { pb: 2 } }}>
|
|
53
|
+
<Stack
|
|
54
|
+
direction={{ xs: "column", sm: "row" }}
|
|
55
|
+
spacing={{ xs: 1.25, sm: 1 }}
|
|
56
|
+
alignItems={{ xs: "stretch", sm: "center" }}
|
|
57
|
+
>
|
|
58
|
+
<Stack
|
|
59
|
+
direction="row"
|
|
60
|
+
spacing={1}
|
|
61
|
+
alignItems="center"
|
|
62
|
+
sx={{ width: "100%" }}
|
|
63
|
+
>
|
|
64
|
+
<DragHandle attributes={attributes} listeners={listeners} />
|
|
65
|
+
|
|
66
|
+
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
67
|
+
<Box
|
|
68
|
+
sx={{
|
|
69
|
+
display: "flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
gap: 1,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<ColumnCardTitle value={field.Data} />
|
|
75
|
+
|
|
76
|
+
{/* Mobile close icon */}
|
|
77
|
+
<IconButton
|
|
78
|
+
onClick={() => onRemove(field.Data)}
|
|
79
|
+
aria-label={`Remove ${field.Data}`}
|
|
80
|
+
color="error"
|
|
81
|
+
size="small"
|
|
82
|
+
sx={{
|
|
83
|
+
ml: "auto",
|
|
84
|
+
display: { xs: "inline-flex", sm: "none" },
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<CloseIcon />
|
|
88
|
+
</IconButton>
|
|
89
|
+
</Box>
|
|
90
|
+
|
|
91
|
+
<Box
|
|
92
|
+
onFocusCapture={(e) => {
|
|
93
|
+
if (e.target instanceof HTMLInputElement)
|
|
94
|
+
setIsEditingLabel(true);
|
|
95
|
+
}}
|
|
96
|
+
onBlurCapture={() => setIsEditingLabel(false)}
|
|
97
|
+
>
|
|
98
|
+
<EditableLabel
|
|
99
|
+
value={field.Label ?? ""}
|
|
100
|
+
onCommit={(next) => onChange(field.Data, { Label: next })}
|
|
101
|
+
/>
|
|
102
|
+
</Box>
|
|
103
|
+
</Box>
|
|
104
|
+
</Stack>
|
|
105
|
+
|
|
106
|
+
<ColumnCardOptions
|
|
107
|
+
value={{
|
|
108
|
+
isSortable: !!field.IsSortable,
|
|
109
|
+
isSearchable: !!field.IsSearchable,
|
|
110
|
+
}}
|
|
111
|
+
onChange={(patch) => {
|
|
112
|
+
const next: Partial<Column> = {};
|
|
113
|
+
if (patch.isSortable !== undefined)
|
|
114
|
+
next.IsSortable = patch.isSortable;
|
|
115
|
+
if (patch.isSearchable !== undefined)
|
|
116
|
+
next.IsSearchable = patch.isSearchable;
|
|
117
|
+
onChange(field.Data, next);
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
{/* Desktop close icon */}
|
|
122
|
+
<IconButton
|
|
123
|
+
onClick={() => onRemove(field.Data)}
|
|
124
|
+
aria-label={`Remove ${field.Data}`}
|
|
125
|
+
color="error"
|
|
126
|
+
sx={{ display: { xs: "none", sm: "inline-flex" } }}
|
|
127
|
+
>
|
|
128
|
+
<CloseIcon />
|
|
129
|
+
</IconButton>
|
|
130
|
+
</Stack>
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DraggableAttributes,
|
|
3
|
+
DraggableSyntheticListeners,
|
|
4
|
+
} from "@dnd-kit/core";
|
|
5
|
+
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
|
6
|
+
import { Box, IconButton } from "@mui/material";
|
|
7
|
+
|
|
8
|
+
export type DragHandleProps = {
|
|
9
|
+
attributes: DraggableAttributes;
|
|
10
|
+
listeners?: DraggableSyntheticListeners;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function DragHandle({
|
|
14
|
+
attributes,
|
|
15
|
+
listeners,
|
|
16
|
+
}: Readonly<DragHandleProps>) {
|
|
17
|
+
return (
|
|
18
|
+
<Box
|
|
19
|
+
sx={{
|
|
20
|
+
display: "flex",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
cursor: "grab",
|
|
23
|
+
touchAction: "none",
|
|
24
|
+
flexShrink: 0,
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<IconButton size="small" {...attributes} {...listeners}>
|
|
28
|
+
<DragIndicatorIcon />
|
|
29
|
+
</IconButton>
|
|
30
|
+
</Box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
2
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
3
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
IconButton,
|
|
7
|
+
Stack,
|
|
8
|
+
TextField,
|
|
9
|
+
Tooltip,
|
|
10
|
+
Typography,
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
|
|
14
|
+
export type EditableLabelProps = {
|
|
15
|
+
value: string;
|
|
16
|
+
onCommit: (next: string) => void;
|
|
17
|
+
labelPrefix?: string;
|
|
18
|
+
inputWidth?: { xs: string | number; sm: string | number };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function EditableLabel({
|
|
22
|
+
value,
|
|
23
|
+
onCommit,
|
|
24
|
+
labelPrefix = "label:",
|
|
25
|
+
inputWidth = { xs: "100%", sm: 260 },
|
|
26
|
+
}: Readonly<EditableLabelProps>) {
|
|
27
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
28
|
+
const [draft, setDraft] = React.useState(value);
|
|
29
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (!isEditing) return;
|
|
33
|
+
setDraft(value);
|
|
34
|
+
queueMicrotask(() => inputRef.current?.focus());
|
|
35
|
+
}, [isEditing, value]);
|
|
36
|
+
|
|
37
|
+
const commit = () => {
|
|
38
|
+
const next = draft.trim();
|
|
39
|
+
setIsEditing(false);
|
|
40
|
+
if (next !== value) onCommit(next);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const cancel = () => {
|
|
44
|
+
setIsEditing(false);
|
|
45
|
+
setDraft(value);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Stack
|
|
50
|
+
direction={{ xs: "row" }}
|
|
51
|
+
spacing={{ xs: 1 }}
|
|
52
|
+
alignItems={{ xs: "center" }}
|
|
53
|
+
sx={{ mt: 0.25 }}
|
|
54
|
+
>
|
|
55
|
+
<Typography variant="body2" color="text.secondary" sx={{ flexShrink: 0 }}>
|
|
56
|
+
{labelPrefix}
|
|
57
|
+
</Typography>
|
|
58
|
+
|
|
59
|
+
<Box
|
|
60
|
+
sx={{
|
|
61
|
+
display: "flex",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
gap: 1,
|
|
64
|
+
minWidth: 0,
|
|
65
|
+
width: "100%",
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{isEditing ? (
|
|
69
|
+
<>
|
|
70
|
+
<TextField
|
|
71
|
+
inputRef={inputRef}
|
|
72
|
+
value={draft}
|
|
73
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
74
|
+
size="small"
|
|
75
|
+
variant="outlined"
|
|
76
|
+
onKeyDown={(e) => {
|
|
77
|
+
if (e.key === "Enter") commit();
|
|
78
|
+
if (e.key === "Escape") cancel();
|
|
79
|
+
}}
|
|
80
|
+
onBlur={commit}
|
|
81
|
+
sx={{
|
|
82
|
+
width: inputWidth,
|
|
83
|
+
maxWidth: "100%",
|
|
84
|
+
"& .MuiOutlinedInput-root": { height: 32 },
|
|
85
|
+
"& .MuiOutlinedInput-input": {
|
|
86
|
+
padding: "6px 8px",
|
|
87
|
+
fontSize: "0.875rem",
|
|
88
|
+
},
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<Tooltip title="Save">
|
|
93
|
+
<IconButton
|
|
94
|
+
size="small"
|
|
95
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
96
|
+
onClick={commit}
|
|
97
|
+
>
|
|
98
|
+
<CheckIcon fontSize="small" />
|
|
99
|
+
</IconButton>
|
|
100
|
+
</Tooltip>
|
|
101
|
+
|
|
102
|
+
<Tooltip title="Cancel">
|
|
103
|
+
<IconButton
|
|
104
|
+
size="small"
|
|
105
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
106
|
+
onClick={cancel}
|
|
107
|
+
>
|
|
108
|
+
<CloseIcon fontSize="small" />
|
|
109
|
+
</IconButton>
|
|
110
|
+
</Tooltip>
|
|
111
|
+
</>
|
|
112
|
+
) : (
|
|
113
|
+
<>
|
|
114
|
+
<Typography
|
|
115
|
+
variant="body2"
|
|
116
|
+
color="text.secondary"
|
|
117
|
+
sx={{ fontWeight: 600 }}
|
|
118
|
+
>
|
|
119
|
+
{value}
|
|
120
|
+
</Typography>
|
|
121
|
+
|
|
122
|
+
<Tooltip title="Edit label">
|
|
123
|
+
<IconButton size="small" onClick={() => setIsEditing(true)}>
|
|
124
|
+
<EditIcon fontSize="small" />
|
|
125
|
+
</IconButton>
|
|
126
|
+
</Tooltip>
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
</Box>
|
|
130
|
+
</Stack>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnCard } from "./column-card";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Paper } from "@mui/material";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import type { EditorQueries } from "../editor";
|
|
5
|
+
import { ColumnList } from "./column-list";
|
|
6
|
+
import { ColumnListSkeleton } from "./column-list-skeleton";
|
|
7
|
+
import { ColumnListTitle } from "./column-list-title";
|
|
8
|
+
|
|
9
|
+
interface ColumnProps {
|
|
10
|
+
currentResourceKey: string;
|
|
11
|
+
editorQueries: EditorQueries;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The view config column list section that handles
|
|
16
|
+
* fetching the initial view config data thats stored in the db
|
|
17
|
+
* and handles loading state skeleton view while data is pending
|
|
18
|
+
*/
|
|
19
|
+
export function ColumnListSection({
|
|
20
|
+
currentResourceKey,
|
|
21
|
+
editorQueries,
|
|
22
|
+
}: Readonly<ColumnProps>) {
|
|
23
|
+
const { data: config, isPending } = useQuery(
|
|
24
|
+
editorQueries.viewConfigQuery(currentResourceKey),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Paper
|
|
29
|
+
elevation={1}
|
|
30
|
+
sx={{
|
|
31
|
+
p: { xs: 2, sm: 3 },
|
|
32
|
+
mb: 3,
|
|
33
|
+
borderRadius: 2,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<ColumnListTitle selectedResourceKey={currentResourceKey} />
|
|
37
|
+
{isPending || !config ? (
|
|
38
|
+
<ColumnListSkeleton />
|
|
39
|
+
) : (
|
|
40
|
+
<ColumnList
|
|
41
|
+
key={currentResourceKey}
|
|
42
|
+
resourceKey={currentResourceKey}
|
|
43
|
+
initialState={config}
|
|
44
|
+
editorQueries={editorQueries}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</Paper>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Box, Skeleton } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
const CARD_HEIGHT = {
|
|
4
|
+
sm: 130,
|
|
5
|
+
md: 100,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function ColumnListSkeleton() {
|
|
9
|
+
const skeletonKeys = ["s1", "s2", "s3", "s4"];
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<Box
|
|
13
|
+
sx={{
|
|
14
|
+
display: "flex",
|
|
15
|
+
justifyContent: "flex-end",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
position: "relative",
|
|
18
|
+
mb: 2,
|
|
19
|
+
height: 40,
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Skeleton
|
|
23
|
+
variant="rounded"
|
|
24
|
+
animation="wave"
|
|
25
|
+
sx={{
|
|
26
|
+
minWidth: 150,
|
|
27
|
+
height: 36.5, // Standard MUI 'medium' button height
|
|
28
|
+
borderRadius: 1, // Match MUI default button border-radius (4px)
|
|
29
|
+
ml: "auto",
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
</Box>
|
|
33
|
+
{skeletonKeys.map((key) => (
|
|
34
|
+
<Skeleton
|
|
35
|
+
key={key}
|
|
36
|
+
variant="rounded"
|
|
37
|
+
animation="wave"
|
|
38
|
+
sx={{
|
|
39
|
+
width: "100%",
|
|
40
|
+
mb: 2,
|
|
41
|
+
height: { ...CARD_HEIGHT },
|
|
42
|
+
borderRadius: 2,
|
|
43
|
+
opacity: 0.4, // Softens the "block" look
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Column } from "@evenicanpm/portal-types-schemas";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reassigns sequential ColumnOrder values (1..n) to visible fields.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeVisibleOrders(list: Column[]): Column[] {
|
|
7
|
+
return list.map((f, idx) => ({ ...f, ColumnOrder: idx + 1 }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns fields that are currently visible (ColumnOrder not null),
|
|
12
|
+
* sorted by ColumnOrder.
|
|
13
|
+
*/
|
|
14
|
+
export function getVisibleFields(fields: Column[]): Column[] {
|
|
15
|
+
return fields
|
|
16
|
+
.filter((f) => f.ColumnOrder != null)
|
|
17
|
+
.sort((a, b) => (a.ColumnOrder ?? 1) - (b.ColumnOrder ?? 0));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns fields that are currently removed (ColumnOrder null),
|
|
22
|
+
* sorted alphabetically by label or data key.
|
|
23
|
+
*/
|
|
24
|
+
export function getRemovedFields(fields: Column[]): Column[] {
|
|
25
|
+
return fields
|
|
26
|
+
.filter((f) => f.ColumnOrder == null)
|
|
27
|
+
.sort((a, b) => (a.Label ?? a.Data).localeCompare(b.Label ?? b.Data));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Applies a partial update to a single field by id.
|
|
32
|
+
*/
|
|
33
|
+
export function patchField(
|
|
34
|
+
fields: Column[],
|
|
35
|
+
id: string,
|
|
36
|
+
patch: Partial<Column>,
|
|
37
|
+
): Column[] {
|
|
38
|
+
return fields.map((f) => (f.Data === id ? { ...f, ...patch } : f));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Removes a field from the visible list by setting ColumnOrder to null
|
|
43
|
+
* and re-normalizes remaining visible fields.
|
|
44
|
+
*/
|
|
45
|
+
export function removeField(fields: Column[], id: string): Column[] {
|
|
46
|
+
const next = fields.map((f) =>
|
|
47
|
+
f.Data === id ? { ...f, ColumnOrder: null } : f,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const stillVisible = getVisibleFields(next);
|
|
51
|
+
const renumbered = normalizeVisibleOrders(stillVisible);
|
|
52
|
+
|
|
53
|
+
return next.map((f) => renumbered.find((r) => r.Data === f.Data) ?? f);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Adds a previously removed field back to the end of the visible list
|
|
58
|
+
* and re-normalizes ordering.
|
|
59
|
+
*/
|
|
60
|
+
export function addFieldToEnd(fields: Column[], id: string): Column[] {
|
|
61
|
+
const next = fields.map((f) =>
|
|
62
|
+
f.Data === id ? { ...f, ColumnOrder: 999999 } : f,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const stillVisible = getVisibleFields(next);
|
|
66
|
+
const renumbered = normalizeVisibleOrders(stillVisible);
|
|
67
|
+
|
|
68
|
+
return next.map((f) => renumbered.find((r) => r.Data === f.Data) ?? f);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Reorders visible fields after a drag operation and updates ColumnOrder.
|
|
73
|
+
*/
|
|
74
|
+
export function reorderVisibleFields(
|
|
75
|
+
fields: Column[],
|
|
76
|
+
activeId: string,
|
|
77
|
+
overId: string,
|
|
78
|
+
arrayMoveFn: <T>(array: T[], from: number, to: number) => T[],
|
|
79
|
+
): Column[] {
|
|
80
|
+
const visible = getVisibleFields(fields);
|
|
81
|
+
|
|
82
|
+
const oldIndex = visible.findIndex((f) => f.Data === activeId);
|
|
83
|
+
const newIndex = visible.findIndex((f) => f.Data === overId);
|
|
84
|
+
|
|
85
|
+
if (oldIndex < 0 || newIndex < 0) return fields;
|
|
86
|
+
|
|
87
|
+
const moved = normalizeVisibleOrders(
|
|
88
|
+
arrayMoveFn(visible, oldIndex, newIndex),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return fields.map((f) => moved.find((m) => m.Data === f.Data) ?? f);
|
|
92
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Box, Typography } from "@mui/material";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
selectedResourceKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ColumnListTitle({ selectedResourceKey }: Readonly<Props>) {
|
|
10
|
+
return (
|
|
11
|
+
<Typography variant="h6" gutterBottom fontWeight={600}>
|
|
12
|
+
Configure Fields for:{" "}
|
|
13
|
+
<Box component="span" color="primary.main">
|
|
14
|
+
{selectedResourceKey}
|
|
15
|
+
</Box>
|
|
16
|
+
</Typography>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCenter,
|
|
3
|
+
DndContext,
|
|
4
|
+
type DragEndEvent,
|
|
5
|
+
PointerSensor,
|
|
6
|
+
TouchSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
} from "@dnd-kit/core";
|
|
10
|
+
import {
|
|
11
|
+
arrayMove,
|
|
12
|
+
SortableContext,
|
|
13
|
+
verticalListSortingStrategy,
|
|
14
|
+
} from "@dnd-kit/sortable";
|
|
15
|
+
import type { Column, ViewConfig } from "@evenicanpm/portal-types-schemas";
|
|
16
|
+
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
|
17
|
+
import {
|
|
18
|
+
Box,
|
|
19
|
+
Button,
|
|
20
|
+
Card,
|
|
21
|
+
CardContent,
|
|
22
|
+
Chip,
|
|
23
|
+
Menu,
|
|
24
|
+
MenuItem,
|
|
25
|
+
Typography,
|
|
26
|
+
} from "@mui/material";
|
|
27
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
28
|
+
import { useState } from "react";
|
|
29
|
+
import type { EditorQueries } from "../editor";
|
|
30
|
+
import { ColumnCard } from "./column-card";
|
|
31
|
+
import {
|
|
32
|
+
addFieldToEnd,
|
|
33
|
+
getRemovedFields,
|
|
34
|
+
getVisibleFields,
|
|
35
|
+
patchField,
|
|
36
|
+
removeField,
|
|
37
|
+
reorderVisibleFields,
|
|
38
|
+
} from "./column-list-state";
|
|
39
|
+
|
|
40
|
+
export type ColumnListProps = {
|
|
41
|
+
resourceKey: string;
|
|
42
|
+
initialState: ViewConfig;
|
|
43
|
+
editorQueries: EditorQueries;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function ColumnList({
|
|
47
|
+
resourceKey,
|
|
48
|
+
initialState,
|
|
49
|
+
editorQueries,
|
|
50
|
+
}: Readonly<ColumnListProps>) {
|
|
51
|
+
const columns = initialState.Columns;
|
|
52
|
+
const actions = initialState.Actions;
|
|
53
|
+
const [fields, setFields] = useState<Column[]>(columns ?? []);
|
|
54
|
+
const [addAnchor, setAddAnchor] = useState<null | HTMLElement>(null);
|
|
55
|
+
const addOpen = Boolean(addAnchor);
|
|
56
|
+
|
|
57
|
+
const sensors = useSensors(
|
|
58
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
59
|
+
useSensor(TouchSensor, {
|
|
60
|
+
activationConstraint: { delay: 150, tolerance: 5 },
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const qc = useQueryClient();
|
|
65
|
+
|
|
66
|
+
const saveConfig = useMutation({
|
|
67
|
+
...editorQueries.saveViewConfigMutation(resourceKey),
|
|
68
|
+
onSuccess: () => {
|
|
69
|
+
qc.invalidateQueries({ queryKey: ["viewConfig", resourceKey] });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const visibleFields = getVisibleFields(fields);
|
|
74
|
+
const removedFields = getRemovedFields(fields);
|
|
75
|
+
|
|
76
|
+
const onDragEnd = (event: DragEndEvent) => {
|
|
77
|
+
const { active, over } = event;
|
|
78
|
+
if (!over) return;
|
|
79
|
+
if (active.id === over.id) return;
|
|
80
|
+
|
|
81
|
+
setFields((prev) =>
|
|
82
|
+
reorderVisibleFields(prev, String(active.id), String(over.id), arrayMove),
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onChangeField = (id: string, patch: Partial<Column>) => {
|
|
87
|
+
setFields((prev) => patchField(prev, id, patch));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const onRemoveField = (id: string) => {
|
|
91
|
+
setFields((prev) => removeField(prev, id));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onAddField = (id: string) => {
|
|
95
|
+
setFields((prev) => addFieldToEnd(prev, id));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isDirty =
|
|
99
|
+
JSON.stringify(initialState.Columns) !== JSON.stringify(fields);
|
|
100
|
+
|
|
101
|
+
const onSaveClick = () => {
|
|
102
|
+
if (!isDirty) return;
|
|
103
|
+
|
|
104
|
+
saveConfig.mutate({
|
|
105
|
+
key: resourceKey,
|
|
106
|
+
viewConfig: { Columns: fields, Actions: actions },
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<Box
|
|
113
|
+
sx={{
|
|
114
|
+
display: "flex",
|
|
115
|
+
justifyContent: "flex-end",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
position: "relative",
|
|
118
|
+
mb: 2,
|
|
119
|
+
height: 40,
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{isDirty && (
|
|
123
|
+
<Chip
|
|
124
|
+
label="Unsaved Changes"
|
|
125
|
+
size="small"
|
|
126
|
+
color="warning"
|
|
127
|
+
variant="outlined"
|
|
128
|
+
sx={{
|
|
129
|
+
fontWeight: "bold",
|
|
130
|
+
height: 20,
|
|
131
|
+
position: "absolute",
|
|
132
|
+
left: 0,
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
<Button
|
|
137
|
+
variant="contained"
|
|
138
|
+
onClick={onSaveClick}
|
|
139
|
+
disabled={!isDirty}
|
|
140
|
+
sx={{ minWidth: 150 }}
|
|
141
|
+
>
|
|
142
|
+
Save Changes
|
|
143
|
+
</Button>
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
<DndContext
|
|
147
|
+
sensors={sensors}
|
|
148
|
+
collisionDetection={closestCenter}
|
|
149
|
+
onDragEnd={onDragEnd}
|
|
150
|
+
>
|
|
151
|
+
{fields.length === 0 ? (
|
|
152
|
+
<Card
|
|
153
|
+
variant="outlined"
|
|
154
|
+
sx={{
|
|
155
|
+
textAlign: "center",
|
|
156
|
+
color: "text.secondary",
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<CardContent>
|
|
160
|
+
<Typography variant="subtitle1">No fields found</Typography>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
) : (
|
|
164
|
+
<SortableContext
|
|
165
|
+
items={visibleFields.map((f) => f.Data)}
|
|
166
|
+
strategy={verticalListSortingStrategy}
|
|
167
|
+
>
|
|
168
|
+
<Box>
|
|
169
|
+
{visibleFields.map((field) => (
|
|
170
|
+
<ColumnCard
|
|
171
|
+
key={field.Data}
|
|
172
|
+
field={field}
|
|
173
|
+
onChange={onChangeField}
|
|
174
|
+
onRemove={onRemoveField}
|
|
175
|
+
/>
|
|
176
|
+
))}
|
|
177
|
+
|
|
178
|
+
<Box sx={{ display: "flex", justifyContent: "center", mt: 1 }}>
|
|
179
|
+
<Button
|
|
180
|
+
variant="outlined"
|
|
181
|
+
startIcon={<AddCircleOutlineIcon />}
|
|
182
|
+
onClick={(e) => setAddAnchor(e.currentTarget)}
|
|
183
|
+
disabled={removedFields.length === 0}
|
|
184
|
+
>
|
|
185
|
+
Add column
|
|
186
|
+
</Button>
|
|
187
|
+
</Box>
|
|
188
|
+
|
|
189
|
+
<Menu
|
|
190
|
+
anchorEl={addAnchor}
|
|
191
|
+
open={addOpen}
|
|
192
|
+
onClose={() => setAddAnchor(null)}
|
|
193
|
+
>
|
|
194
|
+
{removedFields.map((f) => (
|
|
195
|
+
<MenuItem
|
|
196
|
+
key={f.Data}
|
|
197
|
+
onClick={() => {
|
|
198
|
+
onAddField(f.Data);
|
|
199
|
+
setAddAnchor(null);
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{f.Label || f.Data}
|
|
203
|
+
</MenuItem>
|
|
204
|
+
))}
|
|
205
|
+
</Menu>
|
|
206
|
+
</Box>
|
|
207
|
+
</SortableContext>
|
|
208
|
+
)}
|
|
209
|
+
</DndContext>
|
|
210
|
+
</>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnListSection } from "./column-list-section";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle } from "@mui/icons-material";
|
|
4
|
+
import { Avatar, Box, Paper, Stack, Typography } from "@mui/material";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Currently selected resource */
|
|
8
|
+
selectedResourceKey: string;
|
|
9
|
+
/** A pojo with providers -> images */
|
|
10
|
+
providerImageMap: Record<string, string>;
|
|
11
|
+
/** A pojo with resources -> providers */
|
|
12
|
+
resourceProviderMap: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Editor view header contains provider details and other
|
|
17
|
+
* resource/view config data not related to the column list.
|
|
18
|
+
*/
|
|
19
|
+
export function EditorDMSCard({
|
|
20
|
+
selectedResourceKey,
|
|
21
|
+
providerImageMap,
|
|
22
|
+
resourceProviderMap,
|
|
23
|
+
}: Readonly<Props>) {
|
|
24
|
+
const selectedResourceProvider = resourceProviderMap[selectedResourceKey];
|
|
25
|
+
const logoUrl = providerImageMap[selectedResourceProvider];
|
|
26
|
+
return (
|
|
27
|
+
<Paper
|
|
28
|
+
elevation={1}
|
|
29
|
+
sx={{
|
|
30
|
+
p: { xs: 2, sm: 3 },
|
|
31
|
+
mb: 3,
|
|
32
|
+
borderRadius: 2,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<Stack
|
|
36
|
+
direction={{ xs: "column", sm: "row" }}
|
|
37
|
+
justifyContent="space-between"
|
|
38
|
+
alignItems={{ xs: "stretch", sm: "center" }}
|
|
39
|
+
spacing={2}
|
|
40
|
+
>
|
|
41
|
+
<Stack direction="row" spacing={2} alignItems="center">
|
|
42
|
+
<Avatar
|
|
43
|
+
src={logoUrl}
|
|
44
|
+
alt={`${selectedResourceKey}-${logoUrl}`}
|
|
45
|
+
sx={{
|
|
46
|
+
width: 56,
|
|
47
|
+
height: 56,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<Box>
|
|
51
|
+
<Typography variant="h5" fontWeight={700}>
|
|
52
|
+
Resource: {selectedResourceKey || "None"}
|
|
53
|
+
</Typography>
|
|
54
|
+
<Stack
|
|
55
|
+
direction="row"
|
|
56
|
+
spacing={1}
|
|
57
|
+
alignItems="center"
|
|
58
|
+
sx={{ mt: 0.5 }}
|
|
59
|
+
>
|
|
60
|
+
<Typography variant="body2" color="text.secondary">
|
|
61
|
+
provider: {selectedResourceProvider}
|
|
62
|
+
</Typography>
|
|
63
|
+
|
|
64
|
+
<Stack direction="row" spacing={0.5} alignItems="center">
|
|
65
|
+
<CheckCircle sx={{ fontSize: 16, color: "success.main" }} />
|
|
66
|
+
<Typography
|
|
67
|
+
variant="body2"
|
|
68
|
+
sx={{ color: "success.main", fontWeight: 600 }}
|
|
69
|
+
>
|
|
70
|
+
enabled
|
|
71
|
+
</Typography>
|
|
72
|
+
</Stack>
|
|
73
|
+
</Stack>
|
|
74
|
+
</Box>
|
|
75
|
+
</Stack>
|
|
76
|
+
</Stack>
|
|
77
|
+
</Paper>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Box, Paper, Tab, Tabs, Typography } from "@mui/material";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** List of resource keys to select from */
|
|
7
|
+
resourceList: string[];
|
|
8
|
+
/** Currently selected key */
|
|
9
|
+
selectedResourceKey: string;
|
|
10
|
+
/** Action to take when resource changes. */
|
|
11
|
+
handleChangeResource: (id: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tab-based selection menu for resource keys on the editor page.
|
|
16
|
+
*/
|
|
17
|
+
function EditorResourceTabs({
|
|
18
|
+
resourceList,
|
|
19
|
+
selectedResourceKey,
|
|
20
|
+
handleChangeResource,
|
|
21
|
+
}: Readonly<Props>) {
|
|
22
|
+
return (
|
|
23
|
+
<Box sx={{ my: 3 }}>
|
|
24
|
+
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
|
|
25
|
+
Selected Resource
|
|
26
|
+
</Typography>
|
|
27
|
+
<Paper>
|
|
28
|
+
<Tabs
|
|
29
|
+
value={selectedResourceKey}
|
|
30
|
+
onChange={(_, v) => handleChangeResource(v)}
|
|
31
|
+
variant="fullWidth"
|
|
32
|
+
>
|
|
33
|
+
{resourceList.map((docType) => (
|
|
34
|
+
<Tab key={docType} label={docType} value={docType} />
|
|
35
|
+
))}
|
|
36
|
+
</Tabs>
|
|
37
|
+
</Paper>
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { EditorResourceTabs };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ViewConfig } from "@evenicanpm/portal-types-schemas";
|
|
3
|
+
import { Box } from "@mui/material";
|
|
4
|
+
import type { QueryKey } from "@tanstack/react-query";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { ColumnListSection } from "./column-list";
|
|
7
|
+
import { EditorDMSCard } from "./editor-dms-card";
|
|
8
|
+
import { EditorResourceTabs } from "./editor-tabs";
|
|
9
|
+
|
|
10
|
+
export type SaveInput = { key: string; viewConfig: ViewConfig };
|
|
11
|
+
|
|
12
|
+
export type ViewConfigQuery = (resourceKey: string) => {
|
|
13
|
+
queryKey: QueryKey;
|
|
14
|
+
queryFn: () => Promise<ViewConfig>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SaveViewConfigMutation = (resourceKey: string) => {
|
|
18
|
+
mutationKey: QueryKey;
|
|
19
|
+
mutationFn: (input: SaveInput) => Promise<ViewConfig>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type EditorQueries = {
|
|
23
|
+
viewConfigQuery: ViewConfigQuery;
|
|
24
|
+
saveViewConfigMutation: SaveViewConfigMutation;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface EditorProps {
|
|
28
|
+
resourceList: string[];
|
|
29
|
+
providerMetaData: Record<string, string>;
|
|
30
|
+
resourceProviderMap: Record<string, string>;
|
|
31
|
+
editorQueries: EditorQueries;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Top-Level editor component.
|
|
35
|
+
*/
|
|
36
|
+
export function Editor({
|
|
37
|
+
resourceList,
|
|
38
|
+
providerMetaData,
|
|
39
|
+
resourceProviderMap,
|
|
40
|
+
editorQueries,
|
|
41
|
+
}: Readonly<EditorProps>) {
|
|
42
|
+
const [currentResource, setCurrentResource] = useState(resourceList[0]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Box sx={{ p: { sm: 3 }, maxWidth: 1200, mx: "auto" }}>
|
|
46
|
+
<EditorResourceTabs
|
|
47
|
+
resourceList={resourceList}
|
|
48
|
+
handleChangeResource={setCurrentResource}
|
|
49
|
+
selectedResourceKey={currentResource}
|
|
50
|
+
/>
|
|
51
|
+
<EditorDMSCard
|
|
52
|
+
selectedResourceKey={currentResource}
|
|
53
|
+
providerImageMap={providerMetaData}
|
|
54
|
+
resourceProviderMap={resourceProviderMap}
|
|
55
|
+
/>
|
|
56
|
+
<ColumnListSection
|
|
57
|
+
currentResourceKey={currentResource}
|
|
58
|
+
editorQueries={editorQueries}
|
|
59
|
+
/>
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Editor, type EditorProps } from "./components/editor";
|
package/src/index.ts
ADDED