feat: retrofits array to new collapsible component

This commit is contained in:
James
2022-07-13 20:09:50 -07:00
parent 270dd22f08
commit aa89251a3b
32 changed files with 696 additions and 184 deletions

View File

@@ -88,7 +88,6 @@
"@babel/preset-typescript": "^7.12.1",
"@babel/register": "^7.11.5",
"@date-io/date-fns": "^2.10.6",
"@faceless-ui/collapsibles": "^1.0.0",
"@faceless-ui/modal": "^1.1.7",
"@faceless-ui/scroll-info": "^1.2.3",
"@faceless-ui/window-info": "^2.0.2",

View 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);
}
}
}

View 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>
);
}}
/>
);
};

View 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
}

View File

@@ -1,6 +1,9 @@
@import '../../../scss/styles.scss';
.collapsible {
--toggle-pad-h: #{base(.75)};
--toggle-pad-v: #{base(.5)};
border: 1px solid var(--theme-elevation-200);
border-radius: $style-radius-m;
@@ -8,20 +11,42 @@
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 {
@extend %btn-reset;
@extend %body;
text-align: left;
cursor: pointer;
background: var(--theme-elevation-50);
border-top-right-radius: $style-radius-s;
border-top-left-radius: $style-radius-s;
width: 100%;
display: flex;
align-items: center;
padding: base(.5) base(1);
&:hover {
background: var(--theme-elevation-100);
padding: var(--toggle-pad-v) var(--toggle-pad-h);
}
&__toggle--has-drag-handle {
padding-left: base(1.5);
}
&--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 {
margin: 0 0 0 auto;
transform: rotate(.5turn);
}
&__content {
background-color: var(--theme-elevation-0);
border-bottom-left-radius: $style-radius-s;
border-bottom-right-radius: $style-radius-s;
padding: $baseline;
}

View File

@@ -3,39 +3,75 @@ import AnimateHeight from 'react-animate-height';
import { Props } from './types';
import { CollapsibleProvider, useCollapsible } from './provider';
import Chevron from '../../icons/Chevron';
import DragHandle from '../../icons/Drag';
import './index.scss';
const baseClass = 'collapsible';
export const Collapsible: React.FC<Props> = ({ children, onToggle, className, header, initCollapsed }) => {
const [collapsed, setCollapsed] = useState(Boolean(initCollapsed));
export const Collapsible: React.FC<Props> = ({
children,
collapsed: collapsedFromProps,
onToggle,
className,
header,
initCollapsed,
dragHandleProps,
actions,
}) => {
const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed));
const [hovered, setHovered] = useState(false);
const isNested = useCollapsible();
const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal;
return (
<div className={[
baseClass,
className,
dragHandleProps && `${baseClass}--has-drag-handle`,
collapsed && `${baseClass}--collapsed`,
isNested && `${baseClass}--nested`,
hovered && `${baseClass}--hovered`,
].filter(Boolean).join(' ')}
>
<CollapsibleProvider>
<div
className={`${baseClass}__toggle-wrap`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{dragHandleProps && (
<div
className={`${baseClass}__drag`}
{...dragHandleProps}
>
<DragHandle />
</div>
)}
<button
type="button"
className={`${baseClass}__toggle ${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`}
className={[
`${baseClass}__toggle`,
`${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`,
dragHandleProps && `${baseClass}__toggle--has-drag-handle`,
].filter(Boolean).join(' ')}
onClick={() => {
if (typeof onToggle === 'function') onToggle(!collapsed);
setCollapsed(!collapsed);
setCollapsedLocal(!collapsed);
}}
>
{header && (
<div className={`${baseClass}__header-wrap`}>
{header}
{header && header}
</button>
<div className={`${baseClass}__actions-wrap`}>
{actions && (
<div className={`${baseClass}__actions`}>
{actions}
</div>
)}
<Chevron className={`${baseClass}__indicator`} />
</button>
</div>
</div>
<AnimateHeight
height={collapsed ? 0 : 'auto'}
duration={200}

View File

@@ -1,9 +1,13 @@
import React from 'react';
import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd';
export type Props = {
collapsed?: boolean
className?: string
header?: React.ReactNode
actions?: React.ReactNode
children: React.ReactNode
onToggle?: (collapsed: boolean) => void
initCollapsed?: boolean
dragHandleProps?: DraggableProvidedDragHandleProps
}

View File

@@ -7,6 +7,7 @@ const baseClass = 'popup-button';
const PopupButton: React.FC<Props> = (props) => {
const {
className,
buttonType,
button,
setActive,
@@ -15,6 +16,7 @@ const PopupButton: React.FC<Props> = (props) => {
const classes = [
baseClass,
className,
`${baseClass}--${buttonType}`,
].filter(Boolean).join(' ');

View File

@@ -1,4 +1,5 @@
export type Props = {
className?: string
buttonType: 'custom' | 'default' | 'none',
button: React.ReactNode,
setActive: (active: boolean) => void,

View File

@@ -43,7 +43,7 @@
&--size-small {
.popup__content {
@include shadow-sm;
@include shadow-m;
}
&.popup--h-align-left {
@@ -69,7 +69,7 @@
&--size-wide {
.popup__content {
@include shadow-sm;
@include shadow-m;
&:after {
border: 12px solid transparent;
@@ -109,11 +109,11 @@
&--h-align-center {
.popup__content {
left: 50%;
transform: translateX(-0%);
transform: translateX(-50%);
&:after {
left: 50%;
transform: translateX(-0%);
transform: translateX(-50%);
}
}
}

View File

@@ -13,6 +13,7 @@ const baseClass = 'popup';
const Popup: React.FC<Props> = (props) => {
const {
className,
buttonClassName,
render,
size = 'small',
color = 'light',
@@ -129,21 +130,11 @@ const Popup: React.FC<Props> = (props) => {
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
>
<PopupButton
buttonType={buttonType}
button={button}
setActive={setActive}
active={active}
/>
<PopupButton {...{ className: buttonClassName, buttonType, button, setActive, active }} />
</div>
)
: (
<PopupButton
buttonType={buttonType}
button={button}
setActive={setActive}
active={active}
/>
<PopupButton {...{ className: buttonClassName, buttonType, button, setActive, active }} />
)}
</div>

View File

@@ -2,6 +2,7 @@ import { CSSProperties } from 'react';
export type Props = {
className?: string
buttonClassName?: string
render?: (any) => React.ReactNode,
children?: React.ReactNode,
verticalAlign?: 'top' | 'bottom'

View File

@@ -76,28 +76,6 @@ const DraggableSection: React.FC<Props> = (props) => {
<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
height={isCollapsed ? 0 : 'auto'}
duration={200}

View File

@@ -1,6 +1,6 @@
import React from 'react';
export type DescriptionFunction = (value: unknown) => string
export type DescriptionFunction = (value?: unknown) => string
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
@@ -8,7 +8,7 @@ export type Description = string | DescriptionFunction | DescriptionComponent
export type Props = {
description?: Description
value: unknown;
value?: unknown;
}
export function isComponent(description: Description): description is DescriptionComponent {

View File

@@ -1,4 +1,5 @@
import equal from 'deep-equal';
import ObjectID from 'bson-objectid';
import { unflatten, flatten } from 'flatley';
import flattenFilters from './flattenFilters';
import getSiblingData from './getSiblingData';
@@ -109,6 +110,32 @@ function fieldReducer(state: Fields, action): Fields {
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': {
const { moveFromIndex, moveToIndex, path } = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);

View File

@@ -1,10 +1,9 @@
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 withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import DraggableSection from '../../DraggableSection';
import reducer from '../rowReducer';
import reducer, { Row } from '../rowReducer';
import { useForm } from '../../Form/context';
import buildStateFromSchema from '../../Form/buildStateFromSchema';
import useField from '../../useField';
@@ -13,14 +12,18 @@ import Error from '../../Error';
import { array } from '../../../../../fields/validations';
import Banner from '../../../elements/Banner';
import FieldDescription from '../../FieldDescription';
import { Props } from './types';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
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';
const baseClass = 'field-type array';
const baseClass = 'array-field';
const ArrayFieldType: React.FC<Props> = (props) => {
const {
@@ -52,6 +55,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
// eslint-disable-next-line react/destructuring-assignment
const label = props?.label ?? props?.labels?.singular;
const { preferencesKey, preferences } = useDocumentInfo();
const { setPreference } = usePreferences();
const [rows, dispatchRows] = useReducer(reducer, []);
const formContext = useForm();
const { user } = useAuth();
@@ -81,20 +86,26 @@ const ArrayFieldType: React.FC<Props> = (props) => {
condition,
});
const addRow = useCallback(async (rowIndex) => {
const addRow = useCallback(async (rowIndex: number) => {
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
dispatchRows({ type: 'ADD', rowIndex });
setValue(value as number + 1);
}, [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 });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
setValue(value as number - 1);
}, [dispatchRows, dispatchFields, path, value, setValue]);
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
}, [dispatchRows, dispatchFields, path]);
@@ -106,10 +117,57 @@ const ArrayFieldType: React.FC<Props> = (props) => {
moveRow(sourceIndex, destinationIndex);
}, [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(() => {
const data = formContext.getDataByPath(path);
dispatchRows({ type: 'SET_ALL', data: data || [] });
}, [formContext, path]);
const data = formContext.getDataByPath<Row[]>(path);
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed });
}, [formContext, path, preferences]);
useEffect(() => {
setValue(rows?.length || 0, true);
@@ -124,6 +182,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const hasMaxRows = maxRows && rows.length >= maxRows;
const classes = [
'field-type',
baseClass,
className,
].filter(Boolean).join(' ');
@@ -140,7 +199,29 @@ const ArrayFieldType: React.FC<Props> = (props) => {
/>
</div>
<header className={`${baseClass}__header`}>
<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
value={value}
description={description}
@@ -153,23 +234,50 @@ const ArrayFieldType: React.FC<Props> = (props) => {
{...provided.droppableProps}
>
{rows.length > 0 && rows.map((row, i) => (
<DraggableSection
readOnly={readOnly}
<Draggable
key={row.id}
id={row.id}
blockType="array"
label={labels.singular}
draggableId={row.id}
index={i}
isDragDisabled={readOnly}
>
{(providedDrag) => (
<div
ref={providedDrag.innerRef}
{...providedDrag.draggableProps}
>
<Collapsible
collapsed={row.collapsed}
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
className={`${baseClass}__row`}
key={row.id}
dragHandleProps={providedDrag.dragHandleProps}
header={`${labels.singular} ${i + 1}`}
actions={!readOnly ? (
<ArrayAction
rowCount={rows.length}
rowIndex={i}
duplicateRow={duplicateRow}
addRow={addRow}
removeRow={removeRow}
moveRow={moveRow}
parentPath={path}
fieldTypes={fieldTypes}
fieldSchema={fields}
permissions={permissions}
hasMaxRows={hasMaxRows}
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)) && (
<Banner type="error">
@@ -195,7 +303,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
{(!readOnly && (!hasMaxRows)) && (
<div className={`${baseClass}__add-button-wrap`}>
<Button
onClick={() => addRow(value)}
onClick={() => addRow(value as number)}
buttonStyle="icon-label"
icon="plus"
iconStyle="with-border"

View File

@@ -1,8 +1,7 @@
@import '../../../../scss/styles.scss';
.field-type.array {
.array-field {
margin: base(2) 0;
min-width: base(15);
&__header {
h3 {
@@ -12,12 +11,40 @@
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 {
position: relative;
}
&__add-button-wrap {
margin-left: base(0);
margin-top: base(1);
.btn {
color: var(--theme-elevation-400);
@@ -29,35 +56,7 @@
}
}
.section .section {
margin-top: 0;
}
.section__content {
>div>div {
width: 100%;
}
}
.render-fields {
.row {
&:last-child {
.field-type: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);
}
}
}

View File

@@ -54,8 +54,7 @@ const Blocks: React.FC<Props> = (props) => {
const path = pathFromProps || name;
const { preferencesKey, preferences } = useDocumentInfo();
const { getPreference, setPreference } = usePreferences();
const { setPreference } = usePreferences();
const [rows, dispatchRows] = useReducer(reducer, []);
const formContext = useForm();
const { user } = useAuth();

View File

@@ -8,6 +8,7 @@ import toKebabCase from '../../../../../utilities/toKebabCase';
import { usePreferences } from '../../../utilities/Preferences';
import { DocumentPreferences } from '../../../../../preferences/types';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import FieldDescription from '../../FieldDescription';
import './index.scss';
@@ -23,6 +24,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
admin: {
readOnly,
className,
description,
},
} = props;
@@ -45,11 +47,8 @@ const CollapsibleField: React.FC<Props> = (props) => {
});
}, [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 (
<React.Fragment>
<Collapsible
initCollapsed={Boolean(preferences.fields[fieldPreferencesKey]?.collapsed)}
className={[
@@ -71,6 +70,10 @@ const CollapsibleField: React.FC<Props> = (props) => {
}))}
/>
</Collapsible>
<FieldDescription
description={description}
/>
</React.Fragment>
);
};

View File

@@ -1,14 +1,55 @@
import ObjectID from 'bson-objectid';
const reducer = (currentState, action) => {
const {
type, rowIndex, moveFromIndex, moveToIndex, data, blockType, collapsedState, collapsed, id,
} = action;
export type Row = {
id: string
collapsed?: boolean
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];
switch (type) {
switch (action.type) {
case 'SET_ALL': {
const { data, collapsedState } = action;
if (Array.isArray(data)) {
return data.map((dataRow, i) => {
const row = {
@@ -25,6 +66,8 @@ const reducer = (currentState, action) => {
}
case 'SET_COLLAPSE': {
const { collapsed, id } = action;
const matchedRowIndex = stateCopy.findIndex(({ id: rowID }) => rowID === id);
if (matchedRowIndex > -1 && stateCopy[matchedRowIndex]) {
@@ -34,10 +77,25 @@ const reducer = (currentState, action) => {
return stateCopy;
}
case 'SET_ALL_COLLAPSED': {
const { collapse } = action;
const newState = stateCopy.map((row) => ({
...row,
collapsed: collapse,
}));
console.log(newState);
return newState;
}
case 'ADD': {
const { rowIndex, blockType } = action;
const newRow = {
id: new ObjectID().toHexString(),
open: true,
collapsed: false,
blockType: undefined,
};
@@ -48,11 +106,14 @@ const reducer = (currentState, action) => {
return stateCopy;
}
case 'REMOVE':
case 'REMOVE': {
const { rowIndex } = action;
stateCopy.splice(rowIndex, 1);
return stateCopy;
}
case 'MOVE': {
const { moveFromIndex, moveToIndex } = action;
const movingRowState = { ...stateCopy[moveFromIndex] };
stateCopy.splice(moveFromIndex, 1);
stateCopy.splice(moveToIndex, 0, movingRowState);

View File

@@ -0,0 +1,10 @@
@import '../../../scss/styles';
.icon--drag-handle {
height: $baseline;
width: $baseline;
.fill {
fill: var(--theme-elevation-800);
}
}

View 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;

View File

@@ -0,0 +1,10 @@
@import '../../../scss/styles';
.icon--more {
height: $baseline;
width: $baseline;
.fill {
fill: var(--theme-elevation-800);
}
}

View 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;

View File

@@ -18,7 +18,7 @@ const AccountView: React.FC = () => {
const { setStepNav } = useStepNav();
const { user, permissions } = useAuth();
const [initialState, setInitialState] = useState({});
const { id } = useDocumentInfo();
const { id, preferences } = useDocumentInfo();
const {
serverURL,
@@ -44,7 +44,7 @@ const AccountView: React.FC = () => {
const collectionPermissions = permissions?.collections?.[adminUser];
const [{ data, isLoading }] = usePayloadAPI(
const [{ data, isLoading: isLoadingDocument }] = usePayloadAPI(
`${serverURL}${api}/${collection?.slug}/${user?.id}?depth=0`,
{ initialParams: { 'fallback-locale': 'null' } },
);
@@ -85,7 +85,7 @@ const AccountView: React.FC = () => {
hasSavePermission,
initialState,
apiURL,
isLoading,
isLoading: isLoadingDocument || !preferences,
}}
/>
</NegativeFieldGutterProvider>

View File

@@ -20,7 +20,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const { setStepNav } = useStepNav();
const { permissions, user } = useAuth();
const [initialState, setInitialState] = useState({});
const { getVersions } = useDocumentInfo();
const { getVersions, preferences } = useDocumentInfo();
const {
serverURL,
@@ -50,7 +50,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
setInitialState(state);
}, [getVersions, fields, user, locale]);
const [{ data, isLoading }] = usePayloadAPI(
const [{ data, isLoading: isLoadingDocument }] = usePayloadAPI(
`${serverURL}${api}/globals/${slug}`,
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
);
@@ -82,7 +82,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
DefaultComponent={DefaultGlobal}
CustomComponent={CustomEdit}
componentProps={{
isLoading,
isLoading: isLoadingDocument || !preferences,
data: dataToRender,
permissions: globalPermissions,
initialState,

View File

@@ -44,7 +44,7 @@ const EditView: React.FC<IndexProps> = (props) => {
const { setStepNav } = useStepNav();
const [initialState, setInitialState] = useState({});
const { permissions, user } = useAuth();
const { getVersions } = useDocumentInfo();
const { getVersions, preferences } = useDocumentInfo();
const onSave = useCallback(async (json: any) => {
getVersions();
@@ -56,7 +56,7 @@ const EditView: React.FC<IndexProps> = (props) => {
}
}, [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),
{ 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]);
useEffect(() => {
if (isLoading) {
if (isLoadingDocument) {
return;
}
const awaitInitialState = async () => {
@@ -106,7 +106,7 @@ const EditView: React.FC<IndexProps> = (props) => {
};
awaitInitialState();
}, [dataToRender, fields, isEditing, id, user, locale, isLoading]);
}, [dataToRender, fields, isEditing, id, user, locale, isLoadingDocument]);
if (isError) {
return (
@@ -125,7 +125,7 @@ const EditView: React.FC<IndexProps> = (props) => {
DefaultComponent={DefaultEdit}
CustomComponent={CustomEdit}
componentProps={{
isLoading,
isLoading: isLoadingDocument || !preferences,
data: dataToRender,
collection,
permissions: collectionPermissions,

View File

@@ -25,16 +25,16 @@
}
.Toastify__toast--success {
color: var(--color-success-900);
color: var(--color-success-100);
background: var(--color-success-500);
.Toastify__progress-bar {
background-color: var(--color-success-900);
background-color: var(--color-success-100);
}
}
.Toastify__close-button--success {
color: var(--color-success-900);
color: var(--color-success-100);
}
.Toastify__toast--error {

View File

@@ -70,7 +70,11 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--color-success-500);
//////////////////////////////
@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 {

View File

@@ -8,7 +8,7 @@ export default buildConfig({
slug: 'array-fields',
fields: [
{
name: 'array',
name: 'items',
type: 'array',
required: true,
fields: [
@@ -59,6 +59,9 @@ export default buildConfig({
{
label: 'Collapsible Field',
type: 'collapsible',
admin: {
description: 'This is a collapsible field.',
},
fields: [
{
name: 'text',

View File

@@ -1,5 +1,5 @@
export const arrayDoc = {
array: [
items: [
{
text: 'first row',
},

View File

@@ -1265,14 +1265,6 @@
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
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":
version "1.2.0"
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"
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"
resolved "https://registry.npmjs.org/react-animate-height/-/react-animate-height-2.1.2.tgz#9b450fc64d46f10f5e07da8d0d5e2c47b9f15030"
integrity sha512-A9jfz/4CTdsIsE7WCQtO9UkOpMBcBRh8LxyHl2eoZz1ki02jpyUL5xt58gabd0CyeLQ8fRyQ+s2lyV2Ufu8Owg==