@fluxo-dev/shell 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +7 -0
  4. package/jest.config.ts +11 -0
  5. package/package.json +4 -0
  6. package/project.json +43 -0
  7. package/src/index.ts +1 -0
  8. package/src/lib/app.module.css +1 -0
  9. package/src/lib/app.spec.tsx +17 -0
  10. package/src/lib/app.tsx +374 -0
  11. package/src/lib/components/error-page/404.tsx +16 -0
  12. package/src/lib/components/routes/main-routes.tsx +26 -0
  13. package/src/lib/layouts/blank/blank-layout.tsx +10 -0
  14. package/src/lib/layouts/main/app-bar.tsx +192 -0
  15. package/src/lib/layouts/main/app-layout.tsx +52 -0
  16. package/src/lib/layouts/main/content.tsx +10 -0
  17. package/src/lib/layouts/main/footer-bar.tsx +76 -0
  18. package/src/lib/layouts/main/navbar-item.module.css +49 -0
  19. package/src/lib/layouts/main/navbar-item.tsx +129 -0
  20. package/src/lib/layouts/main/navbar.tsx +38 -0
  21. package/src/lib/layouts/main/search-bar.tsx +44 -0
  22. package/src/lib/layouts/main/shell-spotlight.tsx +58 -0
  23. package/src/lib/models/app-jit.ts +6 -0
  24. package/src/lib/pages/general/fluxo-plugin-loader.tsx +22 -0
  25. package/src/lib/pages/login/account-confirmation.tsx +86 -0
  26. package/src/lib/pages/login/change-password.tsx +58 -0
  27. package/src/lib/pages/login/forgot-password.tsx +53 -0
  28. package/src/lib/pages/login/login-page.tsx +70 -0
  29. package/src/lib/pages/login/reset-password.tsx +91 -0
  30. package/src/lib/pages/settings/apps/add-jit-app.tsx +79 -0
  31. package/src/lib/pages/settings/apps/app-item.tsx +157 -0
  32. package/src/lib/pages/settings/apps/app-jit-item.tsx +105 -0
  33. package/src/lib/pages/settings/apps/apps-list.tsx +171 -0
  34. package/src/lib/pages/settings/apps/embedded-module-item.tsx +66 -0
  35. package/src/lib/pages/settings/apps/install-or-upgrade-app.tsx +105 -0
  36. package/src/lib/pages/settings/menu-designer/custom-node.module.css +34 -0
  37. package/src/lib/pages/settings/menu-designer/custom-node.tsx +160 -0
  38. package/src/lib/pages/settings/menu-designer/menu-create-form.tsx +109 -0
  39. package/src/lib/pages/settings/menu-designer/menu-designer.module.css +19 -0
  40. package/src/lib/pages/settings/menu-designer/menu-designer.tsx +463 -0
  41. package/src/lib/pages/settings/menu-designer/menu-item-edit.tsx +115 -0
  42. package/src/lib/pages/settings/menu-designer/placeholder.module.css +8 -0
  43. package/src/lib/pages/settings/menu-designer/placeholder.tsx +13 -0
  44. package/src/lib/pages/settings/security/components/domani-role/domain-role-form.tsx +67 -0
  45. package/src/lib/pages/settings/security/components/domani-role/domain-role-grid.tsx +139 -0
  46. package/src/lib/pages/settings/security/components/permission/create-custom-permission-form.tsx +64 -0
  47. package/src/lib/pages/settings/security/components/permission/create-scope-form.tsx +158 -0
  48. package/src/lib/pages/settings/security/components/permission/permission-grid.tsx +304 -0
  49. package/src/lib/pages/settings/security/components/permission/permission-navigator.tsx +320 -0
  50. package/src/lib/pages/settings/security/components/permission/update-permission.tsx +54 -0
  51. package/src/lib/pages/settings/security/components/route-rule/route-rule-create-form.tsx +59 -0
  52. package/src/lib/pages/settings/security/components/route-rule/route-rule-grid.tsx +229 -0
  53. package/src/lib/pages/settings/security/components/user/user-form.tsx +123 -0
  54. package/src/lib/pages/settings/security/components/user/users-grid.tsx +127 -0
  55. package/src/lib/pages/settings/settings.tsx +298 -0
  56. package/src/lib/pages/settings/token/token-row.tsx +69 -0
  57. package/src/lib/pages/settings/token/tokens.tsx +73 -0
  58. package/src/lib/pages/settings/ui/ui.tsx +149 -0
  59. package/src/lib/services/app-jit-manager.ts +92 -0
  60. package/src/lib/services/app-menu.service.ts +35 -0
  61. package/src/lib/utils/icon-factory.tsx +25 -0
  62. package/src/lib/utils/security/only-not-logged-route.tsx +17 -0
  63. package/src/lib/utils/security/protected-route.tsx +47 -0
  64. package/tsconfig.json +20 -0
  65. package/tsconfig.lib.json +24 -0
  66. package/tsconfig.spec.json +20 -0
@@ -0,0 +1,109 @@
1
+ import {Button, Group, Select, Stack, TextInput} from "@mantine/core";
2
+ import {useCallback, useEffect, useState} from "react";
3
+ import {AppMenuContextType, DomainRole, FluxoClientSys, User} from "@fluxo-dev/api-client-base";
4
+ import {showNotification} from "@mantine/notifications";
5
+
6
+ export interface MenuCreateFormProps {
7
+ onCreated: () => void;
8
+ onCancel: () => void;
9
+ }
10
+ export function MenuCreateForm(props: MenuCreateFormProps) {
11
+
12
+ const [context, setContext] = useState<string | null>('1');
13
+ const [user, setUser] = useState<string | null>();
14
+ const [role, setRole] = useState<string | null>();
15
+ const [name, setName] = useState<string>();
16
+
17
+ const [roles, setRoles] = useState<DomainRole[]>([]);
18
+ const [users, setUsers] = useState<User[]>([]);
19
+
20
+ useEffect(() => {
21
+ FluxoClientSys.Instance.security.domainRole.find(999,0).then((roles) => {
22
+ setRoles(roles.results);
23
+ })
24
+
25
+ FluxoClientSys.Instance.security.user.getAllUsers().then((users) => {
26
+ setUsers(users);
27
+ })
28
+ }, []);
29
+
30
+ const saveButtonIsDisabled = useCallback(() => {
31
+ if(context === '1' && !role) return true;
32
+ if(context === '2' && !user) return true;
33
+ if(!name) return true;
34
+
35
+ return false;
36
+ }, [context, role, user, name]);
37
+
38
+ const onSubmit = () => {
39
+ if(saveButtonIsDisabled()) return;
40
+
41
+ FluxoClientSys.Instance.configuration.createAppMenu({
42
+ contextType: context === '1' ? AppMenuContextType.DomainRole : AppMenuContextType.User,
43
+ name: name!,
44
+ domainRoleId: context === '1' ? +role! : 0,
45
+ copyItemsFromWebMenuId: 0,
46
+ userId: context === '2' ? user! : undefined,
47
+ }).then(() => {
48
+ props.onCreated();
49
+ showNotification({
50
+ title: 'Menu creato',
51
+ message: 'Il menu è stato creato con successo',
52
+ color: 'green',
53
+ autoClose: 5000,
54
+ })
55
+ })
56
+ .catch((err) => {
57
+ showNotification({
58
+ title: 'Errore',
59
+ message: 'Si è verificato un errore durante la creazione del menu',
60
+ color: 'red',
61
+ autoClose: 5000,
62
+ })
63
+ })
64
+ }
65
+
66
+ return (
67
+ <Stack>
68
+ <TextInput
69
+ label={'Descrizione'}
70
+ value={name}
71
+ onChange={(e) => setName(e.currentTarget.value)}
72
+ placeholder={'Ruolo amministrazione'} />
73
+ <Select
74
+ allowDeselect={false}
75
+ label={'Contesto'}
76
+ value={context}
77
+ description={'Seleziona il contesto in cui il menu sarà visibile (Ruolo o Utente)'}
78
+ onChange={(value) => setContext(value)}
79
+ data={[{label: 'Ruolo', value: '1'}, {label: 'Utente', value: '2'}]}
80
+ />
81
+
82
+ {context === '1' &&
83
+ <Select
84
+ label={'Ruolo'}
85
+ value={role}
86
+ onChange={(value) => setRole(value)}
87
+ description={'Seleziona il ruolo a cui il menu sarà visibile'}
88
+ data={roles.map((role) => ({label: `${role.name} (${role.identifier})`, value: role.id!.toString()}))}
89
+ />
90
+ }
91
+
92
+ {context === '2' &&
93
+ <Select
94
+ label={'Utente'}
95
+ value={user}
96
+ onChange={(value) => setUser(value)}
97
+ description={'Seleziona l\'utente a cui il menu sarà visibile'}
98
+ data={users.map((user) => ({label: `${user.userData.FirstName} ${user.userData.LastName} (${user.email})`, value: user.id!.toString()}))}
99
+ />
100
+ }
101
+
102
+ <Group justify={'end'}>
103
+ <Button onClick={props.onCancel} variant={'outline'}>Annulla</Button>
104
+ <Button disabled={saveButtonIsDisabled()} onClick={onSubmit}>Salva</Button>
105
+ </Group>
106
+
107
+ </Stack>
108
+ )
109
+ }
@@ -0,0 +1,19 @@
1
+ .app {
2
+ height: 100%;
3
+ }
4
+
5
+ .treeRoot {
6
+ height: 100%;
7
+ }
8
+
9
+ .draggingSource {
10
+ opacity: 0.3;
11
+ }
12
+
13
+ .placeholderContainer {
14
+ position: relative;
15
+ }
16
+
17
+ .dropTarget {
18
+ background-color: #e8f0fe;
19
+ }
@@ -0,0 +1,463 @@
1
+ import React, {useEffect, useState} from "react";
2
+ import {getBackendOptions, MultiBackend, NodeModel, Tree,} from "@minoru/react-dnd-treeview";
3
+ import {DndProvider} from "react-dnd";
4
+ import {DataCard} from "@fluxo-dev/ui-components";
5
+ import {CustomNode} from "./custom-node";
6
+ import {Placeholder} from "./placeholder";
7
+ import styles from './menu-designer.module.css';
8
+ import {
9
+ IconApps,
10
+ IconArrowDown,
11
+ IconBoxModel,
12
+ IconDeviceFloppy,
13
+ IconFolderPlus,
14
+ IconPlus,
15
+ IconX
16
+ } from '@tabler/icons-react';
17
+ import {IFluxoRootState, useAppSelector} from "@fluxo-dev/state";
18
+ import {AppMenu, AppMenuContextType, AppMenuItem, Feature, FeatureList, FluxoClientSys} from "@fluxo-dev/api-client-base";
19
+ import {
20
+ Button,
21
+ Center,
22
+ Chip,
23
+ Divider,
24
+ Grid,
25
+ Group,
26
+ Loader,
27
+ LoadingOverlay,
28
+ Modal,
29
+ rem,
30
+ Select,
31
+ Stack
32
+ } from "@mantine/core";
33
+ import {MenuItemEdit} from "./menu-item-edit";
34
+ import {showNotification} from "@mantine/notifications";
35
+ import {useNavigate} from "react-router-dom";
36
+ import {MenuCreateForm} from "./menu-create-form";
37
+
38
+ export function MenuDesigner() {
39
+
40
+ const [sysFeatures, setSysFeatures] = useState<FeatureList>();
41
+ const [currentSelectedFeatures, setCurrentSelectedFeatures] = useState<Feature[]>();
42
+ const [currentSelectedFeatureId, setCurrentSelectedFeatureId] = useState<string | undefined | null>();
43
+ const [featureSearchValue, setFeatureSearchValue] = useState<string | undefined>();
44
+ const [treeData, setTreeData] = useState<NodeModel<AppMenuItem>[]>([]);
45
+ const [editMenuItemModalOpened, setEditMenuItemModalOpened] = useState<boolean>(false);
46
+ const [editMenuItem, setEditMenuItem] = useState<AppMenuItem>();
47
+ const [editNodeId, setEditNodeId] = useState<string | number>();
48
+ const [loadData, setLoadData] = useState<boolean>(false);
49
+ const [webMenu, setWebMenu] = useState<AppMenu[]>([]);
50
+ const [currentWebMenu, setCurrentWebMenu] = useState<AppMenu | undefined>(undefined);
51
+ const [currentWebMenuLoaded, setCurrentWebMenuLoaded] = useState<boolean>(false);
52
+ const [showMenuCreateForm, setShowMenuCreateForm] = useState<boolean>(false);
53
+
54
+ const [currentAppMenuItemsAreChanged, setCurrentAppMenuItemsAreChanged] = useState<boolean>(false);
55
+
56
+ const handleDrop = (newTree: NodeModel<AppMenuItem>[]) => setTreeData(newTree);
57
+
58
+ const {appMenu} = useAppSelector((state: IFluxoRootState) => state.layout);
59
+ const navigation = useNavigate();
60
+
61
+ useEffect(() => {
62
+ //const result = generateMenuObjectFromMenu((appMenu as AppMenuItem[]));
63
+ //setTreeData(result);
64
+ }, [appMenu])
65
+
66
+ useEffect(() => {
67
+ loadAppMenuList();
68
+ }, []);
69
+
70
+ const loadAppMenuList = () => {
71
+ FluxoClientSys.Instance.configuration.getAppMenuList()
72
+ .then((r) => {
73
+ setWebMenu(r);
74
+ setCurrentWebMenu(r[0]);
75
+ setCurrentWebMenuLoaded(true);
76
+ })
77
+ }
78
+
79
+ useEffect(() => {
80
+
81
+ if(currentWebMenu) {
82
+ onWebMenuChanged(currentWebMenu.id);
83
+ }
84
+
85
+ }, [currentWebMenu]);
86
+
87
+ useEffect(() => {
88
+
89
+ if(treeData) {
90
+ const r = generateMenuFromDesigner(treeData);
91
+ }
92
+
93
+ }, [treeData])
94
+
95
+ useEffect(() => {
96
+ FluxoClientSys.Instance.configuration.getSystemFeatures()
97
+ .then((res) => {
98
+ setSysFeatures(res);
99
+ })
100
+ }, [])
101
+
102
+ const generateMenuFromDesigner = (nodes: NodeModel<AppMenuItem>[], parent: string | number | undefined = 'root'): AppMenuItem[] => {
103
+
104
+ const res: AppMenuItem[] = [];
105
+
106
+ nodes.filter(a => a.parent === parent).map((item) => {
107
+
108
+ item.data!.children = generateMenuFromDesigner(nodes.filter(a => a.id !== item.id), item.id);
109
+ res.push(item.data!);
110
+
111
+ });
112
+
113
+ return res;
114
+ }
115
+
116
+ const generateMenuObjectFromMenu = (menu: AppMenuItem[], parent: string | number | undefined = 'root'): NodeModel<AppMenuItem>[] => {
117
+
118
+ const res: NodeModel<AppMenuItem>[] = [];
119
+
120
+ menu.map((menuItem: AppMenuItem) => {
121
+
122
+ const omit = (prop: string, { [prop]: _, ...rest }) => rest as AppMenuItem;
123
+
124
+ const tmpId = getRandomIntInclusive(1000000,9999999);
125
+ const obj: NodeModel<AppMenuItem> = {
126
+ droppable: true,
127
+ data: omit('children', menuItem),
128
+ text: menuItem.label,
129
+ id: tmpId,
130
+ parent: parent,
131
+ };
132
+
133
+ res.push(obj)
134
+
135
+ if(menuItem.children && menuItem.children.length > 0)
136
+ res.push(...generateMenuObjectFromMenu(menuItem.children, tmpId))
137
+
138
+ });
139
+
140
+ return res;
141
+ }
142
+
143
+ function getRandomIntInclusive(min: number, max: number) {
144
+ min = Math.ceil(min);
145
+ max = Math.floor(max);
146
+ return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
147
+ }
148
+
149
+ const addGroupToMenuHandler = () => {
150
+
151
+ const tmpId = getRandomIntInclusive(1000000,9999999);
152
+
153
+ const tData = [...treeData]
154
+ tData?.push({
155
+ id: tmpId,
156
+ droppable: true,
157
+ data: {icon: 'dashboard', featureIdentifier: '', children: [], label: 'Nuovo gruppo', routerLink: undefined, isGroup: true},
158
+ parent: 'root',
159
+ text: 'Nuovo gruppo',
160
+ })
161
+ setTreeData(tData);
162
+ setCurrentAppMenuItemsAreChanged(true);
163
+ }
164
+
165
+ const addFeatureToMenuHandler = () => {
166
+ const currentFeature = sysFeatures?.features.filter(a => a.identifier === currentSelectedFeatureId)[0];
167
+
168
+ if(!currentFeature)
169
+ return;
170
+
171
+ const tmpId = getRandomIntInclusive(1000000,9999999);
172
+
173
+ const menuItemLabel = currentFeature.identifier === 'webclient.routing.menugroup' ? 'Nuovo gruppo' : currentFeature.description;
174
+
175
+ if(currentFeature) {
176
+ const tData = [...treeData]
177
+ tData?.push({
178
+ id: tmpId,
179
+ droppable: true,
180
+ data: {
181
+ icon: 'dashboard',
182
+ featureIdentifier: currentFeature.identifier,
183
+ children: [],
184
+ label: menuItemLabel,
185
+ routerLink: currentFeature.routerLink,
186
+ isGroup: false,
187
+ menuItemId: currentFeature.menuItemId,
188
+ },
189
+ parent: 'root',
190
+ text: menuItemLabel,
191
+ })
192
+ setTreeData(tData);
193
+ setCurrentAppMenuItemsAreChanged(true);
194
+ }
195
+ }
196
+
197
+ const saveButtonHandler = () => {
198
+
199
+ if(currentWebMenu === undefined)
200
+ return;
201
+
202
+ setLoadData(true);
203
+ const result = generateAppMenuItem(treeData);
204
+
205
+ FluxoClientSys.Instance.configuration.saveAppMenu(currentWebMenu.id, result)
206
+ .then((res) => {
207
+
208
+ showNotification({
209
+ title: 'Salvataggio completato',
210
+ message: `Il menu è stato salvato con successo`,
211
+ color: 'green',
212
+ })
213
+
214
+ navigation(0);
215
+
216
+ }).catch((err) => {
217
+
218
+ showNotification({
219
+ title: 'Errore',
220
+ message: `Errore durante il salvataggio del menu. ${err.message}`,
221
+ color: 'red',
222
+ })
223
+
224
+ })
225
+ .finally(() => {
226
+ setLoadData(false);
227
+ })
228
+ }
229
+
230
+ const generateAppMenuItem = (menuItems: NodeModel<AppMenuItem>[], parent: string | number = 'root') => {
231
+
232
+ const appMenuItems: AppMenuItem[] = []
233
+
234
+ menuItems.filter(a => a.parent === parent).forEach((node) => {
235
+
236
+ const currentMenuItem = node.data!
237
+
238
+ currentMenuItem.children = generateAppMenuItem(menuItems, node.id);
239
+
240
+ appMenuItems.push(currentMenuItem);
241
+ })
242
+
243
+ return appMenuItems;
244
+
245
+ }
246
+
247
+ const updateAppMenuItem = (nodeId: string | number, menuItem: AppMenuItem) => {
248
+ const tData = [...treeData];
249
+
250
+ tData.forEach((n) => {
251
+ if(n.id === nodeId) {
252
+ n.text = menuItem.label;
253
+ n.data = menuItem;
254
+ }
255
+ })
256
+
257
+ setTreeData(tData);
258
+ setEditMenuItemModalOpened(false);
259
+ }
260
+
261
+ const deleteAppMenuItem = (nodeId: string | number) => {
262
+ const tData = deleteDescendants([...treeData], nodeId);
263
+ setTreeData(tData);
264
+ }
265
+
266
+ function deleteDescendants(nodes: NodeModel<AppMenuItem>[], nodeId: number | string): NodeModel<AppMenuItem>[] {
267
+ const descendants = nodes.filter((node) => node.parent === nodeId);
268
+ descendants.forEach((descendant) => {
269
+ nodes = deleteDescendants(nodes, descendant.id);
270
+ });
271
+ return nodes.filter((node) => node.id !== nodeId);
272
+ }
273
+
274
+ const onWebMenuChanged = (webMenuId: number) => {
275
+ setCurrentWebMenuLoaded(false);
276
+
277
+ FluxoClientSys.Instance.configuration.getAppMenu(webMenuId)
278
+ .then((res) => {
279
+ const result = generateMenuObjectFromMenu((res as AppMenuItem[]));
280
+ setTreeData(result);
281
+ setCurrentWebMenuLoaded(true);
282
+ setCurrentAppMenuItemsAreChanged(false);
283
+ })
284
+
285
+ }
286
+
287
+ return (
288
+ <Stack gap={8}>
289
+
290
+ <DataCard title={'Gestione Menu'}>
291
+ <Stack>
292
+ <Grid gutter={16} align={'flex-start'}>
293
+ <Grid.Col span={10.5}>
294
+ <Select
295
+ allowDeselect={false}
296
+ value={currentWebMenu?.id.toString()} data={webMenu?.map(a => {return {label: a.name, value: a.id.toString()}})}
297
+ onChange={(v) => {
298
+ const tmp = webMenu?.filter(a => a.id.toString() === v)[0];
299
+ setCurrentWebMenu(tmp);
300
+ }}
301
+ />
302
+ </Grid.Col>
303
+ <Grid.Col span={1.5}>
304
+ <Button fullWidth leftSection={<IconPlus size={18} />} onClick={() => setShowMenuCreateForm(true)}>Crea menu</Button>
305
+ </Grid.Col>
306
+ </Grid>
307
+ </Stack>
308
+ </DataCard>
309
+
310
+ {!currentWebMenuLoaded &&
311
+ <Center>
312
+ <Loader size={32} />
313
+ </Center>
314
+ }
315
+
316
+ {currentWebMenu && currentWebMenuLoaded &&
317
+ <>
318
+ <Center>
319
+ <IconArrowDown size={48} color={'gray'} />
320
+ </Center>
321
+ <DataCard title={`Menu designer - ${currentWebMenu.name}`}
322
+ elements={[
323
+ <Chip
324
+ icon={<IconDeviceFloppy style={{ width: rem(14), height: rem(14) }} />}
325
+ color={currentAppMenuItemsAreChanged ? 'yellow' : 'green'}
326
+ variant="filled"
327
+ size={'xs'}
328
+ checked={true}
329
+ >
330
+ {currentAppMenuItemsAreChanged ? 'Modifiche non salvate' : 'Salvato'}
331
+ </Chip>
332
+ ]}
333
+ >
334
+
335
+ <Stack>
336
+ <Grid gutter={16} align={'flex-start'}>
337
+ <LoadingOverlay visible={loadData} />
338
+
339
+ <Grid.Col span={{lg: 3}}>
340
+ {sysFeatures &&
341
+ <Select
342
+ placeholder="Seleziona un modulo"
343
+ onChange={(module) => {
344
+ setCurrentSelectedFeatures(sysFeatures?.features.filter(a => a.moduleKey === module));
345
+ setCurrentSelectedFeatureId(null);
346
+ setFeatureSearchValue(undefined);
347
+ }}
348
+ leftSection={<IconBoxModel />}
349
+ data={sysFeatures?.modules.map((e) => {
350
+ return {
351
+ value: `${e.key}.${e.name}`,
352
+ label: `${e.description} (${e.name})`,
353
+ }
354
+ })}
355
+ />
356
+ }
357
+ </Grid.Col>
358
+ <Grid.Col span={{lg: 9}}>
359
+ <Group align={'end'}>
360
+ <div style={{width: 'calc(100% - 336px)'}}>
361
+ <Select
362
+ disabled={!currentSelectedFeatures}
363
+ placeholder="Seleziona una feature"
364
+ searchable
365
+ leftSection={<IconApps />}
366
+ value={currentSelectedFeatureId}
367
+ onChange={(v) => {
368
+ setCurrentSelectedFeatureId(v)
369
+ }}
370
+ onSearchChange={(v) => setFeatureSearchValue(v)}
371
+ searchValue={featureSearchValue}
372
+ clearable
373
+ data={currentSelectedFeatures === undefined ? [] : currentSelectedFeatures?.map((e) => {
374
+ return {
375
+ value: e.identifier,
376
+ label: e.name,
377
+ }
378
+ })}
379
+ />
380
+ </div>
381
+ <div style={{width: 150}}>
382
+ <Button onClick={() => addFeatureToMenuHandler()} disabled={!currentSelectedFeatureId} fullWidth leftSection={<IconPlus size={18} />}>Voce</Button>
383
+ </div>
384
+ <div style={{width: 150}}>
385
+ <Button onClick={() => addGroupToMenuHandler()} fullWidth leftSection={<IconFolderPlus size={18} />}>Gruppo</Button>
386
+ </div>
387
+ </Group>
388
+ </Grid.Col>
389
+ </Grid>
390
+ <DndProvider backend={MultiBackend} options={getBackendOptions()}>
391
+ <Tree
392
+ listComponent={'div'}
393
+ listItemComponent={'div'}
394
+ placeholderComponent={'div'}
395
+ tree={treeData ?? []}
396
+ rootId={'root'}
397
+ sort={false}
398
+ onDrop={handleDrop}
399
+ dropTargetOffset={10}
400
+ canDrop={(tree, { dragSource, dropTargetId, dropTarget }) => {
401
+ return dropTarget?.data?.isGroup || dropTarget === undefined;
402
+ }}
403
+ classes={{
404
+ root: styles["treeRoot"],
405
+ draggingSource: styles["draggingSource"],
406
+ placeholder: styles["placeholderContainer"]
407
+ }}
408
+ placeholderRender={(node, { depth }) => (
409
+ <Placeholder node={node} depth={depth} />
410
+ )}
411
+ render={(node, { depth, isOpen, onToggle }) => (
412
+ <CustomNode
413
+ nodes={treeData ?? []}
414
+ node={node}
415
+ depth={depth}
416
+ isOpen={isOpen}
417
+ onToggle={onToggle}
418
+ onDeleteButtonClick={(nodeId) => deleteAppMenuItem(nodeId)}
419
+ onEditButtonClick={(i) => {
420
+ setEditMenuItem(i);
421
+ setEditNodeId(node.id);
422
+ setEditMenuItemModalOpened(true);
423
+ }}
424
+ />
425
+ )}
426
+ />
427
+ </DndProvider>
428
+ {(editMenuItem && editNodeId) &&
429
+ <Modal title={'Modifica voce di menu'} closeOnEscape={false} closeOnClickOutside={false} centered opened={editMenuItemModalOpened} onClose={() => setEditMenuItemModalOpened(false)}>
430
+ <MenuItemEdit
431
+ nodeId={editNodeId}
432
+ menuItem={editMenuItem}
433
+ onCancelClick={() => {setEditMenuItemModalOpened(false); setEditMenuItem(undefined)}}
434
+ onSaveClick={(nodeId, i) => updateAppMenuItem(nodeId, i)}
435
+ />
436
+ </Modal>
437
+ }
438
+
439
+ <Divider />
440
+
441
+ <Group justify={'end'}>
442
+ <Button color={'red'} disabled={currentWebMenu.contextType == AppMenuContextType.Default} onClick={() => navigation(0)} variant={'outline'}>Elimina</Button>
443
+ <Button onClick={() => saveButtonHandler()}>Salva</Button>
444
+ </Group>
445
+
446
+ </Stack>
447
+ </DataCard>
448
+ </>
449
+ }
450
+
451
+ <Modal centered title={'Aggiungi menu'} opened={showMenuCreateForm} onClose={() => setShowMenuCreateForm(false)} size={'md'} padding={'md'}>
452
+ <MenuCreateForm
453
+ onCancel={() => setShowMenuCreateForm(false)}
454
+ onCreated={() => {
455
+ setShowMenuCreateForm(false)
456
+ loadAppMenuList();
457
+ }}
458
+ />
459
+ </Modal>
460
+
461
+ </Stack>
462
+ );
463
+ }
@@ -0,0 +1,115 @@
1
+ import {forwardRef, useEffect, useState} from "react";
2
+ import {Button, Divider, Group, Select, Stack, Switch, TextInput, Text} from "@mantine/core";
3
+ import {AppMenuItem} from "@fluxo-dev/api-client-base";
4
+ import {IconFactory} from "../../../utils/icon-factory";
5
+
6
+ export interface MenuItemEditProps {
7
+
8
+ nodeId: string | number,
9
+ menuItem: AppMenuItem,
10
+ onCancelClick: () => void,
11
+ onSaveClick: (nodeId: string | number, item: AppMenuItem) => void;
12
+ }
13
+ export function MenuItemEdit(props: MenuItemEditProps) {
14
+
15
+ const [label, setLabel] = useState(props.menuItem.label);
16
+ const [hasIndex, setHasIndex] = useState<boolean>(props.menuItem.hasIndex ?? false);
17
+ const [icon, setIcon] = useState<string | undefined>(props.menuItem.icon ?? undefined);
18
+ const [identifier] = useState(props.menuItem.featureIdentifier);
19
+ const [routerLink, setRouterLink] = useState<string | undefined>(props.menuItem.routerLink);
20
+ const [routerLinkTextValue, setRouterLinkTextValue] = useState<string>(props.menuItem.routerLink ? props.menuItem.routerLink : '');
21
+ const [routerLinkError, setRouterLinkError] = useState<string | null>();
22
+
23
+ const [isValid, setIsValid] = useState<boolean>(true);
24
+
25
+ const [item, setItem] = useState<AppMenuItem>(props.menuItem);
26
+
27
+ useEffect(() => {
28
+ setItem({
29
+ icon: icon,
30
+ featureIdentifier: identifier,
31
+ hasIndex: hasIndex,
32
+ label: label,
33
+ children: props.menuItem.children,
34
+ routerLink: routerLink,
35
+ isGroup: props.menuItem.isGroup,
36
+ menuItemId: props.menuItem.menuItemId
37
+ });
38
+
39
+ if(!props.menuItem.isGroup) {
40
+ if (label && routerLink && icon)
41
+ setIsValid(true)
42
+ else
43
+ setIsValid(false);
44
+ } else {
45
+ if (label && icon)
46
+ setIsValid(true)
47
+ else
48
+ setIsValid(false);
49
+ }
50
+
51
+ }, [label, hasIndex, icon, routerLink])
52
+
53
+ const handleRouterLinkChanged = (v: string) => {
54
+
55
+ setRouterLinkTextValue(v);
56
+
57
+ const isExternalLink = props.menuItem.featureIdentifier === 'webclient.routing.externallink';
58
+ let regex = /^[\w-]+(?:\/[\w-]+)*$/;
59
+
60
+ if(isExternalLink)
61
+ regex = /^(https?:\/\/)?([\w]+\.)+[\w]{2,63}\.[a-zA-Z]{2,6}[\/\?\#]?.*$/;
62
+
63
+ if (regex.test(v)) {
64
+ setRouterLink(v);
65
+ setRouterLinkError(null)
66
+ } else {
67
+ setRouterLink(undefined);
68
+ setRouterLinkError(isExternalLink ? 'Link non valido' : 'Link non valido, es: registries/product/create')
69
+ }
70
+ }
71
+
72
+ const SelectItem = forwardRef<HTMLDivElement, any>(
73
+ ({ label, value, ...others }: any, ref) => (
74
+ <div ref={ref} {...others}>
75
+ <Group wrap="nowrap">
76
+ {IconFactory.getIcon(value ?? '')}
77
+
78
+ <div>
79
+ <Text size="sm">{label}</Text>
80
+ </div>
81
+ </Group>
82
+ </div>
83
+ )
84
+ );
85
+
86
+ return (
87
+ <Stack>
88
+ <TextInput label={'Etichetta'} value={label} onChange={(v) => setLabel(v.currentTarget.value)} />
89
+
90
+ {!props.menuItem.isGroup &&
91
+ <Switch label={'Indicizzato'} checked={hasIndex} offLabel={'NO'} onLabel={'SI'} onChange={(e) => setHasIndex(e.currentTarget.checked)} />
92
+ }
93
+
94
+ <Select
95
+ leftSection={IconFactory.getIcon(icon ?? '')}
96
+ data={IconFactory.getIconIdentifiers().map((e) => {return {
97
+ label: e,
98
+ value: e,
99
+ }})}
100
+ searchable
101
+ label={'Icona'} value={icon} onChange={(v) => setIcon(v!)}
102
+ />
103
+
104
+ {!props.menuItem.isGroup &&
105
+ <TextInput label={'Route'} disabled={true} value={routerLinkTextValue} error={routerLinkError} onChange={(v) => handleRouterLinkChanged(v.currentTarget.value)} />
106
+ }
107
+
108
+ <Divider />
109
+ <Group align={'end'} justify={'right'}>
110
+ <Button variant={'subtle'} onClick={() => props.onCancelClick()}>Annulla</Button>
111
+ <Button disabled={!isValid} onClick={() => props.onSaveClick(props.nodeId, item)}>Salva</Button>
112
+ </Group>
113
+ </Stack>
114
+ )
115
+ }