mirror of
https://github.com/strapi/strapi.git
synced 2025-09-06 23:28:02 +00:00
Merge branch 'develop' into fix/relations-read-rbac
This commit is contained in:
commit
ec04445427
@ -59,6 +59,10 @@ import { getDisplayName } from '../../utils/users';
|
||||
import { getData, getDataSucceeded } from '../ListViewLayoutManager';
|
||||
|
||||
import { AdminUsersFilter } from './components/AdminUsersFilter';
|
||||
import {
|
||||
AutoCloneFailureModal,
|
||||
type ProhibitedCloningField,
|
||||
} from './components/AutoCloneFailureModal';
|
||||
import { BulkActionButtons } from './components/BulkActions/Buttons';
|
||||
import { Filter } from './components/Filter';
|
||||
import { Table } from './components/Table';
|
||||
@ -597,6 +601,11 @@ const ListViewPage = ({
|
||||
});
|
||||
};
|
||||
|
||||
const [clonedEntryId, setClonedEntryId] = React.useState<Entity.ID | null>(null);
|
||||
const [prohibitedCloningFields, setProhibitedCloningFields] = React.useState<
|
||||
ProhibitedCloningField[]
|
||||
>([]);
|
||||
|
||||
const handleCloneClick =
|
||||
(id: Contracts.CollectionTypes.AutoClone.Params['sourceId']) => async () => {
|
||||
try {
|
||||
@ -613,11 +622,9 @@ const ListViewPage = ({
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
push({
|
||||
pathname: `${pathname}/create/clone/${id}`,
|
||||
state: { from: pathname, error: formatAPIError(err) },
|
||||
search: pluginsQueryParams,
|
||||
});
|
||||
const { prohibitedFields } = err.response?.data.error.details;
|
||||
setClonedEntryId(id);
|
||||
setProhibitedCloningFields(prohibitedFields);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -746,6 +753,12 @@ const ListViewPage = ({
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<AutoCloneFailureModal
|
||||
entryId={clonedEntryId}
|
||||
onClose={() => setClonedEntryId(null)}
|
||||
prohibitedFields={prohibitedCloningFields}
|
||||
pluginQueryParams={pluginsQueryParams}
|
||||
/>
|
||||
{/* Content */}
|
||||
<Table.Root
|
||||
onConfirmDelete={handleConfirmDeleteData}
|
||||
|
@ -0,0 +1,147 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Icon,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalLayout,
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { LinkButton } from '@strapi/design-system/v2';
|
||||
import { ChevronRight } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useLocation, NavLink } from 'react-router-dom';
|
||||
|
||||
import { getTranslation } from '../../../utils/translations';
|
||||
|
||||
import type { Entity } from '@strapi/types';
|
||||
|
||||
type Reason = 'relation' | 'unique';
|
||||
type ProhibitedCloningField = [string[], Reason];
|
||||
|
||||
interface AutoCloneFailureModalProps {
|
||||
onClose: () => void;
|
||||
entryId: Entity.ID | null;
|
||||
prohibitedFields: ProhibitedCloningField[];
|
||||
pluginQueryParams: string;
|
||||
}
|
||||
|
||||
const AutoCloneFailureModal = ({
|
||||
onClose,
|
||||
entryId,
|
||||
prohibitedFields,
|
||||
pluginQueryParams,
|
||||
}: AutoCloneFailureModalProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (!entryId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const editPath = `${pathname}/create/clone/${entryId}?${pluginQueryParams}`;
|
||||
|
||||
const getDefaultErrorMessage = (reason: Reason) => {
|
||||
switch (reason) {
|
||||
case 'relation':
|
||||
return 'Duplicating the relation could remove it from the original entry.';
|
||||
case 'unique':
|
||||
return 'Identical values in a unique field are not allowed';
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout onClose={onClose} labelledBy="title">
|
||||
<ModalHeader>
|
||||
<Typography variant="omega" fontWeight="bold" as="h2" id="title">
|
||||
{formatMessage({
|
||||
id: getTranslation('containers.ListPage.autoCloneModal.header'),
|
||||
defaultMessage: 'Duplicate',
|
||||
})}
|
||||
</Typography>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Typography variant="beta">
|
||||
{formatMessage({
|
||||
id: getTranslation('containers.ListPage.autoCloneModal.title'),
|
||||
defaultMessage: "This entry can't be duplicated directly.",
|
||||
})}
|
||||
</Typography>
|
||||
<Box marginTop={2}>
|
||||
<Typography textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: getTranslation('containers.ListPage.autoCloneModal.description'),
|
||||
defaultMessage:
|
||||
"A new entry will be created with the same content, but you'll have to change the following fields to save it.",
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Flex marginTop={6} gap={2} direction="column" alignItems="stretch">
|
||||
{prohibitedFields.map(([fieldPath, reason]) => (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap={2}
|
||||
alignItems="flex-start"
|
||||
borderColor="neutral200"
|
||||
hasRadius
|
||||
padding={6}
|
||||
key={fieldPath.join()}
|
||||
>
|
||||
<Flex direction="row" as="ol">
|
||||
{fieldPath.map((pathSegment, index) => (
|
||||
<Typography fontWeight="semiBold" as="li" key={index}>
|
||||
{pathSegment}
|
||||
{index !== fieldPath.length - 1 && (
|
||||
<Icon
|
||||
as={ChevronRight}
|
||||
color="neutral500"
|
||||
height={2}
|
||||
width={2}
|
||||
marginLeft={2}
|
||||
marginRight={2}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
))}
|
||||
</Flex>
|
||||
<Typography as="p" textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: getTranslation(`containers.ListPage.autoCloneModal.error.${reason}`),
|
||||
defaultMessage: getDefaultErrorMessage(reason),
|
||||
})}
|
||||
</Typography>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
startActions={
|
||||
<Button onClick={onClose} variant="tertiary">
|
||||
{formatMessage({
|
||||
id: 'cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</Button>
|
||||
}
|
||||
endActions={
|
||||
// @ts-expect-error - types are not inferred correctly through the as prop.
|
||||
<LinkButton as={NavLink} to={editPath}>
|
||||
{formatMessage({
|
||||
id: getTranslation('containers.ListPage.autoCloneModal.create'),
|
||||
defaultMessage: 'Create',
|
||||
})}
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export { AutoCloneFailureModal };
|
||||
export type { ProhibitedCloningField };
|
@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render as renderRTL, screen } from '@tests/utils';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import { AutoCloneFailureModal } from '../AutoCloneFailureModal';
|
||||
|
||||
import type { Location } from 'history';
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
let testLocation: Location = null!;
|
||||
|
||||
const render = (props: React.ComponentProps<typeof AutoCloneFailureModal>) =>
|
||||
renderRTL(<AutoCloneFailureModal {...props} />, {
|
||||
renderOptions: {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Route
|
||||
path="*"
|
||||
render={({ location }) => {
|
||||
testLocation = location;
|
||||
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
initialEntries: ['/content-manager/collection-types/api::model.model?plugins[i18n][locale]=en'],
|
||||
});
|
||||
|
||||
describe('AutoCloneFailureModal', () => {
|
||||
it('renders nothing if there is no entryId', () => {
|
||||
render({ entryId: null, onClose: jest.fn(), prohibitedFields: [], pluginQueryParams: '' });
|
||||
|
||||
expect(screen.queryByText(/duplicate/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles the modal', async () => {
|
||||
const onClose = jest.fn();
|
||||
render({ entryId: 1, onClose, prohibitedFields: [], pluginQueryParams: '' });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows the fields that prevent duplication', async () => {
|
||||
render({
|
||||
entryId: 1,
|
||||
onClose: jest.fn(),
|
||||
prohibitedFields: [
|
||||
[['dynZoneAttrName', 'Unique Component', 'componentAttrName', 'text'], 'unique'],
|
||||
[['oneToOneRelation'], 'relation'],
|
||||
],
|
||||
pluginQueryParams: 'plugins[i18n][locale]=en',
|
||||
});
|
||||
|
||||
const lists = screen.getAllByRole('list');
|
||||
expect(lists).toHaveLength(2);
|
||||
screen.getByText(/identical values in a unique field are not allowed/i);
|
||||
screen.getByText(/duplicating the relation could remove it/i);
|
||||
|
||||
const uniqueSegments = within(lists[0]).getAllByRole('listitem');
|
||||
expect(uniqueSegments).toHaveLength(4);
|
||||
within(uniqueSegments[1]).getByText('Unique Component');
|
||||
within(uniqueSegments[3]).getByText('text');
|
||||
|
||||
const relationSegments = within(lists[1]).getAllByRole('listitem');
|
||||
expect(relationSegments).toHaveLength(1);
|
||||
within(relationSegments[0]).getByText('oneToOneRelation');
|
||||
|
||||
// Links to the edit cloned entry page
|
||||
await user.click(screen.getByRole('link', { name: /create/i }));
|
||||
expect(testLocation.pathname).toBe(
|
||||
'/content-manager/collection-types/api::model.model/create/clone/1'
|
||||
);
|
||||
expect(testLocation.search).toBe('?plugins[i18n][locale]=en');
|
||||
});
|
||||
});
|
@ -779,6 +779,12 @@
|
||||
"content-manager.containers.ListPage.selectedEntriesModal.title": "Publish entries",
|
||||
"content-manager.containers.ListPage.selectedEntriesModal.selectedCount": "<b>{alreadyPublishedCount}</b> {alreadyPublishedCount, plural, =0 {entries} one {entry} other {entries}} already published. <b>{readyToPublishCount}</b> {readyToPublishCount, plural, =0 {entries} one {entry} other {entries}} ready to publish. <b>{withErrorsCount}</b> {withErrorsCount, plural, =0 {entries} one {entry} other {entries}} waiting for action.",
|
||||
"content-manager.containers.ListPage.selectedEntriesModal.publishedCount": "<b>{publishedCount}</b> {publishedCount, plural, =0 {entries} one {entry} other {entries}} published. <b>{withErrorsCount}</b> {withErrorsCount, plural, =0 {entries} one {entry} other {entries}} waiting for action.",
|
||||
"content-manager.containers.ListPage.autoCloneModal.header": "Duplicate",
|
||||
"content-manager.containers.ListPage.autoCloneModal.title": "This entry can't be duplicated directly.",
|
||||
"content-manager.containers.ListPage.autoCloneModal.description": "A new entry will be created with the same content, but you'll have to change the following fields to save it.",
|
||||
"content-manager.containers.ListPage.autoCloneModal.create": "Create",
|
||||
"content-manager.containers.ListPage.autoCloneModal.error.unique": "Identical values in a unique field are not allowed.",
|
||||
"content-manager.containers.ListPage.autoCloneModal.error.relation": "Duplicating the relation could remove it from the original entry.",
|
||||
"content-manager.containers.ListSettingsView.modal-form.edit-label": "Edit {fieldName}",
|
||||
"content-manager.containers.SettingPage.add.field": "Insert another field",
|
||||
"content-manager.containers.SettingPage.add.relational-field": "Insert another related field",
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { setCreatorFields, pipeAsync, errors } from '@strapi/utils';
|
||||
import { setCreatorFields, pipeAsync } from '@strapi/utils';
|
||||
import { getService } from '../utils';
|
||||
import { validateBulkActionInput } from './validation';
|
||||
import { hasProhibitedCloningFields, excludeNotCreatableFields } from './utils/clone';
|
||||
|
||||
const { ApplicationError } = errors;
|
||||
import { getProhibitedCloningFields, excludeNotCreatableFields } from './utils/clone';
|
||||
|
||||
export default {
|
||||
async find(ctx: any) {
|
||||
@ -192,11 +190,16 @@ export default {
|
||||
async autoClone(ctx: any) {
|
||||
const { model } = ctx.params;
|
||||
|
||||
// Trying to automatically clone the entity and model has unique or relational fields
|
||||
if (hasProhibitedCloningFields(model)) {
|
||||
throw new ApplicationError(
|
||||
// Check if the model has fields that prevent auto cloning
|
||||
const prohibitedFields = getProhibitedCloningFields(model);
|
||||
|
||||
if (prohibitedFields.length > 0) {
|
||||
return ctx.badRequest(
|
||||
'Entity could not be cloned as it has unique and/or relational fields. ' +
|
||||
'Please edit those fields manually and save to complete the cloning.'
|
||||
'Please edit those fields manually and save to complete the cloning.',
|
||||
{
|
||||
prohibitedFields,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { hasProhibitedCloningFields } from '../clone';
|
||||
import { getProhibitedCloningFields } from '../clone';
|
||||
|
||||
describe('Populate', () => {
|
||||
const fakeModels = {
|
||||
simple: {
|
||||
modelName: 'Fake simple model',
|
||||
info: {
|
||||
displayName: 'Simple',
|
||||
},
|
||||
attributes: {
|
||||
text: {
|
||||
type: 'string',
|
||||
@ -21,6 +24,9 @@ describe('Populate', () => {
|
||||
},
|
||||
component: {
|
||||
modelName: 'Fake component model',
|
||||
info: {
|
||||
displayName: 'Fake component',
|
||||
},
|
||||
attributes: {
|
||||
componentAttrName: {
|
||||
type: 'component',
|
||||
@ -30,6 +36,9 @@ describe('Populate', () => {
|
||||
},
|
||||
componentUnique: {
|
||||
modelName: 'Fake component model',
|
||||
info: {
|
||||
displayName: 'Unique Component',
|
||||
},
|
||||
attributes: {
|
||||
componentAttrName: {
|
||||
type: 'component',
|
||||
@ -55,12 +64,51 @@ describe('Populate', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
relation: {
|
||||
relations: {
|
||||
modelName: 'Fake relation oneToMany model',
|
||||
attributes: {
|
||||
relationAttrName: {
|
||||
one_way: {
|
||||
type: 'relation',
|
||||
relation: 'oneToOne',
|
||||
target: 'simple',
|
||||
},
|
||||
one_to_one: {
|
||||
type: 'relation',
|
||||
relation: 'oneToOne',
|
||||
target: 'simple',
|
||||
private: true,
|
||||
inversedBy: 'one_to_one_kitchensink',
|
||||
},
|
||||
one_to_many: {
|
||||
type: 'relation',
|
||||
relation: 'oneToMany',
|
||||
target: 'simple',
|
||||
mappedBy: 'many_to_one_kitchensink',
|
||||
},
|
||||
many_to_one: {
|
||||
type: 'relation',
|
||||
relation: 'manyToOne',
|
||||
target: 'simple',
|
||||
inversedBy: 'one_to_many_kitchensinks',
|
||||
},
|
||||
many_to_manys: {
|
||||
type: 'relation',
|
||||
relation: 'manyToMany',
|
||||
target: 'simple',
|
||||
inversedBy: 'many_to_many_kitchensinks',
|
||||
},
|
||||
many_way: {
|
||||
type: 'relation',
|
||||
relation: 'oneToMany',
|
||||
target: 'simple',
|
||||
},
|
||||
morph_to_one: {
|
||||
type: 'relation',
|
||||
relation: 'morphToOne',
|
||||
},
|
||||
morph_to_many: {
|
||||
type: 'relation',
|
||||
relation: 'morphToMany',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -86,48 +134,48 @@ describe('Populate', () => {
|
||||
});
|
||||
|
||||
test('model without unique fields', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('simple');
|
||||
expect(hasProhibitedFields).toEqual(false);
|
||||
const prohibitedFields = getProhibitedCloningFields('simple');
|
||||
expect(prohibitedFields).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('model with unique fields', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('simpleUnique');
|
||||
expect(hasProhibitedFields).toEqual(true);
|
||||
const prohibitedFields = getProhibitedCloningFields('simpleUnique');
|
||||
expect(prohibitedFields).toEqual([[['text'], 'unique']]);
|
||||
});
|
||||
|
||||
test('model with component', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('component');
|
||||
expect(hasProhibitedFields).toEqual(false);
|
||||
const prohibitedFields = getProhibitedCloningFields('component');
|
||||
expect(prohibitedFields).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('model with component & unique fields', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('componentUnique');
|
||||
expect(hasProhibitedFields).toEqual(true);
|
||||
});
|
||||
|
||||
test('model with component & unique fields', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('componentUnique');
|
||||
expect(hasProhibitedFields).toEqual(true);
|
||||
const prohibitedFields = getProhibitedCloningFields('componentUnique');
|
||||
expect(prohibitedFields).toEqual([[['componentAttrName', 'text'], 'unique']]);
|
||||
});
|
||||
|
||||
test('model with dynamic zone', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('dynZone');
|
||||
expect(hasProhibitedFields).toEqual(false);
|
||||
const prohibitedFields = getProhibitedCloningFields('dynZone');
|
||||
expect(prohibitedFields).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('model with dynamic zone', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('dynZoneUnique');
|
||||
expect(hasProhibitedFields).toEqual(true);
|
||||
test('model with unique component in dynamic zone', () => {
|
||||
const prohibitedFields = getProhibitedCloningFields('dynZoneUnique');
|
||||
expect(prohibitedFields).toEqual([
|
||||
[['dynZoneAttrName', 'Unique Component', 'componentAttrName', 'text'], 'unique'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('model with relation', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('relation');
|
||||
expect(hasProhibitedFields).toEqual(true);
|
||||
test('model with relations', () => {
|
||||
const prohibitedFields = getProhibitedCloningFields('relations');
|
||||
expect(prohibitedFields).toEqual([
|
||||
[['one_to_one'], 'relation'],
|
||||
[['one_to_many'], 'relation'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('model with media', () => {
|
||||
const hasProhibitedFields = hasProhibitedCloningFields('media');
|
||||
expect(hasProhibitedFields).toEqual(false);
|
||||
const prohibitedFields = getProhibitedCloningFields('media');
|
||||
expect(prohibitedFields).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,36 +3,75 @@ import strapiUtils from '@strapi/utils';
|
||||
|
||||
const { isVisibleAttribute } = strapiUtils.contentTypes;
|
||||
|
||||
function isProhibitedRelation(model: any, attributeName: any) {
|
||||
/**
|
||||
* Use an array of strings to represent the path to a field, so we can show breadcrumbs in the admin
|
||||
* We can't use special characters as delimiters, because the path includes display names
|
||||
* for dynamic zone components, which can contain any character.
|
||||
*/
|
||||
type ProhibitedCloningField = [string[], 'unique' | 'relation'];
|
||||
|
||||
function checkRelation(model: any, attributeName: any, path: string[]): ProhibitedCloningField[] {
|
||||
// we don't care about createdBy, updatedBy, localizations etc.
|
||||
if (!isVisibleAttribute(model, attributeName)) {
|
||||
return false;
|
||||
// Return empty array and not null so we can always spread the result
|
||||
return [];
|
||||
}
|
||||
|
||||
return true;
|
||||
/**
|
||||
* Only one-to-many and one-to-one (when they're reversed, not one-way) are dangerous,
|
||||
* because the other relations don't "steal" the relation from the entry we're cloning
|
||||
*/
|
||||
const { relation, inversedBy, mappedBy } = model.attributes[attributeName];
|
||||
|
||||
if (
|
||||
['oneToOne', 'oneToMany'].includes(relation) &&
|
||||
[mappedBy, inversedBy].some((key) => key != null)
|
||||
) {
|
||||
return [[[...path, attributeName], 'relation']];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasProhibitedCloningFields = (uid: any): boolean => {
|
||||
const getProhibitedCloningFields = (
|
||||
uid: any,
|
||||
pathPrefix: string[] = []
|
||||
): ProhibitedCloningField[] => {
|
||||
const model = strapi.getModel(uid);
|
||||
|
||||
return Object.keys(model.attributes).some((attributeName: any) => {
|
||||
const prohibitedFields = Object.keys(model.attributes).reduce<ProhibitedCloningField[]>(
|
||||
(acc, attributeName) => {
|
||||
const attribute: any = model.attributes[attributeName];
|
||||
const attributePath = [...pathPrefix, attributeName];
|
||||
|
||||
switch (attribute.type) {
|
||||
case 'relation':
|
||||
return isProhibitedRelation(model, attributeName);
|
||||
return [...acc, ...checkRelation(model, attributeName, pathPrefix)];
|
||||
case 'component':
|
||||
return hasProhibitedCloningFields(attribute.component);
|
||||
return [...acc, ...getProhibitedCloningFields(attribute.component, attributePath)];
|
||||
case 'dynamiczone':
|
||||
return (attribute.components || []).some((componentUID: any) =>
|
||||
hasProhibitedCloningFields(componentUID)
|
||||
);
|
||||
return [
|
||||
...acc,
|
||||
...(attribute.components || []).flatMap((componentUID: any) =>
|
||||
getProhibitedCloningFields(componentUID, [
|
||||
...attributePath,
|
||||
strapi.getModel(componentUID).info.displayName,
|
||||
])
|
||||
),
|
||||
];
|
||||
case 'uid':
|
||||
return true;
|
||||
return [...acc, [attributePath, 'unique']];
|
||||
default:
|
||||
return attribute?.unique ?? false;
|
||||
if (attribute?.unique) {
|
||||
return [...acc, [attributePath, 'unique']];
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return prohibitedFields;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -79,4 +118,4 @@ const excludeNotCreatableFields =
|
||||
}, body);
|
||||
};
|
||||
|
||||
export { hasProhibitedCloningFields, excludeNotCreatableFields };
|
||||
export { getProhibitedCloningFields, excludeNotCreatableFields };
|
||||
|
Loading…
x
Reference in New Issue
Block a user