@map-colonies/react-components 3.8.1 → 3.10.2
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/.env +1 -0
- package/.storybook/main.js +2 -5
- package/CHANGELOG.md +54 -0
- package/dist/assets/img/dragIcon.png +0 -0
- package/dist/assets/img/dragIconLight.png +0 -0
- package/dist/assets/img/glyphicons_067_cleaning.png +0 -0
- package/dist/assets/img/glyphicons_094_vector_path_square.png +0 -0
- package/dist/assets/img/glyphicons_095_vector_path_circle.png +0 -0
- package/dist/assets/img/glyphicons_096_vector_path_polygon.png +0 -0
- package/dist/assets/img/glyphicons_097_vector_path_line.png +0 -0
- package/dist/assets/img/glyphicons_242_google_maps.png +0 -0
- package/dist/assets/img/map-marker.gif +0 -0
- package/dist/autocomplete/autocomplete.css +25 -0
- package/dist/autocomplete/autocomplete.d.ts +33 -0
- package/dist/autocomplete/autocomplete.js +480 -0
- package/dist/autocomplete/index.d.ts +1 -0
- package/dist/autocomplete/index.js +5 -0
- package/dist/box/box.d.ts +3 -0
- package/dist/box/box.js +35 -0
- package/dist/box/index.d.ts +1 -0
- package/dist/box/index.js +5 -0
- package/dist/cesium-map/data-sources/custom.data-source.d.ts +5 -0
- package/dist/cesium-map/data-sources/custom.data-source.js +23 -0
- package/dist/cesium-map/data-sources/drawings.data-source.d.ts +34 -0
- package/dist/cesium-map/data-sources/drawings.data-source.js +187 -0
- package/dist/cesium-map/data-sources/index.d.ts +2 -0
- package/dist/cesium-map/data-sources/index.js +14 -0
- package/dist/cesium-map/entities/entity.d.ts +5 -0
- package/dist/cesium-map/entities/entity.description.d.ts +6 -0
- package/dist/cesium-map/entities/entity.description.js +27 -0
- package/dist/cesium-map/entities/entity.js +23 -0
- package/dist/cesium-map/entities/graphics/polygon.graphics.d.ts +5 -0
- package/dist/cesium-map/entities/graphics/polygon.graphics.js +23 -0
- package/dist/cesium-map/entities/graphics/polyline.graphics.d.ts +5 -0
- package/dist/cesium-map/entities/graphics/polyline.graphics.js +23 -0
- package/dist/cesium-map/entities/graphics/rectangle.graphics.d.ts +5 -0
- package/dist/cesium-map/entities/graphics/rectangle.graphics.js +23 -0
- package/dist/cesium-map/entities/index.d.ts +4 -0
- package/dist/cesium-map/entities/index.js +16 -0
- package/dist/cesium-map/index.d.ts +7 -0
- package/dist/cesium-map/index.js +19 -0
- package/dist/cesium-map/layers/3d.tileset.d.ts +7 -0
- package/dist/cesium-map/layers/3d.tileset.js +39 -0
- package/dist/cesium-map/layers/3d.tileset.update.d.ts +1 -0
- package/dist/cesium-map/layers/3d.tileset.update.js +5 -0
- package/dist/cesium-map/layers/geojson.layer.d.ts +5 -0
- package/dist/cesium-map/layers/geojson.layer.js +23 -0
- package/dist/cesium-map/layers/imagery.layer.d.ts +6 -0
- package/dist/cesium-map/layers/imagery.layer.js +64 -0
- package/dist/cesium-map/layers/index.d.ts +7 -0
- package/dist/cesium-map/layers/index.js +19 -0
- package/dist/cesium-map/layers/osm.layer.d.ts +9 -0
- package/dist/cesium-map/layers/osm.layer.js +36 -0
- package/dist/cesium-map/layers/wms.layer.d.ts +9 -0
- package/dist/cesium-map/layers/wms.layer.js +36 -0
- package/dist/cesium-map/layers/wmts.layer.d.ts +9 -0
- package/dist/cesium-map/layers/wmts.layer.js +36 -0
- package/dist/cesium-map/layers/xyz.layer.d.ts +9 -0
- package/dist/cesium-map/layers/xyz.layer.js +36 -0
- package/dist/cesium-map/layers-manager.d.ts +47 -0
- package/dist/cesium-map/layers-manager.js +228 -0
- package/dist/cesium-map/map.css +54 -0
- package/dist/cesium-map/map.d.ts +46 -0
- package/dist/cesium-map/map.js +273 -0
- package/dist/cesium-map/map.types.d.ts +8 -0
- package/dist/cesium-map/map.types.js +12 -0
- package/dist/cesium-map/proxied.types.d.ts +19 -0
- package/dist/cesium-map/proxied.types.js +89 -0
- package/dist/cesium-map/settings/base-maps.css +37 -0
- package/dist/cesium-map/settings/base-maps.d.ts +7 -0
- package/dist/cesium-map/settings/base-maps.js +78 -0
- package/dist/cesium-map/settings/scene-modes.css +19 -0
- package/dist/cesium-map/settings/scene-modes.d.ts +7 -0
- package/dist/cesium-map/settings/scene-modes.js +65 -0
- package/dist/cesium-map/settings/settings.css +49 -0
- package/dist/cesium-map/settings/settings.d.ts +23 -0
- package/dist/cesium-map/settings/settings.js +79 -0
- package/dist/cesium-map/terrain-providers/custom/dummy-quantized-mesh-tile.d.ts +3 -0
- package/dist/cesium-map/terrain-providers/custom/dummy-quantized-mesh-tile.js +245 -0
- package/dist/cesium-map/terrain-providers/custom/quantized-mesh-decoder.d.ts +9 -0
- package/dist/cesium-map/terrain-providers/custom/quantized-mesh-decoder.js +202 -0
- package/dist/cesium-map/terrain-providers/custom/quantized-mesh-terrain-provider.d.ts +50 -0
- package/dist/cesium-map/terrain-providers/custom/quantized-mesh-terrain-provider.js +136 -0
- package/dist/cesium-map/tools/cesium/primitives-conversions.cesium.d.ts +2 -0
- package/dist/cesium-map/tools/cesium/primitives-conversions.cesium.js +38 -0
- package/dist/cesium-map/tools/coordinates-tracker.tool.css +11 -0
- package/dist/cesium-map/tools/coordinates-tracker.tool.d.ts +7 -0
- package/dist/cesium-map/tools/coordinates-tracker.tool.js +78 -0
- package/dist/cesium-map/tools/draw/drawHelper.css +101 -0
- package/dist/cesium-map/tools/draw/drawHelper.d.ts +28 -0
- package/dist/cesium-map/tools/draw/drawHelper.js +1694 -0
- package/dist/cesium-map/tools/geojson/geojson-to-primitive.d.ts +4 -0
- package/dist/cesium-map/tools/geojson/geojson-to-primitive.js +41 -0
- package/dist/cesium-map/tools/geojson/index.d.ts +2 -0
- package/dist/cesium-map/tools/geojson/index.js +14 -0
- package/dist/cesium-map/tools/geojson/point.geojson.d.ts +3 -0
- package/dist/cesium-map/tools/geojson/point.geojson.js +21 -0
- package/dist/cesium-map/tools/geojson/polygon.geojson.d.ts +3 -0
- package/dist/cesium-map/tools/geojson/polygon.geojson.js +24 -0
- package/dist/cesium-map/tools/geojson/rectangle.geojson.d.ts +3 -0
- package/dist/cesium-map/tools/geojson/rectangle.geojson.js +44 -0
- package/dist/cesium-map/tools/inspector.tool.d.ts +4 -0
- package/dist/cesium-map/tools/inspector.tool.js +33 -0
- package/dist/cesium-map/tools/scale-tracker.tool.css +16 -0
- package/dist/cesium-map/tools/scale-tracker.tool.d.ts +8 -0
- package/dist/cesium-map/tools/scale-tracker.tool.js +158 -0
- package/dist/cesium-map/tools/terranian-height.tool.d.ts +4 -0
- package/dist/cesium-map/tools/terranian-height.tool.js +113 -0
- package/dist/cssbaseline/cssbaseline.d.ts +5 -0
- package/dist/cssbaseline/cssbaseline.js +41 -0
- package/dist/cssbaseline/index.d.ts +1 -0
- package/dist/cssbaseline/index.js +6 -0
- package/dist/date-picker/date-picker.css +9 -0
- package/dist/date-picker/date-picker.d.ts +14 -0
- package/dist/date-picker/date-picker.js +78 -0
- package/dist/date-picker/index.d.ts +1 -0
- package/dist/date-picker/index.js +13 -0
- package/dist/date-range-picker/date-range-picker.css +9 -0
- package/dist/date-range-picker/date-range-picker.d.ts +26 -0
- package/dist/date-range-picker/date-range-picker.form-control.css +3 -0
- package/dist/date-range-picker/date-range-picker.form-control.d.ts +28 -0
- package/dist/date-range-picker/date-range-picker.form-control.js +95 -0
- package/dist/date-range-picker/date-range-picker.js +104 -0
- package/dist/date-range-picker/index.d.ts +2 -0
- package/dist/date-range-picker/index.js +14 -0
- package/dist/file-picker/file-picker.css +62 -0
- package/dist/file-picker/file-picker.d.ts +276 -0
- package/dist/file-picker/file-picker.js +151 -0
- package/dist/file-picker/fs-map.json +1557 -0
- package/dist/file-picker/index.d.ts +2 -0
- package/dist/file-picker/index.js +14 -0
- package/dist/file-picker/localization.d.ts +11 -0
- package/dist/file-picker/localization.js +124 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +23 -16
- package/dist/map-filter-container/container-map.css +5 -0
- package/dist/map-filter-container/container-map.d.ts +11 -0
- package/dist/map-filter-container/container-map.js +31 -0
- package/dist/map-filter-container/index.d.ts +1 -0
- package/dist/map-filter-container/index.js +13 -0
- package/dist/map-filter-container/map-filter-container.d.ts +9 -0
- package/dist/map-filter-container/map-filter-container.js +78 -0
- package/dist/map-filter-container/polygon-selection-ui.d.ts +12 -0
- package/dist/map-filter-container/polygon-selection-ui.js +62 -0
- package/dist/models/defaults.d.ts +28 -0
- package/dist/models/defaults.js +32 -0
- package/dist/models/enums.d.ts +14 -0
- package/dist/models/enums.js +20 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +13 -0
- package/dist/ol-map/feature.d.ts +6 -0
- package/dist/ol-map/feature.js +20 -0
- package/dist/ol-map/index.d.ts +6 -0
- package/dist/ol-map/index.js +18 -0
- package/dist/ol-map/interactions/draw.d.ts +8 -0
- package/dist/ol-map/interactions/draw.js +44 -0
- package/dist/ol-map/interactions/index.d.ts +1 -0
- package/dist/ol-map/interactions/index.js +13 -0
- package/dist/ol-map/layers/index.d.ts +3 -0
- package/dist/ol-map/layers/index.js +15 -0
- package/dist/ol-map/layers/tile-layer.d.ts +9 -0
- package/dist/ol-map/layers/tile-layer.js +48 -0
- package/dist/ol-map/layers/vector-layer.d.ts +4 -0
- package/dist/ol-map/layers/vector-layer.js +48 -0
- package/dist/ol-map/layers/vector-tile-layer.d.ts +10 -0
- package/dist/ol-map/layers/vector-tile-layer.js +66 -0
- package/dist/ol-map/map.css +17 -0
- package/dist/ol-map/map.d.ts +14 -0
- package/dist/ol-map/map.js +117 -0
- package/dist/ol-map/source/index.d.ts +6 -0
- package/dist/ol-map/source/index.js +18 -0
- package/dist/ol-map/source/mvt.d.ts +11 -0
- package/dist/ol-map/source/mvt.js +37 -0
- package/dist/ol-map/source/osm.d.ts +2 -0
- package/dist/ol-map/source/osm.js +14 -0
- package/dist/ol-map/source/vector-source.d.ts +4 -0
- package/dist/ol-map/source/vector-source.js +45 -0
- package/dist/ol-map/source/wms.d.ts +17 -0
- package/dist/ol-map/source/wms.js +30 -0
- package/dist/ol-map/source/wmts.d.ts +21 -0
- package/dist/ol-map/source/wmts.js +59 -0
- package/dist/ol-map/source/xyz.d.ts +12 -0
- package/dist/ol-map/source/xyz.js +27 -0
- package/dist/ol-map/style.d.ts +4 -0
- package/dist/ol-map/style.js +22 -0
- package/dist/popover/index.d.ts +1 -0
- package/dist/popover/index.js +5 -0
- package/dist/popover/popover.d.ts +3 -0
- package/dist/popover/popover.js +35 -0
- package/dist/smart-table/__mock-data__/smartTableMocks.d.ts +7 -0
- package/dist/smart-table/__mock-data__/smartTableMocks.js +17 -0
- package/dist/smart-table/index.d.ts +2 -0
- package/dist/smart-table/index.js +14 -0
- package/dist/smart-table/smart-table-head.d.ts +11 -0
- package/dist/smart-table/smart-table-head.js +22 -0
- package/dist/smart-table/smart-table-row.d.ts +12 -0
- package/dist/smart-table/smart-table-row.js +46 -0
- package/dist/smart-table/smart-table-types.d.ts +9 -0
- package/dist/smart-table/smart-table-types.js +2 -0
- package/dist/smart-table/smart-table.d.ts +17 -0
- package/dist/smart-table/smart-table.js +51 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.js +13 -0
- package/dist/theme/theme.d.ts +8 -0
- package/dist/theme/theme.js +124 -0
- package/dist/utils/map.d.ts +3 -0
- package/dist/utils/map.js +21 -0
- package/dist/utils/projections.d.ts +6 -0
- package/dist/utils/projections.js +10 -0
- package/dist/utils/story.d.ts +12 -0
- package/dist/utils/story.js +2 -0
- package/package.json +103 -100
- package/public/assets/img/map-marker.gif +0 -0
- package/src/lib/cesium-map/map.tsx +22 -12
- package/src/lib/cesium-map/terrain-providers/terrain-provider-heights-tool.stories.tsx +155 -0
- package/src/lib/cesium-map/terrain-providers/terrain-provider.stories.tsx +5 -3
- package/src/lib/cesium-map/tools/coordinates-tracker.tool.tsx +1 -1
- package/src/lib/cesium-map/tools/inspector.tool.tsx +15 -0
- package/src/lib/cesium-map/tools/terranian-height.tool.tsx +167 -0
- package/src/lib/date-range-picker/{stories/DateRangePicker.stories.tsx → date-range-picker.stories.tsx} +5 -5
- package/src/lib/file-picker/file-picker.css +62 -0
- package/src/lib/file-picker/file-picker.stories.tsx +447 -0
- package/src/lib/file-picker/file-picker.tsx +180 -0
- package/src/lib/file-picker/fs-map.json +1557 -0
- package/src/lib/file-picker/index.ts +2 -0
- package/src/lib/file-picker/localization.ts +164 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/models/enums.ts +1 -0
- package/src/lib/smart-table/smart-table-row.spec.tsx +1 -1
- package/tsbuildconfig.json +2 -2
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
body[dir='rtl'] .chonky-fileListWrapper [class^='listContainer-'] {
|
|
2
|
+
direction: rtl !important;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body[dir='rtl']
|
|
6
|
+
.chonky-fileEntryClickableWrapper
|
|
7
|
+
[class^='listFileEntryProperty-'] {
|
|
8
|
+
direction: ltr !important;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body[dir='rtl'] .chonky-chonkyRoot {
|
|
12
|
+
text-align: right;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body[dir='rtl'] .chonky-infoText {
|
|
16
|
+
margin-left: unset;
|
|
17
|
+
margin-right: 12px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body[dir='rtl'] .chonky-contextMenuList .MuiListItemText-root,
|
|
21
|
+
body[dir='rtl'] .chonky-dropdownList .MuiListItemText-root {
|
|
22
|
+
text-align: right;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body[dir='rtl'] .chonky-icon {
|
|
26
|
+
margin-right: unset;
|
|
27
|
+
margin-left: 8px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.chonky-chonkyRoot div[class^='folderBackSide'] {
|
|
31
|
+
box-shadow: unset;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.chonky-chonkyRoot div[class^='folderBackSide']::after {
|
|
35
|
+
border-color: var(--fp-theme-background, #fff)
|
|
36
|
+
var(--fp-theme-background, #fff) transparent transparent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.chonky-chonkyRoot div[class^='listFileEntryIcon-'] {
|
|
40
|
+
color: #d5d5d5;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.chonky-chonkyRoot div[class*='previewFile-d'] {
|
|
44
|
+
background-color: #d5d5d5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.chonky-chonkyRoot div[class^='selectionIndicator-'] {
|
|
48
|
+
background: var(--fp-theme-selection-background, #455570);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.chonky-chonkyRoot {
|
|
52
|
+
background-color: var(--fp-theme-background, #fff) !important;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.chonky-contextMenuList {
|
|
56
|
+
background-color: var(--fp-theme-surface, #fff) !important;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
button[class*='chonky-activeButton'],
|
|
60
|
+
.chonky-selectionSizeText {
|
|
61
|
+
color: var(--fp-theme-primary, #24aee9) !important;
|
|
62
|
+
}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
5
|
+
import React, {
|
|
6
|
+
useCallback,
|
|
7
|
+
useState,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useEffect,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { Story } from '@storybook/react/types-6-0';
|
|
13
|
+
import {
|
|
14
|
+
FormControl,
|
|
15
|
+
FormControlLabel,
|
|
16
|
+
FormLabel,
|
|
17
|
+
Radio,
|
|
18
|
+
RadioGroup,
|
|
19
|
+
} from '@material-ui/core';
|
|
20
|
+
import { Box } from '../box';
|
|
21
|
+
import { SupportedLocales } from '../models';
|
|
22
|
+
import {
|
|
23
|
+
FileActionData,
|
|
24
|
+
FilePicker,
|
|
25
|
+
FileArray,
|
|
26
|
+
FileData,
|
|
27
|
+
FileHelper,
|
|
28
|
+
FilePickerActions,
|
|
29
|
+
FilePickerHandle,
|
|
30
|
+
} from './file-picker';
|
|
31
|
+
import FsMap from './fs-map.json';
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
title: 'File Picker',
|
|
35
|
+
component: FilePicker,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface CustomFileData extends FileData {
|
|
39
|
+
parentId?: string;
|
|
40
|
+
childrenIds?: string[];
|
|
41
|
+
}
|
|
42
|
+
interface CustomFileMap {
|
|
43
|
+
[fileId: string]: CustomFileData;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const prepareCustomFileMap = (): Record<string, unknown> => {
|
|
47
|
+
const baseFileMap = (FsMap.fileMap as unknown) as CustomFileMap;
|
|
48
|
+
const rootFolderId = FsMap.rootFolderId;
|
|
49
|
+
return { baseFileMap, rootFolderId };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Sets up files map and actions
|
|
53
|
+
// eslint-disable-next-line
|
|
54
|
+
const useCustomFileMap = () => {
|
|
55
|
+
const { baseFileMap, rootFolderId } = useMemo(prepareCustomFileMap, []);
|
|
56
|
+
|
|
57
|
+
const [fileMap, setFileMap] = useState<CustomFileMap>(
|
|
58
|
+
baseFileMap as CustomFileMap
|
|
59
|
+
);
|
|
60
|
+
const [currentFolderId, setCurrentFolderId] = useState(rootFolderId);
|
|
61
|
+
|
|
62
|
+
const resetFileMap = useCallback(() => {
|
|
63
|
+
setFileMap(baseFileMap as CustomFileMap);
|
|
64
|
+
setCurrentFolderId(rootFolderId);
|
|
65
|
+
}, [baseFileMap, rootFolderId]);
|
|
66
|
+
|
|
67
|
+
const currentFolderIdRef = useRef(currentFolderId);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
currentFolderIdRef.current = currentFolderId;
|
|
71
|
+
}, [currentFolderId]);
|
|
72
|
+
|
|
73
|
+
const deleteFiles = useCallback((files: CustomFileData[]) => {
|
|
74
|
+
setFileMap((currentFileMap) => {
|
|
75
|
+
const newFileMap = { ...currentFileMap };
|
|
76
|
+
|
|
77
|
+
files.forEach((file) => {
|
|
78
|
+
delete newFileMap[file.id];
|
|
79
|
+
|
|
80
|
+
if (file.parentId) {
|
|
81
|
+
const parent = newFileMap[file.parentId];
|
|
82
|
+
const newChildrenIds = parent.childrenIds?.filter(
|
|
83
|
+
(id) => id !== file.id
|
|
84
|
+
);
|
|
85
|
+
newFileMap[file.parentId] = {
|
|
86
|
+
...parent,
|
|
87
|
+
childrenIds: newChildrenIds,
|
|
88
|
+
childrenCount: newChildrenIds?.length,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return newFileMap;
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const moveFiles = useCallback(
|
|
98
|
+
(
|
|
99
|
+
files: CustomFileData[],
|
|
100
|
+
source: CustomFileData,
|
|
101
|
+
destination: CustomFileData
|
|
102
|
+
) => {
|
|
103
|
+
setFileMap((currentFileMap) => {
|
|
104
|
+
const newFileMap = { ...currentFileMap };
|
|
105
|
+
const moveFileIds = new Set(files.map((f) => f.id));
|
|
106
|
+
|
|
107
|
+
// Delete files from their source folder.
|
|
108
|
+
const newSourceChildrenIds = source.childrenIds?.filter(
|
|
109
|
+
(id) => !moveFileIds.has(id)
|
|
110
|
+
);
|
|
111
|
+
newFileMap[source.id] = {
|
|
112
|
+
...source,
|
|
113
|
+
childrenIds: newSourceChildrenIds,
|
|
114
|
+
childrenCount: newSourceChildrenIds?.length,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Add the files to their destination folder.
|
|
118
|
+
const newDestinationChildrenIds = [
|
|
119
|
+
...(destination.childrenIds as string[]),
|
|
120
|
+
...files.map((f) => f.id),
|
|
121
|
+
];
|
|
122
|
+
newFileMap[destination.id] = {
|
|
123
|
+
...destination,
|
|
124
|
+
childrenIds: newDestinationChildrenIds,
|
|
125
|
+
childrenCount: newDestinationChildrenIds.length,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Finally, update the parent folder ID on the files - from source folder
|
|
129
|
+
// ID to the destination folder ID.
|
|
130
|
+
files.forEach((file) => {
|
|
131
|
+
newFileMap[file.id] = {
|
|
132
|
+
...file,
|
|
133
|
+
parentId: destination.id,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return newFileMap;
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
[]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// TODO: in production we should use UUIDs or MD5 hashes for file paths
|
|
144
|
+
const idCounter = useRef(0);
|
|
145
|
+
const createFolder = useCallback((folderName: string) => {
|
|
146
|
+
setFileMap((currentFileMap) => {
|
|
147
|
+
const newFileMap = { ...currentFileMap };
|
|
148
|
+
|
|
149
|
+
const parentId = currentFolderIdRef.current as string;
|
|
150
|
+
// Create the new folder.
|
|
151
|
+
const newFolderId = `new-folder-${idCounter.current++}`;
|
|
152
|
+
newFileMap[newFolderId] = {
|
|
153
|
+
id: newFolderId,
|
|
154
|
+
name: folderName,
|
|
155
|
+
isDir: true,
|
|
156
|
+
modDate: new Date(),
|
|
157
|
+
parentId: parentId,
|
|
158
|
+
childrenIds: [],
|
|
159
|
+
childrenCount: 0,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Update parent folder to reference the new folder.
|
|
163
|
+
const parent = newFileMap[parentId];
|
|
164
|
+
newFileMap[parentId] = {
|
|
165
|
+
...parent,
|
|
166
|
+
childrenIds: [...(parent.childrenIds as string[]), newFolderId],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return newFileMap;
|
|
170
|
+
});
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
fileMap,
|
|
175
|
+
currentFolderId,
|
|
176
|
+
setCurrentFolderId,
|
|
177
|
+
resetFileMap,
|
|
178
|
+
deleteFiles,
|
|
179
|
+
moveFiles,
|
|
180
|
+
createFolder,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const useFiles = (
|
|
185
|
+
fileMap: CustomFileMap,
|
|
186
|
+
currentFolderId: string
|
|
187
|
+
): FileArray => {
|
|
188
|
+
return useMemo(() => {
|
|
189
|
+
const currentFolder = fileMap[currentFolderId];
|
|
190
|
+
const files = currentFolder.childrenIds
|
|
191
|
+
? currentFolder.childrenIds.map((fileId: string) => fileMap[fileId])
|
|
192
|
+
: [];
|
|
193
|
+
return files;
|
|
194
|
+
}, [currentFolderId, fileMap]);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const useFolderChain = (
|
|
198
|
+
fileMap: CustomFileMap,
|
|
199
|
+
currentFolderId: string
|
|
200
|
+
): FileArray => {
|
|
201
|
+
return useMemo(() => {
|
|
202
|
+
const currentFolder = fileMap[currentFolderId];
|
|
203
|
+
const folderChain = [currentFolder];
|
|
204
|
+
let parentId = currentFolder.parentId;
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
206
|
+
while (parentId) {
|
|
207
|
+
const parentFile = fileMap[parentId];
|
|
208
|
+
// eslint-disable-next-line
|
|
209
|
+
if (parentFile) {
|
|
210
|
+
folderChain.unshift(parentFile);
|
|
211
|
+
parentId = parentFile.parentId;
|
|
212
|
+
} else {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return folderChain;
|
|
217
|
+
}, [currentFolderId, fileMap]);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const useFileActionHandler = (
|
|
221
|
+
setCurrentFolderId: (folderId: string) => void,
|
|
222
|
+
deleteFiles: (files: CustomFileData[]) => void,
|
|
223
|
+
moveFiles: (
|
|
224
|
+
files: FileData[],
|
|
225
|
+
source: FileData,
|
|
226
|
+
destination: FileData
|
|
227
|
+
) => void,
|
|
228
|
+
createFolder: (folderName: string) => void
|
|
229
|
+
): ((data: FileActionData) => void) => {
|
|
230
|
+
return useCallback(
|
|
231
|
+
(data: FileActionData) => {
|
|
232
|
+
if (data.id === FilePickerActions.OpenFiles.id) {
|
|
233
|
+
const { targetFile, files } = data.payload;
|
|
234
|
+
const fileToOpen = targetFile ?? files[0];
|
|
235
|
+
// eslint-disable-next-line
|
|
236
|
+
if (fileToOpen && FileHelper.isDirectory(fileToOpen)) {
|
|
237
|
+
setCurrentFolderId(fileToOpen.id);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
} else if (data.id === FilePickerActions.DeleteFiles.id) {
|
|
241
|
+
deleteFiles(data.state.selectedFilesForAction);
|
|
242
|
+
} else if (data.id === FilePickerActions.MoveFiles.id) {
|
|
243
|
+
moveFiles(
|
|
244
|
+
data.payload.files,
|
|
245
|
+
data.payload.source as FileData,
|
|
246
|
+
data.payload.destination
|
|
247
|
+
);
|
|
248
|
+
} else if (data.id === FilePickerActions.CreateFolder.id) {
|
|
249
|
+
const folderName = prompt('Provide the name for your new folder:');
|
|
250
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
251
|
+
if (folderName) createFolder(folderName);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[createFolder, deleteFiles, moveFiles, setCurrentFolderId]
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const ReadOnlyMode: Story = () => {
|
|
259
|
+
const {
|
|
260
|
+
fileMap,
|
|
261
|
+
currentFolderId,
|
|
262
|
+
setCurrentFolderId,
|
|
263
|
+
// resetFileMap,
|
|
264
|
+
deleteFiles,
|
|
265
|
+
moveFiles,
|
|
266
|
+
createFolder,
|
|
267
|
+
} = useCustomFileMap();
|
|
268
|
+
const files = useFiles(fileMap, currentFolderId as string);
|
|
269
|
+
const folderChain = useFolderChain(fileMap, currentFolderId as string);
|
|
270
|
+
const handleFileAction = useFileActionHandler(
|
|
271
|
+
setCurrentFolderId,
|
|
272
|
+
deleteFiles,
|
|
273
|
+
moveFiles,
|
|
274
|
+
createFolder
|
|
275
|
+
);
|
|
276
|
+
return (
|
|
277
|
+
<Box style={{ height: '600px' }}>
|
|
278
|
+
<FilePicker
|
|
279
|
+
files={files}
|
|
280
|
+
folderChain={folderChain}
|
|
281
|
+
onFileAction={handleFileAction}
|
|
282
|
+
readOnlyMode={true}
|
|
283
|
+
/>
|
|
284
|
+
</Box>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const DarkTheme: Story = () => {
|
|
289
|
+
const {
|
|
290
|
+
fileMap,
|
|
291
|
+
currentFolderId,
|
|
292
|
+
setCurrentFolderId,
|
|
293
|
+
// resetFileMap,
|
|
294
|
+
deleteFiles,
|
|
295
|
+
moveFiles,
|
|
296
|
+
createFolder,
|
|
297
|
+
} = useCustomFileMap();
|
|
298
|
+
const files = useFiles(fileMap, currentFolderId as string);
|
|
299
|
+
const folderChain = useFolderChain(fileMap, currentFolderId as string);
|
|
300
|
+
const handleFileAction = useFileActionHandler(
|
|
301
|
+
setCurrentFolderId,
|
|
302
|
+
deleteFiles,
|
|
303
|
+
moveFiles,
|
|
304
|
+
createFolder
|
|
305
|
+
);
|
|
306
|
+
return (
|
|
307
|
+
<Box style={{ height: '600px' }}>
|
|
308
|
+
<FilePicker
|
|
309
|
+
files={files}
|
|
310
|
+
folderChain={folderChain}
|
|
311
|
+
onFileAction={handleFileAction}
|
|
312
|
+
theme={{
|
|
313
|
+
primary: 'blue',
|
|
314
|
+
background: 'black',
|
|
315
|
+
surface: 'gray',
|
|
316
|
+
textOnBackground: 'white',
|
|
317
|
+
selectionBackground: '#455570',
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
</Box>
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const Localized: Story = () => {
|
|
325
|
+
const {
|
|
326
|
+
fileMap,
|
|
327
|
+
currentFolderId,
|
|
328
|
+
setCurrentFolderId,
|
|
329
|
+
// resetFileMap,
|
|
330
|
+
deleteFiles,
|
|
331
|
+
moveFiles,
|
|
332
|
+
createFolder,
|
|
333
|
+
} = useCustomFileMap();
|
|
334
|
+
const files = useFiles(fileMap, currentFolderId as string);
|
|
335
|
+
const folderChain = useFolderChain(fileMap, currentFolderId as string);
|
|
336
|
+
const handleFileAction = useFileActionHandler(
|
|
337
|
+
setCurrentFolderId,
|
|
338
|
+
deleteFiles,
|
|
339
|
+
moveFiles,
|
|
340
|
+
createFolder
|
|
341
|
+
);
|
|
342
|
+
const [locale, setLocale] = useState<SupportedLocales>(SupportedLocales.HE);
|
|
343
|
+
const handleLocaleChange = useCallback(
|
|
344
|
+
(event) => setLocale(event.target.value),
|
|
345
|
+
[]
|
|
346
|
+
);
|
|
347
|
+
return (
|
|
348
|
+
<>
|
|
349
|
+
<FormControl component="fieldset" style={{ marginBottom: 15 }}>
|
|
350
|
+
<FormLabel component="legend">Pick a language:</FormLabel>
|
|
351
|
+
<RadioGroup
|
|
352
|
+
aria-label="locale"
|
|
353
|
+
name="locale"
|
|
354
|
+
value={locale}
|
|
355
|
+
onChange={handleLocaleChange}
|
|
356
|
+
>
|
|
357
|
+
<FormControlLabel
|
|
358
|
+
value={SupportedLocales.HE}
|
|
359
|
+
control={<Radio />}
|
|
360
|
+
label="עברית"
|
|
361
|
+
/>
|
|
362
|
+
<FormControlLabel
|
|
363
|
+
value={SupportedLocales.RU}
|
|
364
|
+
control={<Radio />}
|
|
365
|
+
label="Русский"
|
|
366
|
+
/>
|
|
367
|
+
<FormControlLabel
|
|
368
|
+
value={SupportedLocales.EN}
|
|
369
|
+
control={<Radio />}
|
|
370
|
+
label="English"
|
|
371
|
+
/>
|
|
372
|
+
</RadioGroup>
|
|
373
|
+
</FormControl>
|
|
374
|
+
<br />
|
|
375
|
+
<Box style={{ height: '600px' }}>
|
|
376
|
+
<FilePicker
|
|
377
|
+
files={files}
|
|
378
|
+
folderChain={folderChain}
|
|
379
|
+
onFileAction={handleFileAction}
|
|
380
|
+
locale={locale}
|
|
381
|
+
/>
|
|
382
|
+
</Box>
|
|
383
|
+
</>
|
|
384
|
+
);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export const FilesSelection: Story = () => {
|
|
388
|
+
const {
|
|
389
|
+
fileMap,
|
|
390
|
+
currentFolderId,
|
|
391
|
+
setCurrentFolderId,
|
|
392
|
+
// resetFileMap,
|
|
393
|
+
deleteFiles,
|
|
394
|
+
moveFiles,
|
|
395
|
+
createFolder,
|
|
396
|
+
} = useCustomFileMap();
|
|
397
|
+
const files = useFiles(fileMap, currentFolderId as string);
|
|
398
|
+
const folderChain = useFolderChain(fileMap, currentFolderId as string);
|
|
399
|
+
const handleFileAction = useFileActionHandler(
|
|
400
|
+
setCurrentFolderId,
|
|
401
|
+
deleteFiles,
|
|
402
|
+
moveFiles,
|
|
403
|
+
createFolder
|
|
404
|
+
);
|
|
405
|
+
const fileBrowserRef = useRef<FilePickerHandle>(null);
|
|
406
|
+
|
|
407
|
+
const logSelection = useCallback(
|
|
408
|
+
(event: React.MouseEvent) => {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
event.stopPropagation();
|
|
411
|
+
if (!fileBrowserRef.current) return;
|
|
412
|
+
console.log(fileBrowserRef.current.getFileSelection());
|
|
413
|
+
},
|
|
414
|
+
[fileBrowserRef]
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const randomizeSelection = useCallback(
|
|
418
|
+
(event: React.MouseEvent) => {
|
|
419
|
+
event.preventDefault();
|
|
420
|
+
event.stopPropagation();
|
|
421
|
+
if (!fileBrowserRef.current) return;
|
|
422
|
+
const randomFileIds = new Set<string>();
|
|
423
|
+
for (const file of files) {
|
|
424
|
+
if (file && Math.random() > 0.5) randomFileIds.add(file.id);
|
|
425
|
+
}
|
|
426
|
+
fileBrowserRef.current.setFileSelection(randomFileIds);
|
|
427
|
+
},
|
|
428
|
+
[files, fileBrowserRef]
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<Box style={{ height: '600px' }}>
|
|
433
|
+
<button type="button" onClick={randomizeSelection}>
|
|
434
|
+
Select files
|
|
435
|
+
</button>
|
|
436
|
+
<button type="button" onClick={logSelection}>
|
|
437
|
+
Log selection
|
|
438
|
+
</button>
|
|
439
|
+
<FilePicker
|
|
440
|
+
ref={fileBrowserRef}
|
|
441
|
+
files={files}
|
|
442
|
+
folderChain={folderChain}
|
|
443
|
+
onFileAction={handleFileAction}
|
|
444
|
+
/>
|
|
445
|
+
</Box>
|
|
446
|
+
);
|
|
447
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ChonkyActions,
|
|
4
|
+
ChonkyFileActionData,
|
|
5
|
+
FileAction,
|
|
6
|
+
FileArray as ChonkyFileArray,
|
|
7
|
+
FileBrowserHandle,
|
|
8
|
+
FileBrowserProps,
|
|
9
|
+
FileData as ChonkyFileData,
|
|
10
|
+
FileHelper as ChonkyFileHelper,
|
|
11
|
+
FullFileBrowser,
|
|
12
|
+
I18nConfig,
|
|
13
|
+
setChonkyDefaults,
|
|
14
|
+
} from 'chonky';
|
|
15
|
+
import { ChonkyIconFA } from 'chonky-icon-fontawesome';
|
|
16
|
+
import { makeStyles } from '@material-ui/core/styles';
|
|
17
|
+
import { Box } from '../box';
|
|
18
|
+
import { SupportedLocales } from '../models';
|
|
19
|
+
import localization from './localization';
|
|
20
|
+
|
|
21
|
+
import './file-picker.css';
|
|
22
|
+
|
|
23
|
+
export type FilePickerHandle = FileBrowserHandle;
|
|
24
|
+
|
|
25
|
+
export type FileActionData = ChonkyFileActionData;
|
|
26
|
+
|
|
27
|
+
export type FileArray = ChonkyFileArray;
|
|
28
|
+
|
|
29
|
+
export type FileData = ChonkyFileData;
|
|
30
|
+
|
|
31
|
+
export type FilePickerAction = FileAction;
|
|
32
|
+
|
|
33
|
+
export class FileHelper extends ChonkyFileHelper {}
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
36
|
+
export const FilePickerActions = ChonkyActions;
|
|
37
|
+
|
|
38
|
+
export type FilePickerView = typeof FilePickerView[keyof typeof FilePickerView];
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
40
|
+
export const FilePickerView = {
|
|
41
|
+
listView: ChonkyActions.EnableListView.id,
|
|
42
|
+
gridView: ChonkyActions.EnableGridView.id,
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
export interface FilePickerTheme {
|
|
46
|
+
primary: string;
|
|
47
|
+
background: string;
|
|
48
|
+
surface: string;
|
|
49
|
+
textOnBackground: string;
|
|
50
|
+
selectionBackground: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// IMPLEMENTATION NOTES: Currently FilePicker component works with his own icon set.
|
|
54
|
+
// In future might be tweaked.
|
|
55
|
+
setChonkyDefaults({ iconComponent: ChonkyIconFA });
|
|
56
|
+
|
|
57
|
+
export interface FilePickerProps extends Partial<FileBrowserProps> {
|
|
58
|
+
theme?: FilePickerTheme;
|
|
59
|
+
styles?: Record<string, string>;
|
|
60
|
+
defaultView?: FilePickerView;
|
|
61
|
+
readOnlyMode?: boolean;
|
|
62
|
+
locale?: SupportedLocales;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const FilePicker: React.FC<FilePickerProps> = React.memo(
|
|
66
|
+
React.forwardRef<FileBrowserHandle, FilePickerProps>(
|
|
67
|
+
(
|
|
68
|
+
{
|
|
69
|
+
theme,
|
|
70
|
+
styles = { height: '100%', minWidth: '600px' },
|
|
71
|
+
defaultView = FilePickerView.listView,
|
|
72
|
+
readOnlyMode = false,
|
|
73
|
+
locale,
|
|
74
|
+
files,
|
|
75
|
+
folderChain,
|
|
76
|
+
onFileAction,
|
|
77
|
+
...props
|
|
78
|
+
},
|
|
79
|
+
ref
|
|
80
|
+
) => {
|
|
81
|
+
// IMPLEMENTATION NOTES: Currently FilePicker component discards the ability to show file thumbnail.
|
|
82
|
+
// In future might be tweaked.
|
|
83
|
+
const thumbnailGenerator = useCallback(
|
|
84
|
+
(file: FileData) => null,
|
|
85
|
+
// file.thumbnailUrl ? `https://chonky.io${file.thumbnailUrl}` : null,
|
|
86
|
+
[]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
makeStyles({
|
|
90
|
+
'@global': {
|
|
91
|
+
'.chonky-dropdownList': {
|
|
92
|
+
backgroundColor: `${theme?.surface as string} !important`,
|
|
93
|
+
},
|
|
94
|
+
'li[class*="chonky-activeButton"]': {
|
|
95
|
+
color: `${theme?.primary as string} !important`,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
})();
|
|
99
|
+
|
|
100
|
+
const toDashCase = (str: string): string => {
|
|
101
|
+
return str.replace(/([A-Z])/g, ($1) => '-' + $1.toLowerCase());
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const themeObject = useMemo((): Record<string, string> => {
|
|
105
|
+
if (theme !== undefined) {
|
|
106
|
+
const processedColors = Object.keys(theme).reduce(
|
|
107
|
+
(acc: Record<string, string>, key) => {
|
|
108
|
+
const val = ((theme as unknown) as Record<string, string>)[key];
|
|
109
|
+
key = key.startsWith('--')
|
|
110
|
+
? key
|
|
111
|
+
: `--fp-theme-${toDashCase(key)}`;
|
|
112
|
+
acc[key] = val;
|
|
113
|
+
return acc;
|
|
114
|
+
},
|
|
115
|
+
{}
|
|
116
|
+
);
|
|
117
|
+
return processedColors;
|
|
118
|
+
}
|
|
119
|
+
return {};
|
|
120
|
+
}, [theme]);
|
|
121
|
+
|
|
122
|
+
const [darkMode, setDarkMode] = useState<boolean>(false);
|
|
123
|
+
const [defaultFileViewActionId, setDefaultFileViewActionId] = useState<
|
|
124
|
+
FilePickerView
|
|
125
|
+
>();
|
|
126
|
+
const [disableDragAndDrop, setDisableDragAndDrop] = useState<boolean>(
|
|
127
|
+
false
|
|
128
|
+
);
|
|
129
|
+
const [fileActions, setFileActions] = useState<FilePickerAction[]>();
|
|
130
|
+
const [i18n, setI18n] = useState<I18nConfig>();
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (theme) {
|
|
133
|
+
setDarkMode(true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setDefaultFileViewActionId(defaultView);
|
|
137
|
+
|
|
138
|
+
if (readOnlyMode) {
|
|
139
|
+
setDisableDragAndDrop(true);
|
|
140
|
+
} else {
|
|
141
|
+
setFileActions([
|
|
142
|
+
ChonkyActions.CreateFolder,
|
|
143
|
+
ChonkyActions.DeleteFiles,
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (locale !== undefined) {
|
|
148
|
+
setI18n(localization[locale]);
|
|
149
|
+
}
|
|
150
|
+
}, [theme, defaultView, readOnlyMode, locale]);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Box
|
|
154
|
+
style={{
|
|
155
|
+
...styles,
|
|
156
|
+
...themeObject,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<FullFileBrowser
|
|
160
|
+
ref={ref}
|
|
161
|
+
files={files ?? []}
|
|
162
|
+
folderChain={folderChain}
|
|
163
|
+
onFileAction={(data: FileActionData): void => {
|
|
164
|
+
if (typeof onFileAction === 'function') {
|
|
165
|
+
void onFileAction(data);
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
thumbnailGenerator={thumbnailGenerator}
|
|
169
|
+
defaultFileViewActionId={defaultFileViewActionId}
|
|
170
|
+
disableDragAndDrop={disableDragAndDrop}
|
|
171
|
+
fileActions={fileActions}
|
|
172
|
+
darkMode={darkMode}
|
|
173
|
+
i18n={i18n}
|
|
174
|
+
{...props}
|
|
175
|
+
/>
|
|
176
|
+
</Box>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
);
|