mirror of
https://github.com/strapi/strapi.git
synced 2025-12-05 03:21:22 +00:00
Merge pull request #11679 from strapi/configure/edit-view
Configure/edit view
This commit is contained in:
commit
f74c9ddf20
@ -54,7 +54,9 @@ const CustomDragLayer = () => {
|
||||
<LayoutDndProvider>
|
||||
<div style={layerStyles}>
|
||||
<div style={getItemStyles(initialOffset, currentOffset, mouseOffset)} className="col-md-2">
|
||||
{itemType === ItemTypes.FIELD && <CardPreview labelField={item.labelField} />}
|
||||
{[ItemTypes.EDIT_RELATION, ItemTypes.EDIT_FIELD].includes(itemType) && (
|
||||
<CardPreview labelField={item.labelField} />
|
||||
)}
|
||||
{itemType === ItemTypes.COMPONENT && (
|
||||
<RepeatableComponentPreview displayedValue={item.displayedValue} />
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
import omit from 'lodash/omit';
|
||||
import take from 'lodash/take';
|
||||
@ -36,6 +37,7 @@ function Inputs({
|
||||
value,
|
||||
}) {
|
||||
const { fields } = useLibrary();
|
||||
const { formatMessage } = useIntl();
|
||||
const { contentType: currentContentTypeLayout } = useContentTypeLayout();
|
||||
|
||||
const disabled = useMemo(() => !get(metadatas, 'editable', true), [metadatas]);
|
||||
@ -187,10 +189,14 @@ function Inputs({
|
||||
<SelectWrapper
|
||||
{...metadatas}
|
||||
{...fieldSchema}
|
||||
description={{
|
||||
id: metadatas.description,
|
||||
defaultMessage: metadatas.description,
|
||||
}}
|
||||
description={
|
||||
metadatas.description
|
||||
? formatMessage({
|
||||
id: metadatas.description,
|
||||
defaultMessage: metadatas.description,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
intlLabel={{
|
||||
id: metadatas.label,
|
||||
defaultMessage: metadatas.label,
|
||||
|
||||
@ -0,0 +1,393 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDrop, useDrag } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { GridItem } from '@strapi/design-system/Grid';
|
||||
import Drag from '@strapi/icons/Drag';
|
||||
import { ItemTypes } from '../../../utils';
|
||||
import FieldButtonContent from './FieldButtonContent';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
position: relative;
|
||||
${({ isFirst, isLast, hasHorizontalPadding }) => {
|
||||
if (isFirst) {
|
||||
return `
|
||||
padding-right: 4px;
|
||||
`;
|
||||
}
|
||||
if (isLast) {
|
||||
return `
|
||||
padding-left: 4px;
|
||||
`;
|
||||
}
|
||||
|
||||
if (hasHorizontalPadding) {
|
||||
return `
|
||||
padding: 0 4px;
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}}
|
||||
${({ showRightCarret, showLeftCarret, theme }) => {
|
||||
if (showRightCarret) {
|
||||
return `
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
background-color: ${theme.colors.primary600};
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
if (showLeftCarret) {
|
||||
return `
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
background-color: ${theme.colors.primary600};
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}};
|
||||
`;
|
||||
const CustomDragIcon = styled(Drag)`
|
||||
height: ${12 / 16}rem;
|
||||
width: ${12 / 16}rem;
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
`;
|
||||
const CustomFlex = styled(Flex)`
|
||||
display: ${({ dragStart }) => (dragStart ? 'none' : 'flex')};
|
||||
opacity: ${({ isDragging, isFullSize, isHidden }) => {
|
||||
if (isDragging && !isFullSize) {
|
||||
return 0.2;
|
||||
}
|
||||
|
||||
if ((isDragging && isFullSize) || isHidden) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}};
|
||||
`;
|
||||
const DragButton = styled(Flex)`
|
||||
cursor: all-scroll;
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.neutral200};
|
||||
`;
|
||||
|
||||
const DisplayedFieldButton = ({
|
||||
attribute,
|
||||
children,
|
||||
index,
|
||||
lastIndex,
|
||||
moveItem,
|
||||
moveRow,
|
||||
name,
|
||||
onDeleteField,
|
||||
onEditField,
|
||||
rowIndex,
|
||||
size,
|
||||
}) => {
|
||||
const [dragStart, setDragStart] = useState(false);
|
||||
const isHidden = name === '_TEMP_';
|
||||
const { setIsDraggingSibling } = useLayoutDnd();
|
||||
const isFullSize = size === 12;
|
||||
|
||||
const dragRef = useRef(null);
|
||||
const dropRef = useRef(null);
|
||||
const [{ clientOffset, isOver }, drop] = useDrop({
|
||||
accept: ItemTypes.EDIT_FIELD,
|
||||
hover(item, monitor) {
|
||||
if (!dropRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We use the hover only to reorder full size items
|
||||
if (item.size !== 12) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragIndex = monitor.getItem().index;
|
||||
const hoverIndex = index;
|
||||
const dragRow = monitor.getItem().rowIndex;
|
||||
const targetRow = rowIndex;
|
||||
|
||||
// Don't replace item with themselves
|
||||
if (dragIndex === hoverIndex && dragRow === targetRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
|
||||
|
||||
// Get vertical middle
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
// Determine mouse position
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
|
||||
// Get pixels to the top
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
// Only perform the move when the mouse has crossed half of the items height
|
||||
// When dragging downwards, only move when the cursor is below 50%
|
||||
// When dragging upwards, only move when the cursor is above 50%
|
||||
|
||||
// Dragging downwards
|
||||
if (dragRow < targetRow && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dragging upwards
|
||||
if (dragRow > targetRow && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveRow(dragRow, targetRow);
|
||||
|
||||
item.rowIndex = targetRow;
|
||||
item.itemIndex = hoverIndex;
|
||||
},
|
||||
drop(item, monitor) {
|
||||
if (!dropRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragIndex = monitor.getItem().index;
|
||||
const hoverIndex = index;
|
||||
const dragRow = monitor.getItem().rowIndex;
|
||||
const targetRow = rowIndex;
|
||||
|
||||
// Don't reorder on drop for full size elements since it is already done in the hover
|
||||
if (item.size === 12) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't replace item with themselves
|
||||
if (dragIndex === hoverIndex && dragRow === targetRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
|
||||
|
||||
// Scroll window if mouse near vertical edge(100px)
|
||||
|
||||
// Horizontal Check --
|
||||
if (
|
||||
Math.abs(monitor.getClientOffset().x - hoverBoundingRect.left) >
|
||||
hoverBoundingRect.width / 1.8
|
||||
) {
|
||||
moveItem(dragIndex, hoverIndex + 1, dragRow, targetRow);
|
||||
|
||||
item.itemIndex = hoverIndex + 1;
|
||||
item.rowIndex = targetRow;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertical Check |
|
||||
|
||||
// Time to actually perform the action
|
||||
moveItem(dragIndex, hoverIndex, dragRow, targetRow);
|
||||
// Note: we're mutating the monitor item here!
|
||||
// Generally it's better to avoid mutations,
|
||||
// but it's good here for the sake of performance
|
||||
// to avoid expensive index searches.
|
||||
|
||||
item.itemIndex = hoverIndex;
|
||||
item.rowIndex = targetRow;
|
||||
},
|
||||
collect: monitor => ({
|
||||
canDrop: monitor.canDrop(),
|
||||
clientOffset: monitor.getClientOffset(),
|
||||
isOver: monitor.isOver(),
|
||||
isOverCurrent: monitor.isOver({ shallow: true }),
|
||||
itemType: monitor.getItemType(),
|
||||
}),
|
||||
});
|
||||
const [{ isDragging, getItem }, drag, dragPreview] = useDrag({
|
||||
type: ItemTypes.EDIT_FIELD,
|
||||
item: () => {
|
||||
setIsDraggingSibling(true);
|
||||
|
||||
return {
|
||||
index,
|
||||
labelField: name,
|
||||
rowIndex,
|
||||
name,
|
||||
size,
|
||||
};
|
||||
},
|
||||
canDrag() {
|
||||
// Each row of the layout has a max size of 12 (based on bootstrap grid system)
|
||||
// So in order to offer a better drop zone we add the _TEMP_ div to complete the remaining substract (12 - existing)
|
||||
// Those divs cannot be dragged
|
||||
// If we wanted to offer the ability to create new lines in the layout (which will come later)
|
||||
// We will need to add a 12 size _TEMP_ div to offer a drop target between each existing row.
|
||||
return name !== '_TEMP_';
|
||||
},
|
||||
collect: monitor => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
getItem: monitor.getItem(),
|
||||
}),
|
||||
end: () => {
|
||||
setIsDraggingSibling(false);
|
||||
},
|
||||
});
|
||||
|
||||
// Remove the default preview when the item is being dragged
|
||||
// The preview is handled by the DragLayer
|
||||
useEffect(() => {
|
||||
dragPreview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, [dragPreview]);
|
||||
|
||||
// Create the refs
|
||||
// We need 1 for the drop target
|
||||
// 1 for the drag target
|
||||
const refs = {
|
||||
dragRef: drag(dragRef),
|
||||
dropRef: drop(dropRef),
|
||||
};
|
||||
|
||||
let showLeftCarret = false;
|
||||
let showRightCarret = false;
|
||||
|
||||
if (dropRef.current && clientOffset) {
|
||||
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
|
||||
|
||||
showLeftCarret =
|
||||
isOver &&
|
||||
getItem.size !== 12 &&
|
||||
Math.abs(clientOffset.x - hoverBoundingRect.left) < hoverBoundingRect.width / 2;
|
||||
showRightCarret =
|
||||
isOver &&
|
||||
getItem.size !== 12 &&
|
||||
Math.abs(clientOffset.x - hoverBoundingRect.left) > hoverBoundingRect.width / 2;
|
||||
|
||||
if (name === '_TEMP_') {
|
||||
showLeftCarret = isOver && getItem.size !== 12;
|
||||
showRightCarret = false;
|
||||
}
|
||||
}
|
||||
|
||||
const getHeight = () => {
|
||||
if (attribute && isFullSize) {
|
||||
return `${74 / 16}rem`;
|
||||
}
|
||||
|
||||
return `${32 / 16}rem`;
|
||||
};
|
||||
|
||||
const isFirst = index === 0 && !isFullSize;
|
||||
const isLast = index === lastIndex && !isFullSize;
|
||||
const hasHorizontalPadding = index !== 0 && !isFullSize;
|
||||
|
||||
return (
|
||||
<GridItem col={size}>
|
||||
<Wrapper
|
||||
ref={refs.dropRef}
|
||||
showLeftCarret={showLeftCarret}
|
||||
showRightCarret={showRightCarret}
|
||||
isFirst={isFirst}
|
||||
isLast={isLast}
|
||||
hasHorizontalPadding={hasHorizontalPadding}
|
||||
onDrag={() => {
|
||||
if (isFullSize && !dragStart) {
|
||||
setDragStart(true);
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
if (isFullSize) {
|
||||
setDragStart(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dragStart && isFullSize && (
|
||||
<Box
|
||||
// style={{ display: isDragging ? 'block' : 'none' }}
|
||||
width="100%"
|
||||
height="2px"
|
||||
background="primary600"
|
||||
/>
|
||||
)}
|
||||
<CustomFlex
|
||||
width={isFullSize && dragStart ? 0 : '100%'}
|
||||
borderColor="neutral150"
|
||||
hasRadius
|
||||
background="neutral100"
|
||||
minHeight={getHeight()}
|
||||
alignItems="stretch"
|
||||
isDragging={isDragging}
|
||||
dragStart={dragStart}
|
||||
isFullSize={isFullSize}
|
||||
isHidden={isHidden}
|
||||
>
|
||||
<DragButton
|
||||
as="button"
|
||||
type="button"
|
||||
ref={refs.dragRef}
|
||||
onClick={e => e.stopPropagation()}
|
||||
alignItems="center"
|
||||
paddingLeft={3}
|
||||
paddingRight={3}
|
||||
// Disable the keyboard navigation since the drag n drop isn't accessible with the keyboard for the moment
|
||||
tabIndex={-1}
|
||||
>
|
||||
<CustomDragIcon />
|
||||
</DragButton>
|
||||
{!isHidden && (
|
||||
<FieldButtonContent
|
||||
attribute={attribute}
|
||||
onEditField={onEditField}
|
||||
onDeleteField={onDeleteField}
|
||||
>
|
||||
{children}
|
||||
</FieldButtonContent>
|
||||
)}
|
||||
</CustomFlex>
|
||||
</Wrapper>
|
||||
</GridItem>
|
||||
);
|
||||
};
|
||||
|
||||
DisplayedFieldButton.defaultProps = {
|
||||
attribute: undefined,
|
||||
};
|
||||
|
||||
DisplayedFieldButton.propTypes = {
|
||||
attribute: PropTypes.shape({
|
||||
components: PropTypes.array,
|
||||
component: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
}),
|
||||
children: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
moveItem: PropTypes.func.isRequired,
|
||||
moveRow: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onDeleteField: PropTypes.func.isRequired,
|
||||
onEditField: PropTypes.func.isRequired,
|
||||
rowIndex: PropTypes.number.isRequired,
|
||||
lastIndex: PropTypes.number.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default DisplayedFieldButton;
|
||||
@ -1,24 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
import { Button } from '@strapi/design-system/Button';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
||||
import { Stack } from '@strapi/design-system/Stack';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { VisuallyHidden } from '@strapi/design-system/VisuallyHidden';
|
||||
import { SimpleMenu, MenuItem } from '@strapi/design-system/SimpleMenu';
|
||||
import Plus from '@strapi/icons/Plus';
|
||||
import { getTrad } from '../../../utils';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import FieldButton from './FieldButton';
|
||||
import RowsLayout from './RowsLayout';
|
||||
import LinkToCTB from './LinkToCTB';
|
||||
|
||||
const DisplayedFields = ({ editLayout, editLayoutRemainingFields, onRemoveField, onAddField }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { setEditFieldToSelect, attributes, modifiedData } = useLayoutDnd();
|
||||
|
||||
return (
|
||||
<Stack size={4}>
|
||||
@ -32,47 +27,23 @@ const DisplayedFields = ({ editLayout, editLayoutRemainingFields, onRemoveField,
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* Since the drag n drop will not be available, this text will be hidden for the moment */}
|
||||
{/* <Box>
|
||||
<Box>
|
||||
<Typography variant="pi" textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: 'containers.SettingPage.editSettings.description',
|
||||
defaultMessage: 'Drag & drop the fields to build the layout',
|
||||
})}
|
||||
</Typography>
|
||||
</Box> */}
|
||||
</Box>
|
||||
</div>
|
||||
<LinkToCTB />
|
||||
</Flex>
|
||||
<Box padding={4} hasRadius borderStyle="dashed" borderWidth="1px" borderColor="neutral300">
|
||||
<Stack size={2}>
|
||||
{editLayout.map(row => (
|
||||
<Grid gap={4} key={row.rowId}>
|
||||
{row.rowContent.map((rowItem, index) => {
|
||||
const attribute = get(attributes, [rowItem.name], {});
|
||||
const attributeLabel = get(
|
||||
modifiedData,
|
||||
['metadatas', rowItem.name, 'edit', 'label'],
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<GridItem key={rowItem.name} col={rowItem.size}>
|
||||
{rowItem.name !== '_TEMP_' ? (
|
||||
<FieldButton
|
||||
onEditField={() => setEditFieldToSelect(rowItem.name)}
|
||||
onDeleteField={() => onRemoveField(row.rowId, index)}
|
||||
attribute={attribute}
|
||||
>
|
||||
{attributeLabel || rowItem.name}
|
||||
</FieldButton>
|
||||
) : (
|
||||
<VisuallyHidden />
|
||||
)}
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
{editLayout.map((row, index) => (
|
||||
<React.Fragment key={row.rowId}>
|
||||
<RowsLayout row={row} rowIndex={index} onRemoveField={onRemoveField} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<SimpleMenu
|
||||
id="label"
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { IconButton } from '@strapi/design-system/IconButton';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import Drag from '@strapi/icons/Drag';
|
||||
import Pencil from '@strapi/icons/Pencil';
|
||||
import Trash from '@strapi/icons/Trash';
|
||||
import { useIntl } from 'react-intl';
|
||||
import ComponentFieldList from './ComponentFieldList';
|
||||
import DynamicZoneList from './DynamicZoneList';
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
|
||||
const CustomIconButton = styled(IconButton)`
|
||||
background-color: transparent;
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
`;
|
||||
const CustomDragIcon = styled(Drag)`
|
||||
height: ${12 / 16}rem;
|
||||
width: ${12 / 16}rem;
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
`;
|
||||
const CustomFlex = styled(Flex)`
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.neutral200};
|
||||
`;
|
||||
|
||||
const FieldButton = ({ attribute, onEditField, onDeleteField, children }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const getHeight = () => {
|
||||
const higherFields = ['json', 'text', 'file', 'media', 'component', 'richtext', 'dynamiczone'];
|
||||
|
||||
if (attribute && higherFields.includes(attribute.type)) {
|
||||
return `${74 / 16}rem`;
|
||||
}
|
||||
|
||||
return `${32 / 16}rem`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
width="100%"
|
||||
borderColor="neutral150"
|
||||
hasRadius
|
||||
background="neutral100"
|
||||
minHeight={getHeight()}
|
||||
alignItems="stretch"
|
||||
>
|
||||
<CustomFlex alignItems="center" paddingLeft={3} paddingRight={3}>
|
||||
<CustomDragIcon />
|
||||
</CustomFlex>
|
||||
<Box overflow="hidden" width="100%">
|
||||
<Flex paddingLeft={3} alignItems="baseline" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography fontWeight="bold" textColor="neutral800">
|
||||
{children}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Flex>
|
||||
<CustomIconButton
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('containers.ListSettingsView.modal-form.edit-label'),
|
||||
defaultMessage: `Edit {fieldName}`,
|
||||
},
|
||||
{ fieldName: children }
|
||||
)}
|
||||
onClick={onEditField}
|
||||
icon={<Pencil />}
|
||||
noBorder
|
||||
/>
|
||||
<CustomIconButton
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('app.component.table.delete'),
|
||||
defaultMessage: `Delete {target}`,
|
||||
},
|
||||
{
|
||||
target: children,
|
||||
}
|
||||
)}
|
||||
data-testid="delete-field"
|
||||
onClick={onDeleteField}
|
||||
icon={<Trash />}
|
||||
noBorder
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{attribute?.type === 'component' && (
|
||||
<ComponentFieldList componentUid={attribute.component} />
|
||||
)}
|
||||
{attribute?.type === 'dynamiczone' && <DynamicZoneList components={attribute.components} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
FieldButton.defaultProps = {
|
||||
attribute: undefined,
|
||||
};
|
||||
|
||||
FieldButton.propTypes = {
|
||||
attribute: PropTypes.shape({
|
||||
components: PropTypes.array,
|
||||
component: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
}),
|
||||
onEditField: PropTypes.func.isRequired,
|
||||
onDeleteField: PropTypes.func.isRequired,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FieldButton;
|
||||
@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { IconButton } from '@strapi/design-system/IconButton';
|
||||
import { Text } from '@strapi/design-system/Text';
|
||||
import Pencil from '@strapi/icons/Pencil';
|
||||
import Trash from '@strapi/icons/Trash';
|
||||
import { getTrad } from '../../../utils';
|
||||
import ComponentFieldList from './ComponentFieldList';
|
||||
import DynamicZoneList from './DynamicZoneList';
|
||||
|
||||
const CustomIconButton = styled(IconButton)`
|
||||
background-color: transparent;
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
`;
|
||||
|
||||
const FieldButtonContent = ({ attribute, onEditField, onDeleteField, children }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<Box overflow="hidden" width="100%">
|
||||
<Flex paddingLeft={3} alignItems="baseline" justifyContent="space-between">
|
||||
<Box>
|
||||
<Text textColor="neutral800" bold>
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex>
|
||||
<CustomIconButton
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('containers.ListSettingsView.modal-form.edit-label'),
|
||||
defaultMessage: `Edit {fieldName}`,
|
||||
},
|
||||
{ fieldName: children }
|
||||
)}
|
||||
onClick={onEditField}
|
||||
icon={<Pencil />}
|
||||
noBorder
|
||||
/>
|
||||
<CustomIconButton
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('app.component.table.delete'),
|
||||
defaultMessage: `Delete {target}`,
|
||||
},
|
||||
{
|
||||
target: children,
|
||||
}
|
||||
)}
|
||||
data-testid="delete-field"
|
||||
onClick={onDeleteField}
|
||||
icon={<Trash />}
|
||||
noBorder
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{attribute?.type === 'component' && <ComponentFieldList componentUid={attribute.component} />}
|
||||
{attribute?.type === 'dynamiczone' && <DynamicZoneList components={attribute.components} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
FieldButtonContent.defaultProps = {
|
||||
attribute: undefined,
|
||||
};
|
||||
|
||||
FieldButtonContent.propTypes = {
|
||||
attribute: PropTypes.shape({
|
||||
components: PropTypes.array,
|
||||
component: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
}),
|
||||
onEditField: PropTypes.func.isRequired,
|
||||
onDeleteField: PropTypes.func.isRequired,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FieldButtonContent;
|
||||
@ -0,0 +1,135 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDrop, useDrag } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import Drag from '@strapi/icons/Drag';
|
||||
import { ItemTypes } from '../../../utils';
|
||||
import FieldButtonContent from './FieldButtonContent';
|
||||
|
||||
const CustomDragIcon = styled(Drag)`
|
||||
height: ${12 / 16}rem;
|
||||
width: ${12 / 16}rem;
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
`;
|
||||
const CustomFlex = styled(Flex)`
|
||||
opacity: ${({ isDragging }) => (isDragging ? 0 : 1)};
|
||||
`;
|
||||
const DragButton = styled(Flex)`
|
||||
cursor: all-scroll;
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.neutral200};
|
||||
`;
|
||||
|
||||
const RelationalFieldButton = ({
|
||||
attribute,
|
||||
onEditField,
|
||||
onDeleteField,
|
||||
children,
|
||||
index,
|
||||
name,
|
||||
onMoveField,
|
||||
}) => {
|
||||
const dragButtonRef = useRef();
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: ItemTypes.EDIT_RELATION,
|
||||
hover(item) {
|
||||
if (!dragButtonRef.current) {
|
||||
return;
|
||||
}
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
|
||||
// Don't replace items with themselves
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
onMoveField(dragIndex, hoverIndex);
|
||||
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag({
|
||||
type: ItemTypes.EDIT_RELATION,
|
||||
item: () => {
|
||||
return { index, labelField: children, name };
|
||||
},
|
||||
collect: monitor => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dragPreview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, [dragPreview]);
|
||||
|
||||
drag(drop(dragButtonRef));
|
||||
|
||||
const getHeight = () => {
|
||||
const higherFields = ['json', 'text', 'file', 'media', 'component', 'richtext', 'dynamiczone'];
|
||||
|
||||
if (attribute && higherFields.includes(attribute.type)) {
|
||||
return `${74 / 16}rem`;
|
||||
}
|
||||
|
||||
return `${32 / 16}rem`;
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomFlex
|
||||
width="100%"
|
||||
borderColor="neutral150"
|
||||
hasRadius
|
||||
background="neutral100"
|
||||
minHeight={getHeight()}
|
||||
alignItems="stretch"
|
||||
isDragging={isDragging}
|
||||
>
|
||||
<DragButton
|
||||
as="button"
|
||||
type="button"
|
||||
ref={dragButtonRef}
|
||||
onClick={e => e.stopPropagation()}
|
||||
alignItems="center"
|
||||
paddingLeft={3}
|
||||
paddingRight={3}
|
||||
// Disable the keyboard navigation since the drag n drop isn't accessible with the keyboard for the moment
|
||||
tabIndex={-1}
|
||||
>
|
||||
<CustomDragIcon />
|
||||
</DragButton>
|
||||
<FieldButtonContent
|
||||
attribute={attribute}
|
||||
onEditField={onEditField}
|
||||
onDeleteField={onDeleteField}
|
||||
>
|
||||
{children}
|
||||
</FieldButtonContent>
|
||||
</CustomFlex>
|
||||
);
|
||||
};
|
||||
|
||||
RelationalFieldButton.defaultProps = {
|
||||
attribute: undefined,
|
||||
};
|
||||
|
||||
RelationalFieldButton.propTypes = {
|
||||
attribute: PropTypes.shape({
|
||||
components: PropTypes.array,
|
||||
component: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
}),
|
||||
onEditField: PropTypes.func.isRequired,
|
||||
onDeleteField: PropTypes.func.isRequired,
|
||||
children: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onMoveField: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RelationalFieldButton;
|
||||
@ -9,8 +9,8 @@ import { Stack } from '@strapi/design-system/Stack';
|
||||
import { SimpleMenu, MenuItem } from '@strapi/design-system/SimpleMenu';
|
||||
import Plus from '@strapi/icons/Plus';
|
||||
import { getTrad } from '../../../utils';
|
||||
import FieldButton from './FieldButton';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import RelationalFieldButton from './RelationalFieldButton';
|
||||
|
||||
const RelationalFields = ({
|
||||
relationsLayout,
|
||||
@ -19,7 +19,7 @@ const RelationalFields = ({
|
||||
onAddField,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { setEditFieldToSelect, modifiedData } = useLayoutDnd();
|
||||
const { setEditFieldToSelect, modifiedData, onMoveRelation } = useLayoutDnd();
|
||||
|
||||
return (
|
||||
<Stack size={4}>
|
||||
@ -32,15 +32,14 @@ const RelationalFields = ({
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* Since the drag n drop will not be available, this text will be hidden for the moment */}
|
||||
{/* <Box>
|
||||
<Box>
|
||||
<Typography variant="pi" textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: 'containers.SettingPage.editSettings.description',
|
||||
defaultMessage: 'Drag & drop the fields to build the layout',
|
||||
})}
|
||||
</Typography>
|
||||
</Box> */}
|
||||
</Box>
|
||||
</div>
|
||||
<Box padding={4} hasRadius borderStyle="dashed" borderWidth="1px" borderColor="neutral300">
|
||||
<Stack size={2}>
|
||||
@ -52,13 +51,16 @@ const RelationalFields = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<FieldButton
|
||||
<RelationalFieldButton
|
||||
onEditField={() => setEditFieldToSelect(relationName)}
|
||||
onDeleteField={() => onRemoveField(index)}
|
||||
key={relationName}
|
||||
index={index}
|
||||
name={relationName}
|
||||
onMoveField={onMoveRelation}
|
||||
>
|
||||
{relationLabel || relationName}
|
||||
</FieldButton>
|
||||
</RelationalFieldButton>
|
||||
);
|
||||
})}
|
||||
<SimpleMenu
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import get from 'lodash/get';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import DisplayedFieldButton from './DisplayedFieldButton';
|
||||
|
||||
const RowItemsLayout = ({ rowItem, onRemoveField, rowId, rowIndex, index, lastIndex }) => {
|
||||
const { setEditFieldToSelect, attributes, modifiedData, moveRow, moveItem } = useLayoutDnd();
|
||||
const attribute = get(attributes, [rowItem.name], {});
|
||||
const attributeLabel = get(modifiedData, ['metadatas', rowItem.name, 'edit', 'label'], '');
|
||||
|
||||
return (
|
||||
<DisplayedFieldButton
|
||||
onEditField={() => setEditFieldToSelect(rowItem.name)}
|
||||
onDeleteField={() => onRemoveField(rowId, index)}
|
||||
attribute={attribute}
|
||||
index={index}
|
||||
lastIndex={lastIndex}
|
||||
rowIndex={rowIndex}
|
||||
name={rowItem.name}
|
||||
size={rowItem.size}
|
||||
moveRow={moveRow}
|
||||
moveItem={moveItem}
|
||||
>
|
||||
{attributeLabel || rowItem.name}
|
||||
</DisplayedFieldButton>
|
||||
);
|
||||
};
|
||||
|
||||
RowItemsLayout.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
lastIndex: PropTypes.number.isRequired,
|
||||
onRemoveField: PropTypes.func.isRequired,
|
||||
rowId: PropTypes.number.isRequired,
|
||||
rowIndex: PropTypes.number.isRequired,
|
||||
rowItem: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RowItemsLayout;
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Grid } from '@strapi/design-system/Grid';
|
||||
import RowItemsLayout from './RowItemsLayout';
|
||||
|
||||
const RowsLayout = ({ row, onRemoveField, rowIndex }) => {
|
||||
return (
|
||||
<Grid>
|
||||
{row.rowContent.map((rowItem, index) => {
|
||||
return (
|
||||
<RowItemsLayout
|
||||
key={rowItem.name}
|
||||
rowItem={rowItem}
|
||||
index={index}
|
||||
rowId={row.rowId}
|
||||
onRemoveField={onRemoveField}
|
||||
rowIndex={rowIndex}
|
||||
lastIndex={row.rowContent.length - 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
RowsLayout.propTypes = {
|
||||
onRemoveField: PropTypes.func.isRequired,
|
||||
row: PropTypes.object.isRequired,
|
||||
rowIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default RowsLayout;
|
||||
@ -38,6 +38,7 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
const [reducerState, dispatch] = useReducer(reducer, initialState, () =>
|
||||
init(initialState, mainLayout, components)
|
||||
);
|
||||
const [isDraggingSibling, setIsDraggingSibling] = useState(false);
|
||||
const { trackUsage } = useTracking();
|
||||
const toggleNotification = useNotification();
|
||||
const { goBack } = useHistory();
|
||||
@ -151,6 +152,50 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
submitMutation.mutate(body);
|
||||
};
|
||||
|
||||
const handleMoveRelation = (fromIndex, toIndex) => {
|
||||
dispatch({
|
||||
type: 'MOVE_RELATION',
|
||||
fromIndex,
|
||||
toIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMoveField = (fromIndex, toIndex) => {
|
||||
dispatch({
|
||||
type: 'MOVE_FIELD',
|
||||
fromIndex,
|
||||
toIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const moveItem = (dragIndex, hoverIndex, dragRowIndex, hoverRowIndex) => {
|
||||
// Same row = just reorder
|
||||
if (dragRowIndex === hoverRowIndex) {
|
||||
dispatch({
|
||||
type: 'REORDER_ROW',
|
||||
dragRowIndex,
|
||||
dragIndex,
|
||||
hoverIndex,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'REORDER_DIFF_ROW',
|
||||
dragIndex,
|
||||
hoverIndex,
|
||||
dragRowIndex,
|
||||
hoverRowIndex,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const moveRow = (fromIndex, toIndex) => {
|
||||
dispatch({
|
||||
type: 'MOVE_ROW',
|
||||
fromIndex,
|
||||
toIndex,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutDndProvider
|
||||
isContentTypeView={isContentTypeView}
|
||||
@ -160,6 +205,10 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
componentLayouts={componentLayouts}
|
||||
selectedField={metaToEdit}
|
||||
fieldForm={metaForm}
|
||||
onMoveRelation={handleMoveRelation}
|
||||
onMoveField={handleMoveField}
|
||||
moveRow={moveRow}
|
||||
moveItem={moveItem}
|
||||
setEditFieldToSelect={name => {
|
||||
dispatch({
|
||||
type: 'SET_FIELD_TO_EDIT',
|
||||
@ -167,6 +216,8 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
});
|
||||
handleToggleModal();
|
||||
}}
|
||||
isDraggingSibling={isDraggingSibling}
|
||||
setIsDraggingSibling={setIsDraggingSibling}
|
||||
>
|
||||
<Main>
|
||||
<Helmet
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,8 @@ import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { ThemeProvider } from '@strapi/design-system/ThemeProvider';
|
||||
import { lightTheme } from '@strapi/design-system/themes';
|
||||
import EditSettingsView from '../index';
|
||||
@ -57,12 +59,14 @@ const makeApp = (history, layout) => {
|
||||
<QueryClientProvider client={client}>
|
||||
<IntlProvider messages={{ en: {} }} textComponent="span" locale="en">
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<EditSettingsView
|
||||
mainLayout={layout || mainLayout}
|
||||
components={components}
|
||||
isContentTypeView
|
||||
slug="api::address.address"
|
||||
/>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<EditSettingsView
|
||||
mainLayout={layout || mainLayout}
|
||||
components={components}
|
||||
isContentTypeView
|
||||
slug="api::address.address"
|
||||
/>
|
||||
</DndProvider>
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user