feat: retrofits array to new collapsible component
This commit is contained in:
@@ -88,7 +88,6 @@
|
|||||||
"@babel/preset-typescript": "^7.12.1",
|
"@babel/preset-typescript": "^7.12.1",
|
||||||
"@babel/register": "^7.11.5",
|
"@babel/register": "^7.11.5",
|
||||||
"@date-io/date-fns": "^2.10.6",
|
"@date-io/date-fns": "^2.10.6",
|
||||||
"@faceless-ui/collapsibles": "^1.0.0",
|
|
||||||
"@faceless-ui/modal": "^1.1.7",
|
"@faceless-ui/modal": "^1.1.7",
|
||||||
"@faceless-ui/scroll-info": "^1.2.3",
|
"@faceless-ui/scroll-info": "^1.2.3",
|
||||||
"@faceless-ui/window-info": "^2.0.2",
|
"@faceless-ui/window-info": "^2.0.2",
|
||||||
|
|||||||
54
src/admin/components/elements/ArrayAction/index.scss
Normal file
54
src/admin/components/elements/ArrayAction/index.scss
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
|
.array-actions {
|
||||||
|
&__button {
|
||||||
|
@extend %btn-reset;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--theme-elevation-0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.popup--active .array-actions__button {
|
||||||
|
background: var(--theme-elevation-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button,
|
||||||
|
&__action {
|
||||||
|
@extend %btn-reset;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
@extend %btn-reset;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-right: base(.25);
|
||||||
|
|
||||||
|
.stroke {
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__move-up {
|
||||||
|
svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/admin/components/elements/ArrayAction/index.tsx
Normal file
95
src/admin/components/elements/ArrayAction/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Popup from '../Popup';
|
||||||
|
import More from '../../icons/More';
|
||||||
|
import Chevron from '../../icons/Chevron';
|
||||||
|
import { Props } from './types';
|
||||||
|
import Plus from '../../icons/Plus';
|
||||||
|
import X from '../../icons/X';
|
||||||
|
import Copy from '../../icons/Copy';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const baseClass = 'array-actions';
|
||||||
|
|
||||||
|
export const ArrayAction: React.FC<Props> = ({
|
||||||
|
moveRow,
|
||||||
|
index,
|
||||||
|
rowCount,
|
||||||
|
addRow,
|
||||||
|
duplicateRow,
|
||||||
|
removeRow,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
horizontalAlign="center"
|
||||||
|
className={baseClass}
|
||||||
|
buttonClassName={`${baseClass}__button`}
|
||||||
|
button={<More />}
|
||||||
|
render={({ close }) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{index !== 0 && (
|
||||||
|
<button
|
||||||
|
className={`${baseClass}__action ${baseClass}__move-up`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
moveRow(index, index - 1);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chevron />
|
||||||
|
Move Up
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{index < rowCount - 1 && (
|
||||||
|
<button
|
||||||
|
className={`${baseClass}__action ${baseClass}__move-down`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
moveRow(index, index + 1);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chevron />
|
||||||
|
Move Down
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={`${baseClass}__action ${baseClass}__add`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
addRow(index);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
Add Below
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${baseClass}__action ${baseClass}__duplicate`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
duplicateRow(index);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy />
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${baseClass}__action ${baseClass}__remove`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
removeRow(index);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
src/admin/components/elements/ArrayAction/types.ts
Normal file
8
src/admin/components/elements/ArrayAction/types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type Props = {
|
||||||
|
addRow: (current: number) => void
|
||||||
|
duplicateRow: (current: number) => void
|
||||||
|
removeRow: (index: number) => void
|
||||||
|
moveRow: (from: number, to: number) => void
|
||||||
|
index: number
|
||||||
|
rowCount: number
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
@import '../../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
.collapsible {
|
.collapsible {
|
||||||
|
--toggle-pad-h: #{base(.75)};
|
||||||
|
--toggle-pad-v: #{base(.5)};
|
||||||
|
|
||||||
border: 1px solid var(--theme-elevation-200);
|
border: 1px solid var(--theme-elevation-200);
|
||||||
border-radius: $style-radius-m;
|
border-radius: $style-radius-m;
|
||||||
|
|
||||||
@@ -8,20 +11,42 @@
|
|||||||
border: 1px solid var(--theme-elevation-300);
|
border: 1px solid var(--theme-elevation-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__toggle-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hovered {
|
||||||
|
.collapsible__drag {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible__toggle {
|
||||||
|
background: var(--theme-elevation-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__drag {
|
||||||
|
opacity: .5;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: var(--toggle-pad-v);
|
||||||
|
left: base(.5);
|
||||||
|
}
|
||||||
|
|
||||||
&__toggle {
|
&__toggle {
|
||||||
@extend %btn-reset;
|
@extend %btn-reset;
|
||||||
|
@extend %body;
|
||||||
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--theme-elevation-50);
|
background: var(--theme-elevation-50);
|
||||||
border-top-right-radius: $style-radius-s;
|
border-top-right-radius: $style-radius-s;
|
||||||
border-top-left-radius: $style-radius-s;
|
border-top-left-radius: $style-radius-s;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
padding: var(--toggle-pad-v) var(--toggle-pad-h);
|
||||||
align-items: center;
|
}
|
||||||
padding: base(.5) base(1);
|
|
||||||
|
|
||||||
&:hover {
|
&__toggle--has-drag-handle {
|
||||||
background: var(--theme-elevation-100);
|
padding-left: base(1.5);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--collapsed {
|
&--collapsed {
|
||||||
@@ -35,12 +60,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__actions-wrap {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--toggle-pad-h);
|
||||||
|
top: var(--toggle-pad-v);
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
&__indicator {
|
&__indicator {
|
||||||
margin: 0 0 0 auto;
|
|
||||||
transform: rotate(.5turn);
|
transform: rotate(.5turn);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
|
background-color: var(--theme-elevation-0);
|
||||||
|
border-bottom-left-radius: $style-radius-s;
|
||||||
|
border-bottom-right-radius: $style-radius-s;
|
||||||
padding: $baseline;
|
padding: $baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,39 +3,75 @@ import AnimateHeight from 'react-animate-height';
|
|||||||
import { Props } from './types';
|
import { Props } from './types';
|
||||||
import { CollapsibleProvider, useCollapsible } from './provider';
|
import { CollapsibleProvider, useCollapsible } from './provider';
|
||||||
import Chevron from '../../icons/Chevron';
|
import Chevron from '../../icons/Chevron';
|
||||||
|
import DragHandle from '../../icons/Drag';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const baseClass = 'collapsible';
|
const baseClass = 'collapsible';
|
||||||
|
|
||||||
export const Collapsible: React.FC<Props> = ({ children, onToggle, className, header, initCollapsed }) => {
|
export const Collapsible: React.FC<Props> = ({
|
||||||
const [collapsed, setCollapsed] = useState(Boolean(initCollapsed));
|
children,
|
||||||
|
collapsed: collapsedFromProps,
|
||||||
|
onToggle,
|
||||||
|
className,
|
||||||
|
header,
|
||||||
|
initCollapsed,
|
||||||
|
dragHandleProps,
|
||||||
|
actions,
|
||||||
|
}) => {
|
||||||
|
const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed));
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
const isNested = useCollapsible();
|
const isNested = useCollapsible();
|
||||||
|
|
||||||
|
const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[
|
<div className={[
|
||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
|
dragHandleProps && `${baseClass}--has-drag-handle`,
|
||||||
collapsed && `${baseClass}--collapsed`,
|
collapsed && `${baseClass}--collapsed`,
|
||||||
isNested && `${baseClass}--nested`,
|
isNested && `${baseClass}--nested`,
|
||||||
|
hovered && `${baseClass}--hovered`,
|
||||||
].filter(Boolean).join(' ')}
|
].filter(Boolean).join(' ')}
|
||||||
>
|
>
|
||||||
<CollapsibleProvider>
|
<CollapsibleProvider>
|
||||||
<button
|
<div
|
||||||
type="button"
|
className={`${baseClass}__toggle-wrap`}
|
||||||
className={`${baseClass}__toggle ${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onClick={() => {
|
onMouseLeave={() => setHovered(false)}
|
||||||
if (typeof onToggle === 'function') onToggle(!collapsed);
|
|
||||||
setCollapsed(!collapsed);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{header && (
|
{dragHandleProps && (
|
||||||
<div className={`${baseClass}__header-wrap`}>
|
<div
|
||||||
{header}
|
className={`${baseClass}__drag`}
|
||||||
|
{...dragHandleProps}
|
||||||
|
>
|
||||||
|
<DragHandle />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Chevron className={`${baseClass}__indicator`} />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
className={[
|
||||||
|
`${baseClass}__toggle`,
|
||||||
|
`${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`,
|
||||||
|
dragHandleProps && `${baseClass}__toggle--has-drag-handle`,
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
onClick={() => {
|
||||||
|
if (typeof onToggle === 'function') onToggle(!collapsed);
|
||||||
|
setCollapsedLocal(!collapsed);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{header && header}
|
||||||
|
</button>
|
||||||
|
<div className={`${baseClass}__actions-wrap`}>
|
||||||
|
{actions && (
|
||||||
|
<div className={`${baseClass}__actions`}>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Chevron className={`${baseClass}__indicator`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
height={collapsed ? 0 : 'auto'}
|
height={collapsed ? 0 : 'auto'}
|
||||||
duration={200}
|
duration={200}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
collapsed?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
header?: React.ReactNode
|
header?: React.ReactNode
|
||||||
|
actions?: React.ReactNode
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
onToggle?: (collapsed: boolean) => void
|
onToggle?: (collapsed: boolean) => void
|
||||||
initCollapsed?: boolean
|
initCollapsed?: boolean
|
||||||
|
dragHandleProps?: DraggableProvidedDragHandleProps
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const baseClass = 'popup-button';
|
|||||||
|
|
||||||
const PopupButton: React.FC<Props> = (props) => {
|
const PopupButton: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
buttonType,
|
buttonType,
|
||||||
button,
|
button,
|
||||||
setActive,
|
setActive,
|
||||||
@@ -15,6 +16,7 @@ const PopupButton: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClass,
|
baseClass,
|
||||||
|
className,
|
||||||
`${baseClass}--${buttonType}`,
|
`${baseClass}--${buttonType}`,
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
|
className?: string
|
||||||
buttonType: 'custom' | 'default' | 'none',
|
buttonType: 'custom' | 'default' | 'none',
|
||||||
button: React.ReactNode,
|
button: React.ReactNode,
|
||||||
setActive: (active: boolean) => void,
|
setActive: (active: boolean) => void,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
&--size-small {
|
&--size-small {
|
||||||
.popup__content {
|
.popup__content {
|
||||||
@include shadow-sm;
|
@include shadow-m;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.popup--h-align-left {
|
&.popup--h-align-left {
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
&--size-wide {
|
&--size-wide {
|
||||||
.popup__content {
|
.popup__content {
|
||||||
@include shadow-sm;
|
@include shadow-m;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
border: 12px solid transparent;
|
border: 12px solid transparent;
|
||||||
@@ -109,11 +109,11 @@
|
|||||||
&--h-align-center {
|
&--h-align-center {
|
||||||
.popup__content {
|
.popup__content {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-0%);
|
transform: translateX(-50%);
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-0%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const baseClass = 'popup';
|
|||||||
const Popup: React.FC<Props> = (props) => {
|
const Popup: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
buttonClassName,
|
||||||
render,
|
render,
|
||||||
size = 'small',
|
size = 'small',
|
||||||
color = 'light',
|
color = 'light',
|
||||||
@@ -129,21 +130,11 @@ const Popup: React.FC<Props> = (props) => {
|
|||||||
onMouseEnter={() => setActive(true)}
|
onMouseEnter={() => setActive(true)}
|
||||||
onMouseLeave={() => setActive(false)}
|
onMouseLeave={() => setActive(false)}
|
||||||
>
|
>
|
||||||
<PopupButton
|
<PopupButton {...{ className: buttonClassName, buttonType, button, setActive, active }} />
|
||||||
buttonType={buttonType}
|
|
||||||
button={button}
|
|
||||||
setActive={setActive}
|
|
||||||
active={active}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<PopupButton
|
<PopupButton {...{ className: buttonClassName, buttonType, button, setActive, active }} />
|
||||||
buttonType={buttonType}
|
|
||||||
button={button}
|
|
||||||
setActive={setActive}
|
|
||||||
active={active}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CSSProperties } from 'react';
|
|||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
|
buttonClassName?: string
|
||||||
render?: (any) => React.ReactNode,
|
render?: (any) => React.ReactNode,
|
||||||
children?: React.ReactNode,
|
children?: React.ReactNode,
|
||||||
verticalAlign?: 'top' | 'bottom'
|
verticalAlign?: 'top' | 'bottom'
|
||||||
|
|||||||
@@ -76,28 +76,6 @@ const DraggableSection: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
<div className={`${baseClass}__render-fields-wrapper`}>
|
<div className={`${baseClass}__render-fields-wrapper`}>
|
||||||
|
|
||||||
{blockType === 'blocks' && (
|
|
||||||
<div className={`${baseClass}__section-header`}>
|
|
||||||
<HiddenInput
|
|
||||||
name={`${parentPath}.${rowIndex}.id`}
|
|
||||||
value={id}
|
|
||||||
/>
|
|
||||||
<SectionTitle
|
|
||||||
label={label}
|
|
||||||
path={`${parentPath}.${rowIndex}.blockName`}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
icon="chevron"
|
|
||||||
onClick={() => setRowCollapse(id, !isCollapsed)}
|
|
||||||
buttonStyle="icon-label"
|
|
||||||
className={`toggle-collapse toggle-collapse--is-${isCollapsed ? 'collapsed' : 'open'}`}
|
|
||||||
round
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
height={isCollapsed ? 0 : 'auto'}
|
height={isCollapsed ? 0 : 'auto'}
|
||||||
duration={200}
|
duration={200}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export type DescriptionFunction = (value: unknown) => string
|
export type DescriptionFunction = (value?: unknown) => string
|
||||||
|
|
||||||
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
|
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export type Description = string | DescriptionFunction | DescriptionComponent
|
|||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
description?: Description
|
description?: Description
|
||||||
value: unknown;
|
value?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isComponent(description: Description): description is DescriptionComponent {
|
export function isComponent(description: Description): description is DescriptionComponent {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
|
import ObjectID from 'bson-objectid';
|
||||||
import { unflatten, flatten } from 'flatley';
|
import { unflatten, flatten } from 'flatley';
|
||||||
import flattenFilters from './flattenFilters';
|
import flattenFilters from './flattenFilters';
|
||||||
import getSiblingData from './getSiblingData';
|
import getSiblingData from './getSiblingData';
|
||||||
@@ -109,6 +110,32 @@ function fieldReducer(state: Fields, action): Fields {
|
|||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'DUPLICATE_ROW': {
|
||||||
|
const {
|
||||||
|
rowIndex, path,
|
||||||
|
} = action;
|
||||||
|
|
||||||
|
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
|
||||||
|
|
||||||
|
const duplicate = {
|
||||||
|
...unflattenedRows[rowIndex],
|
||||||
|
id: new ObjectID().toHexString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there are subfields
|
||||||
|
if (Object.keys(duplicate).length > 0) {
|
||||||
|
// Add new object containing subfield names to unflattenedRows array
|
||||||
|
unflattenedRows.splice(rowIndex + 1, 0, duplicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
...remainingFlattenedState,
|
||||||
|
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
|
||||||
|
};
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
case 'MOVE_ROW': {
|
case 'MOVE_ROW': {
|
||||||
const { moveFromIndex, moveToIndex, path } = action;
|
const { moveFromIndex, moveToIndex, path } = action;
|
||||||
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
|
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||||
import { useAuth } from '../../../utilities/Auth';
|
import { useAuth } from '../../../utilities/Auth';
|
||||||
import withCondition from '../../withCondition';
|
import withCondition from '../../withCondition';
|
||||||
import Button from '../../../elements/Button';
|
import Button from '../../../elements/Button';
|
||||||
import DraggableSection from '../../DraggableSection';
|
import reducer, { Row } from '../rowReducer';
|
||||||
import reducer from '../rowReducer';
|
|
||||||
import { useForm } from '../../Form/context';
|
import { useForm } from '../../Form/context';
|
||||||
import buildStateFromSchema from '../../Form/buildStateFromSchema';
|
import buildStateFromSchema from '../../Form/buildStateFromSchema';
|
||||||
import useField from '../../useField';
|
import useField from '../../useField';
|
||||||
@@ -13,14 +12,18 @@ import Error from '../../Error';
|
|||||||
import { array } from '../../../../../fields/validations';
|
import { array } from '../../../../../fields/validations';
|
||||||
import Banner from '../../../elements/Banner';
|
import Banner from '../../../elements/Banner';
|
||||||
import FieldDescription from '../../FieldDescription';
|
import FieldDescription from '../../FieldDescription';
|
||||||
import { Props } from './types';
|
|
||||||
|
|
||||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||||
import { useOperation } from '../../../utilities/OperationProvider';
|
import { useOperation } from '../../../utilities/OperationProvider';
|
||||||
|
import { Collapsible } from '../../../elements/Collapsible';
|
||||||
|
import RenderFields from '../../RenderFields';
|
||||||
|
import { fieldAffectsData } from '../../../../../fields/config/types';
|
||||||
|
import { Props } from './types';
|
||||||
|
import { usePreferences } from '../../../utilities/Preferences';
|
||||||
|
import { ArrayAction } from '../../../elements/ArrayAction';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const baseClass = 'field-type array';
|
const baseClass = 'array-field';
|
||||||
|
|
||||||
const ArrayFieldType: React.FC<Props> = (props) => {
|
const ArrayFieldType: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
@@ -52,6 +55,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
// eslint-disable-next-line react/destructuring-assignment
|
// eslint-disable-next-line react/destructuring-assignment
|
||||||
const label = props?.label ?? props?.labels?.singular;
|
const label = props?.label ?? props?.labels?.singular;
|
||||||
|
|
||||||
|
const { preferencesKey, preferences } = useDocumentInfo();
|
||||||
|
const { setPreference } = usePreferences();
|
||||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||||
const formContext = useForm();
|
const formContext = useForm();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -81,20 +86,26 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
condition,
|
condition,
|
||||||
});
|
});
|
||||||
|
|
||||||
const addRow = useCallback(async (rowIndex) => {
|
const addRow = useCallback(async (rowIndex: number) => {
|
||||||
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale });
|
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale });
|
||||||
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
|
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
|
||||||
dispatchRows({ type: 'ADD', rowIndex });
|
dispatchRows({ type: 'ADD', rowIndex });
|
||||||
setValue(value as number + 1);
|
setValue(value as number + 1);
|
||||||
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]);
|
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]);
|
||||||
|
|
||||||
const removeRow = useCallback((rowIndex) => {
|
const duplicateRow = useCallback(async (rowIndex: number) => {
|
||||||
|
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
|
||||||
|
dispatchRows({ type: 'ADD', rowIndex });
|
||||||
|
setValue(value as number + 1);
|
||||||
|
}, [dispatchRows, dispatchFields, path, setValue, value]);
|
||||||
|
|
||||||
|
const removeRow = useCallback((rowIndex: number) => {
|
||||||
dispatchRows({ type: 'REMOVE', rowIndex });
|
dispatchRows({ type: 'REMOVE', rowIndex });
|
||||||
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
|
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
|
||||||
setValue(value as number - 1);
|
setValue(value as number - 1);
|
||||||
}, [dispatchRows, dispatchFields, path, value, setValue]);
|
}, [dispatchRows, dispatchFields, path, value, setValue]);
|
||||||
|
|
||||||
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
|
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
|
||||||
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
|
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
|
||||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
||||||
}, [dispatchRows, dispatchFields, path]);
|
}, [dispatchRows, dispatchFields, path]);
|
||||||
@@ -106,10 +117,57 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
moveRow(sourceIndex, destinationIndex);
|
moveRow(sourceIndex, destinationIndex);
|
||||||
}, [moveRow]);
|
}, [moveRow]);
|
||||||
|
|
||||||
|
const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => {
|
||||||
|
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed });
|
||||||
|
|
||||||
|
if (preferencesKey) {
|
||||||
|
const preferencesToSet = preferences || { fields: {} };
|
||||||
|
let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed
|
||||||
|
.filter((filterID) => (rows.find((row) => row.id === filterID)))
|
||||||
|
|| [];
|
||||||
|
|
||||||
|
if (!collapsed) {
|
||||||
|
newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID);
|
||||||
|
} else {
|
||||||
|
newCollapsedState.push(rowID);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreference(preferencesKey, {
|
||||||
|
...preferencesToSet,
|
||||||
|
fields: {
|
||||||
|
...preferencesToSet?.fields || {},
|
||||||
|
[path]: {
|
||||||
|
...preferencesToSet?.fields?.[path],
|
||||||
|
collapsed: newCollapsedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [preferencesKey, preferences, path, setPreference, rows]);
|
||||||
|
|
||||||
|
const toggleCollapseAll = useCallback(async (collapse: boolean) => {
|
||||||
|
dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse });
|
||||||
|
|
||||||
|
if (preferencesKey) {
|
||||||
|
const preferencesToSet = preferences || { fields: {} };
|
||||||
|
|
||||||
|
setPreference(preferencesKey, {
|
||||||
|
...preferencesToSet,
|
||||||
|
fields: {
|
||||||
|
...preferencesToSet?.fields || {},
|
||||||
|
[path]: {
|
||||||
|
...preferencesToSet?.fields?.[path],
|
||||||
|
collapsed: collapse ? rows.map(({ id: rowID }) => rowID) : [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [path, preferences, preferencesKey, rows, setPreference]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data = formContext.getDataByPath(path);
|
const data = formContext.getDataByPath<Row[]>(path);
|
||||||
dispatchRows({ type: 'SET_ALL', data: data || [] });
|
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed });
|
||||||
}, [formContext, path]);
|
}, [formContext, path, preferences]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(rows?.length || 0, true);
|
setValue(rows?.length || 0, true);
|
||||||
@@ -124,6 +182,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
const hasMaxRows = maxRows && rows.length >= maxRows;
|
const hasMaxRows = maxRows && rows.length >= maxRows;
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
|
'field-type',
|
||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
@@ -140,7 +199,29 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<header className={`${baseClass}__header`}>
|
<header className={`${baseClass}__header`}>
|
||||||
<h3>{label}</h3>
|
<div className={`${baseClass}__header-wrap`}>
|
||||||
|
<h3>{label}</h3>
|
||||||
|
<ul className={`${baseClass}__header-actions`}>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleCollapseAll(true)}
|
||||||
|
className={`${baseClass}__header-action`}
|
||||||
|
>
|
||||||
|
Collapse All
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleCollapseAll(false)}
|
||||||
|
className={`${baseClass}__header-action`}
|
||||||
|
>
|
||||||
|
Show All
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<FieldDescription
|
<FieldDescription
|
||||||
value={value}
|
value={value}
|
||||||
description={description}
|
description={description}
|
||||||
@@ -153,23 +234,50 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{rows.length > 0 && rows.map((row, i) => (
|
{rows.length > 0 && rows.map((row, i) => (
|
||||||
<DraggableSection
|
<Draggable
|
||||||
readOnly={readOnly}
|
|
||||||
key={row.id}
|
key={row.id}
|
||||||
id={row.id}
|
draggableId={row.id}
|
||||||
blockType="array"
|
index={i}
|
||||||
label={labels.singular}
|
isDragDisabled={readOnly}
|
||||||
rowCount={rows.length}
|
>
|
||||||
rowIndex={i}
|
{(providedDrag) => (
|
||||||
addRow={addRow}
|
<div
|
||||||
removeRow={removeRow}
|
ref={providedDrag.innerRef}
|
||||||
moveRow={moveRow}
|
{...providedDrag.draggableProps}
|
||||||
parentPath={path}
|
>
|
||||||
fieldTypes={fieldTypes}
|
<Collapsible
|
||||||
fieldSchema={fields}
|
collapsed={row.collapsed}
|
||||||
permissions={permissions}
|
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||||
hasMaxRows={hasMaxRows}
|
className={`${baseClass}__row`}
|
||||||
/>
|
key={row.id}
|
||||||
|
dragHandleProps={providedDrag.dragHandleProps}
|
||||||
|
header={`${labels.singular} ${i + 1}`}
|
||||||
|
actions={!readOnly ? (
|
||||||
|
<ArrayAction
|
||||||
|
rowCount={rows.length}
|
||||||
|
duplicateRow={duplicateRow}
|
||||||
|
addRow={addRow}
|
||||||
|
moveRow={moveRow}
|
||||||
|
removeRow={removeRow}
|
||||||
|
index={i}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
>
|
||||||
|
<RenderFields
|
||||||
|
forceRender
|
||||||
|
readOnly={readOnly}
|
||||||
|
fieldTypes={fieldTypes}
|
||||||
|
permissions={permissions.fields}
|
||||||
|
fieldSchema={fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||||
<Banner type="error">
|
<Banner type="error">
|
||||||
@@ -195,7 +303,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
{(!readOnly && (!hasMaxRows)) && (
|
{(!readOnly && (!hasMaxRows)) && (
|
||||||
<div className={`${baseClass}__add-button-wrap`}>
|
<div className={`${baseClass}__add-button-wrap`}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => addRow(value)}
|
onClick={() => addRow(value as number)}
|
||||||
buttonStyle="icon-label"
|
buttonStyle="icon-label"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
iconStyle="with-border"
|
iconStyle="with-border"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
@import '../../../../scss/styles.scss';
|
@import '../../../../scss/styles.scss';
|
||||||
|
|
||||||
.field-type.array {
|
.array-field {
|
||||||
margin: base(2) 0;
|
margin: base(2) 0;
|
||||||
min-width: base(15);
|
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
h3 {
|
h3 {
|
||||||
@@ -12,12 +11,40 @@
|
|||||||
margin-bottom: base(1);
|
margin-bottom: base(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__header-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-actions {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-action {
|
||||||
|
@extend %btn-reset;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: base(.5);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
margin-bottom: base(.5);
|
||||||
|
}
|
||||||
|
|
||||||
&__error-wrap {
|
&__error-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__add-button-wrap {
|
&__add-button-wrap {
|
||||||
margin-left: base(0);
|
margin-top: base(1);
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
color: var(--theme-elevation-400);
|
color: var(--theme-elevation-400);
|
||||||
@@ -29,35 +56,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section .section {
|
.field-type:last-child {
|
||||||
margin-top: 0;
|
margin-bottom: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.section__content {
|
|
||||||
>div>div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.render-fields {
|
|
||||||
.row {
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
min-width: calc(100vw - #{base(2)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-type.group,
|
|
||||||
.field-type.array,
|
|
||||||
.field-type.blocks {
|
|
||||||
.field-type.array {
|
|
||||||
.field-type.array__add-button-wrap {
|
|
||||||
margin-left: base(3);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ const Blocks: React.FC<Props> = (props) => {
|
|||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const { preferencesKey, preferences } = useDocumentInfo();
|
const { preferencesKey, preferences } = useDocumentInfo();
|
||||||
|
const { setPreference } = usePreferences();
|
||||||
const { getPreference, setPreference } = usePreferences();
|
|
||||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||||
const formContext = useForm();
|
const formContext = useForm();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import toKebabCase from '../../../../../utilities/toKebabCase';
|
|||||||
import { usePreferences } from '../../../utilities/Preferences';
|
import { usePreferences } from '../../../utilities/Preferences';
|
||||||
import { DocumentPreferences } from '../../../../../preferences/types';
|
import { DocumentPreferences } from '../../../../../preferences/types';
|
||||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||||
|
import FieldDescription from '../../FieldDescription';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
|||||||
admin: {
|
admin: {
|
||||||
readOnly,
|
readOnly,
|
||||||
className,
|
className,
|
||||||
|
description,
|
||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -45,32 +47,33 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
}, [preferencesKey, fieldPreferencesKey, getPreference, setPreference]);
|
}, [preferencesKey, fieldPreferencesKey, getPreference, setPreference]);
|
||||||
|
|
||||||
// Do not render until preferences are retrieved
|
|
||||||
// So we can properly render the field as open or closed by default
|
|
||||||
if (!preferences) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<React.Fragment>
|
||||||
initCollapsed={Boolean(preferences.fields[fieldPreferencesKey]?.collapsed)}
|
<Collapsible
|
||||||
className={[
|
initCollapsed={Boolean(preferences.fields[fieldPreferencesKey]?.collapsed)}
|
||||||
'field-type',
|
className={[
|
||||||
baseClass,
|
'field-type',
|
||||||
className,
|
baseClass,
|
||||||
].filter(Boolean).join(' ')}
|
className,
|
||||||
header={<div className={`${baseClass}__label`}>{label}</div>}
|
].filter(Boolean).join(' ')}
|
||||||
onToggle={onToggle}
|
header={<div className={`${baseClass}__label`}>{label}</div>}
|
||||||
>
|
onToggle={onToggle}
|
||||||
<RenderFields
|
>
|
||||||
forceRender
|
<RenderFields
|
||||||
readOnly={readOnly}
|
forceRender
|
||||||
permissions={permissions?.fields}
|
readOnly={readOnly}
|
||||||
fieldTypes={fieldTypes}
|
permissions={permissions?.fields}
|
||||||
fieldSchema={fields.map((field) => ({
|
fieldTypes={fieldTypes}
|
||||||
...field,
|
fieldSchema={fields.map((field) => ({
|
||||||
path: `${path ? `${path}.` : ''}${fieldAffectsData(field) ? field.name : ''}`,
|
...field,
|
||||||
}))}
|
path: `${path ? `${path}.` : ''}${fieldAffectsData(field) ? field.name : ''}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Collapsible>
|
||||||
|
<FieldDescription
|
||||||
|
description={description}
|
||||||
/>
|
/>
|
||||||
</Collapsible>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,55 @@
|
|||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
|
|
||||||
const reducer = (currentState, action) => {
|
export type Row = {
|
||||||
const {
|
id: string
|
||||||
type, rowIndex, moveFromIndex, moveToIndex, data, blockType, collapsedState, collapsed, id,
|
collapsed?: boolean
|
||||||
} = action;
|
blockType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SET_ALL = {
|
||||||
|
type: 'SET_ALL'
|
||||||
|
data: { id?: string, blockType?: string }[]
|
||||||
|
collapsedState?: string[]
|
||||||
|
blockType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SET_COLLAPSE = {
|
||||||
|
type: 'SET_COLLAPSE'
|
||||||
|
id: string
|
||||||
|
collapsed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type SET_ALL_COLLAPSED = {
|
||||||
|
type: 'SET_ALL_COLLAPSED'
|
||||||
|
collapse: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ADD = {
|
||||||
|
type: 'ADD'
|
||||||
|
rowIndex: number
|
||||||
|
blockType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type REMOVE = {
|
||||||
|
type: 'REMOVE'
|
||||||
|
rowIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type MOVE = {
|
||||||
|
type: 'MOVE'
|
||||||
|
moveFromIndex: number
|
||||||
|
moveToIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = SET_ALL | SET_COLLAPSE | SET_ALL_COLLAPSED | ADD | REMOVE | MOVE;
|
||||||
|
|
||||||
|
const reducer = (currentState: Row[], action: Action): Row[] => {
|
||||||
const stateCopy = [...currentState];
|
const stateCopy = [...currentState];
|
||||||
|
|
||||||
switch (type) {
|
switch (action.type) {
|
||||||
case 'SET_ALL': {
|
case 'SET_ALL': {
|
||||||
|
const { data, collapsedState } = action;
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data.map((dataRow, i) => {
|
return data.map((dataRow, i) => {
|
||||||
const row = {
|
const row = {
|
||||||
@@ -25,6 +66,8 @@ const reducer = (currentState, action) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_COLLAPSE': {
|
case 'SET_COLLAPSE': {
|
||||||
|
const { collapsed, id } = action;
|
||||||
|
|
||||||
const matchedRowIndex = stateCopy.findIndex(({ id: rowID }) => rowID === id);
|
const matchedRowIndex = stateCopy.findIndex(({ id: rowID }) => rowID === id);
|
||||||
|
|
||||||
if (matchedRowIndex > -1 && stateCopy[matchedRowIndex]) {
|
if (matchedRowIndex > -1 && stateCopy[matchedRowIndex]) {
|
||||||
@@ -34,10 +77,25 @@ const reducer = (currentState, action) => {
|
|||||||
return stateCopy;
|
return stateCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_ALL_COLLAPSED': {
|
||||||
|
const { collapse } = action;
|
||||||
|
|
||||||
|
const newState = stateCopy.map((row) => ({
|
||||||
|
...row,
|
||||||
|
collapsed: collapse,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(newState);
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
case 'ADD': {
|
case 'ADD': {
|
||||||
|
const { rowIndex, blockType } = action;
|
||||||
|
|
||||||
const newRow = {
|
const newRow = {
|
||||||
id: new ObjectID().toHexString(),
|
id: new ObjectID().toHexString(),
|
||||||
open: true,
|
collapsed: false,
|
||||||
blockType: undefined,
|
blockType: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,11 +106,14 @@ const reducer = (currentState, action) => {
|
|||||||
return stateCopy;
|
return stateCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'REMOVE':
|
case 'REMOVE': {
|
||||||
|
const { rowIndex } = action;
|
||||||
stateCopy.splice(rowIndex, 1);
|
stateCopy.splice(rowIndex, 1);
|
||||||
return stateCopy;
|
return stateCopy;
|
||||||
|
}
|
||||||
|
|
||||||
case 'MOVE': {
|
case 'MOVE': {
|
||||||
|
const { moveFromIndex, moveToIndex } = action;
|
||||||
const movingRowState = { ...stateCopy[moveFromIndex] };
|
const movingRowState = { ...stateCopy[moveFromIndex] };
|
||||||
stateCopy.splice(moveFromIndex, 1);
|
stateCopy.splice(moveFromIndex, 1);
|
||||||
stateCopy.splice(moveToIndex, 0, movingRowState);
|
stateCopy.splice(moveToIndex, 0, movingRowState);
|
||||||
|
|||||||
10
src/admin/components/icons/Drag/index.scss
Normal file
10
src/admin/components/icons/Drag/index.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import '../../../scss/styles';
|
||||||
|
|
||||||
|
.icon--drag-handle {
|
||||||
|
height: $baseline;
|
||||||
|
width: $baseline;
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
fill: var(--theme-elevation-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/admin/components/icons/Drag/index.tsx
Normal file
53
src/admin/components/icons/Drag/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const DragHandle: React.FC<{ className?: string }> = ({ className }) => (
|
||||||
|
<svg
|
||||||
|
className={[
|
||||||
|
'icon icon--drag-handle',
|
||||||
|
className,
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="10.468"
|
||||||
|
cy="14.5"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="14.532"
|
||||||
|
cy="14.5"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="10.468"
|
||||||
|
cy="11.35"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="14.532"
|
||||||
|
cy="11.35"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="10.468"
|
||||||
|
cy="8.3"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="14.532"
|
||||||
|
cy="8.3"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DragHandle;
|
||||||
10
src/admin/components/icons/More/index.scss
Normal file
10
src/admin/components/icons/More/index.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import '../../../scss/styles';
|
||||||
|
|
||||||
|
.icon--more {
|
||||||
|
height: $baseline;
|
||||||
|
width: $baseline;
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
fill: var(--theme-elevation-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/admin/components/icons/More/index.tsx
Normal file
35
src/admin/components/icons/More/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const DragHandle: React.FC<{ className?: string }> = ({ className }) => (
|
||||||
|
<svg
|
||||||
|
className={[
|
||||||
|
'icon icon--more',
|
||||||
|
className,
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="16.3872"
|
||||||
|
cy="12.5"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="12.3872"
|
||||||
|
cy="12.5"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="8.61279"
|
||||||
|
cy="12.5"
|
||||||
|
r="1"
|
||||||
|
className="fill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DragHandle;
|
||||||
@@ -18,7 +18,7 @@ const AccountView: React.FC = () => {
|
|||||||
const { setStepNav } = useStepNav();
|
const { setStepNav } = useStepNav();
|
||||||
const { user, permissions } = useAuth();
|
const { user, permissions } = useAuth();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
const { id } = useDocumentInfo();
|
const { id, preferences } = useDocumentInfo();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
serverURL,
|
serverURL,
|
||||||
@@ -44,7 +44,7 @@ const AccountView: React.FC = () => {
|
|||||||
|
|
||||||
const collectionPermissions = permissions?.collections?.[adminUser];
|
const collectionPermissions = permissions?.collections?.[adminUser];
|
||||||
|
|
||||||
const [{ data, isLoading }] = usePayloadAPI(
|
const [{ data, isLoading: isLoadingDocument }] = usePayloadAPI(
|
||||||
`${serverURL}${api}/${collection?.slug}/${user?.id}?depth=0`,
|
`${serverURL}${api}/${collection?.slug}/${user?.id}?depth=0`,
|
||||||
{ initialParams: { 'fallback-locale': 'null' } },
|
{ initialParams: { 'fallback-locale': 'null' } },
|
||||||
);
|
);
|
||||||
@@ -85,7 +85,7 @@ const AccountView: React.FC = () => {
|
|||||||
hasSavePermission,
|
hasSavePermission,
|
||||||
initialState,
|
initialState,
|
||||||
apiURL,
|
apiURL,
|
||||||
isLoading,
|
isLoading: isLoadingDocument || !preferences,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</NegativeFieldGutterProvider>
|
</NegativeFieldGutterProvider>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
const { setStepNav } = useStepNav();
|
const { setStepNav } = useStepNav();
|
||||||
const { permissions, user } = useAuth();
|
const { permissions, user } = useAuth();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
const { getVersions } = useDocumentInfo();
|
const { getVersions, preferences } = useDocumentInfo();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
serverURL,
|
serverURL,
|
||||||
@@ -50,7 +50,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
}, [getVersions, fields, user, locale]);
|
}, [getVersions, fields, user, locale]);
|
||||||
|
|
||||||
const [{ data, isLoading }] = usePayloadAPI(
|
const [{ data, isLoading: isLoadingDocument }] = usePayloadAPI(
|
||||||
`${serverURL}${api}/globals/${slug}`,
|
`${serverURL}${api}/globals/${slug}`,
|
||||||
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
||||||
);
|
);
|
||||||
@@ -82,7 +82,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
DefaultComponent={DefaultGlobal}
|
DefaultComponent={DefaultGlobal}
|
||||||
CustomComponent={CustomEdit}
|
CustomComponent={CustomEdit}
|
||||||
componentProps={{
|
componentProps={{
|
||||||
isLoading,
|
isLoading: isLoadingDocument || !preferences,
|
||||||
data: dataToRender,
|
data: dataToRender,
|
||||||
permissions: globalPermissions,
|
permissions: globalPermissions,
|
||||||
initialState,
|
initialState,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
const { setStepNav } = useStepNav();
|
const { setStepNav } = useStepNav();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
const { permissions, user } = useAuth();
|
const { permissions, user } = useAuth();
|
||||||
const { getVersions } = useDocumentInfo();
|
const { getVersions, preferences } = useDocumentInfo();
|
||||||
|
|
||||||
const onSave = useCallback(async (json: any) => {
|
const onSave = useCallback(async (json: any) => {
|
||||||
getVersions();
|
getVersions();
|
||||||
@@ -56,7 +56,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [admin, collection, history, isEditing, getVersions, user, id, locale]);
|
}, [admin, collection, history, isEditing, getVersions, user, id, locale]);
|
||||||
|
|
||||||
const [{ data, isLoading, isError }] = usePayloadAPI(
|
const [{ data, isLoading: isLoadingDocument, isError }] = usePayloadAPI(
|
||||||
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
|
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
|
||||||
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
||||||
);
|
);
|
||||||
@@ -97,7 +97,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
}, [setStepNav, isEditing, pluralLabel, dataToRender, slug, useAsTitle, admin]);
|
}, [setStepNav, isEditing, pluralLabel, dataToRender, slug, useAsTitle, admin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) {
|
if (isLoadingDocument) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
@@ -106,7 +106,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
awaitInitialState();
|
awaitInitialState();
|
||||||
}, [dataToRender, fields, isEditing, id, user, locale, isLoading]);
|
}, [dataToRender, fields, isEditing, id, user, locale, isLoadingDocument]);
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +125,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
DefaultComponent={DefaultEdit}
|
DefaultComponent={DefaultEdit}
|
||||||
CustomComponent={CustomEdit}
|
CustomComponent={CustomEdit}
|
||||||
componentProps={{
|
componentProps={{
|
||||||
isLoading,
|
isLoading: isLoadingDocument || !preferences,
|
||||||
data: dataToRender,
|
data: dataToRender,
|
||||||
collection,
|
collection,
|
||||||
permissions: collectionPermissions,
|
permissions: collectionPermissions,
|
||||||
|
|||||||
@@ -25,16 +25,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Toastify__toast--success {
|
.Toastify__toast--success {
|
||||||
color: var(--color-success-900);
|
color: var(--color-success-100);
|
||||||
background: var(--color-success-500);
|
background: var(--color-success-500);
|
||||||
|
|
||||||
.Toastify__progress-bar {
|
.Toastify__progress-bar {
|
||||||
background-color: var(--color-success-900);
|
background-color: var(--color-success-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Toastify__close-button--success {
|
.Toastify__close-button--success {
|
||||||
color: var(--color-success-900);
|
color: var(--color-success-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Toastify__toast--error {
|
.Toastify__toast--error {
|
||||||
|
|||||||
@@ -70,7 +70,11 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--color-success-500);
|
|||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
|
|
||||||
@mixin shadow-sm {
|
@mixin shadow-sm {
|
||||||
box-shadow: 0 2px 3px 0 rgba(0, 2, 4, 0.1), 0 6px 4px -4px rgba(0, 2, 4, 0.02);
|
box-shadow: 0 2px 3px 0 rgba(0, 2, 4, 0.05), 0 10px 4px -8px rgba(0, 2, 4, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin shadow-m {
|
||||||
|
box-shadow: 0 0 30px 0 rgb(0 2 4 / 12%), 0 30px 25px -8px rgb(0 2 4 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin shadow-lg {
|
@mixin shadow-lg {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default buildConfig({
|
|||||||
slug: 'array-fields',
|
slug: 'array-fields',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'array',
|
name: 'items',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
required: true,
|
required: true,
|
||||||
fields: [
|
fields: [
|
||||||
@@ -59,6 +59,9 @@ export default buildConfig({
|
|||||||
{
|
{
|
||||||
label: 'Collapsible Field',
|
label: 'Collapsible Field',
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
|
admin: {
|
||||||
|
description: 'This is a collapsible field.',
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const arrayDoc = {
|
export const arrayDoc = {
|
||||||
array: [
|
items: [
|
||||||
{
|
{
|
||||||
text: 'first row',
|
text: 'first row',
|
||||||
},
|
},
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -1265,14 +1265,6 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||||
|
|
||||||
"@faceless-ui/collapsibles@^1.0.0":
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.npmjs.org/@faceless-ui/collapsibles/-/collapsibles-1.1.1.tgz#89b86e11eceae55ed87caa4de9d395f7b082a938"
|
|
||||||
integrity sha512-7tU5CjXUnW7bKthpMgNKs5TirTb+DQVFASnstqlpe+gXl8W76Rq4Z9m6nS3PnnG2B5h9aqtUVaNgroLc1J+i/w==
|
|
||||||
dependencies:
|
|
||||||
react-animate-height "^2.0.16"
|
|
||||||
react-transition-group "^4.4.2"
|
|
||||||
|
|
||||||
"@faceless-ui/modal@^1.1.7":
|
"@faceless-ui/modal@^1.1.7":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.npmjs.org/@faceless-ui/modal/-/modal-1.2.0.tgz#0ca43e480f83d307dcd84c033fbc82c0619f5d8c"
|
resolved "https://registry.npmjs.org/@faceless-ui/modal/-/modal-1.2.0.tgz#0ca43e480f83d307dcd84c033fbc82c0619f5d8c"
|
||||||
@@ -10322,7 +10314,7 @@ rc@1.2.8, rc@^1.2.7, rc@^1.2.8:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-animate-height@^2.0.16, react-animate-height@^2.0.20:
|
react-animate-height@^2.0.20:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.npmjs.org/react-animate-height/-/react-animate-height-2.1.2.tgz#9b450fc64d46f10f5e07da8d0d5e2c47b9f15030"
|
resolved "https://registry.npmjs.org/react-animate-height/-/react-animate-height-2.1.2.tgz#9b450fc64d46f10f5e07da8d0d5e2c47b9f15030"
|
||||||
integrity sha512-A9jfz/4CTdsIsE7WCQtO9UkOpMBcBRh8LxyHl2eoZz1ki02jpyUL5xt58gabd0CyeLQ8fRyQ+s2lyV2Ufu8Owg==
|
integrity sha512-A9jfz/4CTdsIsE7WCQtO9UkOpMBcBRh8LxyHl2eoZz1ki02jpyUL5xt58gabd0CyeLQ8fRyQ+s2lyV2Ufu8Owg==
|
||||||
|
|||||||
Reference in New Issue
Block a user