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);