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:
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const BlockEditorComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -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,
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
13
packages/twenty-front/src/utils/parseJson.ts
Normal file
13
packages/twenty-front/src/utils/parseJson.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@ -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'],
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
@ -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'>[] };
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
17
packages/twenty-server/src/engine/seeder/seeder.module.ts
Normal file
17
packages/twenty-server/src/engine/seeder/seeder.module.ts
Normal 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 {}
|
||||
161
packages/twenty-server/src/engine/seeder/seeder.service.ts
Normal file
161
packages/twenty-server/src/engine/seeder/seeder.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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 = [
|
||||
{
|
||||
|
||||
@ -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();
|
||||
@ -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,
|
||||
) => {
|
||||
@ -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,
|
||||
) => {
|
||||
@ -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,
|
||||
) => {
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
};
|
||||
@ -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>,
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user