mirror of
https://github.com/strapi/strapi.git
synced 2025-07-31 21:09:39 +00:00
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:
commit
2ebb0b3ad7
61
examples/admin-development/.babelrc.json
Normal file
61
examples/admin-development/.babelrc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
15
examples/admin-development/.storybook/main.js
Normal file
15
examples/admin-development/.storybook/main.js
Normal 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',
|
||||
};
|
27
examples/admin-development/.storybook/preview.js
Normal file
27
examples/admin-development/.storybook/preview.js
Normal 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>
|
||||
),
|
||||
];
|
9
examples/admin-development/Introduction.stories.mdx
Normal file
9
examples/admin-development/Introduction.stories.mdx
Normal 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
|
42
examples/admin-development/package.json
Normal file
42
examples/admin-development/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
import ReactSelect from './ReactSelect';
|
||||
|
||||
export default ReactSelect;
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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;
|
@ -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;
|
@ -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} />
|
@ -0,0 +1,3 @@
|
||||
import SelectTree from './SelectTree';
|
||||
|
||||
export default SelectTree;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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],
|
||||
}
|
||||
`;
|
@ -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 }
|
||||
);
|
||||
}
|
@ -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;
|
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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']);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user