parent
b6c5791d6c
commit
0655f5c382
@ -54,6 +54,9 @@
|
||||
"@codemirror/state": "6.1.4",
|
||||
"@codemirror/theme-one-dark": "6.1.0",
|
||||
"@codemirror/view": "6.6.0",
|
||||
"@dnd-kit/core": "6.0.5",
|
||||
"@dnd-kit/sortable": "7.0.1",
|
||||
"@dnd-kit/utilities": "3.2.0",
|
||||
"@emotion/babel-preset-css-prop": "11.10.0",
|
||||
"@emotion/css": "11.10.5",
|
||||
"@emotion/react": "11.10.5",
|
||||
@ -121,7 +124,6 @@
|
||||
"react-redux": "8.0.5",
|
||||
"react-router-dom": "6.4.4",
|
||||
"react-scroll-sync": "0.11.0",
|
||||
"react-sortable-hoc": "2.0.0",
|
||||
"react-textarea-autosize": "8.4.0",
|
||||
"react-topbar-progress-indicator": "4.1.1",
|
||||
"react-virtualized-auto-sizer": "1.0.7",
|
||||
@ -235,6 +237,7 @@
|
||||
"postcss-scss": "4.0.6",
|
||||
"prettier": "2.8.0",
|
||||
"process": "0.11.10",
|
||||
"prop-types": "15.8.1",
|
||||
"react-refresh": "0.14.0",
|
||||
"react-svg-loader": "3.0.3",
|
||||
"rimraf": "3.0.2",
|
||||
|
@ -5,7 +5,6 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { logoutUser as logoutUserAction } from '@staticcms/core/actions/auth';
|
||||
import {
|
||||
changeDraftFieldValidation as changeDraftFieldValidationAction,
|
||||
createDraftDuplicateFromEntry as createDraftDuplicateFromEntryAction,
|
||||
createEmptyDraft as createEmptyDraftAction,
|
||||
deleteDraftLocalBackup as deleteDraftLocalBackupAction,
|
||||
@ -385,7 +384,6 @@ const mapDispatchToProps = {
|
||||
retrieveLocalBackup: retrieveLocalBackupAction,
|
||||
persistLocalBackup: persistLocalBackupAction,
|
||||
deleteLocalBackup: deleteLocalBackupAction,
|
||||
changeDraftFieldValidation: changeDraftFieldValidationAction,
|
||||
createDraftDuplicateFromEntry: createDraftDuplicateFromEntryAction,
|
||||
createEmptyDraft: createEmptyDraftAction,
|
||||
discardDraft: discardDraftAction,
|
||||
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
changeDraftField as changeDraftFieldAction,
|
||||
changeDraftFieldValidation as changeDraftFieldValidationAction,
|
||||
changeDraftFieldValidation,
|
||||
} from '@staticcms/core/actions/entries';
|
||||
import { getAsset as getAssetAction } from '@staticcms/core/actions/media';
|
||||
import {
|
||||
@ -20,10 +20,13 @@ import { query as queryAction } from '@staticcms/core/actions/search';
|
||||
import { borders, colors, lengths, transitions } from '@staticcms/core/components/UI/styles';
|
||||
import { transientOptions } from '@staticcms/core/lib';
|
||||
import useMemoCompare from '@staticcms/core/lib/hooks/useMemoCompare';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import { resolveWidget } from '@staticcms/core/lib/registry';
|
||||
import { getFieldLabel } from '@staticcms/core/lib/util/field.util';
|
||||
import { validate } from '@staticcms/core/lib/util/validation.util';
|
||||
import { selectFieldErrors } from '@staticcms/core/reducers/entryDraft';
|
||||
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
|
||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||
|
||||
import type {
|
||||
Field,
|
||||
@ -146,7 +149,6 @@ const EditorControl = ({
|
||||
isHidden = false,
|
||||
locale,
|
||||
mediaPaths,
|
||||
changeDraftFieldValidation,
|
||||
openMediaLibrary,
|
||||
parentPath,
|
||||
query,
|
||||
@ -158,6 +160,10 @@ const EditorControl = ({
|
||||
changeDraftField,
|
||||
i18n,
|
||||
}: TranslatedProps<EditorControlProps>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const id = useUUID();
|
||||
|
||||
const widgetName = field.widget;
|
||||
const widget = resolveWidget(widgetName) as Widget<ValueOrNestedValue>;
|
||||
const fieldHint = field.hint;
|
||||
@ -168,8 +174,10 @@ const EditorControl = ({
|
||||
);
|
||||
|
||||
const [dirty, setDirty] = useState(!isEmpty(value));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const errors = useMemo(() => fieldsErrors[path] ?? [], [fieldsErrors[path]]);
|
||||
|
||||
const fieldErrorsSelector = useMemo(() => selectFieldErrors(path), [path]);
|
||||
const errors = useAppSelector(fieldErrorsSelector);
|
||||
|
||||
const hasErrors = (submitted || dirty) && Boolean(errors.length);
|
||||
|
||||
const handleGetAsset: GetAssetFunction = useMemo(
|
||||
@ -181,12 +189,17 @@ const EditorControl = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty && !submitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateValue = async () => {
|
||||
await validate(path, field, value, widget, changeDraftFieldValidation, t);
|
||||
const errors = await validate(field, value, widget, t);
|
||||
dispatch(changeDraftFieldValidation(path, errors));
|
||||
};
|
||||
|
||||
validateValue();
|
||||
}, [field, value, changeDraftFieldValidation, path, t, widget, dirty]);
|
||||
}, [dispatch, field, path, t, value, widget, dirty, submitted]);
|
||||
|
||||
const handleChangeDraftField = useCallback(
|
||||
(value: ValueOrNestedValue) => {
|
||||
@ -209,7 +222,7 @@ const EditorControl = ({
|
||||
<ControlContainer $isHidden={isHidden}>
|
||||
<>
|
||||
{createElement(widget.control, {
|
||||
key: `field_${path}`,
|
||||
key: id,
|
||||
collection,
|
||||
config,
|
||||
entry,
|
||||
@ -318,7 +331,6 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
changeDraftField: changeDraftFieldAction,
|
||||
changeDraftFieldValidation: changeDraftFieldValidationAction,
|
||||
openMediaLibrary: openMediaLibraryAction,
|
||||
clearMediaControl: clearMediaControlAction,
|
||||
removeMediaControl: removeMediaControlAction,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
|
||||
import { transientOptions } from '@staticcms/core/lib/util';
|
||||
import { buttons, colors, lengths, transitions } from './styles';
|
||||
|
||||
import type { ComponentClass, MouseEvent, ReactNode } from 'react';
|
||||
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
|
||||
interface TopBarProps {
|
||||
$isVariableTypesList: boolean;
|
||||
@ -74,16 +75,15 @@ const DragIconContainer = styled(TopBarButtonSpan)`
|
||||
`;
|
||||
|
||||
export interface DragHandleProps {
|
||||
dragHandleHOC: (render: () => ReactNode) => ComponentClass;
|
||||
listeners: SyntheticListenerMap | undefined;
|
||||
}
|
||||
|
||||
const DragHandle = ({ dragHandleHOC }: DragHandleProps) => {
|
||||
const Handle = dragHandleHOC(() => (
|
||||
<DragIconContainer>
|
||||
const DragHandle = ({ listeners }: DragHandleProps) => {
|
||||
return (
|
||||
<DragIconContainer {...listeners}>
|
||||
<DragHandleIcon />
|
||||
</DragIconContainer>
|
||||
));
|
||||
return <Handle />;
|
||||
);
|
||||
};
|
||||
|
||||
export interface ListItemTopBarProps {
|
||||
@ -92,8 +92,8 @@ export interface ListItemTopBarProps {
|
||||
collapsed?: boolean;
|
||||
onCollapseToggle?: (event: MouseEvent) => void;
|
||||
onRemove: (event: MouseEvent) => void;
|
||||
dragHandleHOC: (render: () => ReactNode) => ComponentClass;
|
||||
isVariableTypesList?: boolean;
|
||||
listeners: SyntheticListenerMap | undefined;
|
||||
}
|
||||
|
||||
const ListItemTopBar = ({
|
||||
@ -102,8 +102,8 @@ const ListItemTopBar = ({
|
||||
collapsed = false,
|
||||
onCollapseToggle,
|
||||
onRemove,
|
||||
dragHandleHOC,
|
||||
isVariableTypesList = false,
|
||||
listeners,
|
||||
}: ListItemTopBarProps) => {
|
||||
return (
|
||||
<TopBar className={className} $collapsed={collapsed} $isVariableTypesList={isVariableTypesList}>
|
||||
@ -120,7 +120,7 @@ const ListItemTopBar = ({
|
||||
<StyledTitle key="title" onClick={onCollapseToggle}>
|
||||
{title}
|
||||
</StyledTitle>
|
||||
{dragHandleHOC ? <DragHandle dragHandleHOC={dragHandleHOC} /> : null}
|
||||
{listeners ? <DragHandle listeners={listeners} /> : null}
|
||||
{onRemove ? (
|
||||
<TopBarButton onClick={onRemove}>
|
||||
<CloseIcon />
|
||||
|
6
core/src/lib/hooks/useUUID.ts
Normal file
6
core/src/lib/hooks/useUUID.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default function useUUID() {
|
||||
return useMemo(() => uuid(), []);
|
||||
}
|
@ -78,11 +78,9 @@ export function validatePattern({
|
||||
}
|
||||
|
||||
export async function validate(
|
||||
path: string,
|
||||
field: Field,
|
||||
value: ValueOrNestedValue,
|
||||
widget: Widget<any, any>,
|
||||
onValidate: (path: string, errors: FieldError[]) => void,
|
||||
t: t,
|
||||
): Promise<FieldError[]> {
|
||||
const validValue = widget.getValidValue(value);
|
||||
@ -100,6 +98,5 @@ export async function validate(
|
||||
}
|
||||
}
|
||||
|
||||
onValidate(path, errors);
|
||||
return errors;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import { set } from '../lib/util/object.util';
|
||||
|
||||
import type { EntriesAction } from '../actions/entries';
|
||||
import type { Entry, FieldsErrors } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
|
||||
export interface EntryDraftState {
|
||||
original?: Entry;
|
||||
@ -300,3 +301,7 @@ function entryDraftReducer(
|
||||
}
|
||||
|
||||
export default entryDraftReducer;
|
||||
|
||||
export const selectFieldErrors = (path: string) => (state: RootState) => {
|
||||
return state.entryDraft.fieldsErrors[path] ?? [];
|
||||
};
|
||||
|
@ -2,10 +2,10 @@ import { styled } from '@mui/material/styles';
|
||||
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
|
||||
import Outline from '@staticcms/core/components/UI/Outline';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import transientOptions from '@staticcms/core/lib/util/transientOptions';
|
||||
import languages from './data/languages';
|
||||
import SettingsButton from './SettingsButton';
|
||||
@ -146,7 +146,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
setSettingsVisible(false);
|
||||
}, []);
|
||||
|
||||
const uniqueId = useMemo(() => uuid(), []);
|
||||
const uniqueId = useUUID();
|
||||
|
||||
// If `allow_language_selection` is not set, default to true. Otherwise, use its value.
|
||||
const allowLanguageSelection = useMemo(
|
||||
|
@ -3,15 +3,13 @@ import PhotoIcon from '@mui/icons-material/Photo';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
|
||||
import Outline from '@staticcms/core/components/UI/Outline';
|
||||
import { borders, effects, lengths, shadows } from '@staticcms/core/components/UI/styles';
|
||||
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import { basename, transientOptions } from '@staticcms/core/lib/util';
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
|
||||
@ -137,67 +135,43 @@ interface SortableImageProps {
|
||||
onReplace: MouseEventHandler;
|
||||
}
|
||||
|
||||
const SortableImage = SortableElement<SortableImageProps>(
|
||||
({ itemValue, getAsset, field, onRemove, onReplace }: SortableImageProps) => {
|
||||
const [assetSource, setAssetSource] = useState('');
|
||||
useEffect(() => {
|
||||
const getImage = async () => {
|
||||
const asset = (await getAsset(itemValue, field))?.toString() ?? '';
|
||||
setAssetSource(asset);
|
||||
};
|
||||
const SortableImage: FC<SortableImageProps> = ({
|
||||
itemValue,
|
||||
getAsset,
|
||||
field,
|
||||
onRemove,
|
||||
onReplace,
|
||||
}: SortableImageProps) => {
|
||||
const [assetSource, setAssetSource] = useState('');
|
||||
useEffect(() => {
|
||||
const getImage = async () => {
|
||||
const asset = (await getAsset(itemValue, field))?.toString() ?? '';
|
||||
setAssetSource(asset);
|
||||
};
|
||||
|
||||
getImage();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemValue]);
|
||||
getImage();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemValue]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ImageWrapper key="image-wrapper" $sortable>
|
||||
<Image key="image" src={assetSource} />
|
||||
</ImageWrapper>
|
||||
<SortableImageButtons
|
||||
key="image-buttons"
|
||||
onRemove={onRemove}
|
||||
onReplace={onReplace}
|
||||
></SortableImageButtons>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<ImageWrapper key="image-wrapper" $sortable>
|
||||
<Image key="image" src={assetSource} />
|
||||
</ImageWrapper>
|
||||
<SortableImageButtons
|
||||
key="image-buttons"
|
||||
onRemove={onRemove}
|
||||
onReplace={onReplace}
|
||||
></SortableImageButtons>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledSortableMultiImageWrapper = styled('div')`
|
||||
const StyledMultiImageWrapper = styled('div')`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
interface SortableMultiImageWrapperProps {
|
||||
items: string[];
|
||||
getAsset: GetAssetFunction<FileOrImageField>;
|
||||
field: FileOrImageField;
|
||||
onRemoveOne: (index: number) => MouseEventHandler;
|
||||
onReplaceOne: (index: number) => MouseEventHandler;
|
||||
}
|
||||
|
||||
const SortableMultiImageWrapper = SortableContainer<SortableMultiImageWrapperProps>(
|
||||
({ items, getAsset, field, onRemoveOne, onReplaceOne }: SortableMultiImageWrapperProps) => {
|
||||
return (
|
||||
<StyledSortableMultiImageWrapper key="multi-image-wrapper">
|
||||
{items.map((itemValue, index) => (
|
||||
<SortableImage
|
||||
key={`item-${itemValue}`}
|
||||
index={index}
|
||||
itemValue={itemValue}
|
||||
getAsset={getAsset}
|
||||
field={field}
|
||||
onRemove={onRemoveOne(index)}
|
||||
onReplace={onReplaceOne(index)}
|
||||
/>
|
||||
))}
|
||||
</StyledSortableMultiImageWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const FileLink = styled('a')`
|
||||
margin-bottom: 20px;
|
||||
font-weight: normal;
|
||||
@ -247,7 +221,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
hasErrors,
|
||||
t,
|
||||
}) => {
|
||||
const controlID = useMemo(() => uuid(), []);
|
||||
const controlID = useUUID();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||
|
||||
@ -345,15 +319,16 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
[config, controlID, field, openMediaLibrary, internalValue],
|
||||
);
|
||||
|
||||
const onSortEnd = useCallback(
|
||||
({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||
if (Array.isArray(internalValue)) {
|
||||
const newValue = arrayMoveImmutable(internalValue, oldIndex, newIndex);
|
||||
handleOnChange(newValue);
|
||||
}
|
||||
},
|
||||
[handleOnChange, internalValue],
|
||||
);
|
||||
// TODO Readd when multiple uploads is supported
|
||||
// const onSortEnd = useCallback(
|
||||
// ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||
// if (Array.isArray(internalValue)) {
|
||||
// const newValue = arrayMoveImmutable(internalValue, oldIndex, newIndex);
|
||||
// handleOnChange(newValue);
|
||||
// }
|
||||
// },
|
||||
// [handleOnChange, internalValue],
|
||||
// );
|
||||
|
||||
const renderFileLink = useCallback((link: string | undefined | null) => {
|
||||
const size = MAX_DISPLAY_LENGTH;
|
||||
@ -393,18 +368,18 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
|
||||
if (isMultiple(internalValue)) {
|
||||
return (
|
||||
<SortableMultiImageWrapper
|
||||
key="mulitple-image-wrapper"
|
||||
items={internalValue}
|
||||
onSortEnd={onSortEnd}
|
||||
onRemoveOne={onRemoveOne}
|
||||
onReplaceOne={onReplaceOne}
|
||||
distance={4}
|
||||
getAsset={getAsset}
|
||||
field={field}
|
||||
axis="xy"
|
||||
lockToContainerEdges={true}
|
||||
></SortableMultiImageWrapper>
|
||||
<StyledMultiImageWrapper key="multi-image-wrapper">
|
||||
{internalValue.map((itemValue, index) => (
|
||||
<SortableImage
|
||||
key={`item-${itemValue}`}
|
||||
itemValue={itemValue}
|
||||
getAsset={getAsset}
|
||||
field={field}
|
||||
onRemove={onRemoveOne(index)}
|
||||
onReplace={onReplaceOne(index)}
|
||||
/>
|
||||
))}
|
||||
</StyledMultiImageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -428,16 +403,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
}
|
||||
|
||||
return <FileLinks key="single-file-links">{renderFileLink(internalValue)}</FileLinks>;
|
||||
}, [
|
||||
assetSource,
|
||||
field,
|
||||
getAsset,
|
||||
internalValue,
|
||||
onRemoveOne,
|
||||
onReplaceOne,
|
||||
onSortEnd,
|
||||
renderFileLink,
|
||||
]);
|
||||
}, [assetSource, field, getAsset, internalValue, onRemoveOne, onReplaceOne, renderFileLink]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
const subject = forImage ? 'image' : 'file';
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { DndContext } from '@dnd-kit/core';
|
||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { SortableContainer } from 'react-sortable-hoc';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import FieldLabel from '@staticcms/core/components/UI/FieldLabel';
|
||||
@ -12,10 +14,15 @@ import transientOptions from '@staticcms/core/lib/util/transientOptions';
|
||||
import ListItem from './ListItem';
|
||||
import { resolveFieldKeyType, TYPES_KEY } from './typedListHelpers';
|
||||
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import type {
|
||||
Entry,
|
||||
Field,
|
||||
FieldsErrors,
|
||||
I18nSettings,
|
||||
ListField,
|
||||
ObjectValue,
|
||||
UnknownField,
|
||||
ValueOrNestedValue,
|
||||
WidgetControlProps,
|
||||
} from '@staticcms/core/interface';
|
||||
@ -51,17 +58,77 @@ const StyledSortableList = styled(
|
||||
`,
|
||||
);
|
||||
|
||||
interface SortableListProps {
|
||||
items: ObjectValue[];
|
||||
collapsed: boolean;
|
||||
renderItem: (item: ObjectValue, index: number) => JSX.Element;
|
||||
interface SortableItemProps {
|
||||
id: string;
|
||||
item: ObjectValue;
|
||||
index: number;
|
||||
valueType: ListValueType;
|
||||
handleRemove: (index: number, event: MouseEvent) => void;
|
||||
entry: Entry<ObjectValue>;
|
||||
field: ListField;
|
||||
fieldsErrors: FieldsErrors;
|
||||
submitted: boolean;
|
||||
isFieldDuplicate: ((field: Field<UnknownField>) => boolean) | undefined;
|
||||
isFieldHidden: ((field: Field<UnknownField>) => boolean) | undefined;
|
||||
locale: string | undefined;
|
||||
path: string;
|
||||
value: Record<string, ObjectValue>;
|
||||
i18n: I18nSettings | undefined;
|
||||
}
|
||||
|
||||
const SortableList = SortableContainer<SortableListProps>(
|
||||
({ items, collapsed, renderItem }: SortableListProps) => {
|
||||
return <StyledSortableList $collapsed={collapsed}>{items.map(renderItem)}</StyledSortableList>;
|
||||
},
|
||||
);
|
||||
const SortableItem: FC<SortableItemProps> = ({
|
||||
id,
|
||||
item,
|
||||
index,
|
||||
valueType,
|
||||
handleRemove,
|
||||
entry,
|
||||
field,
|
||||
fieldsErrors,
|
||||
submitted,
|
||||
isFieldDuplicate,
|
||||
isFieldHidden,
|
||||
locale,
|
||||
path,
|
||||
i18n,
|
||||
}) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
if (valueType === null) {
|
||||
return <div key={id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes}>
|
||||
<ListItem
|
||||
index={index}
|
||||
id={id}
|
||||
key={`sortable-item-${id}`}
|
||||
valueType={valueType}
|
||||
handleRemove={handleRemove}
|
||||
data-testid={`object-control-${index}`}
|
||||
entry={entry}
|
||||
field={field}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isFieldDuplicate={isFieldDuplicate}
|
||||
isFieldHidden={isFieldHidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item as Record<string, ObjectValue>}
|
||||
i18n={i18n}
|
||||
listeners={listeners}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export enum ListValueType {
|
||||
MULTIPLE,
|
||||
@ -200,8 +267,15 @@ const ListControl: FC<WidgetControlProps<ObjectValue[], ListField>> = ({
|
||||
[collapsed],
|
||||
);
|
||||
|
||||
const onSortEnd = useCallback(
|
||||
({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||
const handleDragEnd = useCallback(
|
||||
({ active, over }: DragEndEvent) => {
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldIndex = keys.indexOf(active.id as string);
|
||||
const newIndex = keys.indexOf(over.id as string);
|
||||
|
||||
// Update value
|
||||
setKeys(arrayMoveImmutable(keys, oldIndex, newIndex));
|
||||
onChange(arrayMoveImmutable(internalValue, oldIndex, newIndex));
|
||||
@ -209,49 +283,6 @@ const ListControl: FC<WidgetControlProps<ObjectValue[], ListField>> = ({
|
||||
[onChange, internalValue, keys],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
(item: ObjectValue, index: number) => {
|
||||
const key = keys[index];
|
||||
if (valueType === null) {
|
||||
return <div key={key} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
index={index}
|
||||
key={key}
|
||||
valueType={valueType}
|
||||
handleRemove={handleRemove}
|
||||
data-testid={`object-control-${index}`}
|
||||
entry={entry}
|
||||
field={field}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isFieldDuplicate={isFieldDuplicate}
|
||||
isFieldHidden={isFieldHidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item as Record<string, ObjectValue>}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
keys,
|
||||
valueType,
|
||||
handleRemove,
|
||||
entry,
|
||||
field,
|
||||
fieldsErrors,
|
||||
submitted,
|
||||
isFieldDuplicate,
|
||||
isFieldHidden,
|
||||
locale,
|
||||
path,
|
||||
i18n,
|
||||
],
|
||||
);
|
||||
|
||||
if (valueType === null) {
|
||||
return null;
|
||||
}
|
||||
@ -277,15 +308,33 @@ const ListControl: FC<WidgetControlProps<ObjectValue[], ListField>> = ({
|
||||
t={t}
|
||||
/>
|
||||
{internalValue.length > 0 ? (
|
||||
<SortableList
|
||||
key="sortable-list"
|
||||
collapsed={collapsed}
|
||||
items={internalValue}
|
||||
renderItem={renderItem}
|
||||
onSortEnd={onSortEnd}
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
/>
|
||||
<DndContext key="dnd-context" onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={keys}>
|
||||
<StyledSortableList $collapsed={collapsed}>
|
||||
{internalValue.map((item, index) => (
|
||||
<SortableItem
|
||||
index={index}
|
||||
key={keys[index]}
|
||||
id={keys[index]}
|
||||
item={item}
|
||||
valueType={valueType}
|
||||
handleRemove={handleRemove}
|
||||
data-testid={`object-control-${index}`}
|
||||
entry={entry}
|
||||
field={field}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isFieldDuplicate={isFieldDuplicate}
|
||||
isFieldHidden={isFieldHidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item as Record<string, ObjectValue>}
|
||||
i18n={i18n}
|
||||
/>
|
||||
))}
|
||||
</StyledSortableList>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : null}
|
||||
<Outline key="outline" hasLabel hasError={hasErrors} />
|
||||
</StyledListWrapper>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import partial from 'lodash/partial';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||
|
||||
import EditorControl from '@staticcms/core/components/Editor/EditorControlPane/EditorControl';
|
||||
import ListItemTopBar from '@staticcms/core/components/UI/ListItemTopBar';
|
||||
@ -15,6 +14,7 @@ import {
|
||||
import { ListValueType } from './ListControl';
|
||||
import { getTypedFieldForValue } from './typedListHelpers';
|
||||
|
||||
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import type {
|
||||
Entry,
|
||||
EntryData,
|
||||
@ -29,8 +29,6 @@ const StyledListItem = styled('div')`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const SortableStyledListItem = SortableElement<{ children: JSX.Element }>(StyledListItem);
|
||||
|
||||
const StyledListItemTopBar = styled(ListItemTopBar)`
|
||||
background-color: ${colors.textFieldBorder};
|
||||
`;
|
||||
@ -95,10 +93,13 @@ interface ListItemProps
|
||||
> {
|
||||
valueType: ListValueType;
|
||||
index: number;
|
||||
id: string;
|
||||
listeners: SyntheticListenerMap | undefined;
|
||||
handleRemove: (index: number, event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
const ListItem: FC<ListItemProps> = ({
|
||||
id,
|
||||
index,
|
||||
entry,
|
||||
field,
|
||||
@ -112,6 +113,7 @@ const ListItem: FC<ListItemProps> = ({
|
||||
handleRemove,
|
||||
value,
|
||||
i18n,
|
||||
listeners,
|
||||
}) => {
|
||||
const [objectLabel, objectField] = useMemo((): [string, ListField | ObjectField] => {
|
||||
const childObjectField: ObjectField = {
|
||||
@ -185,21 +187,21 @@ const ListItem: FC<ListItemProps> = ({
|
||||
const isHidden = isFieldHidden && isFieldHidden(field);
|
||||
|
||||
return (
|
||||
<SortableStyledListItem key="sortable-list-item" index={index}>
|
||||
<StyledListItem key="sortable-list-item">
|
||||
<>
|
||||
<StyledListItemTopBar
|
||||
key="list-item-top-bar"
|
||||
collapsed={collapsed}
|
||||
onCollapseToggle={handleCollapseToggle}
|
||||
onRemove={partial(handleRemove, index)}
|
||||
dragHandleHOC={SortableHandle}
|
||||
data-testid={`styled-list-item-top-bar-${index}`}
|
||||
data-testid={`styled-list-item-top-bar-${id}`}
|
||||
title={objectLabel}
|
||||
isVariableTypesList={valueType === ListValueType.MIXED}
|
||||
listeners={listeners}
|
||||
/>
|
||||
<StyledObjectFieldWrapper $collapsed={collapsed}>
|
||||
<EditorControl
|
||||
key={index}
|
||||
key={`control-${id}`}
|
||||
field={objectField}
|
||||
value={value}
|
||||
fieldsErrors={fieldsErrors}
|
||||
@ -216,7 +218,7 @@ const ListItem: FC<ListItemProps> = ({
|
||||
</StyledObjectFieldWrapper>
|
||||
<Outline key="outline" />
|
||||
</>
|
||||
</SortableStyledListItem>
|
||||
</StyledListItem>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,9 +2,9 @@ import { styled } from '@mui/material/styles';
|
||||
import { findNodePath, setNodes } from '@udecode/plate';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Frame from 'react-frame-component';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import Outline from '@staticcms/core/components/UI/Outline';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||
import CodeBlockFrame from './CodeBlockFrame';
|
||||
|
||||
@ -49,7 +49,7 @@ const CodeBlockElement: FC<PlateRenderElementProps<MdValue, MdCodeBlockElement>>
|
||||
const [codeHasFocus, setCodeHasFocus] = useState(false);
|
||||
|
||||
const { attributes, nodeProps, element, editor, children } = props;
|
||||
const id = useMemo(() => uuid(), []);
|
||||
const id = useUUID();
|
||||
|
||||
const lang = ('lang' in element ? element.lang : '') as string | undefined;
|
||||
const code = ('code' in element ? element.code ?? '' : '') as string;
|
||||
|
@ -990,7 +990,7 @@
|
||||
"@babel/helper-validator-option" "^7.18.6"
|
||||
"@babel/plugin-transform-typescript" "^7.18.6"
|
||||
|
||||
"@babel/runtime@7.20.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.0", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@7.20.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.20.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3"
|
||||
integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==
|
||||
@ -1319,6 +1319,37 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@dnd-kit/accessibility@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c"
|
||||
integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@6.0.5":
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.5.tgz#5670ad0dcc83cd51dbf2fa8c6a5c8af4ac0c1989"
|
||||
integrity sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.0.0"
|
||||
"@dnd-kit/utilities" "^3.2.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/sortable@7.0.1":
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-7.0.1.tgz#99c6012bbab4d8bb726c0eef7b921a338c404fdb"
|
||||
integrity sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@3.2.0", "@dnd-kit/utilities@^3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.0.tgz#b3e956ea63a1347c9d0e1316b037ddcc6140acda"
|
||||
integrity sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@emoji-mart/data@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.0.8.tgz#07f9603878b9a813ba16a6ebbabd8515f3d1b91d"
|
||||
@ -6731,7 +6762,7 @@ interpret@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
|
||||
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
|
||||
|
||||
invariant@^2.2.2, invariant@^2.2.4:
|
||||
invariant@^2.2.2:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||
@ -9270,7 +9301,7 @@ prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
prop-types@15.8.1, prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@ -9517,15 +9548,6 @@ react-scroll-sync@0.11.0:
|
||||
dependencies:
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react-sortable-hoc@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7"
|
||||
integrity sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
invariant "^2.2.4"
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react-svg-core@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-svg-core/-/react-svg-core-3.0.3.tgz#5d856efeaa4d089b0afeebe885b20b8c9500d162"
|
||||
|
Loading…
x
Reference in New Issue
Block a user