scaffold record page layout + fields view when adding an object (#19977)
## Summary
Extends `yarn twenty add` → **Object** so it scaffolds a complete record
page out
of the box:
- A **record-page-fields view** (`<name>-record-page-fields.ts`,
FIELDS_WIDGET)
pre-populated with the `name` field plus the auto-generated default
fields (`createdAt`,
`updatedAt`, `createdBy`, `updatedBy`) — the default-field entries are
emitted as
`generateDefaultFieldUniversalIdentifier({ objectUniversalIdentifier,
fieldName: '...' })`
calls rather than pre-computed UUIDs, so the generated file
double-serves as
documentation for the public util.
- A **record page layout** (`<name>-record-page-layout.ts`) with a Home
tab whose Fields
widget points at the new view (via `viewUniversalIdentifier`), plus a
Timeline tab.
- The companion prompt now covers all three artefacts (was view + nav
menu item).
Fix: Server-side, renames `viewId` → `viewUniversalIdentifier` on the
universal-flat FIELDS
widget configuration so it is consistent with other universal-flat
references. The DB-side
DTO keeps `viewId` (now typed as `SerializedRelation`), and the
conversion utils map
between the two.
<img width="337" height="349" alt="Screenshot 2026-04-22 at 15 40 22"
src="https://github.com/user-attachments/assets/59e36540-1761-46b0-808d-648c68604268"
/>
This commit is contained in:
@@ -3,7 +3,7 @@ import inquirer from 'inquirer';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join, relative } from 'path';
|
||||
import { SyncableEntity } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, ViewType } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-fu
|
||||
import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-navigation-menu-item-template';
|
||||
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
|
||||
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
|
||||
import { getRecordPageLayoutBaseFile } from '@/cli/utilities/entity/entity-record-page-layout-template';
|
||||
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
|
||||
import { getAgentBaseFile } from '@/cli/utilities/entity/entity-agent-template';
|
||||
import { getSkillBaseFile } from '@/cli/utilities/entity/entity-skill-template';
|
||||
@@ -27,6 +28,7 @@ const APP_FOLDER = 'src';
|
||||
export class EntityAddCommand {
|
||||
private lastObjectUniversalIdentifier: string | undefined;
|
||||
private lastNameFieldUniversalIdentifier: string | undefined;
|
||||
private lastObjectLabelSingular: string | undefined;
|
||||
|
||||
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
|
||||
try {
|
||||
@@ -59,7 +61,7 @@ export class EntityAddCommand {
|
||||
);
|
||||
|
||||
if (entity === SyncableEntity.Object) {
|
||||
await this.promptAndCreateViewAndNavigationMenuItem(name, path);
|
||||
await this.promptAndCreateObjectCompanions(name, path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -81,6 +83,7 @@ export class EntityAddCommand {
|
||||
|
||||
this.lastObjectUniversalIdentifier = objectUniversalIdentifier;
|
||||
this.lastNameFieldUniversalIdentifier = nameFieldUniversalIdentifier;
|
||||
this.lastObjectLabelSingular = entityData.labelSingular;
|
||||
|
||||
const file = getObjectBaseFile({
|
||||
data: entityData,
|
||||
@@ -191,27 +194,28 @@ export class EntityAddCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async promptAndCreateViewAndNavigationMenuItem(
|
||||
private async promptAndCreateObjectCompanions(
|
||||
objectName: string,
|
||||
customPath?: string,
|
||||
): Promise<void> {
|
||||
const { createViewAndNavItem } = await inquirer.prompt<{
|
||||
createViewAndNavItem: boolean;
|
||||
const { createCompanions } = await inquirer.prompt<{
|
||||
createCompanions: boolean;
|
||||
}>([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'createViewAndNavItem',
|
||||
name: 'createCompanions',
|
||||
message:
|
||||
'Also create a view and navigation menu item for this object? (recommended)',
|
||||
'Also create a view, navigation menu item, and record page layout for this object? (recommended)',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!createViewAndNavItem || !this.lastObjectUniversalIdentifier) {
|
||||
if (!createCompanions || !this.lastObjectUniversalIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewUniversalIdentifier = v4();
|
||||
const fieldsWidgetViewUniversalIdentifier = v4();
|
||||
|
||||
const viewFile = getViewBaseFile({
|
||||
name: `all-${kebabCase(objectName)}`,
|
||||
@@ -258,6 +262,41 @@ export class EntityAddCommand {
|
||||
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, viewFilePath)),
|
||||
);
|
||||
|
||||
const recordPageFieldsViewFields = this.buildRecordPageFieldsViewFields(
|
||||
this.lastNameFieldUniversalIdentifier,
|
||||
);
|
||||
|
||||
const recordPageFieldsViewFile = getViewBaseFile({
|
||||
name: `${kebabCase(objectName)}-record-page-fields`,
|
||||
universalIdentifier: fieldsWidgetViewUniversalIdentifier,
|
||||
objectUniversalIdentifier: this.lastObjectUniversalIdentifier,
|
||||
type: ViewType.FIELDS_WIDGET,
|
||||
fields: recordPageFieldsViewFields,
|
||||
});
|
||||
|
||||
const recordPageFieldsViewFileName = `${kebabCase(objectName)}-record-page-fields.ts`;
|
||||
const recordPageFieldsViewFilePath = join(
|
||||
viewFolderPath,
|
||||
recordPageFieldsViewFileName,
|
||||
);
|
||||
|
||||
if (await pathExists(recordPageFieldsViewFilePath)) {
|
||||
const { overwrite } = await this.handleFileExist();
|
||||
|
||||
if (!overwrite) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(recordPageFieldsViewFilePath, recordPageFieldsViewFile);
|
||||
|
||||
console.log(
|
||||
chalk.green(`✓ Created record-page-fields view:`),
|
||||
chalk.cyan(
|
||||
relative(CURRENT_EXECUTION_DIRECTORY, recordPageFieldsViewFilePath),
|
||||
),
|
||||
);
|
||||
|
||||
const navFile = getNavigationMenuItemBaseFile({
|
||||
name: objectName,
|
||||
type: 'OBJECT',
|
||||
@@ -291,6 +330,75 @@ export class EntityAddCommand {
|
||||
chalk.green(`✓ Created navigation menu item:`),
|
||||
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, navFilePath)),
|
||||
);
|
||||
|
||||
const recordPageLayoutFile = getRecordPageLayoutBaseFile({
|
||||
objectLabelSingular: this.lastObjectLabelSingular ?? objectName,
|
||||
objectUniversalIdentifier: this.lastObjectUniversalIdentifier,
|
||||
fieldsWidgetViewUniversalIdentifier,
|
||||
});
|
||||
|
||||
const pageLayoutFolderPath = customPath
|
||||
? join(CURRENT_EXECUTION_DIRECTORY, customPath)
|
||||
: join(
|
||||
CURRENT_EXECUTION_DIRECTORY,
|
||||
APP_FOLDER,
|
||||
this.getFolderName(SyncableEntity.PageLayout),
|
||||
);
|
||||
|
||||
await ensureDir(pageLayoutFolderPath);
|
||||
|
||||
const recordPageLayoutFileName = `${kebabCase(objectName)}-record-page-layout.ts`;
|
||||
const recordPageLayoutFilePath = join(
|
||||
pageLayoutFolderPath,
|
||||
recordPageLayoutFileName,
|
||||
);
|
||||
|
||||
if (await pathExists(recordPageLayoutFilePath)) {
|
||||
const { overwrite } = await this.handleFileExist();
|
||||
|
||||
if (!overwrite) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(recordPageLayoutFilePath, recordPageLayoutFile);
|
||||
|
||||
console.log(
|
||||
chalk.green(`✓ Created record page layout:`),
|
||||
chalk.cyan(
|
||||
relative(CURRENT_EXECUTION_DIRECTORY, recordPageLayoutFilePath),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private buildRecordPageFieldsViewFields(
|
||||
nameFieldUniversalIdentifier: string | undefined,
|
||||
) {
|
||||
const autoGeneratedFieldNames = [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
] as const;
|
||||
|
||||
const autoGeneratedFields = autoGeneratedFieldNames.map((fieldName) => ({
|
||||
defaultFieldName: fieldName,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
}));
|
||||
|
||||
const fields = nameFieldUniversalIdentifier
|
||||
? [
|
||||
{
|
||||
fieldMetadataUniversalIdentifier: nameFieldUniversalIdentifier,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
...autoGeneratedFields,
|
||||
]
|
||||
: autoGeneratedFields;
|
||||
|
||||
return fields.map((field, index) => ({ ...field, position: index }));
|
||||
}
|
||||
|
||||
private async getEntity() {
|
||||
|
||||
@@ -72,8 +72,8 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
// Default isVisible is true, default size is 200
|
||||
expect(result).toContain('"isVisible": true');
|
||||
expect(result).toContain('"size": 200');
|
||||
expect(result).toContain('isVisible: true');
|
||||
expect(result).toContain('size: 200');
|
||||
});
|
||||
|
||||
it('should generate unique UUID when not provided', () => {
|
||||
@@ -113,4 +113,41 @@ describe('getViewBaseFile', () => {
|
||||
// At least 2 UUIDs: one for the view and one for the field
|
||||
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should emit a generateDefaultFieldUniversalIdentifier call for fields using defaultFieldName', () => {
|
||||
const result = getViewBaseFile({
|
||||
name: 'view-default-field',
|
||||
objectUniversalIdentifier: 'obj-abc-123',
|
||||
fields: [
|
||||
{
|
||||
defaultFieldName: 'createdAt',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toContain(
|
||||
"import {\n defineView,\n generateDefaultFieldUniversalIdentifier,\n} from 'twenty-sdk/define';",
|
||||
);
|
||||
expect(result).toContain(
|
||||
'fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier({',
|
||||
);
|
||||
expect(result).toContain("objectUniversalIdentifier: 'obj-abc-123'");
|
||||
expect(result).toContain("fieldName: 'createdAt'");
|
||||
});
|
||||
|
||||
it('should not import generateDefaultFieldUniversalIdentifier when no field uses defaultFieldName', () => {
|
||||
const result = getViewBaseFile({
|
||||
name: 'view-literal-only',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataUniversalIdentifier: 'field-uuid-1',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).not.toContain('generateDefaultFieldUniversalIdentifier');
|
||||
expect(result).toContain("import { defineView } from 'twenty-sdk/define';");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getRecordPageLayoutBaseFile = ({
|
||||
objectLabelSingular,
|
||||
objectUniversalIdentifier,
|
||||
fieldsWidgetViewUniversalIdentifier,
|
||||
}: {
|
||||
objectLabelSingular: string;
|
||||
objectUniversalIdentifier: string;
|
||||
fieldsWidgetViewUniversalIdentifier: string;
|
||||
}) => {
|
||||
return `import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
name: 'Default ${objectLabelSingular} Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: '${objectUniversalIdentifier}',
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
title: 'Home',
|
||||
position: 10,
|
||||
icon: 'IconHome',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
title: 'Fields',
|
||||
type: 'FIELDS',
|
||||
configuration: {
|
||||
configurationType: 'FIELDS',
|
||||
viewUniversalIdentifier: '${fieldsWidgetViewUniversalIdentifier}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
title: 'Timeline',
|
||||
position: 20,
|
||||
icon: 'IconTimelineEvent',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
title: 'Timeline',
|
||||
type: 'TIMELINE',
|
||||
configuration: {
|
||||
configurationType: 'TIMELINE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
`;
|
||||
};
|
||||
@@ -1,37 +1,67 @@
|
||||
import { kebabCase } from '@/cli/utilities/string/kebab-case';
|
||||
import { type ViewType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type ViewFieldTemplate = {
|
||||
type ViewFieldTemplateBase = {
|
||||
universalIdentifier?: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
position: number;
|
||||
isVisible?: boolean;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
type ViewFieldTemplate =
|
||||
| (ViewFieldTemplateBase & { fieldMetadataUniversalIdentifier: string })
|
||||
| (ViewFieldTemplateBase & { defaultFieldName: string });
|
||||
|
||||
const renderFieldEntry = ({
|
||||
field,
|
||||
index,
|
||||
objectUniversalIdentifier,
|
||||
}: {
|
||||
field: ViewFieldTemplate;
|
||||
index: number;
|
||||
objectUniversalIdentifier: string;
|
||||
}) => {
|
||||
const universalIdentifier = field.universalIdentifier ?? v4();
|
||||
const position = field.position ?? index;
|
||||
const isVisible = field.isVisible ?? true;
|
||||
const size = field.size ?? 200;
|
||||
|
||||
const fieldMetadataUniversalIdentifierLine =
|
||||
'defaultFieldName' in field
|
||||
? ` fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectUniversalIdentifier: '${objectUniversalIdentifier}',
|
||||
fieldName: '${field.defaultFieldName}',
|
||||
})`
|
||||
: ` fieldMetadataUniversalIdentifier: '${field.fieldMetadataUniversalIdentifier}'`;
|
||||
|
||||
return ` {
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
${fieldMetadataUniversalIdentifierLine},
|
||||
position: ${position},
|
||||
isVisible: ${isVisible},
|
||||
size: ${size},
|
||||
}`;
|
||||
};
|
||||
|
||||
export const getViewBaseFile = ({
|
||||
name,
|
||||
universalIdentifier = v4(),
|
||||
objectUniversalIdentifier = 'fill-later',
|
||||
fields = [],
|
||||
type,
|
||||
}: {
|
||||
name: string;
|
||||
universalIdentifier?: string;
|
||||
objectUniversalIdentifier?: string;
|
||||
fields?: ViewFieldTemplate[];
|
||||
type?: ViewType;
|
||||
}) => {
|
||||
const kebabCaseName = kebabCase(name);
|
||||
|
||||
const formattedFields = fields.map((field, index) => {
|
||||
const uid = field.universalIdentifier ?? v4();
|
||||
return {
|
||||
universalIdentifier: uid,
|
||||
fieldMetadataUniversalIdentifier: field.fieldMetadataUniversalIdentifier,
|
||||
position: field.position ?? index,
|
||||
isVisible: field.isVisible ?? true,
|
||||
size: field.size ?? 200,
|
||||
};
|
||||
});
|
||||
const hasDefaultFieldEntry = fields.some(
|
||||
(field) => 'defaultFieldName' in field,
|
||||
);
|
||||
|
||||
const defaultFields = ` // fields: [
|
||||
// {
|
||||
@@ -45,19 +75,30 @@ export const getViewBaseFile = ({
|
||||
const fieldsBlock =
|
||||
fields.length > 0
|
||||
? ` fields: [
|
||||
${formattedFields
|
||||
.map((field) => JSON.stringify(field, null, 2))
|
||||
.join(',\n')},\n
|
||||
${fields
|
||||
.map((field, index) =>
|
||||
renderFieldEntry({ field, index, objectUniversalIdentifier }),
|
||||
)
|
||||
.join(',\n')},
|
||||
],`
|
||||
: defaultFields;
|
||||
|
||||
return `import { defineView } from 'twenty-sdk/define';
|
||||
const typeBlock = type !== undefined ? ` type: '${type}',\n` : '';
|
||||
|
||||
const imports = hasDefaultFieldEntry
|
||||
? `import {
|
||||
defineView,
|
||||
generateDefaultFieldUniversalIdentifier,
|
||||
} from 'twenty-sdk/define';`
|
||||
: `import { defineView } from 'twenty-sdk/define';`;
|
||||
|
||||
return `${imports}
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: '${kebabCaseName}',
|
||||
objectUniversalIdentifier: '${objectUniversalIdentifier}',
|
||||
icon: 'IconList',
|
||||
${typeBlock} icon: 'IconList',
|
||||
position: 0,
|
||||
${fieldsBlock}
|
||||
// filters: [
|
||||
|
||||
+1
-1
@@ -295,7 +295,7 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
|
||||
return {
|
||||
...rest,
|
||||
newFieldDefaultVisibility,
|
||||
viewId: viewUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+14
-8
@@ -40,17 +40,23 @@ export const validateFieldsFlatPageLayoutWidgetForCreation = (
|
||||
return errors;
|
||||
}
|
||||
|
||||
const viewId = (
|
||||
universalConfiguration as { configurationType: string; viewId?: unknown }
|
||||
).viewId;
|
||||
const viewUniversalIdentifier = (
|
||||
universalConfiguration as {
|
||||
configurationType: string;
|
||||
viewUniversalIdentifier?: unknown;
|
||||
}
|
||||
).viewUniversalIdentifier;
|
||||
|
||||
if (isDefined(viewId) && viewId !== null) {
|
||||
if (typeof viewId !== 'string' || !uuidValidate(viewId)) {
|
||||
if (isDefined(viewUniversalIdentifier) && viewUniversalIdentifier !== null) {
|
||||
if (
|
||||
typeof viewUniversalIdentifier !== 'string' ||
|
||||
!uuidValidate(viewUniversalIdentifier)
|
||||
) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: t`Invalid viewId for fields widget "${widgetTitle}". Expected a valid UUID`,
|
||||
userFriendlyMessage: msg`Invalid viewId for fields widget`,
|
||||
value: viewId,
|
||||
message: t`Invalid viewUniversalIdentifier for fields widget "${widgetTitle}". Expected a valid UUID`,
|
||||
userFriendlyMessage: msg`Invalid viewUniversalIdentifier for fields widget`,
|
||||
value: viewUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({
|
||||
const universalConfiguration = isFieldsWidget
|
||||
? {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: recordPageFieldsView.universalIdentifier,
|
||||
viewUniversalIdentifier: recordPageFieldsView.universalIdentifier,
|
||||
}
|
||||
: {
|
||||
configurationType:
|
||||
|
||||
+5
-2
@@ -7,7 +7,10 @@ import {
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { type FieldsConfiguration } from 'twenty-shared/types';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
type SerializedRelation,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
|
||||
@@ -21,7 +24,7 @@ export class FieldsConfigurationDTO implements FieldsConfiguration {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
viewId: string | null;
|
||||
viewId: SerializedRelation | null;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
|
||||
+2
-2
@@ -160,7 +160,7 @@ const buildFieldsWidgetConfiguration = ({
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
viewUniversalIdentifier: null,
|
||||
newFieldDefaultVisibility: true,
|
||||
},
|
||||
};
|
||||
@@ -197,7 +197,7 @@ const buildFieldsWidgetConfiguration = ({
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: viewUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
newFieldDefaultVisibility: true,
|
||||
},
|
||||
};
|
||||
|
||||
+2
-5
@@ -255,11 +255,8 @@ export const fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration = ({
|
||||
}
|
||||
|
||||
case WidgetConfigurationType.FIELDS: {
|
||||
const {
|
||||
viewId: viewUniversalIdentifier,
|
||||
newFieldDefaultVisibility,
|
||||
...rest
|
||||
} = universalConfiguration;
|
||||
const { viewUniversalIdentifier, newFieldDefaultVisibility, ...rest } =
|
||||
universalConfiguration;
|
||||
|
||||
let viewId: string | null = null;
|
||||
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ export type FieldConfiguration = {
|
||||
|
||||
export type FieldsConfiguration = {
|
||||
configurationType: 'FIELDS';
|
||||
viewId?: string | null;
|
||||
viewId?: SerializedRelation | null;
|
||||
newFieldDefaultVisibility?: boolean | null;
|
||||
shouldAllowUserToSeeHiddenFields?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user