mirror of
https://github.com/strapi/strapi.git
synced 2025-09-12 18:19:47 +00:00
Merge pull request #21428 from strapi/fix/cm-relations
fix: cm relations
This commit is contained in:
commit
9a4af3a3cc
@ -120,6 +120,7 @@ const EditViewPage = () => {
|
|||||||
|
|
||||||
return transformDocument(schema, components)(form);
|
return transformDocument(schema, components)(form);
|
||||||
}, [document, isCreatingDocument, isSingleType, schema, components]);
|
}, [document, isCreatingDocument, isSingleType, schema, components]);
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return <Page.Error />;
|
return <Page.Error />;
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,10 @@ import {
|
|||||||
ButtonProps,
|
ButtonProps,
|
||||||
} from '@strapi/design-system';
|
} from '@strapi/design-system';
|
||||||
import { Cross, More, WarningCircle } from '@strapi/icons';
|
import { Cross, More, WarningCircle } from '@strapi/icons';
|
||||||
|
import mapValues from 'lodash/fp/mapValues';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useMatch, useNavigate } from 'react-router-dom';
|
import { useMatch, useNavigate } from 'react-router-dom';
|
||||||
import { styled, DefaultTheme } from 'styled-components';
|
import { DefaultTheme } from 'styled-components';
|
||||||
|
|
||||||
import { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes';
|
import { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes';
|
||||||
import { SINGLE_TYPES } from '../../../constants/collections';
|
import { SINGLE_TYPES } from '../../../constants/collections';
|
||||||
@ -35,6 +36,7 @@ import { getTranslation } from '../../../utils/translations';
|
|||||||
|
|
||||||
import type { RelationsFormValue } from './FormInputs/Relations';
|
import type { RelationsFormValue } from './FormInputs/Relations';
|
||||||
import type { DocumentActionComponent } from '../../../content-manager';
|
import type { DocumentActionComponent } from '../../../content-manager';
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------------------------------
|
||||||
* Types
|
* Types
|
||||||
* -----------------------------------------------------------------------------------------------*/
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
@ -493,6 +495,22 @@ const DocumentActionModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const transformData = (data: Record<string, any>): any => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map(transformData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'object' && data !== null) {
|
||||||
|
if ('apiData' in data) {
|
||||||
|
return data.apiData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapValues(transformData)(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------------------------------
|
||||||
* DocumentActionComponents
|
* DocumentActionComponents
|
||||||
* -----------------------------------------------------------------------------------------------*/
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
@ -643,7 +661,7 @@ const PublishAction: DocumentActionComponent = ({
|
|||||||
documentId,
|
documentId,
|
||||||
params,
|
params,
|
||||||
},
|
},
|
||||||
formValues
|
transformData(formValues)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ('data' in res && collectionType !== SINGLE_TYPES) {
|
if ('data' in res && collectionType !== SINGLE_TYPES) {
|
||||||
@ -797,7 +815,7 @@ const UpdateAction: DocumentActionComponent = ({
|
|||||||
documentId: cloneMatch.params.origin!,
|
documentId: cloneMatch.params.origin!,
|
||||||
params,
|
params,
|
||||||
},
|
},
|
||||||
document
|
transformData(document)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ('data' in res) {
|
if ('data' in res) {
|
||||||
@ -823,7 +841,7 @@ const UpdateAction: DocumentActionComponent = ({
|
|||||||
documentId,
|
documentId,
|
||||||
params,
|
params,
|
||||||
},
|
},
|
||||||
document
|
transformData(document)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -841,7 +859,7 @@ const UpdateAction: DocumentActionComponent = ({
|
|||||||
model,
|
model,
|
||||||
params,
|
params,
|
||||||
},
|
},
|
||||||
document
|
transformData(document)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ('data' in res && collectionType !== SINGLE_TYPES) {
|
if ('data' in res && collectionType !== SINGLE_TYPES) {
|
||||||
|
@ -84,7 +84,13 @@ function useHandleDisconnect(fieldName: string, consumerName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addFieldRow(`${fieldName}.disconnect`, { id: relation.id });
|
addFieldRow(`${fieldName}.disconnect`, {
|
||||||
|
id: relation.id,
|
||||||
|
apiData: {
|
||||||
|
documentId: relation.documentId,
|
||||||
|
locale: relation.locale,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return handleDisconnect;
|
return handleDisconnect;
|
||||||
@ -145,14 +151,23 @@ const RelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
|
|||||||
const isMorph = props.attribute.relation.toLowerCase().includes('morph');
|
const isMorph = props.attribute.relation.toLowerCase().includes('morph');
|
||||||
const isDisabled = isMorph || disabled;
|
const isDisabled = isMorph || disabled;
|
||||||
|
|
||||||
const { id: componentId, uid } = useComponent('RelationsField', ({ uid, id }) => ({ id, uid }));
|
const { componentId, componentUID } = useComponent('RelationsField', ({ uid, id }) => ({
|
||||||
|
componentId: id,
|
||||||
|
componentUID: uid,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const isSubmitting = useForm('RelationsList', (state) => state.isSubmitting);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [isSubmitting]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We'll always have a documentId in a created entry, so we look for a componentId first.
|
* We'll always have a documentId in a created entry, so we look for a componentId first.
|
||||||
* Same with `uid` and `documentModel`.
|
* Same with `uid` and `documentModel`.
|
||||||
*/
|
*/
|
||||||
const id = componentId ? componentId.toString() : documentId;
|
const id = componentId ? componentId.toString() : documentId;
|
||||||
const model = uid ?? documentModel;
|
const model = componentUID ?? documentModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `name` prop is a complete path to the field, e.g. `field1.field2.field3`.
|
* The `name` prop is a complete path to the field, e.g. `field1.field2.field3`.
|
||||||
@ -199,6 +214,7 @@ const RelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
|
|||||||
|
|
||||||
const realServerRelationsCount =
|
const realServerRelationsCount =
|
||||||
'pagination' in data && data.pagination ? data.pagination.total : 0;
|
'pagination' in data && data.pagination ? data.pagination.total : 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Items that are already connected, but reordered would be in
|
* Items that are already connected, but reordered would be in
|
||||||
* this list, so to get an accurate figure, we remove them.
|
* this list, so to get an accurate figure, we remove them.
|
||||||
@ -259,6 +275,10 @@ const RelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
|
|||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
id: relation.id,
|
id: relation.id,
|
||||||
|
apiData: {
|
||||||
|
documentId: relation.documentId,
|
||||||
|
locale: relation.locale,
|
||||||
|
},
|
||||||
status: relation.status,
|
status: relation.status,
|
||||||
/**
|
/**
|
||||||
* If there's a last item, that's the first key we use to generate out next one.
|
* If there's a last item, that's the first key we use to generate out next one.
|
||||||
@ -268,7 +288,7 @@ const RelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
|
|||||||
[props.mainField?.name ?? 'documentId']: relation[props.mainField?.name ?? 'documentId'],
|
[props.mainField?.name ?? 'documentId']: relation[props.mainField?.name ?? 'documentId'],
|
||||||
label: getRelationLabel(relation, props.mainField),
|
label: getRelationLabel(relation, props.mainField),
|
||||||
// @ts-expect-error – targetModel does exist on the attribute, but it's not typed.
|
// @ts-expect-error – targetModel does exist on the attribute, but it's not typed.
|
||||||
href: `../${COLLECTION_TYPES}/${props.attribute.targetModel}/${relation.documentId}`,
|
href: `../${COLLECTION_TYPES}/${props.attribute.targetModel}/${relation.documentId}?${relation.locale ? `plugins[i18n][locale]=${relation.locale}` : ''}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ONE_WAY_RELATIONS.includes(props.attribute.relation)) {
|
if (ONE_WAY_RELATIONS.includes(props.attribute.relation)) {
|
||||||
@ -294,7 +314,8 @@ const RelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
|
|||||||
<StyledFlex direction="column" alignItems="start" gap={2} width="100%">
|
<StyledFlex direction="column" alignItems="start" gap={2} width="100%">
|
||||||
<RelationsInput
|
<RelationsInput
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
id={id}
|
// NOTE: we should not default to using the documentId if the component is being created (componentUID is undefined)
|
||||||
|
id={componentUID ? (componentId ? `${componentId}` : '') : documentId}
|
||||||
label={`${label} ${relationsCount > 0 ? `(${relationsCount})` : ''}`}
|
label={`${label} ${relationsCount > 0 ? `(${relationsCount})` : ''}`}
|
||||||
model={model}
|
model={model}
|
||||||
onChange={handleConnect}
|
onChange={handleConnect}
|
||||||
@ -389,7 +410,7 @@ const addLabelAndHref =
|
|||||||
// Fallback to `id` if there is no `mainField` value, which will overwrite the above `documentId` property with the exact same data.
|
// Fallback to `id` if there is no `mainField` value, which will overwrite the above `documentId` property with the exact same data.
|
||||||
[mainField?.name ?? 'documentId']: relation[mainField?.name ?? 'documentId'],
|
[mainField?.name ?? 'documentId']: relation[mainField?.name ?? 'documentId'],
|
||||||
label: getRelationLabel(relation, mainField),
|
label: getRelationLabel(relation, mainField),
|
||||||
href: `${href}/${relation.documentId}`,
|
href: `${href}/${relation.documentId}?${relation.locale ? `plugins[i18n][locale]=${relation.locale}` : ''}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -454,6 +475,7 @@ const RelationsInput = ({
|
|||||||
* individual components. Therefore we split the string and take the last item.
|
* individual components. Therefore we split the string and take the last item.
|
||||||
*/
|
*/
|
||||||
const [targetField] = name.split('.').slice(-1);
|
const [targetField] = name.split('.').slice(-1);
|
||||||
|
|
||||||
searchForTrigger({
|
searchForTrigger({
|
||||||
model,
|
model,
|
||||||
targetField,
|
targetField,
|
||||||
@ -701,9 +723,7 @@ const RelationsList = ({
|
|||||||
*/
|
*/
|
||||||
const connectedRelations = newData
|
const connectedRelations = newData
|
||||||
.reduce<Relation[]>((acc, relation, currentIndex, array) => {
|
.reduce<Relation[]>((acc, relation, currentIndex, array) => {
|
||||||
const relationOnServer = serverData.find(
|
const relationOnServer = serverData.find((oldRelation) => oldRelation.id === relation.id);
|
||||||
(oldRelation) => oldRelation.documentId === relation.documentId
|
|
||||||
);
|
|
||||||
|
|
||||||
const relationInFront = array[currentIndex + 1];
|
const relationInFront = array[currentIndex + 1];
|
||||||
|
|
||||||
@ -712,11 +732,23 @@ const RelationsList = ({
|
|||||||
? {
|
? {
|
||||||
before: relationInFront.documentId,
|
before: relationInFront.documentId,
|
||||||
locale: relationInFront.locale,
|
locale: relationInFront.locale,
|
||||||
status: relationInFront.status,
|
status:
|
||||||
|
'publishedAt' in relationInFront && relationInFront.publishedAt
|
||||||
|
? 'published'
|
||||||
|
: 'draft',
|
||||||
}
|
}
|
||||||
: { end: true };
|
: { end: true };
|
||||||
|
|
||||||
const relationWithPosition: Relation = { ...relation, position };
|
const relationWithPosition: Relation = {
|
||||||
|
...relation,
|
||||||
|
...{
|
||||||
|
apiData: {
|
||||||
|
documentId: relation.documentId,
|
||||||
|
locale: relation.locale,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return [...acc, relationWithPosition];
|
return [...acc, relationWithPosition];
|
||||||
}
|
}
|
||||||
@ -899,7 +931,7 @@ const ListItem = ({ data, index, style }: ListItemProps) => {
|
|||||||
} = data;
|
} = data;
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const { href, documentId, label, status } = relations[index];
|
const { href, id, label, status } = relations[index];
|
||||||
|
|
||||||
const [{ handlerId, isDragging, handleKeyDown }, relationRef, dropRef, dragRef, dragPreviewRef] =
|
const [{ handlerId, isDragging, handleKeyDown }, relationRef, dropRef, dragRef, dragPreviewRef] =
|
||||||
useDragAndDrop<number, Omit<RelationDragPreviewProps, 'width'>, HTMLDivElement>(
|
useDragAndDrop<number, Omit<RelationDragPreviewProps, 'width'>, HTMLDivElement>(
|
||||||
@ -910,7 +942,7 @@ const ListItem = ({ data, index, style }: ListItemProps) => {
|
|||||||
item: {
|
item: {
|
||||||
displayedValue: label,
|
displayedValue: label,
|
||||||
status,
|
status,
|
||||||
id: documentId,
|
id: id,
|
||||||
index,
|
index,
|
||||||
},
|
},
|
||||||
onMoveItem: handleMoveItem,
|
onMoveItem: handleMoveItem,
|
||||||
|
@ -67,11 +67,8 @@ const relationsApi = contentManagerApi.injectEndpoints({
|
|||||||
* Relations will always have unique IDs, so we can therefore assume
|
* Relations will always have unique IDs, so we can therefore assume
|
||||||
* that we only need to push the new items to the cache.
|
* that we only need to push the new items to the cache.
|
||||||
*/
|
*/
|
||||||
const existingIds = currentCache.results.map((item) => item.documentId);
|
|
||||||
const uniqueNewItems = newItems.results.filter(
|
currentCache.results.push(...prepareTempKeys(newItems.results, currentCache.results));
|
||||||
(item) => !existingIds.includes(item.documentId)
|
|
||||||
);
|
|
||||||
currentCache.results.push(...prepareTempKeys(uniqueNewItems, currentCache.results));
|
|
||||||
currentCache.pagination = newItems.pagination;
|
currentCache.pagination = newItems.pagination;
|
||||||
} else if (newItems.pagination.page === 1) {
|
} else if (newItems.pagination.page === 1) {
|
||||||
/**
|
/**
|
||||||
|
@ -49,17 +49,39 @@ const sanitizeMainField = (model: any, mainField: any, userAbility: any) => {
|
|||||||
return mainField;
|
return mainField;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addStatusToRelations = async (uid: UID.ContentType, relations: RelationEntity[]) => {
|
/**
|
||||||
if (!contentTypes.hasDraftAndPublish(strapi.contentTypes[uid])) {
|
*
|
||||||
|
* All relations sent to this function should have the same status or no status
|
||||||
|
*/
|
||||||
|
const addStatusToRelations = async (targetUid: UID.Schema, relations: RelationEntity[]) => {
|
||||||
|
if (!contentTypes.hasDraftAndPublish(strapi.getModel(targetUid))) {
|
||||||
return relations;
|
return relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentMetadata = getService('document-metadata');
|
const documentMetadata = getService('document-metadata');
|
||||||
const documentsAvailableStatus = await documentMetadata.getManyAvailableStatus(uid, relations);
|
|
||||||
|
if (!relations.length) {
|
||||||
|
return relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstRelation = relations[0];
|
||||||
|
|
||||||
|
const filters: any = {
|
||||||
|
documentId: { $in: relations.map((r) => r.documentId) },
|
||||||
|
// NOTE: find the "opposite" status
|
||||||
|
publishedAt: firstRelation.publishedAt !== null ? { $null: true } : { $notNull: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableStatus = await strapi.query(targetUid).findMany({
|
||||||
|
select: ['id', 'documentId', 'locale', 'updatedAt', 'createdAt', 'publishedAt'],
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
|
||||||
return relations.map((relation: RelationEntity) => {
|
return relations.map((relation: RelationEntity) => {
|
||||||
const availableStatuses = documentsAvailableStatus.filter(
|
const availableStatuses = availableStatus.filter(
|
||||||
(availableDocument: RelationEntity) => availableDocument.documentId === relation.documentId
|
(availableDocument: RelationEntity) =>
|
||||||
|
availableDocument.documentId === relation.documentId &&
|
||||||
|
(relation.locale ? availableDocument.locale === relation.locale : true)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -396,14 +418,14 @@ export default {
|
|||||||
attribute,
|
attribute,
|
||||||
targetField,
|
targetField,
|
||||||
fieldsToSelect,
|
fieldsToSelect,
|
||||||
source: {
|
status,
|
||||||
schema: { uid: sourceUid },
|
source: { schema: sourceSchema },
|
||||||
},
|
target: { schema: targetSchema },
|
||||||
target: {
|
|
||||||
schema: { uid: targetUid },
|
|
||||||
},
|
|
||||||
} = await this.extractAndValidateRequestInfo(ctx, id);
|
} = await this.extractAndValidateRequestInfo(ctx, id);
|
||||||
|
|
||||||
|
const { uid: sourceUid } = sourceSchema;
|
||||||
|
const { uid: targetUid } = targetSchema;
|
||||||
|
|
||||||
const permissionQuery = await getService('permission-checker')
|
const permissionQuery = await getService('permission-checker')
|
||||||
.create({ userAbility, model: targetUid })
|
.create({ userAbility, model: targetUid })
|
||||||
.sanitizedQuery.read({ fields: fieldsToSelect });
|
.sanitizedQuery.read({ fields: fieldsToSelect });
|
||||||
@ -424,6 +446,23 @@ export default {
|
|||||||
// Ensure response is an array
|
// Ensure response is an array
|
||||||
.then((res) => ({ results: res ? [res] : [] }));
|
.then((res) => ({ results: res ? [res] : [] }));
|
||||||
|
|
||||||
|
const filters: {
|
||||||
|
publishedAt?: Record<string, any>;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (sourceSchema?.options?.draftAndPublish) {
|
||||||
|
if (targetSchema?.options?.draftAndPublish) {
|
||||||
|
if (status === 'published') {
|
||||||
|
filters.publishedAt = { $notNull: true };
|
||||||
|
} else {
|
||||||
|
filters.publishedAt = { $null: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (targetSchema?.options?.draftAndPublish) {
|
||||||
|
// NOTE: we must return the drafts as some targets might not have a published version yet
|
||||||
|
filters.publishedAt = { $null: true };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If user does not have access to specific relations (custom conditions),
|
* If user does not have access to specific relations (custom conditions),
|
||||||
* only the ids of the relations are returned.
|
* only the ids of the relations are returned.
|
||||||
@ -434,10 +473,11 @@ export default {
|
|||||||
* The response contains the union of the two queries.
|
* The response contains the union of the two queries.
|
||||||
*/
|
*/
|
||||||
const res = await loadRelations({ id: entryId }, targetField, {
|
const res = await loadRelations({ id: entryId }, targetField, {
|
||||||
select: ['id', 'documentId', 'locale', 'publishedAt'],
|
select: ['id', 'documentId', 'locale', 'publishedAt', 'updatedAt'],
|
||||||
ordering: 'desc',
|
ordering: 'desc',
|
||||||
page: ctx.request.query.page,
|
page: ctx.request.query.page,
|
||||||
pageSize: ctx.request.query.pageSize,
|
pageSize: ctx.request.query.pageSize,
|
||||||
|
filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -458,6 +498,7 @@ export default {
|
|||||||
ordering: 'desc',
|
ordering: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: the order is very import to make sure sanitized relations are kept in priority
|
||||||
const relationsUnion = uniqBy('id', concat(sanitizedRes.results, res.results));
|
const relationsUnion = uniqBy('id', concat(sanitizedRes.results, res.results));
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
@ -7,11 +7,11 @@ import type { DocumentMetadata } from '../../../shared/contracts/collection-type
|
|||||||
import { getValidatableFieldsPopulate } from './utils/populate';
|
import { getValidatableFieldsPopulate } from './utils/populate';
|
||||||
|
|
||||||
export interface DocumentVersion {
|
export interface DocumentVersion {
|
||||||
id: number;
|
id: string | number;
|
||||||
documentId: Modules.Documents.ID;
|
documentId: Modules.Documents.ID;
|
||||||
locale: string;
|
locale?: string;
|
||||||
updatedAt: string | null | Date;
|
updatedAt?: string | null | Date;
|
||||||
publishedAt: string | null | Date;
|
publishedAt?: string | null | Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AVAILABLE_STATUS_FIELDS = [
|
const AVAILABLE_STATUS_FIELDS = [
|
||||||
@ -86,7 +86,9 @@ export default ({ strapi }: { strapi: Core.Strapi }) => ({
|
|||||||
const versionsByLocale = groupBy('locale', allVersions);
|
const versionsByLocale = groupBy('locale', allVersions);
|
||||||
|
|
||||||
// Delete the current locale
|
// Delete the current locale
|
||||||
delete versionsByLocale[version.locale];
|
if (version.locale) {
|
||||||
|
delete versionsByLocale[version.locale];
|
||||||
|
}
|
||||||
|
|
||||||
// For each locale, get the ones with the same status
|
// For each locale, get the ones with the same status
|
||||||
// There will not be a draft and a version counterpart if the content
|
// There will not be a draft and a version counterpart if the content
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import '@strapi/types';
|
import '@strapi/types';
|
||||||
|
|
||||||
import { DocumentManagerService } from 'src/services/document-manager';
|
import { DocumentManagerService } from 'src/services/document-manager';
|
||||||
|
import DocumentMetadata from 'src/services/document-metadata';
|
||||||
|
|
||||||
type Services = {
|
type Services = {
|
||||||
'document-manager': DocumentManagerService;
|
'document-manager': DocumentManagerService;
|
||||||
|
'document-metadata': typeof DocumentMetadata;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -291,7 +291,10 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Load any unidirectional relation targetting the old published entries
|
// Load any unidirectional relation targetting the old published entries
|
||||||
const relationsToSync = await unidirectionalRelations.load(uid, oldPublishedVersions);
|
const relationsToSync = await unidirectionalRelations.load(uid, {
|
||||||
|
newVersions: draftsToPublish,
|
||||||
|
oldVersions: oldPublishedVersions,
|
||||||
|
});
|
||||||
|
|
||||||
// Delete old published versions
|
// Delete old published versions
|
||||||
await async.map(oldPublishedVersions, (entry: any) => entries.delete(entry.id));
|
await async.map(oldPublishedVersions, (entry: any) => entries.delete(entry.id));
|
||||||
@ -302,7 +305,11 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Sync unidirectional relations with the new published entries
|
// Sync unidirectional relations with the new published entries
|
||||||
await unidirectionalRelations.sync(oldPublishedVersions, publishedEntries, relationsToSync);
|
await unidirectionalRelations.sync(
|
||||||
|
[...oldPublishedVersions, ...draftsToPublish],
|
||||||
|
publishedEntries,
|
||||||
|
relationsToSync
|
||||||
|
);
|
||||||
|
|
||||||
publishedEntries.forEach(emitEvent('entry.publish'));
|
publishedEntries.forEach(emitEvent('entry.publish'));
|
||||||
|
|
||||||
@ -358,7 +365,10 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Load any unidirectional relation targeting the old drafts
|
// Load any unidirectional relation targeting the old drafts
|
||||||
const relationsToSync = await unidirectionalRelations.load(uid, oldDrafts);
|
const relationsToSync = await unidirectionalRelations.load(uid, {
|
||||||
|
newVersions: versionsToDraft,
|
||||||
|
oldVersions: oldDrafts,
|
||||||
|
});
|
||||||
|
|
||||||
// Delete old drafts
|
// Delete old drafts
|
||||||
await async.map(oldDrafts, (entry: any) => entries.delete(entry.id));
|
await async.map(oldDrafts, (entry: any) => entries.delete(entry.id));
|
||||||
@ -369,7 +379,11 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Sync unidirectional relations with the new draft entries
|
// Sync unidirectional relations with the new draft entries
|
||||||
await unidirectionalRelations.sync(oldDrafts, draftEntries, relationsToSync);
|
await unidirectionalRelations.sync(
|
||||||
|
[...oldDrafts, ...versionsToDraft],
|
||||||
|
draftEntries,
|
||||||
|
relationsToSync
|
||||||
|
);
|
||||||
|
|
||||||
draftEntries.forEach(emitEvent('entry.draft-discard'));
|
draftEntries.forEach(emitEvent('entry.draft-discard'));
|
||||||
return { documentId, entries: draftEntries };
|
return { documentId, entries: draftEntries };
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
/* eslint-disable no-continue */
|
/* eslint-disable no-continue */
|
||||||
import { keyBy } from 'lodash/fp';
|
import { keyBy, omit } from 'lodash/fp';
|
||||||
|
|
||||||
import { UID, Schema } from '@strapi/types';
|
import { UID, Schema } from '@strapi/types';
|
||||||
|
|
||||||
|
interface LoadContext {
|
||||||
|
oldVersions: { id: string; locale: string }[];
|
||||||
|
newVersions: { id: string; locale: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads lingering relations that need to be updated when overriding a published or draft entry.
|
* Loads lingering relations that need to be updated when overriding a published or draft entry.
|
||||||
* This is necessary because the relations are uni-directional and the target entry is not aware of the source entry.
|
* This is necessary because the relations are uni-directional and the target entry is not aware of the source entry.
|
||||||
* This is not the case for bi-directional relations, where the target entry is also linked to the source entry.
|
* This is not the case for bi-directional relations, where the target entry is also linked to the source entry.
|
||||||
*
|
|
||||||
* @param uid The content type uid
|
|
||||||
* @param oldEntries The old entries that are being overridden
|
|
||||||
* @returns An array of relations that need to be updated with the join table reference.
|
|
||||||
*/
|
*/
|
||||||
const load = async (uid: UID.ContentType, oldEntries: { id: string; locale: string }[]) => {
|
const load = async (uid: UID.ContentType, { oldVersions, newVersions }: LoadContext) => {
|
||||||
const updates = [] as any;
|
const updates = [] as any;
|
||||||
|
|
||||||
// Iterate all components and content types to find relations that need to be updated
|
// Iterate all components and content types to find relations that need to be updated
|
||||||
@ -27,29 +28,80 @@ const load = async (uid: UID.ContentType, oldEntries: { id: string; locale: stri
|
|||||||
/**
|
/**
|
||||||
* Only consider unidirectional relations
|
* Only consider unidirectional relations
|
||||||
*/
|
*/
|
||||||
if (attribute.type !== 'relation') continue;
|
if (
|
||||||
if (attribute.target !== uid) continue;
|
attribute.type !== 'relation' ||
|
||||||
if (attribute.inversedBy || attribute.mappedBy) continue;
|
attribute.target !== uid ||
|
||||||
const joinTable = attribute.joinTable;
|
attribute.inversedBy ||
|
||||||
// TODO: joinColumn relations
|
attribute.mappedBy
|
||||||
if (!joinTable) continue;
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const { name } = joinTable.inverseJoinColumn;
|
// TODO: joinColumn relations
|
||||||
|
const joinTable = attribute.joinTable;
|
||||||
|
if (!joinTable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name: sourceColumnName } = joinTable.joinColumn;
|
||||||
|
const { name: targetColumnName } = joinTable.inverseJoinColumn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all relations that need to be updated
|
* Load all relations that need to be updated
|
||||||
*/
|
*/
|
||||||
const oldEntriesIds = oldEntries.map((entry) => entry.id);
|
// NOTE: when the model has draft and publish, we can assume relation are only draft to draft & published to published
|
||||||
const relations = await strapi.db
|
const ids = oldVersions.map((entry) => entry.id);
|
||||||
|
|
||||||
|
const oldVersionsRelations = await strapi.db
|
||||||
.getConnection()
|
.getConnection()
|
||||||
.select('*')
|
.select('*')
|
||||||
.from(joinTable.name)
|
.from(joinTable.name)
|
||||||
.whereIn(name, oldEntriesIds)
|
.whereIn(targetColumnName, ids)
|
||||||
.transacting(trx);
|
.transacting(trx);
|
||||||
|
|
||||||
if (relations.length === 0) continue;
|
if (oldVersionsRelations.length > 0) {
|
||||||
|
updates.push({ joinTable, relations: oldVersionsRelations });
|
||||||
|
}
|
||||||
|
|
||||||
updates.push({ joinTable, relations });
|
/**
|
||||||
|
* if publishing
|
||||||
|
* if published version exists
|
||||||
|
* updated published versions links
|
||||||
|
* else
|
||||||
|
* create link to newly published version
|
||||||
|
*
|
||||||
|
* if discarding
|
||||||
|
* if published version link exists & not draft version link
|
||||||
|
* create link to new draft version
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!model.options?.draftAndPublish) {
|
||||||
|
const ids = newVersions.map((entry) => entry.id);
|
||||||
|
|
||||||
|
const newVersionsRelations = await strapi.db
|
||||||
|
.getConnection()
|
||||||
|
.select('*')
|
||||||
|
.from(joinTable.name)
|
||||||
|
.whereIn(targetColumnName, ids)
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
if (newVersionsRelations.length > 0) {
|
||||||
|
// when publishing a draft that doesn't have a published version yet,
|
||||||
|
// copy the links to the draft over to the published version
|
||||||
|
// when discarding a published version, if no drafts exists
|
||||||
|
const discardToAdd = newVersionsRelations
|
||||||
|
.filter((relation) => {
|
||||||
|
const matchingOldVerion = oldVersionsRelations.find((oldRelation) => {
|
||||||
|
return oldRelation[sourceColumnName] === relation[sourceColumnName];
|
||||||
|
});
|
||||||
|
|
||||||
|
return !matchingOldVerion;
|
||||||
|
})
|
||||||
|
.map(omit('id'));
|
||||||
|
|
||||||
|
updates.push({ joinTable, relations: discardToAdd });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -89,8 +141,9 @@ const sync = async (
|
|||||||
// Iterate old relations that are deleted and insert the new ones
|
// Iterate old relations that are deleted and insert the new ones
|
||||||
for (const { joinTable, relations } of oldRelations) {
|
for (const { joinTable, relations } of oldRelations) {
|
||||||
// Update old ids with the new ones
|
// Update old ids with the new ones
|
||||||
|
const column = joinTable.inverseJoinColumn.name;
|
||||||
|
|
||||||
const newRelations = relations.map((relation) => {
|
const newRelations = relations.map((relation) => {
|
||||||
const column = joinTable.inverseJoinColumn.name;
|
|
||||||
const newId = oldEntriesMap[relation[column]];
|
const newId = oldEntriesMap[relation[column]];
|
||||||
return { ...relation, [column]: newId };
|
return { ...relation, [column]: newId };
|
||||||
});
|
});
|
||||||
|
@ -138,7 +138,7 @@ describe('Relation permissions', () => {
|
|||||||
|
|
||||||
const shopEntry = await createEntry(
|
const shopEntry = await createEntry(
|
||||||
'api::shop.shop',
|
'api::shop.shop',
|
||||||
{ name: 'Shop', products: [product.id] },
|
{ name: 'Shop', products: [product.documentId] },
|
||||||
populateShop
|
populateShop
|
||||||
);
|
);
|
||||||
shop = shopEntry.data;
|
shop = shopEntry.data;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user