Refactored and improved seeds (#8695)

- Added a new Seeder service to help with custom object seeds
- Added RichTextFieldInput to edit a rich text field directly on the
table, but deactivated it for now.
This commit is contained in:
Lucas Bordeau
2024-12-24 14:44:52 +01:00
committed by GitHub
parent 4f329d6005
commit e9717603f2
52 changed files with 5807 additions and 86 deletions

View File

@ -17,7 +17,6 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
import { RichTextFieldInput } from '@/object-record/record-field/meta-types/input/components/RichTextFieldInput';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
@ -31,7 +30,6 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
@ -167,8 +165,6 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldRichText(fieldDefinition) ? (
<RichTextFieldInput />
) : isFieldArray(fieldDefinition) ? (
<ArrayFieldInput
onCancel={onCancel}

View File

@ -28,6 +28,8 @@ import { RecordForSelect } from '@/object-record/relation-picker/types/RecordFor
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
@ -111,6 +113,10 @@ export const usePersistField = () => {
isFieldRawJson(fieldDefinition) &&
isFieldRawJsonValue(valueToPersist);
const fieldIsRichText =
isFieldRichText(fieldDefinition) &&
isFieldRichTextValue(valueToPersist);
const fieldIsArray =
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);
@ -131,7 +137,8 @@ export const usePersistField = () => {
fieldIsMultiSelect ||
fieldIsAddress ||
fieldIsRawJson ||
fieldIsArray;
fieldIsArray ||
fieldIsRichText;
if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName;

View File

@ -1,11 +1,12 @@
import { useTextFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useTextFieldDisplay';
import { useRichTextFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextFieldDisplay';
import { getFirstNonEmptyLineOfRichText } from '@/ui/input/editor/utils/getFirstNonEmptyLineOfRichText';
import { PartialBlock } from '@blocknote/core';
export const RichTextFieldDisplay = () => {
const { fieldValue } = useTextFieldDisplay();
const parsedField =
fieldValue === '' ? null : (JSON.parse(fieldValue) as PartialBlock[]);
const { fieldValue } = useRichTextFieldDisplay();
return <>{getFirstNonEmptyLineOfRichText(parsedField)}</>;
return (
<div>
<span>{getFirstNonEmptyLineOfRichText(fieldValue)}</span>
</div>
);
};

View File

@ -0,0 +1,68 @@
import { useContext } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
import { PartialBlock } from '@blocknote/core';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
export const useRichTextField = () => {
const { recordId, fieldDefinition, hotkeyScope, maxWidth } =
useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.RichText,
isFieldRichText,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldRichTextValue>(
recordStoreFamilySelector({
recordId,
fieldName: fieldName,
}),
);
const fieldRichTextValue = isFieldRichTextValue(fieldValue) ? fieldValue : '';
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldRichTextValue>(`${recordId}-${fieldName}`);
const draftValue = useRecoilValue(getDraftValueSelector());
const draftValueParsed: PartialBlock[] = isNonEmptyString(draftValue)
? JSON.parse(draftValue)
: draftValue;
const persistField = usePersistField();
const persistRichTextField = (nextValue: PartialBlock[]) => {
if (!nextValue) {
persistField(null);
} else {
const parsedValueToPersist = JSON.stringify(nextValue);
persistField(parsedValueToPersist);
}
};
return {
draftValue: draftValueParsed,
setDraftValue,
maxWidth,
fieldDefinition,
fieldValue: fieldRichTextValue,
setFieldValue,
hotkeyScope,
persistRichTextField,
};
};

View File

@ -0,0 +1,36 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { PartialBlock } from '@blocknote/core';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { parseJson } from '~/utils/parseJson';
import { FieldContext } from '../../contexts/FieldContext';
export const useRichTextFieldDisplay = () => {
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.RichText,
isFieldRichText,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldRichTextValue | undefined>(
recordId,
fieldName,
);
const fieldValueParsed = parseJson<PartialBlock[]>(fieldValue);
return {
fieldDefinition,
fieldValue: fieldValueParsed,
hotkeyScope,
};
};

View File

@ -1,5 +1,59 @@
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useRichTextField } from '@/object-record/record-field/meta-types/hooks/useRichTextField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { BlockEditorComponentInstanceContext } from '@/ui/input/editor/contexts/BlockEditorCompoponeInstanceContext';
import { PartialBlock } from '@blocknote/core';
import { useCreateBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';
export const RichTextFieldInput = () => {
return <RichTextFieldDisplay />;
import { useContext, useRef } from 'react';
const StyledRichTextContainer = styled.div`
height: 400px;
width: 500px;
overflow: auto;
`;
export type RichTextFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;
};
export const RichTextFieldInput = ({
onClickOutside,
}: RichTextFieldInputProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { recordId } = useContext(FieldContext);
const { draftValue, hotkeyScope, persistRichTextField, fieldDefinition } =
useRichTextField();
const editor = useCreateBlockNote({
initialContent: draftValue,
domAttributes: { editor: { class: 'editor' } },
schema: BLOCK_SCHEMA,
});
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
onClickOutside?.(() => persistRichTextField(editor.document), event);
};
useRegisterInputEvents<PartialBlock[]>({
inputRef: containerRef,
inputValue: draftValue,
onClickOutside: handleClickOutside,
hotkeyScope,
});
return (
<StyledRichTextContainer ref={containerRef}>
<BlockEditorComponentInstanceContext.Provider
value={{ instanceId: `${recordId}-${fieldDefinition.fieldMetadataId}` }}
>
<BlockEditor editor={editor} />
</BlockEditorComponentInstanceContext.Provider>
</StyledRichTextContainer>
);
};

View File

@ -46,6 +46,8 @@ export type FieldDateMetadata = {
};
};
export type FieldNumberVariant = 'number' | 'percentage';
export type FieldNumberMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
@ -53,7 +55,7 @@ export type FieldNumberMetadata = {
isPositive?: boolean;
settings?: {
decimals?: number;
type?: 'percentage' | 'number';
type?: FieldNumberVariant;
};
};
@ -209,6 +211,7 @@ export type FieldMetadata =
| FieldActorMetadata
| FieldArrayMetadata
| FieldTsVectorMetadata;
export type FieldTextValue = string;
export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ?
export type FieldDateTimeValue = string | null;
@ -255,7 +258,7 @@ export type FieldRelationValue<
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
export type FieldJsonValue = Record<string, Json> | Json[] | null;
export type FieldRichTextValue = Record<string, Json> | Json[] | null;
export type FieldRichTextValue = null | string;
export type FieldActorValue = {
source: string;

View File

@ -22,6 +22,7 @@ import {
FieldRatingMetadata,
FieldRawJsonMetadata,
FieldRelationMetadata,
FieldRichTextMetadata,
FieldSelectMetadata,
FieldTextMetadata,
FieldUuidMetadata,
@ -68,7 +69,7 @@ type AssertFieldMetadataFunction = <
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: E extends 'RICH_TEXT'
? FieldTextMetadata
? FieldRichTextMetadata
: E extends 'ACTOR'
? FieldActorMetadata
: E extends 'ARRAY'

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
import { FieldRichTextValue } from '../FieldMetadata';
export const richTextSchema: z.ZodType<FieldRichTextValue> = z.union([
z.null(), // Exclude literal values other than null
z.string(),
]);
export const isFieldRichTextValue = (
fieldValue: unknown,
): fieldValue is FieldRichTextValue =>
richTextSchema.safeParse(fieldValue).success;

View File

@ -5,6 +5,7 @@ import { RecordTableCellWrapper } from '@/object-record/record-table/record-tabl
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
export const RecordTableCellsVisible = () => {
const { isSelected } = useRecordTableRowContextOrThrow();
@ -15,6 +16,10 @@ export const RecordTableCellsVisible = () => {
visibleTableColumnsComponentSelector,
);
if (!isNonEmptyArray(visibleTableColumns)) {
return null;
}
const tableColumnsAfterFirst = visibleTableColumns.slice(1);
return (

View File

@ -25,7 +25,7 @@ export const SettingsOptionIconCustomizer = ({
<StyledIconCustomizer zoom={zoom} rotate={rotate}>
<Icon
size={theme.icon.size.lg}
color={theme.IllustrationIcon.color.grey}
color={theme.IllustrationIcon.color.gray}
stroke={theme.icon.stroke.md}
/>
</StyledIconCustomizer>

View File

@ -125,7 +125,7 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
[FieldMetadataType.RichText]: {
label: 'Rich Text',
Icon: IllustrationIconSetting,
exampleValue: { key: 'value' },
exampleValue: "{ key: 'value' }",
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig<FieldRichTextValue>,
[FieldMetadataType.Array]: {

View File

@ -19,14 +19,17 @@ interface BlockEditorProps {
onBlur?: () => void;
onPaste?: (event: ClipboardEvent) => void;
onChange?: () => void;
readonly?: boolean;
}
const StyledEditor = styled.div`
width: 100%;
& .editor {
background: ${({ theme }) => theme.background.primary};
font-size: 13px;
color: ${({ theme }) => theme.font.color.primary};
min-height: 400px;
}
& .editor [class^='_inlineContent']:before {
color: ${({ theme }) => theme.font.color.tertiary};
@ -124,6 +127,7 @@ export const BlockEditor = ({
onBlur,
onChange,
onPaste,
readonly,
}: BlockEditorProps) => {
const theme = useTheme();
const blockNoteTheme = theme.name === 'light' ? 'light' : 'dark';
@ -155,6 +159,7 @@ export const BlockEditor = ({
theme={blockNoteTheme}
slashMenu={false}
sideMenu={false}
editable={!readonly}
>
<CustomSideMenu editor={editor} />
<SuggestionMenuController

View File

@ -0,0 +1,66 @@
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { isSlashMenuOpenComponentState } from '@/ui/input/editor/states/isSlashMenuOpenComponentState';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
export type BlockEditorDropdownFocusEffectProps = {
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
};
export const BlockEditorDropdownFocusEffect = ({
editor,
}: BlockEditorDropdownFocusEffectProps) => {
const isSlashMenuOpenState = useRecoilComponentCallbackStateV2(
isSlashMenuOpenComponentState,
);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const updateCallBack = useRecoilCallback(
({ snapshot, set }) =>
(event: any) => {
// TODO: This triggers before the onClick event of the slash menu item, so the click outside of the editor dropdown is triggered and everything closes.
// This is due to useRecoilCallback being executed before the onClick event of the slash menu item.
const eventWantsToOpen = event.show === true;
const isAlreadyOpen = snapshot
.getLoadable(isSlashMenuOpenState)
.getValue();
const shouldOpen = eventWantsToOpen && !isAlreadyOpen;
if (shouldOpen) {
setActiveDropdownFocusIdAndMemorizePrevious('custom-slash-menu');
set(isSlashMenuOpenState, true);
return;
}
const eventWantsToClose = event.show === false;
const isAlreadyClosed = !isAlreadyOpen;
const shouldClose = eventWantsToClose && !isAlreadyClosed;
if (shouldClose) {
goBackToPreviousDropdownFocusId();
set(isSlashMenuOpenState, false);
return;
}
},
[
isSlashMenuOpenState,
setActiveDropdownFocusIdAndMemorizePrevious,
goBackToPreviousDropdownFocusId,
],
);
editor.suggestionMenus.on('update /', updateCallBack);
return <></>;
};

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const BlockEditorComponentInstanceContext =
createComponentInstanceContext();

View File

@ -0,0 +1,8 @@
import { BlockEditorComponentInstanceContext } from '@/ui/input/editor/contexts/BlockEditorCompoponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isSlashMenuOpenComponentState = createComponentStateV2<boolean>({
key: 'isSlashMenuOpenComponentState',
defaultValue: false,
componentInstanceContext: BlockEditorComponentInstanceContext,
});

View File

@ -11,6 +11,14 @@ export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => {
.getLoadable(activeDropdownFocusIdState)
.getValue();
const activeDropdownFocusId = snapshot
.getLoadable(activeDropdownFocusIdState)
.getValue();
if (activeDropdownFocusId === dropdownId) {
return;
}
set(previousDropdownFocusIdState, focusedDropdownId);
set(activeDropdownFocusIdState, dropdownId);
},

View File

@ -50,7 +50,10 @@ export const mapViewFieldsToColumnDefinitions = ({
isSortable: correspondingColumnDefinition.isSortable,
isFilterable: correspondingColumnDefinition.isFilterable,
defaultValue: correspondingColumnDefinition.defaultValue,
settings: correspondingColumnDefinition.metadata.settings,
settings:
'settings' in correspondingColumnDefinition.metadata
? correspondingColumnDefinition.metadata.settings
: undefined,
} as ColumnDefinition<FieldMetadata>;
})
.filter(isDefined);

View File

@ -4066,7 +4066,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
},
{
"id": "f59e3f72-2ac5-44cc-9a12-8530f9550cc3",
"color": "grey",
"color": "gray",
"label": "Archived",
"value": "ARCHIVED",
"position": 3
@ -10971,7 +10971,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"options": [
{
"id": "c7ff1ce0-2e47-44de-9761-867c25f68a90",
"color": "grey",
"color": "gray",
"label": "Not started",
"value": "NOT_STARTED",
"position": 0
@ -14966,7 +14966,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"position": 1
},
{
"color": "grey",
"color": "gray",
"label": "Deactivated",
"value": "DEACTIVATED",
"position": 2

View File

@ -0,0 +1,13 @@
import { isDefined } from 'twenty-ui';
export const parseJson = <T>(json: string | undefined | null) => {
if (!isDefined(json)) {
return null;
}
try {
return JSON.parse(json) as T;
} catch (e) {
return null;
}
};

View File

@ -1,7 +1,11 @@
module.exports = {
plugins: ['@stylistic'],
extends: ['../../.eslintrc.cjs'],
ignorePatterns: ['src/engine/workspace-manager/demo-objects-prefill-data/**'],
ignorePatterns: [
'src/engine/workspace-manager/demo-objects-prefill-data/**',
'src/engine/seeder/data-seeds/**',
'src/engine/seeder/metadata-seeds/**',
],
overrides: [
{
files: ['*.ts'],

View File

@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
import { AppModule } from 'src/app.module';
import { DatabaseCommandModule } from 'src/database/commands/database-command.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module';
import { WorkspaceHealthCommandModule } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module';
import { WorkspaceMigrationRunnerCommandsModule } from 'src/engine/workspace-manager/workspace-migration-runner/commands/workspace-migration-runner-commands.module';
@ -15,6 +17,8 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
WorkspaceCleanerModule,
WorkspaceHealthCommandModule,
WorkspaceMigrationRunnerCommandsModule,
ObjectMetadataModule,
FieldMetadataModule,
],
})
export class CommandModule {}

View File

@ -42,7 +42,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view';
import { seedViewWithDemoData } from 'src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@ -211,7 +211,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
);
}
const viewDefinitionsWithId = await viewPrefillData(
const viewDefinitionsWithId = await seedViewWithDemoData(
entityManager,
dataSourceMetadata.schema,
objectMetadataMap,

View File

@ -1,7 +1,19 @@
import { IsString, IsNumber, IsOptional, IsNotEmpty } from 'class-validator';
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
import { IsValidGraphQLEnumName } from 'src/engine/metadata-modules/field-metadata/validators/is-valid-graphql-enum-name.validator';
export type TagColor =
| 'green'
| 'turquoise'
| 'sky'
| 'blue'
| 'purple'
| 'pink'
| 'red'
| 'orange'
| 'yellow'
| 'gray';
export class FieldMetadataDefaultOption {
@IsOptional()
@IsString()
@ -22,5 +34,5 @@ export class FieldMetadataDefaultOption {
export class FieldMetadataComplexOption extends FieldMetadataDefaultOption {
@IsNotEmpty()
@IsString()
color: string;
color: TagColor;
}

View File

@ -752,7 +752,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
} else if (error instanceof NameNotAvailableException) {
throw new FieldMetadataException(
`Name "${fieldMetadataInput.name}" is not available`,
`Name "${fieldMetadataInput.name}" is not available, check that it is not duplicating another field's name.`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} else {

View File

@ -6,25 +6,27 @@ export enum NumberDataType {
BIGINT = 'bigint',
}
type FieldMetadataDefaultSettings = {
export type FieldMetadataDefaultSettings = {
isForeignKey?: boolean;
};
type FieldMetadataNumberSettings = {
export type FieldNumberVariant = 'number' | 'percentage';
export type FieldMetadataNumberSettings = {
dataType: NumberDataType;
decimals?: number;
type?: string;
type?: FieldNumberVariant;
};
type FieldMetadataTextSettings = {
export type FieldMetadataTextSettings = {
displayedMaxRows?: number;
};
type FieldMetadataDateSettings = {
export type FieldMetadataDateSettings = {
displayAsRelativeDate?: boolean;
};
type FieldMetadataDateTimeSettings = {
export type FieldMetadataDateTimeSettings = {
displayAsRelativeDate?: boolean;
};

View File

@ -1,20 +1,21 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { Repository } from 'typeorm';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util';
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
@Injectable()
@ValidatorConstraint({ name: 'isFieldMetadataDefaultValue', async: true })
@ -22,7 +23,8 @@ export class IsFieldMetadataDefaultValue
implements ValidatorConstraintInterface
{
constructor(
private readonly fieldMetadataService: FieldMetadataService,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly loggerService: LoggerService,
) {}
@ -44,7 +46,11 @@ export class IsFieldMetadataDefaultValue
let fieldMetadata: FieldMetadataEntity;
try {
fieldMetadata = await this.fieldMetadataService.findOneOrFail(id);
fieldMetadata = await this.fieldMetadataRepository.findOneOrFail({
where: {
id,
},
});
} catch {
return false;
}

View File

@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ValidationArguments, ValidatorConstraint } from 'class-validator';
import { Repository } from 'typeorm';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import {
FieldMetadataEntity,
FieldMetadataType,
@ -16,7 +17,10 @@ import { validateOptionsForType } from 'src/engine/metadata-modules/field-metada
export class IsFieldMetadataOptions {
private validationErrors: string[] = [];
constructor(private readonly fieldMetadataService: FieldMetadataService) {}
constructor(
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {}
async validate(
value: FieldMetadataOptions,
@ -36,7 +40,9 @@ export class IsFieldMetadataOptions {
let fieldMetadata: FieldMetadataEntity;
try {
fieldMetadata = await this.fieldMetadataService.findOneOrFail(id);
fieldMetadata = await this.fieldMetadataRepository.findOneOrFail({
where: { id },
});
} catch {
return false;
}

View File

@ -3,17 +3,22 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
BeforeDeleteOneHook,
DeleteOneInputType,
} from '@ptc-org/nestjs-query-graphql';
import { Repository } from 'typeorm';
import { RelationMetadataService } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.service';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@Injectable()
export class BeforeDeleteOneRelation implements BeforeDeleteOneHook {
constructor(readonly relationMetadataService: RelationMetadataService) {}
constructor(
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
) {}
async run(
instance: DeleteOneInputType,
@ -25,12 +30,13 @@ export class BeforeDeleteOneRelation implements BeforeDeleteOneHook {
throw new UnauthorizedException();
}
const relationMetadata =
await this.relationMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
id: instance.id.toString(),
},
});
const relationMetadata = await this.relationMetadataRepository.findOne({
where: {
workspaceId,
id: instance.id.toString(),
},
relations: ['fromFieldMetafata', 'toFieldMetadata'],
});
if (!relationMetadata) {
throw new BadRequestException('Relation does not exist');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
export type ObjectMetadataSeed = Omit<
CreateObjectInput,
'workspaceId' | 'dataSourceId'
> & { fields: Omit<CreateFieldInput, 'objectMetadataId' | 'workspaceId'>[] };

View File

@ -0,0 +1,41 @@
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const PETS_METADATA_SEEDS: ObjectMetadataSeed = {
labelPlural: 'Pets',
labelSingular: 'Pet',
namePlural: 'pets',
nameSingular: 'pet',
icon: 'IconCat',
fields: [
{
type: FieldMetadataType.SELECT,
label: 'Species',
name: 'species',
options: [
{ label: 'Dog', value: 'dog', position: 0, color: 'blue' },
{ label: 'Cat', value: 'cat', position: 1, color: 'red' },
{ label: 'Bird', value: 'bird', position: 2, color: 'green' },
{ label: 'Fish', value: 'fish', position: 3, color: 'yellow' },
{ label: 'Rabbit', value: 'rabbit', position: 4, color: 'purple' },
{ label: 'Hamster', value: 'hamster', position: 5, color: 'orange' },
],
},
{
type: FieldMetadataType.TEXT,
label: 'Comments',
name: 'comments',
},
{
type: FieldMetadataType.NUMBER,
label: 'Age',
name: 'age',
},
{
type: FieldMetadataType.ADDRESS,
label: 'Location',
name: 'location',
},
],
};

View File

@ -0,0 +1,72 @@
import {
FieldMetadataNumberSettings,
FieldMetadataTextSettings,
NumberDataType,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const SURVEY_RESULTS_METADATA_SEEDS: ObjectMetadataSeed = {
labelPlural: 'Survey results',
labelSingular: 'Survey result',
namePlural: 'surveyResults',
nameSingular: 'surveyResult',
icon: 'IconRulerMeasure',
fields: [
{
type: FieldMetadataType.NUMBER,
label: 'Score (Float 3 decimals)',
name: 'score',
settings: {
dataType: NumberDataType.FLOAT,
decimals: 3,
type: 'number',
} as FieldMetadataNumberSettings,
},
{
type: FieldMetadataType.NUMBER,
label: 'Percentage of completion (Float 3 decimals + percentage)',
name: 'percentageOfCompletion',
settings: {
dataType: NumberDataType.FLOAT,
decimals: 6,
type: 'percentage',
} as FieldMetadataNumberSettings,
},
{
type: FieldMetadataType.NUMBER,
label: 'Participants (Int)',
name: 'participants',
settings: {
dataType: NumberDataType.INT,
type: 'number',
} as FieldMetadataNumberSettings,
},
{
type: FieldMetadataType.NUMBER,
label: 'Average estimated number of atoms in the universe (BigInt)',
name: 'averageEstimatedNumberOfAtomsInTheUniverse',
settings: {
dataType: NumberDataType.BIGINT,
type: 'number',
} as FieldMetadataNumberSettings,
},
{
type: FieldMetadataType.TEXT,
label: 'Comments (Max 5 rows)',
name: 'comments',
settings: {
displayedMaxRows: 5,
} as FieldMetadataTextSettings,
},
{
type: FieldMetadataType.TEXT,
label: 'Short notes (Max 1 row)',
name: 'shortNotes',
settings: {
displayedMaxRows: 1,
} as FieldMetadataTextSettings,
},
],
};

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { SeederService } from 'src/engine/seeder/seeder.service';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
ObjectMetadataModule,
FieldMetadataModule,
WorkspaceDataSourceModule,
],
exports: [SeederService],
providers: [SeederService],
})
export class SeederModule {}

View File

@ -0,0 +1,161 @@
import { Injectable } from '@nestjs/common';
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { capitalize } from 'src/utils/capitalize';
import { isDefined } from 'src/utils/is-defined';
@Injectable()
export class SeederService {
constructor(
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async seedCustomObjects(
dataSourceId: string,
workspaceId: string,
metadataSeeds: ObjectMetadataSeed,
dataSeeds: Record<string, any>[],
): Promise<void> {
const createdObjectMetadata = await this.objectMetadataService.createOne({
...metadataSeeds,
dataSourceId,
workspaceId,
});
if (!createdObjectMetadata) {
throw new Error("Object metadata couldn't be created");
}
for (const customField of metadataSeeds.fields) {
await this.fieldMetadataService.createOne({
...customField,
objectMetadataId: createdObjectMetadata.id,
workspaceId,
});
}
const objectMetadataAfterFieldCreation =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: { nameSingular: metadataSeeds.nameSingular },
});
if (!objectMetadataAfterFieldCreation) {
throw new Error(
"Object metadata couldn't be found after field creation.",
);
}
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
const entityManager = workspaceDataSource.createEntityManager();
const filteredFields = metadataSeeds.fields.filter((field) =>
objectMetadataAfterFieldCreation.fields.some(
(f) => f.name === field.name || f.name === `name`,
),
);
if (filteredFields.length === 0) {
throw new Error('No fields found for seeding, check metadata');
}
filteredFields.unshift({
name: 'name',
type: FieldMetadataType.TEXT,
label: 'Name',
});
const fieldMetadataMap = filteredFields
.map((field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeFieldTypeDefinition = compositeTypeDefinitions.get(
field.type,
);
if (!isDefined(compositeFieldTypeDefinition)) {
throw new Error(
`Composite field type definition not found for ${field.type}`,
);
}
const fieldNames = compositeFieldTypeDefinition.properties?.map(
(property) => property.name,
);
return (
fieldNames?.map(
(subFieldName: string) =>
`${field.name}${capitalize(subFieldName)}`,
) ?? []
);
} else {
return field.name;
}
})
.flat()
.filter(isDefined);
const flattenedSeeds = dataSeeds.map((seed) => {
const flattenedSeed = {};
for (const field of filteredFields) {
if (isCompositeFieldMetadataType(field.type)) {
const compositeFieldTypeDefinition = compositeTypeDefinitions.get(
field.type,
);
if (!isDefined(compositeFieldTypeDefinition)) {
throw new Error(
`Composite field type definition not found for ${field.type}`,
);
}
const fieldNames = compositeFieldTypeDefinition.properties
?.map((property) => property.name)
.filter(isDefined);
for (const subFieldName of fieldNames) {
flattenedSeed[`${field.name}${capitalize(subFieldName)}`] =
seed?.[field.name]?.[subFieldName];
}
} else {
flattenedSeed[field.name] = seed[field.name];
}
}
return flattenedSeed;
});
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}._${objectMetadataAfterFieldCreation.nameSingular}`, [
...fieldMetadataMap,
'position',
])
.orIgnore()
.values(
flattenedSeeds.map((flattenedSeed, index) => ({
...flattenedSeed,
position: index,
})),
)
.returning('*')
.execute();
}
}

View File

@ -1,6 +1,6 @@
import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from "src/engine/workspace-manager/demo-objects-prefill-data/workspace-member";
import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from "src/engine/workspace-manager/demo-objects-prefill-data/seed-workspace-member-with-demo-data";
export const companiesDemo = [
export const COMPANIES_DEMO = [
{
name: 'Google',
domainName: 'goo.gle',

View File

@ -1,4 +1,4 @@
import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from "src/engine/workspace-manager/demo-objects-prefill-data/workspace-member";
import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from "src/engine/workspace-manager/demo-objects-prefill-data/seed-workspace-member-with-demo-data";
export const peopleDemo = [
{

View File

@ -1,8 +1,8 @@
import { EntityManager } from 'typeorm';
import { companiesDemo } from 'src/engine/workspace-manager/demo-objects-prefill-data/companies-demo.json';
import { COMPANIES_DEMO } from 'src/engine/workspace-manager/demo-objects-prefill-data/companies-demo.json';
export const companyPrefillDemoData = async (
export const seedCompanyWithDemoData = async (
entityManager: EntityManager,
schemaName: string,
) => {
@ -22,7 +22,7 @@ export const companyPrefillDemoData = async (
])
.orIgnore()
.values(
companiesDemo.map((company, index) => ({ ...company, position: index })),
COMPANIES_DEMO.map((company, index) => ({ ...company, position: index })),
)
.returning('*')
.execute();

View File

@ -1,4 +1,4 @@
import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from 'src/engine/workspace-manager/demo-objects-prefill-data/workspace-member';
import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from 'src/engine/workspace-manager/demo-objects-prefill-data/seed-workspace-member-with-demo-data';
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
@ -32,7 +32,7 @@ const generateOpportunities = (companies) => {
}));
};
export const opportunityPrefillDemoData = async (
export const seedOpportunityWithDemoData = async (
entityManager: EntityManager,
schemaName: string,
) => {

View File

@ -2,7 +2,7 @@ import { EntityManager } from 'typeorm';
import { peopleDemo } from 'src/engine/workspace-manager/demo-objects-prefill-data/people-demo.json';
export const personPrefillDemoData = async (
export const seedPersonWithDemoData = async (
entityManager: EntityManager,
schemaName: string,
) => {

View File

@ -8,7 +8,7 @@ export const DEMO_SEED_WORKSPACE_MEMBER_IDS = {
TIM: '20202020-1553-45c6-a028-5a9064cce07e',
};
export const workspaceMemberPrefillData = async (
export const seedWorkspaceMemberWithDemoData = async (
entityManager: EntityManager,
schemaName: string,
) => {

View File

@ -3,13 +3,13 @@ import { DataSource, EntityManager } from 'typeorm';
import { seedWorkspaceFavorites } from 'src/database/typeorm-seeds/workspace/favorites';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite';
import { companyPrefillDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/company';
import { opportunityPrefillDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/opportunity';
import { personPrefillDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/person';
import { workspaceMemberPrefillData } from 'src/engine/workspace-manager/demo-objects-prefill-data/workspace-member';
import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view';
import { seedCompanyWithDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/seed-company-with-demo-data';
import { seedOpportunityWithDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/seed-opportunity-with-demo-data';
import { seedPersonWithDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/seed-person-with-demo-data';
import { seedWorkspaceMemberWithDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/seed-workspace-member-with-demo-data';
import { seedViewWithDemoData } from 'src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data';
export const demoObjectsPrefillData = async (
export const seedWorkspaceWithDemoData = async (
workspaceDataSource: DataSource,
schemaName: string,
objectMetadata: ObjectMetadataEntity[],
@ -30,11 +30,11 @@ export const demoObjectsPrefillData = async (
await workspaceDataSource.transaction(
async (entityManager: EntityManager) => {
await companyPrefillDemoData(entityManager, schemaName);
await personPrefillDemoData(entityManager, schemaName);
await opportunityPrefillDemoData(entityManager, schemaName);
await seedCompanyWithDemoData(entityManager, schemaName);
await seedPersonWithDemoData(entityManager, schemaName);
await seedOpportunityWithDemoData(entityManager, schemaName);
const viewDefinitionsWithId = await viewPrefillData(
const viewDefinitionsWithId = await seedViewWithDemoData(
entityManager,
schemaName,
objectMetadataMap,
@ -48,7 +48,7 @@ export const demoObjectsPrefillData = async (
entityManager,
schemaName,
);
await workspaceMemberPrefillData(entityManager, schemaName);
await seedWorkspaceMemberWithDemoData(entityManager, schemaName);
},
);
};

View File

@ -13,7 +13,7 @@ import { workflowRunsAllView } from 'src/engine/workspace-manager/standard-objec
import { workflowVersionsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view';
import { workflowsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view';
export const viewPrefillData = async (
export const seedViewWithDemoData = async (
entityManager: EntityManager,
schemaName: string,
objectMetadataMap: Record<string, ObjectMetadataEntity>,

View File

@ -5,7 +5,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite';
import { companyPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/company';
import { personPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/person';
import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view';
import { seedViewWithDemoData } from 'src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data';
export const standardObjectsPrefillData = async (
workspaceDataSource: DataSource,
@ -37,7 +37,7 @@ export const standardObjectsPrefillData = async (
workspaceDataSource.transaction(async (entityManager: EntityManager) => {
await companyPrefillData(entityManager, schemaName);
await personPrefillData(entityManager, schemaName);
const viewDefinitionsWithId = await viewPrefillData(
const viewDefinitionsWithId = await seedViewWithDemoData(
entityManager,
schemaName,
objectMetadataMap,

View File

@ -4,6 +4,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { SeederModule } from 'src/engine/seeder/seeder.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@ -15,6 +16,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
WorkspaceDataSourceModule,
WorkspaceMigrationModule,
ObjectMetadataModule,
SeederModule,
DataSourceModule,
WorkspaceSyncMetadataModule,
WorkspaceHealthModule,

View File

@ -6,8 +6,13 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { PETS_DATA_SEEDS } from 'src/engine/seeder/data-seeds/pets-data-seeds';
import { SURVEY_RESULTS_DATA_SEEDS } from 'src/engine/seeder/data-seeds/survey-results-data-seeds';
import { PETS_METADATA_SEEDS } from 'src/engine/seeder/metadata-seeds/pets-metadata-seeds';
import { SURVEY_RESULTS_METADATA_SEEDS } from 'src/engine/seeder/metadata-seeds/survey-results-metadata-seeds';
import { SeederService } from 'src/engine/seeder/seeder.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { demoObjectsPrefillData } from 'src/engine/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data';
import { seedWorkspaceWithDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/seed-workspace-with-demo-data';
import { standardObjectsPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@ -17,6 +22,7 @@ export class WorkspaceManagerService {
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly seederService: SeederService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly featureFlagService: FeatureFlagService,
@ -139,12 +145,26 @@ export class WorkspaceManagerService {
workspaceId,
);
await demoObjectsPrefillData(
await seedWorkspaceWithDemoData(
workspaceDataSource,
dataSourceMetadata.schema,
createdObjectMetadata,
isWorkflowEnabled,
);
await this.seederService.seedCustomObjects(
dataSourceMetadata.id,
workspaceId,
PETS_METADATA_SEEDS,
PETS_DATA_SEEDS,
);
await this.seederService.seedCustomObjects(
dataSourceMetadata.id,
workspaceId,
SURVEY_RESULTS_METADATA_SEEDS,
SURVEY_RESULTS_DATA_SEEDS,
);
}
/**

View File

@ -1,6 +1,7 @@
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
RelationMetadataType,
@ -31,7 +32,7 @@ export enum WorkflowVersionStatus {
ARCHIVED = 'ARCHIVED',
}
const WorkflowVersionStatusOptions = [
const WorkflowVersionStatusOptions: FieldMetadataComplexOption[] = [
{
value: WorkflowVersionStatus.DRAFT,
label: 'Draft',

View File

@ -25,6 +25,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
export enum WorkflowStatus {
DRAFT = 'DRAFT',
@ -32,7 +33,7 @@ export enum WorkflowStatus {
DEACTIVATED = 'DEACTIVATED',
}
const WorkflowStatusOptions = [
const WorkflowStatusOptions: FieldMetadataComplexOption[] = [
{
value: WorkflowStatus.DRAFT,
label: 'Draft',
@ -49,7 +50,7 @@ const WorkflowStatusOptions = [
value: WorkflowStatus.DEACTIVATED,
label: 'Deactivated',
position: 2,
color: 'grey',
color: 'gray',
},
];

View File

@ -9,6 +9,7 @@ import {
ThemeContext,
ThemeType,
} from '@ui/theme';
import { isDefined } from '@ui/utilities';
const spacing5 = THEME_COMMON.spacing(5);
const spacing2 = THEME_COMMON.spacing(2);
@ -22,8 +23,20 @@ const StyledTag = styled.h3<{
preventShrink?: boolean;
}>`
align-items: center;
background: ${({ color, theme }) =>
color === 'transparent' ? color : theme.tag.background[color]};
background: ${({ color, theme }) => {
if (color === 'transparent') {
return 'transparent';
} else {
const themeColor = theme.tag.background[color];
if (!isDefined(themeColor)) {
console.warn(`Tag color ${color} is not defined in the theme`);
return theme.tag.background.gray;
} else {
return themeColor;
}
}
}};
border-radius: ${BORDER_COMMON.radius.sm};
color: ${({ color, theme }) =>
color === 'transparent'
@ -103,10 +116,12 @@ export const Tag = ({
variant={variant}
preventShrink={preventShrink}
>
{!!Icon && (
{isDefined(Icon) ? (
<StyledIconContainer>
<Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
) : (
<></>
)}
{preventShrink ? (
<StyledNonShrinkableText>{text}</StyledNonShrinkableText>

View File

@ -4,10 +4,10 @@ import { COLOR } from './Colors';
export const ILLUSTRATION_ICON_DARK = {
color: {
blue: COLOR.blue50,
grey: GRAY_SCALE.gray50,
gray: GRAY_SCALE.gray50,
},
fill: {
blue: COLOR.blue70,
grey: GRAY_SCALE.gray70,
gray: GRAY_SCALE.gray70,
},
};

View File

@ -4,10 +4,10 @@ import { COLOR } from './Colors';
export const ILLUSTRATION_ICON_LIGHT = {
color: {
blue: COLOR.blue40,
grey: GRAY_SCALE.gray40,
gray: GRAY_SCALE.gray40,
},
fill: {
blue: COLOR.blue20,
grey: GRAY_SCALE.gray20,
gray: GRAY_SCALE.gray20,
},
};