fix(content-manager): documentId missing from fields for the view and… (#23604)

* fix(content-manager): documentId missing from fields for the view and filters

---------

Co-authored-by: Mark Kaylor <mark.kaylor@strapi.io>
This commit is contained in:
Adrien L 2025-06-10 17:43:39 +02:00 committed by GitHub
parent 3561ab6db9
commit df5dd4b92e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 131 additions and 90 deletions

View File

@ -169,6 +169,7 @@ describe('useDocument', () => {
}, },
attributes: { attributes: {
id: { type: 'string' }, id: { type: 'string' },
documentId: { type: 'string' },
slug: { type: 'uid' }, slug: { type: 'uid' },
name: { type: 'string' }, name: { type: 'string' },
city: { city: {
@ -293,6 +294,9 @@ describe('useDocument', () => {
id: { id: {
type: 'string', type: 'string',
}, },
documentId: {
type: 'string',
},
name: { name: {
type: 'string', type: 'string',
default: 'toto', default: 'toto',
@ -338,6 +342,7 @@ describe('useDocument', () => {
expect(result.current.getInitialFormValues).toBeInstanceOf(Function); expect(result.current.getInitialFormValues).toBeInstanceOf(Function);
expect(result.current.getInitialFormValues()).toEqual({ expect(result.current.getInitialFormValues()).toEqual({
documentId: '12345',
name: 'Entry 1', name: 'Entry 1',
}); });
}); });

View File

@ -39,7 +39,6 @@ describe('useDocumentActions', () => {
documentId: '12345', documentId: '12345',
}, },
{ {
documentId: '12345',
title: 'test', title: 'test',
content: 'the brown fox jumps over the lazy dog', content: 'the brown fox jumps over the lazy dog',
} }
@ -51,7 +50,7 @@ describe('useDocumentActions', () => {
expect(response).toEqual({ expect(response).toEqual({
data: { data: {
content: 'the brown fox jumps over the lazy dog', content: 'the brown fox jumps over the lazy dog',
documentId: '12345', documentId: '67890',
id: 2, id: 2,
title: 'test', title: 'test',
}, },

View File

@ -681,7 +681,8 @@ const useDocumentActions: UseDocumentActions = () => {
const clone: IUseDocumentActs['clone'] = React.useCallback( const clone: IUseDocumentActs['clone'] = React.useCallback(
async ({ model, documentId, params }, body, trackerProperty) => { async ({ model, documentId, params }, body, trackerProperty) => {
try { try {
const { id: _id, ...restBody } = body; // Omit id and documentId so they are not copied to the clone
const { id: _id, documentId: _documentId, ...restBody } = body;
/** /**
* If we're cloning we want to post directly to this endpoint * If we're cloning we want to post directly to this endpoint

View File

@ -1,6 +1,11 @@
import { testData } from '../../../../tests/data'; import { testData } from '../../../../tests/data';
import { removeProhibitedFields } from '../data'; import { removeProhibitedFields } from '../data';
const defaultFieldsValues = {
name: 'name',
password: '',
};
describe('data', () => { describe('data', () => {
describe('removeProhibitedFields', () => { describe('removeProhibitedFields', () => {
it('should return an empty object', () => { it('should return an empty object', () => {
@ -13,9 +18,9 @@ describe('data', () => {
const { components, contentType } = testData; const { components, contentType } = testData;
expect( expect(
removeProhibitedFields(['password'])(contentType, components)({ name: 'test' }) removeProhibitedFields(['password'])(contentType, components)({ name: 'name' })
).toEqual({ ).toEqual({
name: 'test', name: 'name',
}); });
}); });
@ -24,10 +29,10 @@ describe('data', () => {
expect( expect(
removeProhibitedFields(['password'])(contentType, components)({ removeProhibitedFields(['password'])(contentType, components)({
name: 'test', name: 'name',
password: 'password', password: 'password',
}) })
).toEqual({ name: 'test', password: '' }); ).toEqual(defaultFieldsValues);
}); });
it('should remove all password fields', () => { it('should remove all password fields', () => {
@ -36,106 +41,70 @@ describe('data', () => {
const result = removeProhibitedFields(['password'])(contentType, components)(modifiedData); const result = removeProhibitedFields(['password'])(contentType, components)(modifiedData);
expect(result).toEqual({ expect(result).toEqual({
id: 1,
name: 'name',
createdAt: '2020-04-28T13:22:13.033Z', createdAt: '2020-04-28T13:22:13.033Z',
updatedAt: '2020-04-28T13:22:13.033Z',
password: '',
notrepeatable: {
id: 1,
name: 'name',
password: '',
subcomponotrepeatable: {
id: 4,
name: 'name',
password: '',
},
subrepeatable: [
{
id: 1,
name: 'name',
password: '',
},
{
id: 2,
name: 'name',
password: '',
},
{
id: 3,
name: 'name',
password: '',
},
],
},
repeatable: [
{
id: 2,
name: 'name',
password: '',
subcomponotrepeatable: {
id: 6,
name: 'name',
password: '',
},
subrepeatable: [
{
id: 5,
name: 'name',
password: '',
},
],
},
{
id: 3,
name: 'name',
password: '',
subcomponotrepeatable: null,
subrepeatable: [],
},
],
dz: [ dz: [
{ {
__component: 'compos.sub-compo', __component: 'compos.sub-compo',
id: 7, id: 7,
name: 'name', ...defaultFieldsValues,
password: '',
}, },
{ {
__component: 'compos.test-compo',
id: 4, id: 4,
name: 'name', documentId: '456789',
password: '', ...defaultFieldsValues,
subcomponotrepeatable: null, subcomponotrepeatable: null,
subrepeatable: [], subrepeatable: [],
__component: 'compos.test-compo',
}, },
{ {
__component: 'compos.test-compo',
id: 5, id: 5,
name: 'name', documentId: '567890',
password: '', ...defaultFieldsValues,
subcomponotrepeatable: { subcomponotrepeatable: { id: 9, name: 'name', password: '' },
id: 9, subrepeatable: [{ id: 8, name: 'name', password: '' }],
name: 'name', __component: 'compos.test-compo',
password: '',
},
subrepeatable: [
{
id: 8,
name: 'name',
password: '',
},
],
}, },
{ {
__component: 'compos.test-compo',
id: 6, id: 6,
documentId: '678901',
name: null, name: null,
password: null, password: null,
subcomponotrepeatable: null, subcomponotrepeatable: null,
subrepeatable: [], subrepeatable: [],
__component: 'compos.test-compo',
}, },
], ],
id: 1,
name: 'name',
notrepeatable: {
id: 1,
documentId: '123456',
...defaultFieldsValues,
subcomponotrepeatable: { id: 4, name: 'name', password: '' },
subrepeatable: [
{ id: 1, name: 'name', password: '' },
{ id: 2, name: 'name', password: '' },
{ id: 3, name: 'name', password: '' },
],
},
password: '',
repeatable: [
{
id: 2,
documentId: '234567',
...defaultFieldsValues,
subrepeatable: [{ id: 5, name: 'name', password: '' }],
subcomponotrepeatable: { id: 6, name: 'name', password: '' },
},
{
id: 3,
documentId: '345678',
...defaultFieldsValues,
subrepeatable: [],
subcomponotrepeatable: null,
},
],
updatedAt: '2020-04-28T13:22:13.033Z',
}); });
}); });
}); });

View File

@ -108,6 +108,7 @@ const FiltersImpl = ({ disabled, schema }: FiltersProps) => {
return ( return (
[ [
'id', 'id',
'documentId',
...allowedFields, ...allowedFields,
...DEFAULT_ALLOWED_FILTERS, ...DEFAULT_ALLOWED_FILTERS,
...(canReadAdminUsers ? CREATOR_FIELDS : []), ...(canReadAdminUsers ? CREATOR_FIELDS : []),

View File

@ -20,6 +20,7 @@ const testData = {
createdAt: { type: 'timestamp' }, createdAt: { type: 'timestamp' },
dz: { type: 'dynamiczone', components: ['compos.test-compo', 'compos.sub-compo'] }, dz: { type: 'dynamiczone', components: ['compos.test-compo', 'compos.sub-compo'] },
id: { type: 'integer' }, id: { type: 'integer' },
documentId: { type: 'string' },
name: { type: 'string' }, name: { type: 'string' },
notrepeatable: { notrepeatable: {
type: 'component', type: 'component',
@ -45,6 +46,7 @@ const testData = {
}, },
attributes: { attributes: {
id: { type: 'integer' }, id: { type: 'integer' },
documentId: { type: 'string' },
name: { type: 'string' }, name: { type: 'string' },
password: { type: 'password' }, password: { type: 'password' },
}, },
@ -62,6 +64,7 @@ const testData = {
}, },
attributes: { attributes: {
id: { type: 'integer' }, id: { type: 'integer' },
documentId: { type: 'string' },
name: { type: 'string' }, name: { type: 'string' },
password: { type: 'password' }, password: { type: 'password' },
subcomponotrepeatable: { subcomponotrepeatable: {
@ -83,6 +86,7 @@ const testData = {
{ __component: 'compos.sub-compo', id: 7, name: 'name', password: 'password' }, { __component: 'compos.sub-compo', id: 7, name: 'name', password: 'password' },
{ {
id: 4, id: 4,
documentId: '456789',
name: 'name', name: 'name',
password: 'password', password: 'password',
subcomponotrepeatable: null, subcomponotrepeatable: null,
@ -91,6 +95,7 @@ const testData = {
}, },
{ {
id: 5, id: 5,
documentId: '567890',
name: 'name', name: 'name',
password: 'password', password: 'password',
subcomponotrepeatable: { id: 9, name: 'name', password: 'password' }, subcomponotrepeatable: { id: 9, name: 'name', password: 'password' },
@ -99,6 +104,7 @@ const testData = {
}, },
{ {
id: 6, id: 6,
documentId: '678901',
name: null, name: null,
password: null, password: null,
subcomponotrepeatable: null, subcomponotrepeatable: null,
@ -110,6 +116,7 @@ const testData = {
name: 'name', name: 'name',
notrepeatable: { notrepeatable: {
id: 1, id: 1,
documentId: '123456',
name: 'name', name: 'name',
password: 'password', password: 'password',
subcomponotrepeatable: { id: 4, name: 'name', password: 'password' }, subcomponotrepeatable: { id: 4, name: 'name', password: 'password' },
@ -123,6 +130,7 @@ const testData = {
repeatable: [ repeatable: [
{ {
id: 2, id: 2,
documentId: '234567',
name: 'name', name: 'name',
password: 'password', password: 'password',
subrepeatable: [{ id: 5, name: 'name', password: 'password' }], subrepeatable: [{ id: 5, name: 'name', password: 'password' }],
@ -130,6 +138,7 @@ const testData = {
}, },
{ {
id: 3, id: 3,
documentId: '345678',
name: 'name', name: 'name',
password: 'password', password: 'password',
subrepeatable: [], subrepeatable: [],

View File

@ -18,6 +18,9 @@ const CM_COMPONENTS_MOCK_DATA = [
id: { id: {
type: 'string', type: 'string',
}, },
documentId: {
type: 'string',
},
name: { name: {
type: 'string', type: 'string',
default: 'toto', default: 'toto',
@ -371,6 +374,9 @@ const CM_CONTENT_TYPE_MOCK_DATA = [
id: { id: {
type: 'string', type: 'string',
}, },
documentId: {
type: 'string',
},
name: { name: {
type: 'string', type: 'string',
}, },
@ -488,6 +494,9 @@ const CM_CONTENT_TYPE_MOCK_DATA = [
id: { id: {
type: 'string', type: 'string',
}, },
documentId: {
type: 'string',
},
Title: { Title: {
type: 'string', type: 'string',
default: 'New article', default: 'New article',
@ -695,6 +704,14 @@ const CM_COLLECTION_TYPE_LAYOUT_MOCK_DATA = {
sortable: true, sortable: true,
}, },
}, },
documentId: {
edit: {},
list: {
label: 'documentId',
searchable: true,
sortable: true,
},
},
name: { name: {
edit: { edit: {
label: 'name', label: 'name',

View File

@ -56,7 +56,17 @@ export default {
const confWithUpdatedMetadata = { const confWithUpdatedMetadata = {
...configuration, ...configuration,
metadatas: mapValues(assocMainField, configuration.metadatas), metadatas: {
...mapValues(assocMainField, configuration.metadatas),
documentId: {
edit: {},
list: {
label: 'documentId',
searchable: true,
sortable: true,
},
},
},
}; };
const components = await contentTypeService.findComponentsConfigurations(contentType); const components = await contentTypeService.findComponentsConfigurations(contentType);

View File

@ -27,6 +27,9 @@ export default () => ({
type: 'integer', type: 'integer',
}, },
...formatAttributes(contentType), ...formatAttributes(contentType),
documentId: {
type: 'string',
},
}, },
}; };
}, },

View File

@ -108,8 +108,10 @@ const documentManager = ({ strapi }: { strapi: Core.Strapi }) => {
uid: UID.CollectionType uid: UID.CollectionType
) { ) {
const populate = await buildDeepPopulate(uid); const populate = await buildDeepPopulate(uid);
const params = { const params = {
data: omitIdField(body), // Ensure id and documentId are not copied to the clone
data: omit(['id', 'documentId'], body),
populate, populate,
}; };

View File

@ -17,6 +17,7 @@ export interface DocumentVersion {
const AVAILABLE_STATUS_FIELDS = [ const AVAILABLE_STATUS_FIELDS = [
'id', 'id',
'documentId',
'locale', 'locale',
'updatedAt', 'updatedAt',
'createdAt', 'createdAt',
@ -27,6 +28,7 @@ const AVAILABLE_STATUS_FIELDS = [
]; ];
const AVAILABLE_LOCALES_FIELDS = [ const AVAILABLE_LOCALES_FIELDS = [
'id', 'id',
'documentId',
'locale', 'locale',
'updatedAt', 'updatedAt',
'createdAt', 'createdAt',

View File

@ -79,7 +79,7 @@ const isVisible = (schema: any, name: any) => {
return false; return false;
} }
if (isTimestamp(schema, name) || name === 'id') { if (isTimestamp(schema, name) || name === 'id' || name === 'documentId') {
return false; return false;
} }

View File

@ -38,7 +38,7 @@ async function createDefaultLayouts(schema: any) {
function createDefaultListLayout(schema: any) { function createDefaultListLayout(schema: any) {
return Object.keys(schema.attributes) return Object.keys(schema.attributes)
.filter((name) => isListable(schema, name)) .filter((name) => isListable(schema, name) && name !== 'documentId')
.slice(0, DEFAULT_LIST_LENGTH); .slice(0, DEFAULT_LIST_LENGTH);
} }

View File

@ -23,6 +23,14 @@ function createDefaultMetadatas(schema: any) {
sortable: true, sortable: true,
}, },
}, },
documentId: {
edit: {},
list: {
label: 'documentId',
searchable: true,
sortable: true,
},
},
}; };
} }
@ -95,6 +103,7 @@ async function syncMetadatas(configuration: any, schema: any) {
const attr = schema.attributes[key]; const attr = schema.attributes[key];
const updatedMeta = { edit, list }; const updatedMeta = { edit, list };
// update sortable attr // update sortable attr
if (list.sortable && !isSortable(schema, key)) { if (list.sortable && !isSortable(schema, key)) {
_.set(updatedMeta, ['list', 'sortable'], false); _.set(updatedMeta, ['list', 'sortable'], false);

View File

@ -9,11 +9,25 @@ test.describe('List View', () => {
await login({ page }); await login({ page });
}); });
test('A user can filter entries', async ({ page }) => {
await page.getByRole('link', { name: 'Content Manager' }).click();
await page.getByRole('link', { name: 'Article' }).click();
await page.getByRole('button', { name: 'Filters' }).click();
await page.getByRole('combobox', { name: 'Select field' }).click();
await page.getByRole('option', { name: 'documentId' }).click();
// va0x2nt206hluydibmsoiquc => documentId for article "Why I prefer football over soccer"
await page.getByRole('textbox', { name: 'documentId' }).fill('va0x2nt206hluydibmsoiquc');
await page.getByRole('button', { name: 'Add filter' }).click();
await expect(page.getByText('documentId is va0x2nt206hluydibmsoiquc')).toBeVisible();
// There should be 2 rows, 1 for the header and 1 for the Article entry
await expect(page.getByRole('row')).toHaveCount(2);
});
test('A user should be able to navigate to the ListView of the content manager and see some entries', async ({ test('A user should be able to navigate to the ListView of the content manager and see some entries', async ({
page, page,
}) => { }) => {
await page.getByRole('link', { name: 'Content Manager' }).click(); await page.getByRole('link', { name: 'Content Manager' }).click();
await expect(page).toHaveTitle('Article | Strapi'); await expect(page).toHaveTitle('Article | Strapi');
await expect(page.getByRole('heading', { name: 'Article' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Article' })).toBeVisible();
await expect(page.getByRole('link', { name: /Create new entry/ }).first()).toBeVisible(); await expect(page.getByRole('link', { name: /Create new entry/ }).first()).toBeVisible();