@squiz/resource-browser 1.32.1-alpha.14 → 1.32.1-alpha.16

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 (90) hide show
  1. package/jest.config.ts +12 -1
  2. package/lib/Hooks/useCategorisedSources.d.ts +14 -0
  3. package/lib/Hooks/useCategorisedSources.js +38 -0
  4. package/lib/Hooks/useChildResources.d.ts +19 -0
  5. package/lib/Hooks/useChildResources.js +35 -0
  6. package/lib/Hooks/useResourcePath.d.ts +16 -0
  7. package/lib/Hooks/useResourcePath.js +64 -0
  8. package/lib/Hooks/useSources.d.ts +16 -0
  9. package/lib/Hooks/useSources.js +29 -0
  10. package/lib/Icons/Icon.d.ts +7 -7
  11. package/lib/Icons/Icon.js +7 -9
  12. package/lib/Icons/MatrixResources/Audio.js +1 -1
  13. package/lib/Icons/MatrixResources/Excel.js +1 -1
  14. package/lib/Icons/MatrixResources/MatrixResourceMap.d.ts +6 -6
  15. package/lib/Icons/MatrixResources/MatrixResourceMap.js +6 -6
  16. package/lib/Icons/MatrixResources/Pdf.js +1 -1
  17. package/lib/Icons/MatrixResources/Powerpoint.js +1 -1
  18. package/lib/Icons/MatrixResources/Video.js +1 -1
  19. package/lib/Icons/MatrixResources/Word.js +1 -1
  20. package/lib/Modal/Modal.js +1 -1
  21. package/lib/PreviewPanel/PreviewModal.js +1 -1
  22. package/lib/PreviewPanel/PreviewPanel.d.ts +4 -6
  23. package/lib/PreviewPanel/PreviewPanel.js +11 -39
  24. package/lib/PreviewPanel/details/MatrixResource.d.ts +4 -9
  25. package/lib/PreviewPanel/details/MatrixResource.js +20 -16
  26. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +5 -5
  27. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +3 -3
  28. package/lib/ResourceItem/ResourceItem.d.ts +6 -8
  29. package/lib/ResourceItem/ResourceItem.js +3 -3
  30. package/lib/ResourceList/ResourceList.d.ts +5 -4
  31. package/lib/ResourceList/ResourceList.js +3 -3
  32. package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +4 -5
  33. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +34 -89
  34. package/lib/SourceDropdown/SourceDropdown.d.ts +5 -5
  35. package/lib/SourceDropdown/SourceDropdown.js +19 -27
  36. package/lib/SourceList/SourceList.d.ts +4 -4
  37. package/lib/SourceList/SourceList.js +7 -5
  38. package/lib/index.css +6 -0
  39. package/lib/index.d.ts +6 -29
  40. package/lib/index.js +2 -3
  41. package/lib/uuid.js +1 -3
  42. package/package.json +3 -2
  43. package/src/Hooks/useCategorisedSources.spec.ts +39 -0
  44. package/src/Hooks/useCategorisedSources.ts +46 -0
  45. package/src/Hooks/useChildResources.spec.ts +49 -0
  46. package/src/Hooks/useChildResources.ts +43 -0
  47. package/src/Hooks/useResourcePath.spec.ts +124 -0
  48. package/src/Hooks/useResourcePath.ts +76 -0
  49. package/src/Hooks/useSources.spec.ts +33 -0
  50. package/src/Hooks/useSources.ts +33 -0
  51. package/src/Icons/Icon.stories.tsx +7 -7
  52. package/src/Icons/Icon.tsx +9 -14
  53. package/src/Icons/MatrixResources/Audio.tsx +1 -1
  54. package/src/Icons/MatrixResources/Excel.tsx +1 -1
  55. package/src/Icons/MatrixResources/MatrixResourceMap.ts +7 -7
  56. package/src/Icons/MatrixResources/Pdf.tsx +1 -1
  57. package/src/Icons/MatrixResources/Powerpoint.tsx +1 -1
  58. package/src/Icons/MatrixResources/Video.tsx +1 -1
  59. package/src/Icons/MatrixResources/Word.tsx +1 -1
  60. package/src/Modal/Modal.tsx +1 -1
  61. package/src/PreviewPanel/PreviewModal.tsx +1 -1
  62. package/src/PreviewPanel/PreviewPanel.spec.tsx +20 -62
  63. package/src/PreviewPanel/PreviewPanel.stories.tsx +16 -24
  64. package/src/PreviewPanel/PreviewPanel.tsx +15 -51
  65. package/src/PreviewPanel/details/MatrixResource.tsx +23 -19
  66. package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +13 -23
  67. package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +1 -1
  68. package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +8 -9
  69. package/src/ResourceBreadcrumb/sample-hierarchy.json +15 -25
  70. package/src/ResourceItem/ResourceItem.tsx +10 -12
  71. package/src/ResourceList/ResourceList.spec.tsx +8 -53
  72. package/src/ResourceList/ResourceList.stories.tsx +2 -2
  73. package/src/ResourceList/ResourceList.tsx +12 -10
  74. package/src/ResourceList/sample-resources.json +551 -49
  75. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +196 -315
  76. package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +7 -29
  77. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +63 -127
  78. package/src/SourceDropdown/SourceDropdown.spec.tsx +63 -60
  79. package/src/SourceDropdown/SourceDropdown.stories.tsx +4 -7
  80. package/src/SourceDropdown/SourceDropdown.tsx +34 -41
  81. package/src/SourceList/SourceList.spec.tsx +38 -32
  82. package/src/SourceList/SourceList.tsx +17 -19
  83. package/src/SourceList/sample-sources.json +186 -77
  84. package/src/__mocks__/MockModels.ts +30 -0
  85. package/src/__mocks__/StorybookHelpers.ts +46 -0
  86. package/src/index.stories.tsx +13 -38
  87. package/src/index.tsx +5 -29
  88. package/src/types.d.ts +71 -0
  89. package/src/uuid.ts +2 -4
  90. package/src/SourceDropdown/sample-sources.json +0 -110
@@ -44,9 +44,9 @@ const ResourceBreadcrumb = function ({ hierarchy, onBreadcrumbSelect, onReturnTo
44
44
  react_1.default.createElement("li", { className: "flex items-center mr-3" },
45
45
  react_1.default.createElement("button", { type: "button", onClick: onReturnToRoot },
46
46
  react_1.default.createElement(Icon_1.default, { icon: 'root', "aria-label": "Return to source list", className: "" }))),
47
- hierarchy.map(({ id, label }, index) => {
48
- return (react_1.default.createElement("li", { key: `${id.source}-${id.id}`, className: "resource-breadcrumb__item max-md:hidden flex items-center mr-2 before:content-['/'] before:mr-2" },
49
- index !== hierarchy.length - 1 && (react_1.default.createElement("button", { type: "button", onClick: () => onBreadcrumbSelect(id) },
47
+ hierarchy.map(({ key, label, node }, index) => {
48
+ return (react_1.default.createElement("li", { key: `${key}`, className: "resource-breadcrumb__item max-md:hidden flex items-center mr-2 before:content-['/'] before:mr-2" },
49
+ index !== hierarchy.length - 1 && (react_1.default.createElement("button", { type: "button", onClick: () => onBreadcrumbSelect(node) },
50
50
  react_1.default.createElement("div", { className: `resource-breadcrumb__label`, title: label }, label))),
51
51
  index === hierarchy.length - 1 && (react_1.default.createElement("div", { className: `resource-breadcrumb__label md:font-semibold`, title: label }, label)),
52
52
  hierarchy.length > 3 && index === 0 && (react_1.default.createElement("div", { className: "resource-breadcrumb__expander flex" },
@@ -1,19 +1,17 @@
1
1
  /// <reference types="react" />
2
2
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
3
3
  import { OverlayTriggerState } from 'react-stately';
4
- import { NodeIdentifier } from '../index';
5
- interface ResourceItem {
6
- key: string;
7
- id: NodeIdentifier;
8
- selected: boolean;
4
+ interface ResourceItem<T> {
5
+ item: T;
6
+ selected?: boolean;
9
7
  label: string;
10
8
  type: string;
11
9
  childCount: number;
12
10
  previewModalState: OverlayTriggerState;
13
- onSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
14
- onDrillDown: (node: NodeIdentifier) => void;
11
+ onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
12
+ onDrillDown: (node: T) => void;
15
13
  className: string;
16
14
  allowedTypes?: string[] | undefined;
17
15
  }
18
- declare const ResourceItem: ({ id, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, }: ResourceItem) => JSX.Element;
16
+ declare const ResourceItem: <T>({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, }: ResourceItem<T>) => JSX.Element;
19
17
  export default ResourceItem;
@@ -7,18 +7,18 @@ const react_1 = __importDefault(require("react"));
7
7
  const react_aria_1 = require("react-aria");
8
8
  const Icon_1 = __importDefault(require("../Icons/Icon"));
9
9
  const ModalOpeningButton_1 = __importDefault(require("../Modal/ModalOpeningButton"));
10
- const ResourceItem = ({ id, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, }) => {
10
+ const ResourceItem = ({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, }) => {
11
11
  const { triggerProps, overlayProps } = (0, react_aria_1.useOverlayTrigger)({ type: 'dialog' }, previewModalState);
12
12
  const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(type);
13
13
  const title = isDisabled ? "You can't select this item" : label;
14
14
  return (react_1.default.createElement("li", { className: `flex items-stretch p-1 bg-white border border-grey-200 ${className}` },
15
- react_1.default.createElement(ModalOpeningButton_1.default, { type: "button", ...triggerProps, isDisabled: isDisabled, onPress: () => onSelect(id, overlayProps), className: `
15
+ react_1.default.createElement(ModalOpeningButton_1.default, { type: "button", ...triggerProps, isDisabled: isDisabled, onPress: () => onSelect(item, overlayProps), className: `
16
16
  relative grow flex items-center p-4 rounded ${selected ? 'bg-blue-100 text-blue-400' : ''} ${childCount > 0 ? 'mr-2' : ''} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-100 focus:bg-gray-100'}
17
17
  `, title: title },
18
18
  react_1.default.createElement(Icon_1.default, { icon: type, resourceSource: "matrix", "aria-label": type, className: `mr-4 shrink-0 ${isDisabled && 'opacity-40'}` }),
19
19
  react_1.default.createElement("span", { className: "text-left break-all" }, label),
20
20
  childCount <= 0 && react_1.default.createElement(Icon_1.default, { icon: 'arrow-right', className: "absolute right-5" })),
21
- childCount > 0 && (react_1.default.createElement("button", { type: "button", "aria-label": `Drill down to ${label} children`, onClick: () => onDrillDown(id), className: `relative shrink-0 flex items-center p-4 rounded before:w-px before:h-[calc(100%-0.75rem)] before:bg-gray-200 before:absolute before:top-1.5 before:-left-1 hover:bg-gray-100 focus:bg-gray-100` },
21
+ childCount > 0 && (react_1.default.createElement("button", { type: "button", "aria-label": `Drill down to ${label} children`, onClick: () => onDrillDown(item), className: `relative shrink-0 flex items-center p-4 rounded before:w-px before:h-[calc(100%-0.75rem)] before:bg-gray-200 before:absolute before:top-1.5 before:-left-1 hover:bg-gray-100 focus:bg-gray-100` },
22
22
  react_1.default.createElement("span", { className: "ml-auto flex items-center" },
23
23
  react_1.default.createElement("span", { className: "truncate w-10 text-right", title: String(childCount) }, childCount),
24
24
  react_1.default.createElement(Icon_1.default, { icon: 'arrow-right', className: "ml-1" }))))));
@@ -1,14 +1,15 @@
1
1
  /// <reference types="react" />
2
2
  import { OverlayTriggerState } from 'react-stately';
3
3
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
4
- import { NodeIdentifier, Resource } from '../index';
4
+ import { Resource } from '../types';
5
5
  export interface ResourceListProps {
6
6
  resources: Array<Resource>;
7
+ selectedResource?: Resource | null;
7
8
  previewModalState: OverlayTriggerState;
8
9
  isLoading: boolean;
9
- onResourceSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
10
- onResourceDrillDown: (node: NodeIdentifier) => void;
10
+ onResourceSelect: (resource: Resource, overlayProps: DOMAttributes<FocusableElement>) => void;
11
+ onResourceDrillDown: (resource: Resource) => void;
11
12
  allowedTypes?: string[] | undefined;
12
13
  }
13
- declare const ResourceList: ({ resources, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, }: ResourceListProps) => JSX.Element;
14
+ declare const ResourceList: ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, }: ResourceListProps) => JSX.Element;
14
15
  export default ResourceList;
@@ -29,7 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  const react_1 = __importStar(require("react"));
30
30
  const ResourceItem_1 = __importDefault(require("../ResourceItem/ResourceItem"));
31
31
  const SkeletonListItem_1 = require("../Skeleton/ListItem/SkeletonListItem");
32
- const ResourceList = function ({ resources, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, }) {
32
+ const ResourceList = function ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, }) {
33
33
  const listRef = (0, react_1.useRef)(null);
34
34
  // When resources change, because we are on a new page, reset focus to the list
35
35
  (0, react_1.useEffect)(() => {
@@ -44,8 +44,8 @@ const ResourceList = function ({ resources, previewModalState, isLoading, onReso
44
44
  return react_1.default.createElement(SkeletonListItem_1.SkeletonListItem, { key: index });
45
45
  }))),
46
46
  !isLoading &&
47
- resources.map(({ type, id, selected, label, childCount }) => {
48
- return (react_1.default.createElement(ResourceItem_1.default, { key: `${id.source}-${id.id}`, id: id, selected: selected, label: label, type: type, childCount: childCount, previewModalState: previewModalState, onSelect: onResourceSelect, onDrillDown: onResourceDrillDown, className: "border-b-0 first:mt-0 first:rounded-t-lg last:rounded-b-lg last:border-b", allowedTypes: allowedTypes }));
47
+ resources.map((resource) => {
48
+ return (react_1.default.createElement(ResourceItem_1.default, { key: resource.id, item: resource, selected: resource.id == selectedResource?.id, label: resource.name, type: resource.type.code, childCount: resource.childCount, previewModalState: previewModalState, onSelect: onResourceSelect, onDrillDown: onResourceDrillDown, className: "border-b-0 first:mt-0 first:rounded-t-lg last:rounded-b-lg last:border-b", allowedTypes: allowedTypes }));
49
49
  })));
50
50
  };
51
51
  exports.default = ResourceList;
@@ -1,15 +1,14 @@
1
1
  /// <reference types="react" />
2
2
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
3
- import { NodeIdentifier, Source, Resource, ResourceDetail } from '../index';
3
+ import { Source, Resource, HydratedResourceReference } from '../types';
4
4
  interface ResourcePickerContainerProps {
5
5
  title: string;
6
6
  titleAriaProps: DOMAttributes<FocusableElement>;
7
7
  allowedTypes: string[] | undefined;
8
8
  onRequestSources: () => Promise<Source[]>;
9
- onRequestChildren(id: NodeIdentifier): Promise<Resource[]>;
10
- onRequestResource(id: NodeIdentifier): Promise<ResourceDetail | null>;
11
- onChange(resource: NodeIdentifier | null): void;
9
+ onRequestChildren(source: Source, resource: Resource | null): Promise<Resource[]>;
10
+ onChange(resource: HydratedResourceReference | null): void;
12
11
  onClose: () => void;
13
12
  }
14
- declare function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestChildren, onRequestResource, onChange, onClose, }: ResourcePickerContainerProps): JSX.Element;
13
+ declare function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestChildren, onChange, onClose, }: ResourcePickerContainerProps): JSX.Element;
15
14
  export default ResourcePickerContainer;
@@ -33,113 +33,58 @@ const ResourceList_1 = __importDefault(require("../ResourceList/ResourceList"));
33
33
  const ResourceBreadcrumb_1 = __importDefault(require("../ResourceBreadcrumb/ResourceBreadcrumb"));
34
34
  const PreviewPanel_1 = __importDefault(require("../PreviewPanel/PreviewPanel"));
35
35
  const SourceDropdown_1 = __importDefault(require("../SourceDropdown/SourceDropdown"));
36
- function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestChildren, onRequestResource, onChange, onClose, }) {
36
+ const useResourcePath_1 = require("../Hooks/useResourcePath");
37
+ const useChildResources_1 = require("../Hooks/useChildResources");
38
+ const useSources_1 = require("../Hooks/useSources");
39
+ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestChildren, onChange, onClose, }) {
37
40
  const previewModalState = (0, react_stately_1.useOverlayTriggerState)({});
38
- const [isSourceLoading, setIsSourceLoading] = (0, react_1.useState)(false);
39
- const [isMainLoading, setIsMainLoading] = (0, react_1.useState)(false);
40
- const [isSecondaryLoading, setIsSecondaryLoading] = (0, react_1.useState)(false);
41
- const [currentNode, setCurrentNode] = (0, react_1.useState)(null);
42
- const [selectedId, setSelectedId] = (0, react_1.useState)(null);
41
+ const [selectedResource, setSelectedResource] = (0, react_1.useState)(null);
43
42
  const [previewModalOverlayProps, setPreviewModalOverlayProps] = (0, react_1.useState)({});
44
- const [hierarchy, setHierarchy] = (0, react_1.useState)([]);
45
- const [sources, setSources] = (0, react_1.useState)([]);
46
- const [resources, setResources] = (0, react_1.useState)([]);
47
- const [selectedNodeDetails, setSelectedNodeDetails] = (0, react_1.useState)(null);
48
- const adjustHierarchy = (node, resetHierarchy) => {
49
- const isInHierarchy = hierarchy.find((hNode) => hNode.id === node);
50
- let newHierarchy = [];
51
- // If the node is already in the hierarchy we need to 'jump' back to it
52
- if (isInHierarchy) {
53
- let reachedNode = false;
54
- // Read though the hierarchy and add any nodes before and including the current to the array
55
- hierarchy.forEach((hNode) => {
56
- if (reachedNode === false) {
57
- newHierarchy.push(hNode);
58
- if (hNode.id === node) {
59
- reachedNode = true;
60
- }
61
- }
62
- });
63
- }
64
- else {
65
- let label = resources.find((resource) => resource.id === node)?.label || '';
66
- // Might be a source
67
- if (!label) {
68
- const source = sources.find((source) => source.id === node.source);
69
- if (source) {
70
- label = source.nodes.find((resource) => resource.id === node)?.label || '';
71
- }
72
- }
73
- // If we are jumping to a complete other spot and the container knows it, it can request a complete reset
74
- if (!resetHierarchy) {
75
- newHierarchy = hierarchy.slice();
76
- }
77
- newHierarchy.push({
78
- id: node,
79
- label,
80
- });
81
- }
82
- setHierarchy(newHierarchy);
83
- };
84
- const handleResourceSelected = (0, react_1.useCallback)((node, overlayProps) => {
43
+ const { source, currentResource, hierarchy, setSource, push, popUntil } = (0, useResourcePath_1.useResourcePath)();
44
+ const { isLoading: isSourceLoading, sources } = (0, useSources_1.useSources)({ onRequestSources });
45
+ const { isLoading: isResourcesLoading, resources } = (0, useChildResources_1.useChildResources)({
46
+ source,
47
+ currentResource,
48
+ onRequestChildren,
49
+ });
50
+ const handleResourceDrillDown = (0, react_1.useCallback)((resource) => {
51
+ push(resource);
52
+ }, [push]);
53
+ const handleResourceSelected = (0, react_1.useCallback)((resource, overlayProps) => {
85
54
  setPreviewModalOverlayProps(overlayProps);
86
- setIsSecondaryLoading(true);
87
- setSelectedId(node);
88
- setSelectedNodeDetails(null);
89
- onRequestResource(node).then((detail) => {
90
- setSelectedNodeDetails(detail);
91
- setIsSecondaryLoading(false);
92
- });
55
+ setSelectedResource(resource);
93
56
  }, []);
94
- const handleResourceDrillDown = (0, react_1.useCallback)((node, resetHierarchy) => {
95
- setIsMainLoading(true);
96
- setCurrentNode(node);
97
- adjustHierarchy(node, resetHierarchy || false);
98
- setSelectedId(null);
99
- setSelectedNodeDetails(null);
100
- setResources([]);
101
- onRequestChildren(node).then((resources) => {
102
- setResources(resources);
103
- setIsMainLoading(false);
104
- });
105
- }, [resources, sources]);
57
+ const handleSourceDrilldown = (0, react_1.useCallback)((source) => {
58
+ setSource(source);
59
+ }, [setSource]);
106
60
  const handleReturnToRoot = (0, react_1.useCallback)(() => {
107
- setCurrentNode(null);
108
- setSelectedId(null);
109
- setResources([]);
110
- setHierarchy([]);
111
- setSelectedNodeDetails(null);
112
- }, []);
113
- const handleDetailSelect = (0, react_1.useCallback)((node) => {
114
- onChange(node);
61
+ setSource(null);
62
+ }, [setSource]);
63
+ const handleDetailSelect = (0, react_1.useCallback)((resource) => {
64
+ onChange({ resource, source: source?.source });
115
65
  onClose();
116
- }, []);
66
+ }, [source]);
117
67
  const handleDetailClose = (0, react_1.useCallback)(() => {
118
- setSelectedId(null);
68
+ setSelectedResource(null);
119
69
  }, []);
120
- // On load of component fetch the list of sources
70
+ // When the active node changes clear the selected resource
121
71
  (0, react_1.useEffect)(() => {
122
- setIsSourceLoading(true);
123
- onRequestSources().then((sources) => {
124
- setSources(sources);
125
- setIsSourceLoading(false);
126
- });
127
- }, []);
72
+ setSelectedResource(null);
73
+ }, [hierarchy]);
128
74
  return (react_1.default.createElement("div", { className: "relative flex flex-col h-full" },
129
75
  react_1.default.createElement("div", { className: "flex items-center p-6" },
130
76
  react_1.default.createElement("h2", { ...titleAriaProps, className: "text-xl leading-6 text-gray-800 font-semibold mr-6" }, title),
131
77
  react_1.default.createElement("div", { className: "px-3 border-l border-grey-300 w-300px" },
132
- react_1.default.createElement(SourceDropdown_1.default, { sources: sources, currentSource: hierarchy[0]?.id, isLoading: isSourceLoading, onSourceSelect: handleResourceDrillDown, onRootSelect: handleReturnToRoot })),
78
+ react_1.default.createElement(SourceDropdown_1.default, { sources: sources, selectedSource: source, isLoading: isSourceLoading, onSourceSelect: handleSourceDrilldown, onRootSelect: handleReturnToRoot })),
133
79
  react_1.default.createElement("button", { type: "button", "aria-label": `Close ${title} dialog`, onClick: onClose, className: "absolute top-2 right-2 p-2.5 rounded hover:bg-blue-100 focus:bg-blue-100" },
134
80
  react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg" },
135
81
  react_1.default.createElement("path", { d: "M13.3 0.710017C13.1131 0.522765 12.8595 0.417532 12.595 0.417532C12.3305 0.417532 12.0768 0.522765 11.89 0.710017L6.99997 5.59002L2.10997 0.700017C1.92314 0.512765 1.66949 0.407532 1.40497 0.407532C1.14045 0.407532 0.886802 0.512765 0.699971 0.700017C0.309971 1.09002 0.309971 1.72002 0.699971 2.11002L5.58997 7.00002L0.699971 11.89C0.309971 12.28 0.309971 12.91 0.699971 13.3C1.08997 13.69 1.71997 13.69 2.10997 13.3L6.99997 8.41002L11.89 13.3C12.28 13.69 12.91 13.69 13.3 13.3C13.69 12.91 13.69 12.28 13.3 11.89L8.40997 7.00002L13.3 2.11002C13.68 1.73002 13.68 1.09002 13.3 0.710017Z", fill: "currentColor" })))),
136
82
  react_1.default.createElement("div", { className: "flex border-t border-grey-300 h-[calc(100%-92px)]" },
137
83
  react_1.default.createElement("div", { className: "overflow-y-scroll flex-1 grow-[3] border-r border-gray-300" },
138
84
  react_1.default.createElement("h3", { className: "sr-only" }, "Resource List"),
139
- currentNode === null && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading, onSourceSelect: handleResourceSelected, onSourceDrillDown: handleResourceDrillDown, allowedTypes: allowedTypes })),
140
- currentNode && (react_1.default.createElement(react_1.default.Fragment, null,
141
- react_1.default.createElement(ResourceBreadcrumb_1.default, { hierarchy: hierarchy, onBreadcrumbSelect: handleResourceDrillDown, onReturnToRoot: handleReturnToRoot }),
142
- react_1.default.createElement(ResourceList_1.default, { previewModalState: previewModalState, resources: resources, isLoading: isMainLoading, onResourceSelect: handleResourceSelected, onResourceDrillDown: handleResourceDrillDown, allowedTypes: allowedTypes })))),
143
- react_1.default.createElement(PreviewPanel_1.default, { node: selectedId, resourceDetail: selectedNodeDetails, modalState: previewModalState, isLoading: isSecondaryLoading, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose }))));
85
+ hierarchy.length > 0 && (react_1.default.createElement(ResourceBreadcrumb_1.default, { hierarchy: hierarchy, onBreadcrumbSelect: popUntil, onReturnToRoot: handleReturnToRoot })),
86
+ !source && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading, onSourceSelect: handleSourceDrilldown, onSourceDrillDown: handleSourceDrilldown, allowedTypes: allowedTypes })),
87
+ source && (react_1.default.createElement(ResourceList_1.default, { previewModalState: previewModalState, resources: resources, selectedResource: selectedResource, isLoading: isResourcesLoading, onResourceSelect: handleResourceSelected, onResourceDrillDown: handleResourceDrillDown, allowedTypes: allowedTypes }))),
88
+ react_1.default.createElement(PreviewPanel_1.default, { resource: selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose }))));
144
89
  }
145
90
  exports.default = ResourcePickerContainer;
@@ -1,9 +1,9 @@
1
1
  /// <reference types="react" />
2
- import type { Source, NodeIdentifier } from '../index';
3
- export default function SourceDropdown({ sources, currentSource, isLoading, onRootSelect, onSourceSelect, }: {
4
- sources: Array<Source>;
5
- currentSource: NodeIdentifier | null;
2
+ import type { Source, ScopedSource } from '../types';
3
+ export default function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, }: {
4
+ sources: Source[];
5
+ selectedSource: ScopedSource | null;
6
6
  isLoading: boolean;
7
7
  onRootSelect: () => void;
8
- onSourceSelect: (node: NodeIdentifier, resetHierarchy: boolean) => void;
8
+ onSourceSelect: (source: ScopedSource) => void;
9
9
  }): JSX.Element;
@@ -31,7 +31,9 @@ const interactions_1 = require("@react-aria/interactions");
31
31
  const Spinner_1 = __importDefault(require("../Spinner/Spinner"));
32
32
  const Icon_1 = __importDefault(require("../Icons/Icon"));
33
33
  const uuid_1 = __importDefault(require("../uuid"));
34
- function SourceDropdown({ sources, currentSource, isLoading, onRootSelect, onSourceSelect, }) {
34
+ const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
35
+ function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, }) {
36
+ const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
35
37
  const [uniqueId] = (0, react_1.useState)((0, uuid_1.default)());
36
38
  const buttonRef = (0, react_1.useRef)(null);
37
39
  const [isOpen, setIsOpen] = (0, react_1.useState)(false);
@@ -50,34 +52,23 @@ function SourceDropdown({ sources, currentSource, isLoading, onRootSelect, onSou
50
52
  }
51
53
  },
52
54
  });
53
- const handleSourceClick = (id) => {
55
+ const handleSourceClick = (source) => {
54
56
  setIsOpen(false);
55
57
  buttonRef.current?.focus();
56
- onSourceSelect(id, true);
58
+ onSourceSelect(source);
57
59
  };
58
60
  const handleRootSelect = () => {
59
61
  setIsOpen(false);
60
62
  buttonRef.current?.focus();
61
63
  onRootSelect();
62
64
  };
63
- let currentResource = undefined;
64
- for (let i = 0; i < sources.length; i++) {
65
- const source = sources[i];
66
- if (currentSource?.source === source.id) {
67
- currentResource = source.nodes.find((node) => {
68
- if (node.id.id === currentSource?.id) {
69
- return node;
70
- }
71
- });
72
- }
73
- }
74
65
  return (react_1.default.createElement("div", { ...focusWithinProps, ...keyboardProps, className: "relative w-72 border border-2 rounded border-gray-300" },
75
66
  react_1.default.createElement("button", { ref: buttonRef, type: "button", "aria-label": "Source quick select", "aria-expanded": isOpen, "aria-controls": `${uniqueId}-button-menu`, onClick: () => setIsOpen(!isOpen), className: "relative flex items-center text-sm font-semibold p-2 w-full" },
76
- currentResource && (react_1.default.createElement(react_1.default.Fragment, null,
67
+ selectedSource && (react_1.default.createElement(react_1.default.Fragment, null,
77
68
  react_1.default.createElement("span", { className: "sr-only" }, "current source "),
78
- react_1.default.createElement(Icon_1.default, { icon: currentResource.type, resourceSource: "matrix", "aria-hidden": true, className: "mr-2.5" }),
79
- react_1.default.createElement("div", { className: "truncate max-w-[200px]" }, currentResource.label))),
80
- !currentResource && (react_1.default.createElement(react_1.default.Fragment, null,
69
+ react_1.default.createElement(Icon_1.default, { icon: selectedSource.resource?.type.code, resourceSource: "matrix", "aria-hidden": true, className: "mr-2.5" }),
70
+ react_1.default.createElement("div", { className: "truncate max-w-[200px]" }, selectedSource.resource?.name || selectedSource.source.name))),
71
+ !selectedSource && (react_1.default.createElement(react_1.default.Fragment, null,
81
72
  react_1.default.createElement("span", { className: "sr-only" }, "view "),
82
73
  react_1.default.createElement(Icon_1.default, { icon: 'root', "aria-hidden": true, className: "mr-2.5" }),
83
74
  "All available sources")),
@@ -90,16 +81,17 @@ function SourceDropdown({ sources, currentSource, isLoading, onRootSelect, onSou
90
81
  isLoading && (react_1.default.createElement("li", { className: "mt-6" },
91
82
  react_1.default.createElement(Spinner_1.default, { size: "lg", label: "Loading sources" }))),
92
83
  !isLoading &&
93
- sources.map(({ id: sourceId, name, nodes }, index) => {
94
- return (react_1.default.createElement("li", { key: sourceId, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}` },
84
+ categorisedSources.map(({ key, label, sources }, index) => {
85
+ return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}` },
95
86
  react_1.default.createElement("div", { className: "relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0" },
96
- react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, name)),
97
- nodes?.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${name} nodes`, className: "flex flex-col mt-2" }, nodes.map(({ type, id: nodeId, selected, label }) => {
98
- return (react_1.default.createElement("li", { key: `${sourceId}-${nodeId.id}`, className: "flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b" },
99
- react_1.default.createElement("button", { type: "button", onClick: () => handleSourceClick(nodeId), className: `relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100 ${selected ? 'bg-blue-100 text-blue-400' : ''}` },
100
- react_1.default.createElement(Icon_1.default, { icon: type, resourceSource: "matrix", "aria-label": type, className: "shrink-0 mr-2.5" }),
101
- react_1.default.createElement("span", { className: "text-left mr-7" }, label),
102
- nodeId === currentResource?.id && (react_1.default.createElement(Icon_1.default, { icon: 'selected', "aria-label": "selected", className: "absolute right-4" })))));
87
+ react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, label)),
88
+ sources?.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${label} nodes`, className: "flex flex-col mt-2" }, sources.map(({ source, resource }) => {
89
+ const isSelectedSource = source.id === selectedSource?.source.id && resource?.id === selectedSource?.resource?.id;
90
+ return (react_1.default.createElement("li", { key: `${source.id}-${resource?.id}`, className: "flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b" },
91
+ react_1.default.createElement("button", { type: "button", onClick: () => handleSourceClick({ source, resource }), className: `relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100 ${isSelectedSource ? 'bg-blue-100 text-blue-400' : ''}` },
92
+ react_1.default.createElement(Icon_1.default, { icon: resource?.type.code, resourceSource: "matrix", "aria-label": resource?.type.name, className: "shrink-0 mr-2.5" }),
93
+ react_1.default.createElement("span", { className: "text-left mr-7" }, resource?.name || source.name),
94
+ isSelectedSource && (react_1.default.createElement(Icon_1.default, { icon: 'selected', "aria-label": "selected", className: "absolute right-4" })))));
103
95
  })))));
104
96
  }))));
105
97
  }
@@ -1,13 +1,13 @@
1
1
  /// <reference types="react" />
2
2
  import { OverlayTriggerState } from 'react-stately';
3
3
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
4
- import { NodeIdentifier, Source } from '../index';
4
+ import { Source, ScopedSource } from '../types';
5
5
  export interface SourceListProps {
6
- sources: Array<Source>;
6
+ sources: Source[];
7
7
  previewModalState: OverlayTriggerState;
8
8
  isLoading: boolean;
9
- onSourceSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
10
- onSourceDrillDown: (node: NodeIdentifier) => void;
9
+ onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
10
+ onSourceDrillDown: (node: ScopedSource) => void;
11
11
  allowedTypes?: string[] | undefined;
12
12
  }
13
13
  declare const SourceList: ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, }: SourceListProps) => JSX.Element;
@@ -30,7 +30,9 @@ const react_1 = __importStar(require("react"));
30
30
  const ResourceItem_1 = __importDefault(require("../ResourceItem/ResourceItem"));
31
31
  const SkeletonList_1 = require("../Skeleton/List/SkeletonList");
32
32
  const clsx_1 = __importDefault(require("clsx"));
33
+ const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
33
34
  const SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, }) {
35
+ const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
34
36
  const listRef = (0, react_1.useRef)(null);
35
37
  (0, react_1.useEffect)(() => {
36
38
  if (listRef.current) {
@@ -46,12 +48,12 @@ const SourceList = function ({ sources, previewModalState, isLoading, onSourceSe
46
48
  react_1.default.createElement("li", null,
47
49
  react_1.default.createElement(SkeletonList_1.SkeletonList, { itemCount: 3 })))),
48
50
  !isLoading &&
49
- sources.map(({ id: sourceId, name, nodes }, index) => {
50
- return (react_1.default.createElement("li", { key: sourceId, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}` },
51
+ categorisedSources.map(({ key, label, sources }, index) => {
52
+ return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}` },
51
53
  react_1.default.createElement("div", { className: "relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0" },
52
- react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, name)),
53
- nodes?.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${name} nodes`, className: "flex flex-col" }, nodes.map(({ type, id: nodeId, selected, label, childCount }) => {
54
- return (react_1.default.createElement(ResourceItem_1.default, { key: `${sourceId}-${nodeId.id}`, id: nodeId, selected: selected, label: label, type: type, childCount: childCount, previewModalState: previewModalState, onSelect: onSourceSelect, onDrillDown: onSourceDrillDown, className: "mt-3 rounded-lg", allowedTypes: allowedTypes }));
54
+ react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, label)),
55
+ sources.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${label} nodes`, className: "flex flex-col" }, sources.map(({ source, resource }) => {
56
+ return (react_1.default.createElement(ResourceItem_1.default, { key: `${source.id}-${resource?.id}`, item: { source, resource }, label: resource?.name || source.name, type: resource?.type.code || 'folder', childCount: resource?.childCount || 0, previewModalState: previewModalState, onSelect: onSourceSelect, onDrillDown: onSourceDrillDown, className: "mt-3 rounded-lg", allowedTypes: allowedTypes }));
55
57
  })))));
56
58
  })));
57
59
  };
package/lib/index.css CHANGED
@@ -354,6 +354,9 @@
354
354
  white-space: nowrap;
355
355
  border-width: 0;
356
356
  }
357
+ .squiz-rb-scope .visible {
358
+ visibility: visible;
359
+ }
357
360
  .squiz-rb-scope .collapse {
358
361
  visibility: collapse;
359
362
  }
@@ -405,6 +408,9 @@
405
408
  .squiz-rb-scope .z-50 {
406
409
  z-index: 50;
407
410
  }
411
+ .squiz-rb-scope .z-\[9998\] {
412
+ z-index: 9998;
413
+ }
408
414
  .squiz-rb-scope .z-\[9999\] {
409
415
  z-index: 9999;
410
416
  }
package/lib/index.d.ts CHANGED
@@ -1,37 +1,14 @@
1
1
  import React from 'react';
2
- export type NodeIdentifier = {
3
- source: string | null;
4
- id: string;
5
- };
6
- export type ResourceDetail = {
7
- type: string;
8
- name: string;
9
- properties: Map<string, any>;
10
- };
11
- export type Resource = {
12
- id: NodeIdentifier;
13
- type: string;
14
- selected: boolean;
15
- label: string;
16
- childCount: number;
17
- };
18
- export type Source = {
19
- id: string;
20
- name: string;
21
- nodes: Array<Resource>;
22
- };
23
- export type Hierarchy = {
24
- id: NodeIdentifier;
25
- label: string;
26
- };
27
- export default function ComponentEditorContentBrowser({ showButtonLabel, buttonLabel, buttonIcon, modalTitle, allowedTypes, onRequestSources, onRequestChildren, onRequestResource, onChange, }: {
2
+ import { HydratedResourceReference, Resource, ResourceReference, Source } from './types';
3
+ export type { HydratedResourceReference, Resource, ResourceReference, Source };
4
+ export default function ComponentEditorContentBrowser({ showButtonLabel, buttonLabel, buttonIcon, modalTitle, allowedTypes, onRequestSources, onRequestChildren, onChange, }: {
28
5
  showButtonLabel?: boolean;
29
6
  buttonLabel: string;
30
7
  buttonIcon: React.ReactNode;
31
8
  modalTitle: string;
32
9
  allowedTypes: string[] | undefined;
33
10
  onRequestSources: () => Promise<Source[]>;
34
- onRequestChildren(id: NodeIdentifier): Promise<Resource[]>;
35
- onRequestResource(id: NodeIdentifier): Promise<ResourceDetail | null>;
36
- onChange(resource: NodeIdentifier | null): void;
11
+ onRequestChildren(source: Source, resource: Resource | null): Promise<Resource[]>;
12
+ onRequestResource(reference: ResourceReference): Promise<Resource | null>;
13
+ onChange(resource: HydratedResourceReference | null): void;
37
14
  }): JSX.Element;
package/lib/index.js CHANGED
@@ -4,12 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const react_1 = __importDefault(require("react"));
7
- // import { ResourceBrowserInternalContext } from './InternalContext/InternalContext';
8
7
  const ModalTrigger_1 = __importDefault(require("./Modal/ModalTrigger"));
9
8
  const ResourcePickerContainer_1 = __importDefault(require("./ResourcePickerContainer/ResourcePickerContainer"));
10
- function ComponentEditorContentBrowser({ showButtonLabel, buttonLabel, buttonIcon, modalTitle, allowedTypes, onRequestSources, onRequestChildren, onRequestResource, onChange, }) {
9
+ function ComponentEditorContentBrowser({ showButtonLabel, buttonLabel, buttonIcon, modalTitle, allowedTypes, onRequestSources, onRequestChildren, onChange, }) {
11
10
  const showLabel = showButtonLabel || false;
12
11
  return (react_1.default.createElement("div", { className: "squiz-rb-scope" },
13
- react_1.default.createElement(ModalTrigger_1.default, { showLabel: showLabel, label: buttonLabel, icon: buttonIcon }, (onClose, titleProps) => (react_1.default.createElement(ResourcePickerContainer_1.default, { title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onClose: onClose, onRequestSources: onRequestSources, onRequestChildren: onRequestChildren, onRequestResource: onRequestResource, onChange: onChange })))));
12
+ react_1.default.createElement(ModalTrigger_1.default, { showLabel: showLabel, label: buttonLabel, icon: buttonIcon }, (onClose, titleProps) => (react_1.default.createElement(ResourcePickerContainer_1.default, { title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onClose: onClose, onRequestSources: onRequestSources, onRequestChildren: onRequestChildren, onChange: onChange })))));
14
13
  }
15
14
  exports.default = ComponentEditorContentBrowser;
package/lib/uuid.js CHANGED
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
- // @ts-nocheck
5
3
  function uuid() {
6
- return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
4
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (Number(c) ^ (window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4)))).toString(16));
7
5
  }
8
6
  exports.default = uuid;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/resource-browser",
3
- "version": "1.32.1-alpha.14",
3
+ "version": "1.32.1-alpha.16",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -14,6 +14,7 @@
14
14
  "clean": "rimraf ./lib"
15
15
  },
16
16
  "dependencies": {
17
+ "@squiz/dx-json-schema-lib": "1.32.1-alpha.16",
17
18
  "react-aria": "3.23.1",
18
19
  "react-responsive": "9.0.2",
19
20
  "react-stately": "3.21.0"
@@ -70,5 +71,5 @@
70
71
  "volta": {
71
72
  "node": "18.15.0"
72
73
  },
73
- "gitHead": "e4c2143502807400276f09fe018456d49fd64923"
74
+ "gitHead": "09c056ac1b2e99db11146b4ecb545b10063db835"
74
75
  }
@@ -0,0 +1,39 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { useCategorisedSources } from './useCategorisedSources';
3
+ import { mockResource, mockSource } from '../__mocks__/MockModels';
4
+
5
+ describe('useCategorisedSources', () => {
6
+ it('Should return a list of scoped categories with non-restricted sources in their own category', () => {
7
+ const resources = [mockResource({ name: 'Images' }), mockResource({ name: 'Audio files' })];
8
+ const sources = [
9
+ mockSource({
10
+ id: '1',
11
+ name: 'Matrix source',
12
+ nodes: resources,
13
+ }),
14
+ mockSource({
15
+ id: '2',
16
+ name: 'Unrestricted source',
17
+ }),
18
+ ];
19
+ const {
20
+ result: { current: result },
21
+ } = renderHook(() => useCategorisedSources(sources));
22
+
23
+ expect(result).toEqual([
24
+ {
25
+ key: '1',
26
+ label: 'Matrix source',
27
+ sources: [
28
+ { resource: resources[0], source: sources[0] },
29
+ { resource: resources[1], source: sources[0] },
30
+ ],
31
+ },
32
+ {
33
+ key: 'other',
34
+ label: 'Other systems',
35
+ sources: [{ resource: null, source: sources[1] }],
36
+ },
37
+ ]);
38
+ });
39
+ });