Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored and improved seeds #8695

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@
"nx",
"run",
"twenty-server:command",
"my-command",
"--my-parameter value",
"workspace:seed:demo",
lucasbordeau marked this conversation as resolved.
Show resolved Hide resolved
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const FieldInput = ({
onShiftTab={onShiftTab}
/>
) : isFieldRichText(fieldDefinition) ? (
<RichTextFieldInput />
<RichTextFieldInput onClickOutside={onClickOutside} />
lucasbordeau marked this conversation as resolved.
Show resolved Hide resolved
) : isFieldArray(fieldDefinition) ? (
<ArrayFieldInput
onCancel={onCancel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,6 +113,10 @@ export const usePersistField = () => {
isFieldRawJson(fieldDefinition) &&
isFieldRawJsonValue(valueToPersist);

const fieldIsRichText =
isFieldRichText(fieldDefinition) &&
isFieldRichTextValue(valueToPersist);

const fieldIsArray =
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);

Expand All @@ -131,7 +137,8 @@ export const usePersistField = () => {
fieldIsMultiSelect ||
fieldIsAddress ||
fieldIsRawJson ||
fieldIsArray;
fieldIsArray ||
fieldIsRichText;

if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName;
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 : '';
lucasbordeau marked this conversation as resolved.
Show resolved Hide resolved

const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldRichTextValue>(`${recordId}-${fieldName}`);

const draftValue = useRecoilValue(getDraftValueSelector());

const draftValueParsed: PartialBlock[] = isNonEmptyString(draftValue)
? JSON.parse(draftValue)
: draftValue;
Comment on lines +42 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: JSON.parse could throw if draftValue is malformed JSON - needs try/catch


const persistField = usePersistField();

const persistRichTextField = (nextValue: PartialBlock[]) => {
if (!nextValue) persistField(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing return after setting null - could cause undefined behavior by continuing execution


const parsedValueToPersist = JSON.stringify(nextValue);

try {
persistField(parsedValueToPersist);
} catch (error) {
console.error('Failed to persist rich text field', error);
}
};

return {
draftValue: draftValueParsed,
setDraftValue,
maxWidth,
fieldDefinition,
fieldValue: fieldRichTextValue,
setFieldValue,
hotkeyScope,
persistRichTextField,
};
};
Original file line number Diff line number Diff line change
@@ -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 = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: hook uses FieldContext but doesn't handle case where context is undefined

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: parseJson could return undefined/null - need to handle this case explicitly to avoid runtime errors when using fieldValueParsed


return {
fieldDefinition,
fieldValue: fieldValueParsed,
hotkeyScope,
};
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,76 @@
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
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 { PartialBlock } from '@blocknote/core';
import { useCreateBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';

export const RichTextFieldInput = () => {
return <RichTextFieldDisplay />;
import { 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 { draftValue, hotkeyScope, persistRichTextField } = useRichTextField();

const editor = useCreateBlockNote({
initialContent: draftValue,
domAttributes: { editor: { class: 'editor' } },
schema: BLOCK_SCHEMA,
});

lucasbordeau marked this conversation as resolved.
Show resolved Hide resolved
// const { refs, floatingStyles } = useFloating({
// placement: 'bottom',
// middleware: [
// flip(),
// size({
// padding: 32,
// apply: ({ availableHeight, elements }) => {
// elements.floating.style.maxHeight =
// availableHeight >= elements.floating.scrollHeight
// ? ''
// : `${availableHeight}px`;

// elements.floating.style.height = 'auto';
// },
// boundary: document.querySelector('#root') ?? undefined,
// }),
// offset({
// crossAxis: -6,
// mainAxis: 8,
// }),
// ],
// whileElementsMounted: autoUpdate,
// strategy: 'absolute',
// });

const handleClickOutside = (event: MouseEvent | TouchEvent) => {
onClickOutside?.(() => persistRichTextField(editor.document), event);
};

useRegisterInputEvents<PartialBlock[]>({
inputRef: containerRef,
inputValue: draftValue,
onClickOutside: handleClickOutside,
hotkeyScope,
});

return (
<StyledRichTextContainer ref={containerRef}>
<BlockEditor editor={editor} />
</StyledRichTextContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ export type FieldDateMetadata = {
};
};

export type FieldNumberVariant = 'number' | 'percentage';

export type FieldNumberMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
placeHolder: string;
isPositive?: boolean;
settings?: {
decimals?: number;
type?: 'percentage' | 'number';
type?: FieldNumberVariant;
};
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
FieldRatingMetadata,
FieldRawJsonMetadata,
FieldRelationMetadata,
FieldRichTextMetadata,
FieldSelectMetadata,
FieldTextMetadata,
FieldUuidMetadata,
Expand Down Expand Up @@ -68,7 +69,7 @@ type AssertFieldMetadataFunction = <
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: E extends 'RICH_TEXT'
? FieldTextMetadata
? FieldRichTextMetadata
: E extends 'ACTOR'
? FieldActorMetadata
: E extends 'ARRAY'
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
]);
Comment on lines +4 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Schema only allows null or string, but rich text content is typically stored as JSON string of PartialBlock[] based on usage in useRichTextField.ts


export const isFieldRichTextValue = (
fieldValue: unknown,
): fieldValue is FieldRichTextValue =>
richTextSchema.safeParse(fieldValue).success;
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';

Expand Down Expand Up @@ -43,10 +42,7 @@ export const isFieldValueReadOnly = ({
return true;
}

if (
isDefined(fieldType) &&
(isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
) {
if (isDefined(fieldType) && isFieldActor({ type: fieldType })) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -15,6 +16,10 @@ export const RecordTableCellsVisible = () => {
visibleTableColumnsComponentSelector,
);

if (!isNonEmptyArray(visibleTableColumns)) {
return null;
}

const tableColumnsAfterFirst = visibleTableColumns.slice(1);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' }",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The RichText example value should be an object, not a string. This will cause type errors with FieldRichTextValue. Keep it as { key: 'value' } without quotes.

category: 'Basic',
} as const satisfies SettingsFieldTypeConfig<FieldRichTextValue>,
[FieldMetadataType.Array]: {
Expand Down
Loading
Loading