@ndla/ui 56.0.34-alpha.0 → 56.0.35-alpha.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/dist/panda.buildinfo.json +1 -14
- package/dist/styles.css +0 -48
- package/es/Layout/index.js +1 -3
- package/es/index.js +1 -2
- package/lib/Layout/index.d.ts +0 -2
- package/lib/Layout/index.js +1 -11
- package/lib/index.d.ts +1 -3
- package/lib/index.js +1 -14
- package/package.json +5 -5
- package/src/Layout/index.ts +0 -4
- package/src/index.ts +1 -4
- package/es/Layout/LayoutItem.js +0 -35
- package/es/TreeStructure/TreeStructure.js +0 -318
- package/es/TreeStructure/helperFunctions.js +0 -29
- package/es/TreeStructure/index.js +0 -9
- package/es/TreeStructure/types.js +0 -1
- package/lib/Layout/LayoutItem.d.ts +0 -11
- package/lib/Layout/LayoutItem.js +0 -40
- package/lib/TreeStructure/TreeStructure.d.ts +0 -22
- package/lib/TreeStructure/TreeStructure.js +0 -325
- package/lib/TreeStructure/helperFunctions.d.ts +0 -9
- package/lib/TreeStructure/helperFunctions.js +0 -36
- package/lib/TreeStructure/index.d.ts +0 -9
- package/lib/TreeStructure/index.js +0 -12
- package/lib/TreeStructure/types.d.ts +0 -15
- package/lib/TreeStructure/types.js +0 -5
- package/src/Layout/LayoutItem.tsx +0 -36
- package/src/TreeStructure/TreeStructure.stories.tsx +0 -282
- package/src/TreeStructure/TreeStructure.tsx +0 -354
- package/src/TreeStructure/helperFunctions.ts +0 -18
- package/src/TreeStructure/index.ts +0 -10
- package/src/TreeStructure/types.ts +0 -22
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2024-present, NDLA.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
|
10
|
-
import { useTranslation } from "react-i18next";
|
|
11
|
-
import { Meta, StoryFn } from "@storybook/react";
|
|
12
|
-
import { CloseLine } from "@ndla/icons/action";
|
|
13
|
-
import { CheckLine } from "@ndla/icons/editor";
|
|
14
|
-
import {
|
|
15
|
-
FieldErrorMessage,
|
|
16
|
-
FieldInput,
|
|
17
|
-
FieldLabel,
|
|
18
|
-
FieldRoot,
|
|
19
|
-
FieldHelper,
|
|
20
|
-
IconButton,
|
|
21
|
-
InputContainer,
|
|
22
|
-
Spinner,
|
|
23
|
-
} from "@ndla/primitives";
|
|
24
|
-
import { HStack, styled } from "@ndla/styled-system/jsx";
|
|
25
|
-
import { IFolder } from "@ndla/types-backend/myndla-api";
|
|
26
|
-
import { flattenFolders } from "./helperFunctions";
|
|
27
|
-
import { TreeStructure, TreeStructureProps } from "./TreeStructure";
|
|
28
|
-
|
|
29
|
-
const Container = styled("div", {
|
|
30
|
-
base: {
|
|
31
|
-
display: "flex",
|
|
32
|
-
maxWidth: "surface.large",
|
|
33
|
-
maxHeight: "surface.small",
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const MY_FOLDERS_ID = "folders";
|
|
38
|
-
|
|
39
|
-
const targetResource: TreeStructureProps["targetResource"] = {
|
|
40
|
-
id: "test-resource",
|
|
41
|
-
resourceId: "123",
|
|
42
|
-
resourceType: "concept",
|
|
43
|
-
tags: [],
|
|
44
|
-
path: "",
|
|
45
|
-
created: "",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const STRUCTURE_EXAMPLE: IFolder[] = [
|
|
49
|
-
{
|
|
50
|
-
id: "1",
|
|
51
|
-
name: "Mine favoritter",
|
|
52
|
-
status: "private",
|
|
53
|
-
breadcrumbs: [{ id: "1", name: "Mine Favoritter" }],
|
|
54
|
-
resources: [targetResource],
|
|
55
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
56
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
57
|
-
subfolders: [
|
|
58
|
-
{
|
|
59
|
-
id: "2",
|
|
60
|
-
name: "Eksamen",
|
|
61
|
-
status: "shared",
|
|
62
|
-
breadcrumbs: [
|
|
63
|
-
{ id: "1", name: "Mine Favoritter" },
|
|
64
|
-
{ id: "2", name: "Eksamen" },
|
|
65
|
-
],
|
|
66
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
67
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
68
|
-
resources: [],
|
|
69
|
-
subfolders: [
|
|
70
|
-
{
|
|
71
|
-
id: "3",
|
|
72
|
-
name: "Eksamens oppgaver",
|
|
73
|
-
status: "shared",
|
|
74
|
-
breadcrumbs: [
|
|
75
|
-
{ id: "1", name: "Mine Favoritter" },
|
|
76
|
-
{ id: "2", name: "Eksamen" },
|
|
77
|
-
{ id: "3", name: "Eksamens oppgaver" },
|
|
78
|
-
],
|
|
79
|
-
resources: [],
|
|
80
|
-
subfolders: [],
|
|
81
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
82
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "4",
|
|
86
|
-
name: "Eksamen 2022",
|
|
87
|
-
status: "private",
|
|
88
|
-
breadcrumbs: [
|
|
89
|
-
{ id: "1", name: "Mine Favoritter" },
|
|
90
|
-
{ id: "2", name: "Eksamen" },
|
|
91
|
-
{ id: "4", name: "Eksamen 2022" },
|
|
92
|
-
],
|
|
93
|
-
resources: [],
|
|
94
|
-
subfolders: [],
|
|
95
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
96
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "5",
|
|
102
|
-
name: "Oppgaver",
|
|
103
|
-
status: "shared",
|
|
104
|
-
breadcrumbs: [
|
|
105
|
-
{ id: "1", name: "Mine Favoritter" },
|
|
106
|
-
{ id: "5", name: "Oppgaver" },
|
|
107
|
-
],
|
|
108
|
-
resources: [],
|
|
109
|
-
subfolders: [],
|
|
110
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
111
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
},
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
const FOLDER_TREE_STRUCTURE: IFolder[] = [
|
|
118
|
-
{
|
|
119
|
-
id: MY_FOLDERS_ID,
|
|
120
|
-
name: "Mine mapper",
|
|
121
|
-
status: "private",
|
|
122
|
-
breadcrumbs: [],
|
|
123
|
-
resources: [],
|
|
124
|
-
subfolders: [...STRUCTURE_EXAMPLE],
|
|
125
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
126
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
127
|
-
},
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
export default {
|
|
131
|
-
title: "Components/TreeStructure",
|
|
132
|
-
tags: ["autodocs"],
|
|
133
|
-
component: TreeStructure,
|
|
134
|
-
parameters: {
|
|
135
|
-
inlineStories: true,
|
|
136
|
-
},
|
|
137
|
-
args: {
|
|
138
|
-
defaultOpenFolders: [MY_FOLDERS_ID],
|
|
139
|
-
targetResource: targetResource,
|
|
140
|
-
label: "Velg mappe",
|
|
141
|
-
maxLevel: 5,
|
|
142
|
-
// eslint-disable-next-line no-console
|
|
143
|
-
onSelectFolder: console.log,
|
|
144
|
-
},
|
|
145
|
-
argTypes: {
|
|
146
|
-
folders: { control: false },
|
|
147
|
-
},
|
|
148
|
-
} as Meta<typeof TreeStructure>;
|
|
149
|
-
|
|
150
|
-
export const Default: StoryFn<typeof TreeStructure> = ({ ...args }) => {
|
|
151
|
-
const [structure, setStructure] = useState<IFolder[]>(FOLDER_TREE_STRUCTURE);
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<Container>
|
|
155
|
-
<TreeStructure
|
|
156
|
-
{...args}
|
|
157
|
-
folders={structure}
|
|
158
|
-
newFolderInput={({ parentId, onCancel, onCreate }) => (
|
|
159
|
-
<NewFolder
|
|
160
|
-
structure={structure}
|
|
161
|
-
setStructure={setStructure}
|
|
162
|
-
parentId={parentId}
|
|
163
|
-
onCancel={onCancel}
|
|
164
|
-
onCreate={onCreate}
|
|
165
|
-
/>
|
|
166
|
-
)}
|
|
167
|
-
/>
|
|
168
|
-
</Container>
|
|
169
|
-
);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
interface NewFolderProps {
|
|
173
|
-
parentId: string;
|
|
174
|
-
structure: IFolder[];
|
|
175
|
-
setStructure: Dispatch<SetStateAction<IFolder[]>>;
|
|
176
|
-
onCancel?: () => void;
|
|
177
|
-
onCreate?: (folder: IFolder) => void;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const generateNewFolder = (name: string, id: string, breadcrumbs: { id: string; name: string }[]): IFolder => ({
|
|
181
|
-
id,
|
|
182
|
-
name,
|
|
183
|
-
status: "private",
|
|
184
|
-
subfolders: [],
|
|
185
|
-
breadcrumbs: breadcrumbs.concat({ name, id }),
|
|
186
|
-
resources: [],
|
|
187
|
-
created: "2023-03-03T08:40:23.444Z",
|
|
188
|
-
updated: "2023-03-03T08:40:23.444Z",
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const NewFolder = ({ parentId, onCancel, structure, setStructure, onCreate }: NewFolderProps) => {
|
|
192
|
-
const [name, setName] = useState("");
|
|
193
|
-
const [loading, setLoading] = useState(false);
|
|
194
|
-
const [error, setError] = useState("");
|
|
195
|
-
|
|
196
|
-
const { t } = useTranslation();
|
|
197
|
-
|
|
198
|
-
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
199
|
-
|
|
200
|
-
const onSave = async () => {
|
|
201
|
-
if (error) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (name === "") {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
setLoading(true);
|
|
208
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
209
|
-
setLoading(false);
|
|
210
|
-
const flattenedStructure = flattenFolders(structure);
|
|
211
|
-
const targetFolder = flattenedStructure.find((folder) => folder.id === parentId);
|
|
212
|
-
const newFolderId = (flattenedStructure.length + 1).toString();
|
|
213
|
-
const newFolder = generateNewFolder(name, newFolderId, targetFolder?.breadcrumbs ?? []);
|
|
214
|
-
if (targetFolder) {
|
|
215
|
-
setStructure((oldStructure) => {
|
|
216
|
-
targetFolder.subfolders.unshift(newFolder);
|
|
217
|
-
return oldStructure;
|
|
218
|
-
});
|
|
219
|
-
} else {
|
|
220
|
-
setStructure((old) => [newFolder].concat(old));
|
|
221
|
-
}
|
|
222
|
-
onCreate?.(newFolder);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
useEffect(() => {
|
|
226
|
-
if (name.length === 0) {
|
|
227
|
-
setError("Navn er påkrevd");
|
|
228
|
-
} else {
|
|
229
|
-
setError("");
|
|
230
|
-
}
|
|
231
|
-
}, [name]);
|
|
232
|
-
|
|
233
|
-
return (
|
|
234
|
-
<FieldRoot required invalid={!!error}>
|
|
235
|
-
<FieldLabel srOnly>Mine mapper</FieldLabel>
|
|
236
|
-
<FieldErrorMessage>{error}</FieldErrorMessage>
|
|
237
|
-
<InputContainer>
|
|
238
|
-
<FieldInput
|
|
239
|
-
autoComplete="off"
|
|
240
|
-
disabled={loading}
|
|
241
|
-
ref={inputRef}
|
|
242
|
-
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
243
|
-
autoFocus
|
|
244
|
-
name="name"
|
|
245
|
-
placeholder="Skriv inn mappenavn"
|
|
246
|
-
onChange={(e) => {
|
|
247
|
-
if (!loading) {
|
|
248
|
-
setName(e.currentTarget.value);
|
|
249
|
-
}
|
|
250
|
-
}}
|
|
251
|
-
onKeyDown={(e) => {
|
|
252
|
-
if (e.key === "Escape") {
|
|
253
|
-
e.preventDefault();
|
|
254
|
-
onCancel?.();
|
|
255
|
-
} else if (e.key === "Enter") {
|
|
256
|
-
e.preventDefault();
|
|
257
|
-
onSave();
|
|
258
|
-
}
|
|
259
|
-
}}
|
|
260
|
-
/>
|
|
261
|
-
<HStack gap="3xsmall">
|
|
262
|
-
{!loading ? (
|
|
263
|
-
<>
|
|
264
|
-
{!error && (
|
|
265
|
-
<IconButton variant="tertiary" aria-label={t("save")} title={t("save")} onClick={onSave}>
|
|
266
|
-
<CheckLine />
|
|
267
|
-
</IconButton>
|
|
268
|
-
)}
|
|
269
|
-
<IconButton aria-label={t("close")} title={t("close")} variant="tertiary" onClick={onCancel}>
|
|
270
|
-
<CloseLine />
|
|
271
|
-
</IconButton>
|
|
272
|
-
</>
|
|
273
|
-
) : (
|
|
274
|
-
<FieldHelper>
|
|
275
|
-
<Spinner size="small" aria-label={t("loading")} />
|
|
276
|
-
</FieldHelper>
|
|
277
|
-
)}
|
|
278
|
-
</HStack>
|
|
279
|
-
</InputContainer>
|
|
280
|
-
</FieldRoot>
|
|
281
|
-
);
|
|
282
|
-
};
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2024-present, NDLA.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { KeyboardEvent, useCallback, useMemo, useRef, useState } from "react";
|
|
10
|
-
import { flushSync } from "react-dom";
|
|
11
|
-
import { useTranslation } from "react-i18next";
|
|
12
|
-
import { useTreeView, usePopoverContext, type PopoverOpenChangeDetails } from "@ark-ui/react";
|
|
13
|
-
import { AddLine, HeartFill } from "@ndla/icons/action";
|
|
14
|
-
import { ArrowDownShortLine, ArrowRightShortLine } from "@ndla/icons/common";
|
|
15
|
-
import { FolderUserLine } from "@ndla/icons/contentType";
|
|
16
|
-
import { FolderLine } from "@ndla/icons/editor";
|
|
17
|
-
import {
|
|
18
|
-
Button,
|
|
19
|
-
IconButton,
|
|
20
|
-
PopoverContent,
|
|
21
|
-
PopoverRoot,
|
|
22
|
-
PopoverTrigger,
|
|
23
|
-
Tree,
|
|
24
|
-
TreeBranch,
|
|
25
|
-
TreeBranchContent,
|
|
26
|
-
TreeBranchControl,
|
|
27
|
-
TreeBranchIndicator,
|
|
28
|
-
TreeBranchText,
|
|
29
|
-
TreeBranchTrigger,
|
|
30
|
-
TreeItem,
|
|
31
|
-
TreeItemText,
|
|
32
|
-
TreeLabel,
|
|
33
|
-
TreeRootProvider,
|
|
34
|
-
} from "@ndla/primitives";
|
|
35
|
-
import { HStack, Stack, styled } from "@ndla/styled-system/jsx";
|
|
36
|
-
import { IFolder, IResource } from "@ndla/types-backend/myndla-api";
|
|
37
|
-
import { flattenFolders } from "./helperFunctions";
|
|
38
|
-
import { NewFolderInputFunc } from "./types";
|
|
39
|
-
|
|
40
|
-
export const MAX_LEVEL_FOR_FOLDERS = 5;
|
|
41
|
-
|
|
42
|
-
export interface TreeStructureProps {
|
|
43
|
-
loading?: boolean;
|
|
44
|
-
targetResource?: IResource;
|
|
45
|
-
defaultOpenFolders?: string[];
|
|
46
|
-
folders: IFolder[];
|
|
47
|
-
label?: string;
|
|
48
|
-
maxLevel?: number;
|
|
49
|
-
newFolderInput?: NewFolderInputFunc;
|
|
50
|
-
onSelectFolder?: (id: string) => void;
|
|
51
|
-
ariaDescribedby?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const StyledButton = styled(Button, {
|
|
55
|
-
base: {
|
|
56
|
-
width: "100%",
|
|
57
|
-
justifyContent: "space-between",
|
|
58
|
-
"& span": {
|
|
59
|
-
overflow: "hidden",
|
|
60
|
-
textOverflow: "ellipsis",
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const StyledHStack = styled(HStack, {
|
|
66
|
-
base: {
|
|
67
|
-
overflow: "hidden",
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const StyledHeartFill = styled(HeartFill, {
|
|
72
|
-
base: {
|
|
73
|
-
color: "icon.strong",
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const StyledFolderLine = styled(FolderLine, {
|
|
78
|
-
base: {
|
|
79
|
-
color: "icon.strong",
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const StyledFolderUserLine = styled(FolderUserLine, {
|
|
84
|
-
base: {
|
|
85
|
-
color: "icon.strong",
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const StyledTreeRootProvider = styled(TreeRootProvider, {
|
|
90
|
-
base: {
|
|
91
|
-
width: "100%",
|
|
92
|
-
maxHeight: "inherit",
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const StyledPopoverContent = styled(PopoverContent, {
|
|
97
|
-
base: {
|
|
98
|
-
display: "flex",
|
|
99
|
-
flexDirection: "column",
|
|
100
|
-
gap: "xsmall",
|
|
101
|
-
overflow: "auto",
|
|
102
|
-
maxHeight: "inherit",
|
|
103
|
-
paddingInline: "xsmall",
|
|
104
|
-
paddingBlock: "xsmall",
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const LabelHStack = styled(HStack, {
|
|
109
|
-
base: {
|
|
110
|
-
width: "100%",
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
export const TreeStructure = ({
|
|
115
|
-
folders,
|
|
116
|
-
defaultOpenFolders,
|
|
117
|
-
newFolderInput,
|
|
118
|
-
label,
|
|
119
|
-
targetResource,
|
|
120
|
-
loading,
|
|
121
|
-
maxLevel = MAX_LEVEL_FOR_FOLDERS,
|
|
122
|
-
onSelectFolder,
|
|
123
|
-
ariaDescribedby,
|
|
124
|
-
}: TreeStructureProps) => {
|
|
125
|
-
const [open, setOpen] = useState(false);
|
|
126
|
-
const [selectedValue, setSelectedValue] = useState(defaultOpenFolders?.[defaultOpenFolders?.length - 1] ?? "");
|
|
127
|
-
const [expandedValue, setExpandedValue] = useState<string[]>(defaultOpenFolders ?? []);
|
|
128
|
-
const [focusedValue, setFocusedValue] = useState<string | null>(selectedValue);
|
|
129
|
-
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null);
|
|
130
|
-
const newFolderButtonRef = useRef<HTMLButtonElement>(null);
|
|
131
|
-
const { t } = useTranslation();
|
|
132
|
-
const rootFolderIds = useMemo(() => folders.map((folder) => folder.id), [folders]);
|
|
133
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
134
|
-
|
|
135
|
-
const selectedFolder = useMemo(() => {
|
|
136
|
-
return flattenFolders(folders).find((folder) => folder.id === selectedValue);
|
|
137
|
-
}, [folders, selectedValue]);
|
|
138
|
-
|
|
139
|
-
const disableCreateFolder = useMemo(() => {
|
|
140
|
-
return (selectedFolder?.breadcrumbs.length ?? 0) > maxLevel - 1;
|
|
141
|
-
}, [maxLevel, selectedFolder?.breadcrumbs.length]);
|
|
142
|
-
|
|
143
|
-
const onOpenChange = useCallback((details: PopoverOpenChangeDetails) => {
|
|
144
|
-
setOpen(details.open);
|
|
145
|
-
if (!details.open) {
|
|
146
|
-
setNewFolderParentId(null);
|
|
147
|
-
}
|
|
148
|
-
}, []);
|
|
149
|
-
|
|
150
|
-
const onKeyDown = useCallback((e: KeyboardEvent<HTMLElement>) => {
|
|
151
|
-
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
152
|
-
e.stopPropagation();
|
|
153
|
-
setOpen(true);
|
|
154
|
-
}
|
|
155
|
-
}, []);
|
|
156
|
-
|
|
157
|
-
const onShowInput = useCallback(() => {
|
|
158
|
-
if (disableCreateFolder) return;
|
|
159
|
-
const flattenedFolders = flattenFolders(folders);
|
|
160
|
-
const folder = flattenedFolders.find((folder) => folder.id === selectedValue);
|
|
161
|
-
const newExpandedIds = rootFolderIds.concat(folder?.breadcrumbs.map((bc) => bc.id) ?? []);
|
|
162
|
-
setOpen(true);
|
|
163
|
-
setExpandedValue((prev) => Array.from(new Set([...prev, ...newExpandedIds])));
|
|
164
|
-
setNewFolderParentId(selectedValue);
|
|
165
|
-
}, [disableCreateFolder, folders, rootFolderIds, selectedValue]);
|
|
166
|
-
|
|
167
|
-
const treeView = useTreeView({
|
|
168
|
-
focusedValue,
|
|
169
|
-
onFocusChange: (details) => !!details.focusedValue && setFocusedValue(details.focusedValue),
|
|
170
|
-
expandedValue,
|
|
171
|
-
onExpandedChange: (details) => setExpandedValue(details.expandedValue),
|
|
172
|
-
selectedValue: [selectedValue],
|
|
173
|
-
onSelectionChange: (details) => {
|
|
174
|
-
// TODO: This is currently a bug in zag. The TreeView component simply expects the already selected value to remain selected. As such, always choose the "last" selected value.
|
|
175
|
-
const val = details.selectedValue[details.selectedValue.length - 1];
|
|
176
|
-
if (!val) return;
|
|
177
|
-
if (val === selectedValue && details.focusedValue === selectedValue) {
|
|
178
|
-
setOpen(false);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
setSelectedValue(val);
|
|
182
|
-
onSelectFolder?.(val);
|
|
183
|
-
},
|
|
184
|
-
expandOnClick: false,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const onCreateFolder = useCallback(
|
|
188
|
-
(folder: IFolder | undefined) => {
|
|
189
|
-
if (!folder) return;
|
|
190
|
-
const focus = treeView.focusItem;
|
|
191
|
-
const expand = treeView.expand;
|
|
192
|
-
const select = treeView.select;
|
|
193
|
-
flushSync(() => {
|
|
194
|
-
setOpen(true);
|
|
195
|
-
});
|
|
196
|
-
flushSync(() => {
|
|
197
|
-
expand(folder.breadcrumbs.map((bc) => bc.id));
|
|
198
|
-
});
|
|
199
|
-
flushSync(() => {
|
|
200
|
-
select([folder.id]);
|
|
201
|
-
});
|
|
202
|
-
flushSync(() => {
|
|
203
|
-
focus(folder.id);
|
|
204
|
-
});
|
|
205
|
-
setNewFolderParentId(null);
|
|
206
|
-
},
|
|
207
|
-
[treeView.expand, treeView.focusItem, treeView.select],
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const onAnimationEnd = useCallback(() => {
|
|
211
|
-
if (open && focusedValue) {
|
|
212
|
-
document.getElementById(focusedValue)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
213
|
-
}
|
|
214
|
-
}, [focusedValue, open]);
|
|
215
|
-
|
|
216
|
-
const onCancelFolder = useCallback(() => {
|
|
217
|
-
if (!selectedFolder) return;
|
|
218
|
-
const focusFunc = selectedFolder.subfolders.length ? treeView.focusBranch : treeView.focusItem;
|
|
219
|
-
focusFunc(selectedFolder.id);
|
|
220
|
-
setNewFolderParentId(null);
|
|
221
|
-
}, [selectedFolder, treeView.focusBranch, treeView.focusItem]);
|
|
222
|
-
|
|
223
|
-
const addTooltip = loading
|
|
224
|
-
? t("loading")
|
|
225
|
-
: disableCreateFolder
|
|
226
|
-
? t("treeStructure.maxFoldersAlreadyAdded")
|
|
227
|
-
: t("myNdla.newFolderUnder", { folderName: selectedFolder?.name });
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<StyledTreeRootProvider value={treeView} asChild {...treeView.getRootProps()}>
|
|
231
|
-
<Stack align="flex-end">
|
|
232
|
-
<LabelHStack gap="xsmall" justify="space-between">
|
|
233
|
-
<TreeLabel>{label}</TreeLabel>
|
|
234
|
-
<Button
|
|
235
|
-
size="small"
|
|
236
|
-
variant="tertiary"
|
|
237
|
-
ref={newFolderButtonRef}
|
|
238
|
-
aria-disabled={disableCreateFolder}
|
|
239
|
-
title={addTooltip}
|
|
240
|
-
aria-label={addTooltip}
|
|
241
|
-
loading={loading}
|
|
242
|
-
onClick={onShowInput}
|
|
243
|
-
>
|
|
244
|
-
<AddLine />
|
|
245
|
-
{t("myNdla.newFolder")}
|
|
246
|
-
</Button>
|
|
247
|
-
</LabelHStack>
|
|
248
|
-
<PopoverRoot
|
|
249
|
-
open={open}
|
|
250
|
-
positioning={{ sameWidth: true }}
|
|
251
|
-
onOpenChange={onOpenChange}
|
|
252
|
-
persistentElements={[() => newFolderButtonRef.current]}
|
|
253
|
-
initialFocusEl={() => contentRef.current?.querySelector("input") ?? null}
|
|
254
|
-
>
|
|
255
|
-
<PopoverTrigger asChild>
|
|
256
|
-
<StyledButton
|
|
257
|
-
variant="secondary"
|
|
258
|
-
onKeyDown={onKeyDown}
|
|
259
|
-
aria-haspopup="tree"
|
|
260
|
-
role="combobox"
|
|
261
|
-
aria-describedby={ariaDescribedby}
|
|
262
|
-
aria-activedescendant={focusedValue ?? undefined}
|
|
263
|
-
>
|
|
264
|
-
<span>{selectedFolder?.name}</span>
|
|
265
|
-
<ArrowDownShortLine />
|
|
266
|
-
</StyledButton>
|
|
267
|
-
</PopoverTrigger>
|
|
268
|
-
<StyledPopoverContent onAnimationEnd={onAnimationEnd} ref={contentRef}>
|
|
269
|
-
{!!newFolderParentId &&
|
|
270
|
-
newFolderInput?.({ parentId: newFolderParentId, onCreate: onCreateFolder, onCancel: onCancelFolder })}
|
|
271
|
-
<Tree>
|
|
272
|
-
{folders.map((folder) => (
|
|
273
|
-
<TreeStructureItem key={folder.id} folder={folder} targetResource={targetResource} />
|
|
274
|
-
))}
|
|
275
|
-
</Tree>
|
|
276
|
-
</StyledPopoverContent>
|
|
277
|
-
</PopoverRoot>
|
|
278
|
-
</Stack>
|
|
279
|
-
</StyledTreeRootProvider>
|
|
280
|
-
);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
interface TreeStructureItemProps {
|
|
284
|
-
folder: IFolder;
|
|
285
|
-
targetResource?: IResource;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const TreeStructureItem = ({ folder, targetResource }: TreeStructureItemProps) => {
|
|
289
|
-
const { t } = useTranslation();
|
|
290
|
-
const { setOpen } = usePopoverContext();
|
|
291
|
-
const containsResource =
|
|
292
|
-
targetResource && folder.resources.some((resource) => resource.resourceId === targetResource.resourceId);
|
|
293
|
-
|
|
294
|
-
const FolderIcon = folder.status === "shared" ? StyledFolderUserLine : StyledFolderLine;
|
|
295
|
-
|
|
296
|
-
const onKeyDown = useCallback(
|
|
297
|
-
(e: KeyboardEvent<HTMLElement>) => {
|
|
298
|
-
if (e.key === "Enter") {
|
|
299
|
-
setOpen(false);
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
[setOpen],
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
if (!folder.subfolders.length) {
|
|
306
|
-
return (
|
|
307
|
-
<TreeItem key={folder.id} value={folder.id} onKeyDown={onKeyDown} id={folder.id}>
|
|
308
|
-
<StyledHStack gap="xsmall" justify="space-between">
|
|
309
|
-
<StyledHStack gap="xxsmall" justify="center">
|
|
310
|
-
<FolderIcon />
|
|
311
|
-
<TreeItemText>{folder.name}</TreeItemText>
|
|
312
|
-
</StyledHStack>
|
|
313
|
-
{containsResource && <StyledHeartFill title={t("myNdla.alreadyInFolder")} />}
|
|
314
|
-
</StyledHStack>
|
|
315
|
-
</TreeItem>
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const ariaLabel = folder.status === "shared" ? `${folder.name}. ${t("myNdla.folder.sharing.shared")}` : folder.name;
|
|
320
|
-
|
|
321
|
-
return (
|
|
322
|
-
<TreeBranch key={folder.id} value={folder.id} id={folder.id}>
|
|
323
|
-
<TreeBranchControl onKeyDown={onKeyDown} asChild>
|
|
324
|
-
<StyledHStack gap="xsmall" justify="space-between">
|
|
325
|
-
<StyledHStack gap="xxsmall" justify="center">
|
|
326
|
-
<IconButton variant="clear" asChild>
|
|
327
|
-
<TreeBranchTrigger>
|
|
328
|
-
<TreeBranchIndicator asChild>
|
|
329
|
-
<ArrowRightShortLine />
|
|
330
|
-
</TreeBranchIndicator>
|
|
331
|
-
</TreeBranchTrigger>
|
|
332
|
-
</IconButton>
|
|
333
|
-
<FolderIcon />
|
|
334
|
-
<TreeBranchText aria-label={ariaLabel} title={ariaLabel}>
|
|
335
|
-
{folder.name}
|
|
336
|
-
</TreeBranchText>
|
|
337
|
-
</StyledHStack>
|
|
338
|
-
{containsResource && (
|
|
339
|
-
<StyledHeartFill
|
|
340
|
-
title={t("myNdla.alreadyInFolder")}
|
|
341
|
-
aria-label={t("myNdla.alreadyInFolder")}
|
|
342
|
-
aria-hidden={false}
|
|
343
|
-
/>
|
|
344
|
-
)}
|
|
345
|
-
</StyledHStack>
|
|
346
|
-
</TreeBranchControl>
|
|
347
|
-
<TreeBranchContent>
|
|
348
|
-
{folder.subfolders.map((subfolder) => (
|
|
349
|
-
<TreeStructureItem key={subfolder.id} folder={subfolder} targetResource={targetResource} />
|
|
350
|
-
))}
|
|
351
|
-
</TreeBranchContent>
|
|
352
|
-
</TreeBranch>
|
|
353
|
-
);
|
|
354
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2022-present, NDLA.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { IFolder } from "@ndla/types-backend/myndla-api";
|
|
10
|
-
|
|
11
|
-
export const flattenFolders = (folders: IFolder[], openFolders?: string[]): IFolder[] => {
|
|
12
|
-
return folders.reduce((acc, { subfolders, id, ...rest }) => {
|
|
13
|
-
if (!subfolders || (openFolders && !openFolders.includes(id))) {
|
|
14
|
-
return acc.concat({ subfolders, id, ...rest });
|
|
15
|
-
}
|
|
16
|
-
return acc.concat({ subfolders, id, ...rest }, flattenFolders(subfolders, openFolders));
|
|
17
|
-
}, [] as IFolder[]);
|
|
18
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2022-present, NDLA.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type { TreeStructureProps } from "./TreeStructure";
|
|
10
|
-
export { TreeStructure } from "./TreeStructure";
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2022-present, NDLA.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { ReactNode } from "react";
|
|
10
|
-
import { IFolder } from "@ndla/types-backend/myndla-api";
|
|
11
|
-
|
|
12
|
-
export type OnCreatedFunc = (folder: IFolder | undefined) => void;
|
|
13
|
-
|
|
14
|
-
export type NewFolderInputFunc = ({
|
|
15
|
-
onCancel,
|
|
16
|
-
parentId,
|
|
17
|
-
onCreate,
|
|
18
|
-
}: {
|
|
19
|
-
onCancel: () => void;
|
|
20
|
-
parentId: string;
|
|
21
|
-
onCreate: OnCreatedFunc;
|
|
22
|
-
}) => ReactNode;
|