Merge pull request #13073 from strapi/features/ML-folder-treeselect

SelectTree: Create component to be used in the media library
This commit is contained in:
cyril lopez 2022-04-28 12:53:42 +02:00 committed by GitHub
commit 2ebb0b3ad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1603 additions and 554 deletions

View File

@ -0,0 +1,61 @@
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true,
"loose": true
}
],
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-transform-shorthand-properties",
"@babel/plugin-transform-block-scoping",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
],
[
"@babel/plugin-proposal-private-methods",
{
"loose": true
}
],
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-syntax-dynamic-import",
[
"@babel/plugin-proposal-object-rest-spread",
{
"loose": true,
"useBuiltIns": true
}
],
"@babel/plugin-transform-classes",
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-parameters",
"@babel/plugin-transform-destructuring",
"@babel/plugin-transform-spread",
"@babel/plugin-transform-for-of",
"babel-plugin-macros",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[
"babel-plugin-polyfill-corejs3",
{
"method": "usage-global",
"absoluteImports": "core-js",
"version": "3.21.1"
}
]
]
}

View File

@ -0,0 +1,15 @@
module.exports = {
stories: [
'../*.stories.mdx',
'../../../packages/core/**/admin/src/**/*.stories.mdx',
'../../../packages/core/**/admin/src/**/*.stories.@(js|jsx|ts|tsx)',
'../../../packages/plugins/**/admin/src/**/*.stories.mdx',
'../../../packages/plugins/**/admin/src/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: '@storybook/react',
};

View File

@ -0,0 +1,27 @@
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { IntlProvider } from 'react-intl';
import { MemoryRouter } from 'react-router-dom';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const decorators = [
Story => (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<IntlProvider messages={{}} textComponent="span" locale="en">
<main>
<Story />
</main>
</IntlProvider>
</ThemeProvider>
</MemoryRouter>
),
];

View File

@ -0,0 +1,9 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Welcome to the documentation
Use this app to develop local components in plugins.
To do so just create a story in your plugins `./admin/src` folder

View File

@ -0,0 +1,42 @@
{
"name": "admin-development",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.16.7",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-decorators": "7.16.7",
"@babel/plugin-proposal-export-default-from": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-private-methods": "7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-block-scoping": "7.16.7",
"@babel/plugin-transform-classes": "7.16.7",
"@babel/plugin-transform-destructuring": "7.17.7",
"@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.16.7",
"@babel/preset-env": "7.16.11",
"@babel/preset-typescript": "7.16.7",
"@storybook/addon-actions": "6.4.10",
"@storybook/addon-essentials": "6.4.10",
"@storybook/addon-interactions": "6.4.10",
"@storybook/addon-links": "6.4.10",
"@storybook/react": "^6.3.7",
"@storybook/testing-library": "^0.0.9",
"babel-loader": "^8.2.4",
"babel-plugin-macros": "3.1.0",
"babel-plugin-polyfill-corejs3": "0.5.2",
"core-js": "3.21.1"
},
"dependencies": {},
"scripts": {
"storybook": "start-storybook -p 6007",
"build-storybook": "build-storybook"
}
}

View File

@ -2,7 +2,8 @@ import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import isEmpty from 'lodash/isEmpty';
import Select, { createFilter } from 'react-select';
import { createFilter } from 'react-select';
import { ReactSelect as Select } from '@strapi/helper-plugin';
import { Box } from '@strapi/design-system/Box';
import { Stack } from '@strapi/design-system/Stack';
import { Typography } from '@strapi/design-system/Typography';

View File

@ -7,7 +7,6 @@ import {
import { useLocation } from 'react-router-dom';
import { Link } from '@strapi/design-system/Link';
import { Stack } from '@strapi/design-system/Stack';
import { useTheme } from 'styled-components';
import findIndex from 'lodash/findIndex';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
@ -21,12 +20,8 @@ import { getTrad } from '../../utils';
import Label from './Label';
import SelectOne from '../SelectOne';
import SelectMany from '../SelectMany';
import ClearIndicator from './ClearIndicator';
import DropdownIndicator from './DropdownIndicator';
import IndicatorSeparator from './IndicatorSeparator';
import Option from './Option';
import { connect, select } from './utils';
import getSelectStyles from './utils/getSelectStyles';
const initialPaginationState = {
contains: '',
@ -76,7 +71,6 @@ function SelectWrapper({
onRemoveRelation,
} = useCMEditViewDataManager();
const { pathname } = useLocation();
const theme = useTheme();
const value = get(modifiedData, name, null);
const [state, setState] = useState(initialPaginationState);
@ -277,8 +271,6 @@ function SelectWrapper({
return <NotAllowedInput intlLabel={intlLabel} labelAction={labelAction} />;
}
const styles = getSelectStyles(theme);
return (
<Stack spacing={1}>
<Label
@ -292,9 +284,6 @@ function SelectWrapper({
<Component
addRelation={handleAddRelation}
components={{
ClearIndicator,
DropdownIndicator,
IndicatorSeparator,
Option,
}}
displayNavigationLink={shouldDisplayRelationLink}
@ -314,7 +303,6 @@ function SelectWrapper({
onRemove={onRemoveRelation}
placeholder={placeholder}
searchToPersist={searchToPersist}
styles={styles}
targetModel={targetModel}
value={value}
description={description}

View File

@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import React from 'react';
import Select from 'react-select';
import { useTheme } from 'styled-components';
import ClearIndicator from './components/ClearIndicator';
import DropdownIndicator from './components/DropdownIndicator';
import IndicatorSeparator from './components/IndicatorSeparator';
import getSelectStyles from './utils/getSelectStyles';
const ReactSelect = ({ components, styles, ...props }) => {
const theme = useTheme();
const customStyles = getSelectStyles(theme);
return (
<Select
{...props}
components={{ ClearIndicator, DropdownIndicator, IndicatorSeparator, ...components }}
styles={{ ...customStyles, ...styles }}
/>
);
};
export default ReactSelect;
ReactSelect.defaultProps = {
components: undefined,
styles: undefined,
};
ReactSelect.propTypes = {
components: PropTypes.object,
styles: PropTypes.object,
};

View File

@ -0,0 +1,3 @@
import ReactSelect from './ReactSelect';
export default ReactSelect;

View File

@ -71,7 +71,7 @@ const getSelectStyles = theme => {
option: (base, state) => {
let backgroundColor = base.backgroundColor;
if (state.isFocused) {
if (state.isFocused || state.isSelected) {
backgroundColor = theme.colors.primary100;
}

View File

@ -63,6 +63,7 @@ export { default as PaginationURLQuery } from './components/PaginationURLQuery';
export { default as PageSizeURLQuery } from './components/PageSizeURLQuery';
export { default as RelativeTime } from './components/RelativeTime';
export { default as DateTimePicker } from './components/DateTimePicker';
export { default as ReactSelect } from './components/ReactSelect';
// New icons
export { default as SortIcon } from './icons/SortIcon';

View File

@ -58,6 +58,7 @@
"react-intl": "5.20.2",
"react-router": "^5.2.0",
"react-router-dom": "5.2.0",
"react-select": "4.0.2",
"styled-components": "5.3.3",
"whatwg-fetch": "^3.6.2"
},

View File

@ -0,0 +1,63 @@
import PropTypes from 'prop-types';
import React from 'react';
import { components } from 'react-select';
import styled from 'styled-components';
import { Flex } from '@strapi/design-system/Flex';
import { Icon } from '@strapi/design-system/Icon';
import { pxToRem } from '@strapi/helper-plugin';
import { Typography } from '@strapi/design-system/Typography';
import ChevronUp from '@strapi/icons/ChevronUp';
import ChevronDown from '@strapi/icons/ChevronDown';
const ToggleButton = styled.button`
align-self: flex-end;
margin-left: auto;
`;
const Option = ({ children, data, selectProps, ...props }) => {
const { depth, value, children: options } = data;
const { maxDisplayDepth, openValues, onOptionToggle } = selectProps;
const isOpen = openValues.includes(value);
return (
<>
<components.Option {...props}>
<Flex alignItems="start">
<Typography textColor="neutral800">
<span style={{ paddingLeft: `${Math.min(depth, maxDisplayDepth) * 10}px` }}>
{children}
</span>
</Typography>
{options?.length > 0 && (
<ToggleButton
type="button"
onClick={event => {
event.preventDefault();
event.stopPropagation();
onOptionToggle(value);
}}
>
<Icon width={pxToRem(14)} color="neutral500" as={isOpen ? ChevronUp : ChevronDown} />
</ToggleButton>
)}
</Flex>
</components.Option>
</>
);
};
Option.propTypes = {
children: PropTypes.arrayOf(PropTypes.element).isRequired,
data: PropTypes.object.isRequired,
onToggle: PropTypes.func.isRequired,
selectProps: PropTypes.shape({
maxDisplayDepth: PropTypes.number,
openValues: PropTypes.arrayOf([PropTypes.string, PropTypes.number]),
onOptionToggle: PropTypes.func,
}).isRequired,
};
export default Option;

View File

@ -0,0 +1,83 @@
import React, { useEffect, useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import { ReactSelect as Select } from '@strapi/helper-plugin';
import Option from './Option';
import flattenTree from './utils/flattenTree';
import getOpenValues from './utils/getOpenValues';
const hasParent = option => !option.parent;
const hasParentOrMatchesValue = (option, value) =>
option.value === value || option.parent === value;
const SelectTree = ({ options: defaultOptions, maxDisplayDepth, defaultValue, ...props }) => {
const flatDefaultOptions = useMemo(() => flattenTree(defaultOptions), [defaultOptions]);
const optionsFiltered = useMemo(() => flatDefaultOptions.filter(hasParent), [flatDefaultOptions]);
const [options, setOptions] = useState(optionsFiltered);
const [openValues, setOpenValues] = useState(getOpenValues(flatDefaultOptions, defaultValue));
useEffect(() => {
if (openValues.length === 0) {
setOptions(optionsFiltered);
}
openValues.forEach(value => {
const filtered = flatDefaultOptions.filter(
option => hasParentOrMatchesValue(option, value) || hasParent(option)
);
setOptions(filtered);
});
}, [openValues, flatDefaultOptions, optionsFiltered]);
const handleToggle = value => {
if (openValues.includes(value)) {
setOpenValues(prev => prev.filter(prevData => prevData !== value));
} else {
setOpenValues(prev => [...prev, value]);
}
};
return (
<Select
components={{ Option }}
options={options}
defaultValue={defaultValue}
/* -- custom props, used by the Option component */
maxDisplayDepth={maxDisplayDepth}
openValues={openValues}
onOptionToggle={handleToggle}
/* -- / custom props */
{...props}
/>
);
};
const OptionShape = PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
label: PropTypes.string.isRequired,
children: PropTypes.array,
});
OptionShape.children = PropTypes.arrayOf(PropTypes.shape(OptionShape));
OptionShape.defaultProps = {
children: undefined,
};
SelectTree.defaultProps = {
defaultValue: undefined,
maxDisplayDepth: 5,
};
SelectTree.propTypes = {
defaultValue: PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
}),
maxDisplayDepth: PropTypes.number,
options: PropTypes.arrayOf(OptionShape).isRequired,
};
export default SelectTree;

View File

@ -0,0 +1,62 @@
import { useState } from 'react';
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import SelectTree from './index';
<Meta title="components/SelectTree" />
# SelectTree
This component is used in order to render a tree-structure as a Select. All options which can be passed
into `react-select` are forwarded.
## Usage
<Canvas>
<Story name="base">
{() => {
const [value, setValue] = useState({ value: 22, label: "Folder 2.2" });
const options = [
{
value: 1,
label: 'Folder 1'
},
{
value: 2,
label: 'Folder 2',
children: [
{
value: 21,
label: 'Folder 2.1'
},
{
value: 22,
label: 'Folder 2.2',
children: [
{
value: 221,
label: 'Folder 2.2.1'
},
{
value: 222,
label: 'Folder 2.2.2'
}
]
}
]
},
{
value: 3,
label: 'Folder 3'
}
];
return (
<>
<SelectTree options={options} defaultValue={value} onChange={({ value, label }) => setValue({ value, label })} />
<p>Selected Value: {value.value}</p>
</>
)
}}
</Story>
</Canvas>
<ArgsTable of={SelectTree} />

View File

@ -0,0 +1,3 @@
import SelectTree from './SelectTree';
export default SelectTree;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { render, act } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import SelectTree from '../index';
const FIXTURE_OPTIONS = [
{
value: 'f-1',
label: 'Folder 1',
},
{
value: 'f-2',
label: 'Folder 2',
children: [
{
value: 'f-2-1',
label: 'Folder 2-1',
},
{
value: 'f-2-2',
label: 'Folder 2-2',
children: [
{
value: 'f-2-2-1',
label: 'Folder 2-2-1',
},
],
},
],
},
];
const ComponentFixture = props => (
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<SelectTree defaultValue={{ value: 'f1' }} {...props} />
</ThemeProvider>
</IntlProvider>
);
const setup = props => {
return new Promise(resolve => {
return act(() => {
resolve(render(<ComponentFixture options={FIXTURE_OPTIONS} {...props} />));
});
});
};
describe('SelectTree', () => {
test('renders', async () => {
expect(await setup()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,277 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectTree renders 1`] = `
Object {
"asFragment": [Function],
"baseElement": .c3 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.c0 {
padding-right: 12px;
}
.c1 {
background: transparent;
border: none;
position: relative;
z-index: 1;
}
.c1 svg {
height: 0.6875rem;
width: 0.6875rem;
}
.c1 svg path {
fill: #666687;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
background: none;
border: none;
}
.c2 svg {
width: 0.375rem;
}
<body>
<div>
<div
class=" css-npt6n7-container"
>
<div
class=" css-1sb7bue-control"
>
<div
class=" css-1kj7tu4-ValueContainer"
>
<div
class=" css-p5rves-singleValue"
/>
<div
class="css-reu10z-Input"
>
<div
class=""
style="display: inline-block;"
>
<input
aria-autocomplete="list"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-2-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
>
<button
class="c0 c1 c2"
type="button"
>
<svg
fill="none"
height="1em"
viewBox="0 0 14 8"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
<div
class="c3"
>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
aria-relevant="all"
id="live-region-alert"
role="alert"
/>
</div>
</div>
</body>,
"container": <div>
<div
class=" css-npt6n7-container"
>
<div
class=" css-1sb7bue-control"
>
<div
class=" css-1kj7tu4-ValueContainer"
>
<div
class=" css-p5rves-singleValue"
/>
<div
class="css-reu10z-Input"
>
<div
class=""
style="display: inline-block;"
>
<input
aria-autocomplete="list"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-2-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
>
<button
class="sc-hGnimi sc-bPVzhA sc-evQXBP iaYaJy cDFemq cOhtSo"
type="button"
>
<svg
fill="none"
height="1em"
viewBox="0 0 14 8"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
<div
class="sc-cxpSdN hEuJJy"
>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
aria-relevant="all"
id="live-region-alert"
role="alert"
/>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

View File

@ -0,0 +1,7 @@
export default function flattenTree(tree, parent, depth = 1) {
return tree.flatMap(item =>
item.children
? [{ ...item, parent: parent?.value, depth }, ...flattenTree(item.children, item, depth + 1)]
: { ...item, depth, parent: parent?.value }
);
}

View File

@ -0,0 +1,25 @@
function getOpenValues(options, defaultValue) {
let values = [];
const { value } = defaultValue;
const option = options.find(option => option.value === value);
if (!option) {
return values;
}
values.push(option.value);
let { parent } = option;
while (parent) {
// eslint-disable-next-line no-loop-func
const option = options.find(({ value }) => value === parent);
values.push(option.value);
parent = option.parent;
}
return values;
}
export default getOpenValues;

View File

@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`flattenTree flattens the passed tree structure properly 1`] = `
Array [
Object {
"depth": 1,
"label": "Folder 1",
"parent": undefined,
"value": "f-1",
},
Object {
"children": Array [
Object {
"label": "Folder 2-1",
"value": "f-2-1",
},
Object {
"children": Array [
Object {
"label": "Folder 2-2-1",
"value": "f-2-2-1",
},
],
"label": "Folder 2-2",
"value": "f-2-2",
},
],
"depth": 1,
"label": "Folder 2",
"parent": undefined,
"value": "f-2",
},
Object {
"depth": 2,
"label": "Folder 2-1",
"parent": "f-2",
"value": "f-2-1",
},
Object {
"children": Array [
Object {
"label": "Folder 2-2-1",
"value": "f-2-2-1",
},
],
"depth": 2,
"label": "Folder 2-2",
"parent": "f-2",
"value": "f-2-2",
},
Object {
"depth": 3,
"label": "Folder 2-2-1",
"parent": "f-2-2",
"value": "f-2-2-1",
},
]
`;

View File

@ -0,0 +1,36 @@
import flattenTree from '../flattenTree';
const FIXTURE = [
{
value: 'f-1',
label: 'Folder 1',
},
{
value: 'f-2',
label: 'Folder 2',
children: [
{
value: 'f-2-1',
label: 'Folder 2-1',
},
{
value: 'f-2-2',
label: 'Folder 2-2',
children: [
{
value: 'f-2-2-1',
label: 'Folder 2-2-1',
},
],
},
],
},
];
describe('flattenTree', () => {
test('flattens the passed tree structure properly', () => {
expect(flattenTree(FIXTURE)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,45 @@
import flattenTree from '../flattenTree';
import getOpenValues from '../getOpenValues';
const FIXTURE = flattenTree([
{
value: 'f-1',
label: 'Folder 1',
},
{
value: 'f-2',
label: 'Folder 2',
children: [
{
value: 'f-2-1',
label: 'Folder 2-1',
},
{
value: 'f-2-2',
label: 'Folder 2-2',
children: [
{
value: 'f-2-2-1',
label: 'Folder 2-2-1',
},
],
},
],
},
]);
describe('getOpenValues', () => {
test('returns 1 value for depth = 1', () => {
expect(getOpenValues(FIXTURE, { value: 'f-1' })).toStrictEqual(['f-1']);
});
test('returns 2 values for depth = 2', () => {
expect(getOpenValues(FIXTURE, { value: 'f-2-1' })).toStrictEqual(['f-2-1', 'f-2']);
});
test('returns 3 values for depth = 3', () => {
expect(getOpenValues(FIXTURE, { value: 'f-2-2-1' })).toStrictEqual(['f-2-2-1', 'f-2-2', 'f-2']);
});
});

View File

@ -791,7 +791,7 @@ describe('Admin | containers | RoleCreatePage', () => {
border: 1px solid #4945ff;
}
.c41:hover:not([aria-disabled='true']) .sc-fUCuFg {
.c41:hover:not([aria-disabled='true']) .sc-dSaQTq {
color: #271fe0;
}

View File

@ -860,7 +860,7 @@ describe('Admin | containers | RoleEditPage', () => {
border: 1px solid #4945ff;
}
.c46:hover:not([aria-disabled='true']) .sc-ieCETs {
.c46:hover:not([aria-disabled='true']) .sc-deghWO {
color: #271fe0;
}

1225
yarn.lock

File diff suppressed because it is too large Load Diff