mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
Merge branch 'main' into fix/protect-assets-backup
This commit is contained in:
commit
a3ab33afac
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -4,6 +4,8 @@ updates:
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: '22:00'
|
||||
versioning-strategy: increase
|
||||
ignore:
|
||||
# Only allow patch as minor babel versions need to be upgraded all together
|
||||
@ -24,6 +26,8 @@ updates:
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: '22:00'
|
||||
labels:
|
||||
- 'source: dependencies'
|
||||
- 'pr: chore'
|
||||
|
17
docs/docs/core/admin/ee/intro.md
Normal file
17
docs/docs/core/admin/ee/intro.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Introduction
|
||||
slug: /admin/ee
|
||||
tags:
|
||||
- enterprise-edition
|
||||
---
|
||||
|
||||
# Admin Enterprise Edition
|
||||
|
||||
This section is an overview of all the features related to the Enterprise Edition in Admin:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
140
docs/docs/core/admin/ee/review-workflows.md
Normal file
140
docs/docs/core/admin/ee/review-workflows.md
Normal file
@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Review Workflows
|
||||
slug: /admin/ee/review-workflows
|
||||
description: Review workflow technical design
|
||||
tags:
|
||||
- review-workflows
|
||||
- implementation
|
||||
- tech design
|
||||
---
|
||||
|
||||
# Review Workflows
|
||||
|
||||
## Summary
|
||||
|
||||
The review workflow feature is only available in the Enterprise Edition.
|
||||
That is why, in part, it is completely decoupled from the code of the Community Edition.
|
||||
|
||||
The purpose of this feature is to allow users to assign a tag to the various entities of their Strapi project. This tag is called a 'stage' and is available within what we will call a workflow.
|
||||
|
||||
## Detailed backend design
|
||||
|
||||
The Review Workflow feature have been built with one main consideration, to be decoupled from the Community Edition. As so, the implementation can relate a lot to how a plugin would be built.
|
||||
|
||||
All the backend code related to Review Workflow can be found in `packages/core/admin/ee`.
|
||||
This code is separated into several elements:
|
||||
|
||||
- Two content-types
|
||||
- _strapi_workflows_: `packages/core/admin/ee/server/content-types/workflow/index.js`
|
||||
- _strapi_workflows_stages_: `packages/core/admin/ee/server/content-types/workflow-stage/index.js`
|
||||
- Two controllers
|
||||
- _workflows_: `packages/core/admin/ee/server/controllers/workflows/index.js`
|
||||
- _stages_: `packages/core/admin/ee/server/controllers/workflows/stages/index.js`
|
||||
- One middleware
|
||||
- _contentTypeMiddleware_: `packages/core/admin/ee/server/middlewares/review-workflows.js`
|
||||
- Routes
|
||||
- `packages/core/admin/ee/server/routes/index.js`
|
||||
- Four services
|
||||
- _review-workflows_: `packages/core/admin/ee/server/services/review-workflows/review-workflows.js`
|
||||
- _workflows_: `packages/core/admin/ee/server/services/review-workflows/workflows.js`
|
||||
- _stages_: `packages/core/admin/ee/server/services/review-workflows/stages.js`
|
||||
- _metrics_: `packages/core/admin/ee/server/services/review-workflows/metrics.js`
|
||||
- One decorator
|
||||
- _EntityService_ decorator: `packages/core/admin/ee/server/services/review-workflows/entity-service-decorator.js`
|
||||
- One utils file
|
||||
- _Review workflows utils_: `packages/core/admin/ee/server/utils/review-workflows.js`
|
||||
- A bootstrap and a register part
|
||||
- `packages/core/admin/ee/server/bootstrap.js`
|
||||
- `packages/core/admin/ee/server/register.js`
|
||||
|
||||
### Content types
|
||||
|
||||
#### strapi_workflows
|
||||
|
||||
This content type stores the workflow information and is responsible for holding all the information about stages and their order. In MVP, only one workflow is stored inside the Strapi database.
|
||||
|
||||
#### strapi_workflows_stages
|
||||
|
||||
This content type store the stage information such as its name.
|
||||
|
||||
### Controllers
|
||||
|
||||
#### workflows
|
||||
|
||||
Used to interact with the `strapi_workflows` content-type.
|
||||
|
||||
#### stages
|
||||
|
||||
Used to interact with the `strapi_workflows_stages` content-type.
|
||||
|
||||
### Middlewares
|
||||
|
||||
#### contentTypeMiddleware
|
||||
|
||||
In order to properly manage the options for content-type in the root level of the object, it is necessary to relocate the `reviewWorkflows` option within the `options` object located inside the content-type data. By doing so, we can ensure that all options are consistently organized and easily accessible within their respective data structures. This will also make it simpler to maintain and update the options as needed, providing a more streamlined and efficient workflow for developers working with the system. Therefore, it is recommended to move the reviewWorkflows option to its appropriate location within the options object inside the content-type data before sending it to the admin API.
|
||||
|
||||
### Routes
|
||||
|
||||
The Admin API of the Enterprise Edition includes several routes related to the Review Workflow feature. Here is a list of those routes:
|
||||
|
||||
#### GET `/review-workflows/workflows`
|
||||
|
||||
This route returns a list of all workflows.
|
||||
|
||||
#### GET `/review-workflows/workflows/:id`
|
||||
|
||||
This route returns the details of a specific workflow identified by the id parameter.
|
||||
|
||||
#### GET `/review-workflows/workflows/:workflow_id/stages`
|
||||
|
||||
This route returns a list of all stages associated with a specific workflow identified by the workflow_id parameter.
|
||||
|
||||
#### GET `/review-workflows/workflows/:workflow_id/stages/:id`
|
||||
|
||||
This route returns the details of a specific stage identified by the id parameter and associated with the workflow identified by the workflow_id parameter.
|
||||
|
||||
#### PUT `/review-workflows/workflows/:workflow_id/stages`
|
||||
|
||||
This route updates the stages associated with a specific workflow identified by the workflow_id parameter. The updated stages are passed in the request body.
|
||||
|
||||
#### PUT `/content-manager/(collection|single)-types/:model_uid/:id/stage`
|
||||
|
||||
This route updates the stage of a specific entity identified by the id parameter and belonging to a specific collection identified by the model_uid parameter. The new stage value is passed in the request body.
|
||||
|
||||
### Services
|
||||
|
||||
The Review Workflow feature of the Enterprise Edition includes several services to manipulate workflows and stages. Here is a list of those services:
|
||||
|
||||
#### review-workflows
|
||||
|
||||
This service is used during the bootstrap and register phases of Strapi. Its primary responsibility is to migrate data on entities as needed and add the stage field to the entity schemas.
|
||||
|
||||
#### workflows
|
||||
|
||||
This service is used to manipulate the workflows entities. It provides functionalities to create, retrieve, and update workflows.
|
||||
|
||||
#### stages
|
||||
|
||||
This service is used to manipulate the stages entities and to update stages on other entities. It provides functionalities to create, retrieve, update, and delete stages.
|
||||
|
||||
#### metrics
|
||||
|
||||
This is the telemetry service used to gather information on the usage of this feature. It provides information on the number of workflows and stages created, as well as the frequency of stage updates on entities.
|
||||
|
||||
### Decorators
|
||||
|
||||
#### Entity Service
|
||||
|
||||
The entity service is decorated so that entities can be linked to a default stage upon creation. This allows the entities to be automatically associated with a specific workflow stage when they are created.
|
||||
|
||||
## Alternatives
|
||||
|
||||
The Review Workflow feature is currently included as a core feature within the Strapi repository. However, there has been discussion about potentially moving it to a plugin in the future. While no decision has been made on this subject yet, it is possible that it may happen at some point in the future.
|
||||
|
||||
## Resources
|
||||
|
||||
- https://docs.strapi.io/user-docs/settings/review-workflows
|
||||
- https://docs.strapi.io/user-docs/content-type-builder/creating-new-content-type#creating-a-new-content-type
|
||||
- https://docs.strapi.io/user-docs/users-roles-permissions/configuring-administrator-roles#plugins-and-settings
|
||||
- [Content manager](/content-manager/review-workflows)
|
||||
- [Content type builder](/content-type-builder/review-workflows)
|
17
docs/docs/core/admin/intro.md
Normal file
17
docs/docs/core/admin/intro.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Introduction
|
||||
slug: /admin
|
||||
tags:
|
||||
- admin
|
||||
---
|
||||
|
||||
# Admin
|
||||
|
||||
This section is an overview of all the features related to admin:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
@ -16,17 +16,6 @@ const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
docs: [
|
||||
'index',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Admin',
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Link Strapi Design System',
|
||||
id: 'core/admin/link-strapi-design-system',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Core',
|
||||
@ -38,7 +27,26 @@ const sidebars = {
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Admin',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'core/admin/intro',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Enterprise Edition',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'core/admin/ee/intro',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Review Workflows',
|
||||
id: 'core/admin/ee/review-workflows',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Link Strapi Design System',
|
||||
|
@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDragLayer } from 'react-dnd';
|
||||
import { Box } from '@strapi/design-system';
|
||||
|
||||
function getStyle(initialOffset, currentOffset, mouseOffset) {
|
||||
if (!initialOffset || !currentOffset) {
|
||||
return { display: 'none' };
|
||||
}
|
||||
|
||||
const { x, y } = mouseOffset;
|
||||
|
||||
return {
|
||||
transform: `translate(${x}px, ${y}px)`,
|
||||
};
|
||||
}
|
||||
|
||||
export function DragLayer({ renderItem }) {
|
||||
const { itemType, isDragging, item, initialOffset, currentOffset, mouseOffset } = useDragLayer(
|
||||
(monitor) => ({
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
initialOffset: monitor.getInitialSourceClientOffset(),
|
||||
currentOffset: monitor.getSourceClientOffset(),
|
||||
isDragging: monitor.isDragging(),
|
||||
mouseOffset: monitor.getClientOffset(),
|
||||
})
|
||||
);
|
||||
|
||||
if (!isDragging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
height="100%"
|
||||
left={0}
|
||||
position="fixed"
|
||||
pointerEvents="none"
|
||||
top={0}
|
||||
zIndex={100}
|
||||
width="100%"
|
||||
>
|
||||
<Box style={getStyle(initialOffset, currentOffset, mouseOffset)}>
|
||||
{renderItem({ type: itemType, item })}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
DragLayer.propTypes = {
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './DragLayer';
|
@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { pxToRem } from '@strapi/helper-plugin';
|
||||
import { Box, Flex, Typography, IconButton } from '@strapi/design-system';
|
||||
import { Trash, Drag, CarretDown } from '@strapi/icons';
|
||||
|
||||
const DragPreviewBox = styled(Box)`
|
||||
border: 1px solid ${({ theme }) => theme.colors.neutral200};
|
||||
`;
|
||||
|
||||
const DropdownIconWrapper = styled(Box)`
|
||||
height: ${32 / 16}rem;
|
||||
width: ${32 / 16}rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
height: ${6 / 16}rem;
|
||||
width: ${11 / 16}rem;
|
||||
> path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleButton = styled.button`
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: unset;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const DragPreview = ({ displayedValue }) => {
|
||||
return (
|
||||
<DragPreviewBox
|
||||
paddingLeft={3}
|
||||
paddingRight={3}
|
||||
paddingTop={3}
|
||||
paddingBottom={3}
|
||||
hasRadius
|
||||
background="neutral0"
|
||||
width={pxToRem(300)}
|
||||
>
|
||||
<Flex justifyContent="space-between">
|
||||
<ToggleButton type="button">
|
||||
<Flex>
|
||||
<DropdownIconWrapper background="neutral200">
|
||||
<CarretDown />
|
||||
</DropdownIconWrapper>
|
||||
<Flex gap={2} paddingLeft={6} maxWidth={pxToRem(150)}>
|
||||
<Typography textColor="neutral700" ellipsis>
|
||||
{displayedValue}
|
||||
</Typography>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ToggleButton>
|
||||
<Box paddingLeft={3}>
|
||||
<Flex>
|
||||
<IconButton noBorder>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
<Box paddingLeft={2}>
|
||||
<IconButton noBorder>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</DragPreviewBox>
|
||||
);
|
||||
};
|
||||
|
||||
DragPreview.propTypes = {
|
||||
displayedValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DragPreview;
|
@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDragLayer } from 'react-dnd';
|
||||
import LayoutDndProvider from '../LayoutDndProvider';
|
||||
|
||||
import ItemTypes from '../../utils/ItemTypes';
|
||||
import CardPreview from '../../pages/ListSettingsView/components/CardPreview';
|
||||
|
||||
import ComponentPreview from './ComponentDragPreview';
|
||||
import { RelationDragPreview } from './RelationDragPreview';
|
||||
|
||||
const layerStyles = {
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
function getItemStyles(initialOffset, currentOffset, mouseOffset) {
|
||||
if (!initialOffset || !currentOffset) {
|
||||
return { display: 'none' };
|
||||
}
|
||||
|
||||
const { x, y } = mouseOffset;
|
||||
// TODO adjust
|
||||
const transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
return {
|
||||
transform,
|
||||
WebkitTransform: transform,
|
||||
};
|
||||
}
|
||||
|
||||
const CustomDragLayer = () => {
|
||||
const { itemType, isDragging, item, initialOffset, currentOffset, mouseOffset } = useDragLayer(
|
||||
(monitor) => ({
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
initialOffset: monitor.getInitialSourceClientOffset(),
|
||||
currentOffset: monitor.getSourceClientOffset(),
|
||||
isDragging: monitor.isDragging(),
|
||||
mouseOffset: monitor.getClientOffset(),
|
||||
})
|
||||
);
|
||||
|
||||
if (!isDragging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because a user may have multiple relations / dynamic zones / repeable fields in the same content type,
|
||||
* we append the fieldName for the item type to make them unique, however, we then want to extract that
|
||||
* first type to apply the correct preview.
|
||||
*/
|
||||
const [actualType] = itemType.split('_');
|
||||
|
||||
return (
|
||||
<LayoutDndProvider>
|
||||
<div style={layerStyles}>
|
||||
<div style={getItemStyles(initialOffset, currentOffset, mouseOffset)} className="col-md-2">
|
||||
{[ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(itemType) && (
|
||||
<CardPreview labelField={item.labelField} />
|
||||
)}
|
||||
{actualType === ItemTypes.COMPONENT && (
|
||||
<ComponentPreview displayedValue={item.displayedValue} />
|
||||
)}
|
||||
{actualType === ItemTypes.DYNAMIC_ZONE && (
|
||||
<ComponentPreview displayedValue={item.displayedValue} />
|
||||
)}
|
||||
{actualType === ItemTypes.RELATION && (
|
||||
<RelationDragPreview
|
||||
displayedValue={item.displayedValue}
|
||||
status={item.status}
|
||||
width={item.width}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutDndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomDragLayer;
|
@ -1,5 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
const LayoutDndContext = createContext();
|
||||
|
||||
export default LayoutDndContext;
|
@ -1,3 +1,2 @@
|
||||
export { default as ContentTypeLayoutContext } from './ContentTypeLayout';
|
||||
export { default as LayoutDndContext } from './LayoutDnd';
|
||||
export { default as WysiwygContext } from './Wysiwyg';
|
||||
|
@ -1,7 +1,6 @@
|
||||
export { default as useContentTypeLayout } from './useContentTypeLayout';
|
||||
export { default as useFetchContentTypeLayout } from './useFetchContentTypeLayout';
|
||||
export { default as useFindRedirectionLink } from './useFindRedirectionLink';
|
||||
export { default as useLayoutDnd } from './useLayoutDnd';
|
||||
export { default as usePluginsQueryParams } from './usePluginsQueryParams';
|
||||
export { default as useSyncRbac } from './useSyncRbac';
|
||||
export { default as useWysiwyg } from './useWysiwyg';
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import LayoutDndContext from '../contexts/LayoutDnd';
|
||||
|
||||
const useLayoutDnd = () => useContext(LayoutDndContext);
|
||||
|
||||
export default useLayoutDnd;
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Flex, Typography } from '@strapi/design-system';
|
||||
import { Pencil, Cross, Drag } from '@strapi/icons';
|
||||
import ellipsisCardTitle from '../utils/ellipsisCardTitle';
|
||||
import { pxToRem } from '@strapi/helper-plugin';
|
||||
|
||||
const ActionBox = styled(Flex)`
|
||||
height: ${({ theme }) => theme.spaces[7]};
|
||||
@ -14,9 +14,8 @@ const ActionBox = styled(Flex)`
|
||||
`;
|
||||
|
||||
const DragButton = styled(ActionBox)`
|
||||
padding: 0 ${({ theme }) => theme.spaces[3]};
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.neutral150};
|
||||
cursor: all-scroll;
|
||||
border-right: 1px solid
|
||||
${({ theme, isSibling }) => (isSibling ? theme.colors.neutral150 : theme.colors.primary200)};
|
||||
|
||||
svg {
|
||||
width: ${12 / 16}rem;
|
||||
@ -25,11 +24,6 @@ const DragButton = styled(ActionBox)`
|
||||
`;
|
||||
|
||||
const FieldContainer = styled(Flex)`
|
||||
display: inline-flex;
|
||||
max-height: ${32 / 16}rem;
|
||||
opacity: ${({ transparent }) => (transparent ? 0 : 1)};
|
||||
background-color: ${({ theme, isSibling }) =>
|
||||
isSibling ? theme.colors.neutral100 : theme.colors.primary100};
|
||||
border: 1px solid
|
||||
${({ theme, isSibling }) => (isSibling ? theme.colors.neutral150 : theme.colors.primary200)};
|
||||
|
||||
@ -41,54 +35,60 @@ const FieldContainer = styled(Flex)`
|
||||
fill: ${({ theme, isSibling }) => (isSibling ? undefined : theme.colors.primary600)};
|
||||
}
|
||||
}
|
||||
|
||||
${Typography} {
|
||||
color: ${({ theme, isSibling }) => (isSibling ? undefined : theme.colors.primary600)};
|
||||
}
|
||||
|
||||
${DragButton} {
|
||||
border-right: 1px solid
|
||||
${({ theme, isSibling }) => (isSibling ? theme.colors.neutral150 : theme.colors.primary200)};
|
||||
}
|
||||
`;
|
||||
|
||||
const CardPreview = ({ labelField, transparent, isSibling }) => {
|
||||
const cardEllipsisTitle = ellipsisCardTitle(labelField);
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: ${72 / 16}rem;
|
||||
`;
|
||||
|
||||
export function CardDragPreview({ labelField, transparent, isSibling }) {
|
||||
return (
|
||||
<FieldContainer
|
||||
background={isSibling ? 'neutral100' : 'primary100'}
|
||||
display="inline-flex"
|
||||
gap={3}
|
||||
hasRadius
|
||||
justifyContent="space-between"
|
||||
transparent={transparent}
|
||||
isSibling={isSibling}
|
||||
max-height={pxToRem(32)}
|
||||
maxWidth="min-content"
|
||||
opacity={transparent ? 0 : 1}
|
||||
>
|
||||
<Flex gap={3}>
|
||||
<DragButton alignItems="center">
|
||||
<DragButton alignItems="center" cursor="all-scroll" padding={3}>
|
||||
<Drag />
|
||||
</DragButton>
|
||||
<Typography fontWeight="bold">{cardEllipsisTitle}</Typography>
|
||||
|
||||
<TypographyMaxWidth
|
||||
textColor={isSibling ? undefined : 'primary600'}
|
||||
fontWeight="bold"
|
||||
ellipsis
|
||||
>
|
||||
{labelField}
|
||||
</TypographyMaxWidth>
|
||||
</Flex>
|
||||
<Flex paddingLeft={3}>
|
||||
|
||||
<Flex>
|
||||
<ActionBox alignItems="center">
|
||||
<Pencil />
|
||||
</ActionBox>
|
||||
|
||||
<ActionBox alignItems="center">
|
||||
<Cross />
|
||||
</ActionBox>
|
||||
</Flex>
|
||||
</FieldContainer>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
CardPreview.defaultProps = {
|
||||
CardDragPreview.defaultProps = {
|
||||
isSibling: false,
|
||||
transparent: false,
|
||||
};
|
||||
|
||||
CardPreview.propTypes = {
|
||||
CardDragPreview.propTypes = {
|
||||
isSibling: PropTypes.bool,
|
||||
labelField: PropTypes.string.isRequired,
|
||||
transparent: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CardPreview;
|
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { pxToRem } from '@strapi/helper-plugin';
|
||||
import { Flex, Typography, IconButton } from '@strapi/design-system';
|
||||
import { Trash, Drag, CarretDown } from '@strapi/icons';
|
||||
|
||||
const DropdownIconWrapper = styled(Flex)`
|
||||
border-radius: 50%;
|
||||
|
||||
svg {
|
||||
height: ${6 / 16}rem;
|
||||
width: ${11 / 16}rem;
|
||||
> path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: we shouldn't have to reset a whole button
|
||||
const ToggleButton = styled.button`
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: unset;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export function ComponentDragPreview({ displayedValue }) {
|
||||
return (
|
||||
<Flex
|
||||
background="neutral0"
|
||||
borderColor="neutral200"
|
||||
justifyContent="space-between"
|
||||
gap={3}
|
||||
padding={3}
|
||||
width={pxToRem(300)}
|
||||
>
|
||||
<ToggleButton type="button">
|
||||
<Flex gap={6}>
|
||||
<DropdownIconWrapper
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
background="neutral200"
|
||||
height={pxToRem(32)}
|
||||
width={pxToRem(32)}
|
||||
>
|
||||
<CarretDown />
|
||||
</DropdownIconWrapper>
|
||||
|
||||
<Flex maxWidth={pxToRem(150)}>
|
||||
<Typography textColor="neutral700" ellipsis>
|
||||
{displayedValue}
|
||||
</Typography>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ToggleButton>
|
||||
|
||||
<Flex gap={2}>
|
||||
<IconButton noBorder>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
|
||||
<IconButton noBorder>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
ComponentDragPreview.propTypes = {
|
||||
displayedValue: PropTypes.string.isRequired,
|
||||
};
|
@ -4,10 +4,13 @@ import PropTypes from 'prop-types';
|
||||
import { Box, Flex, IconButton, Typography, Status, Icon } from '@strapi/design-system';
|
||||
import { Drag, Cross } from '@strapi/icons';
|
||||
|
||||
import { getTrad } from '../../utils';
|
||||
import { PUBLICATION_STATES } from '../RelationInputDataManager/constants';
|
||||
import { ChildrenWrapper, FlexWrapper } from '../RelationInput/components/RelationItem';
|
||||
import { LinkEllipsis, DisconnectButton } from '../RelationInput';
|
||||
import { getTrad } from '../../../utils';
|
||||
import { PUBLICATION_STATES } from '../../../components/RelationInputDataManager/constants';
|
||||
import {
|
||||
ChildrenWrapper,
|
||||
FlexWrapper,
|
||||
} from '../../../components/RelationInput/components/RelationItem';
|
||||
import { LinkEllipsis, DisconnectButton } from '../../../components/RelationInput';
|
||||
|
||||
export const RelationDragPreview = ({ status, displayedValue, width }) => {
|
||||
const { formatMessage } = useIntl();
|
@ -12,7 +12,7 @@ import { useIntl } from 'react-intl';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import permissions from '../../../permissions';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import DragLayer from '../../components/DragLayer';
|
||||
import { DragLayer } from '../../../components/DragLayer';
|
||||
import ModelsContext from '../../contexts/ModelsContext';
|
||||
import CollectionTypeRecursivePath from '../CollectionTypeRecursivePath';
|
||||
import ComponentSettingsView from '../ComponentSetttingsView';
|
||||
@ -22,8 +22,45 @@ import SingleTypeRecursivePath from '../SingleTypeRecursivePath';
|
||||
import LeftMenu from './LeftMenu';
|
||||
import useContentManagerInitData from './useContentManagerInitData';
|
||||
|
||||
import ItemTypes from '../../utils/ItemTypes';
|
||||
|
||||
import { CardDragPreview } from './components/CardDragPreview';
|
||||
import { ComponentDragPreview } from './components/ComponentDragPreview';
|
||||
import { RelationDragPreview } from './components/RelationDragPreview';
|
||||
|
||||
const cmPermissions = permissions.contentManager;
|
||||
|
||||
function renderDraglayerItem({ type, item }) {
|
||||
if ([ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(type)) {
|
||||
return <CardDragPreview labelField={item.labelField} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because a user may have multiple relations / dynamic zones / repeable fields in the same content type,
|
||||
* we append the fieldName for the item type to make them unique, however, we then want to extract that
|
||||
* first type to apply the correct preview.
|
||||
*/
|
||||
const [actualType] = type.split('_');
|
||||
|
||||
switch (actualType) {
|
||||
case ItemTypes.COMPONENT:
|
||||
case ItemTypes.DYNAMIC_ZONE:
|
||||
return <ComponentDragPreview displayedValue={item.displayedValue} />;
|
||||
|
||||
case ItemTypes.RELATION:
|
||||
return (
|
||||
<RelationDragPreview
|
||||
displayedValue={item.displayedValue}
|
||||
status={item.status}
|
||||
width={item.width}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const contentTypeMatch = useRouteMatch(`/content-manager/:kind/:uid`);
|
||||
const { status, collectionTypeLinks, singleTypeLinks, models, refetchData } =
|
||||
@ -85,7 +122,7 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<Layout sideNav={<LeftMenu />}>
|
||||
<DragLayer />
|
||||
<DragLayer renderItem={renderDraglayerItem} />
|
||||
<ModelsContext.Provider value={{ refetchData }}>
|
||||
<Switch>
|
||||
<Route path="/content-manager/components/:uid/configurations/edit">
|
||||
|
@ -5,7 +5,7 @@ import { Box, Flex, Typography, Grid, GridItem } from '@strapi/design-system';
|
||||
import { Cog } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
import useLayoutDnd from '../../../hooks/useLayoutDnd';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
|
||||
const ComponentFieldList = ({ componentUid }) => {
|
||||
|
@ -7,7 +7,7 @@ import { Flex, Box, GridItem } from '@strapi/design-system';
|
||||
import { Drag } from '@strapi/icons';
|
||||
import { ItemTypes } from '../../../utils';
|
||||
import FieldButtonContent from './FieldButtonContent';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
position: relative;
|
||||
|
@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import { Box, Flex, Typography } from '@strapi/design-system';
|
||||
|
||||
import { ComponentIcon } from '../../../components/ComponentIcon';
|
||||
import useLayoutDnd from '../../../hooks/useLayoutDnd';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
|
||||
const CustomLink = styled(Flex)`
|
||||
text-decoration: none;
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import styled from 'styled-components';
|
||||
import { getTrad } from '../../../utils';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
import FieldTypeIcon from '../../../components/FieldTypeIcon';
|
||||
import ModalForm from './ModalForm';
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LayoutDndContext from '../../contexts/LayoutDnd';
|
||||
|
||||
function LayoutDndProvider({
|
||||
export const LayoutDndContext = React.createContext();
|
||||
|
||||
export function LayoutDndProvider({
|
||||
attributes,
|
||||
buttonData,
|
||||
children,
|
||||
@ -71,5 +72,3 @@ LayoutDndProvider.propTypes = {
|
||||
selectedItemName: PropTypes.string,
|
||||
setEditFieldToSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LayoutDndProvider;
|
@ -10,7 +10,7 @@ import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
import { Pencil } from '@strapi/icons';
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
import useLayoutDnd from '../../../hooks/useLayoutDnd';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
|
||||
const permissions = [{ action: 'plugin::content-type-builder.read', subject: null }];
|
||||
|
||||
|
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { GridItem, Select, Option } from '@strapi/design-system';
|
||||
import { useSelector, shallowEqual } from 'react-redux';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
import { createPossibleMainFieldsForModelsAndComponents, getInputProps } from '../utils';
|
||||
import { makeSelectModelAndComponentSchemas, selectFieldSizes } from '../../App/selectors';
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import get from 'lodash/get';
|
||||
import { useLayoutDnd } from '../../../hooks';
|
||||
import { useLayoutDnd } from '../hooks/useLayoutDnd';
|
||||
import DisplayedFieldButton from './DisplayedFieldButton';
|
||||
|
||||
const RowItemsLayout = ({ rowItem, onRemoveField, rowId, rowIndex, index, lastIndex }) => {
|
||||
|
@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { LayoutDndContext } from '../components/LayoutDndProvider';
|
||||
|
||||
export function useLayoutDnd() {
|
||||
return React.useContext(LayoutDndContext);
|
||||
}
|
@ -32,7 +32,7 @@ import reducer, { initialState } from './reducer';
|
||||
import init from './init';
|
||||
import DisplayedFields from './components/DisplayedFields';
|
||||
import ModalForm from './components/FormModal';
|
||||
import LayoutDndProvider from '../../components/LayoutDndProvider';
|
||||
import { LayoutDndProvider } from './components/LayoutDndProvider';
|
||||
import { unformatLayout } from './utils/layout';
|
||||
import putCMSettingsEV from './utils/api';
|
||||
import { selectFieldSizes } from '../App/selectors';
|
||||
|
@ -6,8 +6,8 @@ import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Flex, Box, Typography } from '@strapi/design-system';
|
||||
import { Pencil, Cross, Drag } from '@strapi/icons';
|
||||
import CardPreview from './CardPreview';
|
||||
import ellipsisCardTitle from '../utils/ellipsisCardTitle';
|
||||
|
||||
import { CardDragPreview } from '../../App/components/CardDragPreview';
|
||||
import { getTrad, ItemTypes } from '../../../utils';
|
||||
|
||||
const ActionButton = styled.button`
|
||||
@ -85,7 +85,6 @@ const DraggableCard = ({
|
||||
const dropRef = useRef(null);
|
||||
const [, forceRerenderAfterDnd] = useState(false);
|
||||
const editButtonRef = useRef();
|
||||
const cardEllipsisTitle = ellipsisCardTitle(labelField);
|
||||
|
||||
const handleClickEditRow = () => {
|
||||
if (editButtonRef.current) {
|
||||
@ -93,6 +92,7 @@ const DraggableCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: this can be simplified a lot by using the useDragAndDrop() hook
|
||||
const [, drop] = useDrop({
|
||||
accept: ItemTypes.FIELD,
|
||||
hover(item, monitor) {
|
||||
@ -176,8 +176,8 @@ const DraggableCard = ({
|
||||
|
||||
return (
|
||||
<FieldWrapper ref={refs ? refs.dropRef : null}>
|
||||
{isDragging && <CardPreview transparent labelField={cardEllipsisTitle} />}
|
||||
{!isDragging && isDraggingSibling && <CardPreview isSibling labelField={cardEllipsisTitle} />}
|
||||
{isDragging && <CardDragPreview transparent labelField={labelField} />}
|
||||
{!isDragging && isDraggingSibling && <CardDragPreview isSibling labelField={labelField} />}
|
||||
|
||||
{!isDragging && !isDraggingSibling && (
|
||||
<FieldContainer
|
||||
@ -204,7 +204,7 @@ const DraggableCard = ({
|
||||
>
|
||||
<Drag />
|
||||
</DragButton>
|
||||
<Typography fontWeight="bold">{cardEllipsisTitle}</Typography>
|
||||
<Typography fontWeight="bold">{labelField}</Typography>
|
||||
</Flex>
|
||||
<Flex paddingLeft={3}>
|
||||
<ActionButton
|
||||
|
@ -1,19 +0,0 @@
|
||||
import ellipsisCardTitle from '../utils/ellipsisCardTitle';
|
||||
|
||||
describe('CONTENT MANAGER | ListSettingsView | ellipsisCardTitle', () => {
|
||||
it('should return the title without an ellipsis if the title length < 20', () => {
|
||||
const title = 'michka';
|
||||
const result = ellipsisCardTitle(title);
|
||||
const expected = 'michka';
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return the title with an ellipsis if the title length > 20', () => {
|
||||
const title = 'michka_des_ronrons_celestes';
|
||||
const result = ellipsisCardTitle(title);
|
||||
const expected = `${title.substring(0, 20)}...`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
const ellipsisCardTitle = (title) => {
|
||||
const formatedTitle = title.length > 20 ? `${title.substring(0, 20)}...` : title;
|
||||
|
||||
return formatedTitle;
|
||||
};
|
||||
|
||||
export default ellipsisCardTitle;
|
@ -71,7 +71,6 @@ describe('field sizes service', () => {
|
||||
const { setCustomFieldInputSizes, getAllFieldSizes } = createFieldSizesService({ strapi });
|
||||
setCustomFieldInputSizes();
|
||||
const fieldSizes = getAllFieldSizes();
|
||||
console.log(fieldSizes);
|
||||
|
||||
expect(fieldSizes).not.toHaveProperty('plugin::mycustomfields.color');
|
||||
expect(fieldSizes['plugin::mycustomfields.smallColor'].default).toBe(4);
|
||||
|
@ -101,7 +101,7 @@ module.exports = function createComponentBuilder() {
|
||||
contentType
|
||||
.setUID(uid)
|
||||
.set('kind', infos.kind || typeKinds.COLLECTION_TYPE)
|
||||
.set('collectionName', nameToCollectionName(infos.pluralName))
|
||||
.set('collectionName', infos.collectionName || nameToCollectionName(infos.pluralName))
|
||||
.set('info', {
|
||||
singularName: infos.singularName,
|
||||
pluralName: infos.pluralName,
|
||||
|
@ -34,8 +34,8 @@ import pxToRem from '../utils/pxToRem';
|
||||
/**
|
||||
* @preserve
|
||||
* @typedef {Object} AutoReloadOverlayBlockerContextValue
|
||||
* @property {(config: AutoReloadOverlayBlockerConfig) => void} lockApp
|
||||
* @property {() => void} unlockApp
|
||||
* @property {(config: AutoReloadOverlayBlockerConfig) => void} lockAppWithAutoreload
|
||||
* @property {() => void} unlockAppWithAutoreload
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -111,8 +111,8 @@ const AutoReloadOverlayBlockerProvider = ({ children }) => {
|
||||
|
||||
const autoReloadValue = React.useMemo(
|
||||
() => ({
|
||||
lockApp: lockAppWithAutoreload,
|
||||
unlockApp: unlockAppWithAutoreload,
|
||||
lockAppWithAutoreload,
|
||||
unlockAppWithAutoreload,
|
||||
}),
|
||||
[lockAppWithAutoreload, unlockAppWithAutoreload]
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ const { existsSync } = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const fse = require('fs-extra');
|
||||
const { isKebabCase, importDefault } = require('@strapi/utils');
|
||||
const { isEmpty } = require('lodash/fp');
|
||||
|
||||
const DEFAULT_CONTENT_TYPE = {
|
||||
schema: {},
|
||||
@ -115,6 +116,10 @@ const loadContentTypes = async (dir) => {
|
||||
const contentTypeName = normalizeName(fd.name);
|
||||
const contentType = await loadDir(join(dir, fd.name));
|
||||
|
||||
if (isEmpty(contentType) || isEmpty(contentType.schema)) {
|
||||
throw new Error(`Could not load content type found at ${dir}`);
|
||||
}
|
||||
|
||||
contentTypes[normalizeName(contentTypeName)] = _.defaults(contentType, DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,6 @@ describe('Utils', () => {
|
||||
describe('Get Definition Attributes Count', () => {
|
||||
const createMainNode = (members = []) => {
|
||||
return factory.createInterfaceDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
factory.createIdentifier('Foo'),
|
||||
undefined,
|
||||
@ -79,7 +78,6 @@ describe('Utils', () => {
|
||||
|
||||
const createPropertyDeclaration = (name, type) => {
|
||||
return factory.createPropertyDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
factory.createIdentifier(name),
|
||||
undefined,
|
||||
|
Loading…
x
Reference in New Issue
Block a user