Refactor usePersistField (#13775)
This PR introduces refactors and tradeoffs in the API around the events of field input. # Refactored usePersistField The hook `usePersistField` has been refactored to be used anywhere in the app, not just inside a FieldContext. This was meant to solve a bug at the beginning but now it is just used once in `RecordDetailRelationSection` outside of the context, still this is better to have this hook like that for future use cases. We also introduce `usePersistFieldFromFieldInputContext`, for an easier API inside a FieldContext. # Introduced a new `FieldInputEventContext` To remove the drill-down of events, we introduce `FieldInputEventContext`, this allows to set only once the handlers / events. In practice it allows to have an easier time maintaining the events for the many different field inputs, because it matches the pattern we already use of taking everything from a context (`FieldContext`). # Removed drill-down from FieldInput The heavy drill-down in FieldInput has been completely removed, since everything can be derived from `FieldContext` and `FieldInputEventContext`. Also there was some readonly and other specific props, but they were all drilled down from FieldContext, so it was easier to just use FieldContext where needed. # Refactored events of `MultiItemFieldInputProps` The component `MultiItemFieldInputProps` has a contrived API, here we just remove the complex part that was persisting from inside. We now only give a classic API with `onChange` and `onEscape` the rest is left to higher levels, where it should be, because this generic component shouldn't be aware of persisting things. # Extracted the parsing logic of persisted values For each input field component, we now have a clear util that was before bound to the persist call, # Tradeoff with persist times The tradeoff before was that persistField was called anywhere, before exiting the component sometimes, now it is only called by the higher levels like table or show page, which handles this abstraction. This could be challenged, however I think that having a lot of different events, and not just `handleSubmit` and `handleCancel`, convey enough meaning for the higher levels to decide what to do in each case. A `skipPersist` argument was added in events in the rare edge cases where we want to voluntarily skip persisting even with a submit or escape, but that could be challenged because we could also say that we should use cancel for that and stick to that convention. # Handling of the bug in `ActivityRichTextEditor` Initially this refactor was prioritized for solving this bug, which was very annoying for the users. But while fixing it with the new persistField hook I just understood that the problem is not just for record title cells but for anything that is open when we click on a rich text editor. The issue is described here : https://github.com/twentyhq/core-team-issues/issues/1317 So right now I just let it as is. # Stories The stories were checking that a request was sent in some cases where persist was called before a component exiting, now that persist is only called by higher-levels I just removed those tests from the stories, because that should be the responsibility of higher levels. Also a helper `getFieldInputEventContextProviderWithJestMocks` was created that exposes a context and jest mock functions for testing this new API in stories. # Miscellaneous Deactivated tui with nx by default, because it can be annoying.
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"workspaceLayout": { "appsDir": "packages", "libsDir": "packages" },
|
||||
"workspaceLayout": {
|
||||
"appsDir": "packages",
|
||||
"libsDir": "packages"
|
||||
},
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*"],
|
||||
"default": [
|
||||
"{projectRoot}/**/*"
|
||||
],
|
||||
"excludeStories": [
|
||||
"default",
|
||||
"!{projectRoot}/.storybook/*",
|
||||
@@ -29,24 +34,42 @@
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"cache": true,
|
||||
"inputs": ["^production", "production"],
|
||||
"dependsOn": ["^build"]
|
||||
"inputs": [
|
||||
"^production",
|
||||
"production"
|
||||
],
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"start": {
|
||||
"cache": true,
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"start": { "cache": true, "dependsOn": ["^build"] },
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"cache": true,
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"eslintConfig": "{projectRoot}/eslint.config.js",
|
||||
"cache": true,
|
||||
"cacheLocation": "{workspaceRoot}/.cache/eslint"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "fix": true }
|
||||
"ci": {
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"fix": {
|
||||
"fix": true
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"fmt": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -60,10 +83,16 @@
|
||||
"write": false
|
||||
},
|
||||
"configurations": {
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "write": true }
|
||||
"ci": {
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"fix": {
|
||||
"write": true
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -72,49 +101,85 @@
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "tsc -b tsconfig.json --incremental"
|
||||
},
|
||||
"configurations": { "watch": { "watch": true } },
|
||||
"dependsOn": ["^build"]
|
||||
"configurations": {
|
||||
"watch": {
|
||||
"watch": true
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeStories",
|
||||
"{workspaceRoot}/jest.preset.js"
|
||||
],
|
||||
"outputs": ["{projectRoot}/coverage"],
|
||||
"outputs": [
|
||||
"{projectRoot}/coverage"
|
||||
],
|
||||
"options": {
|
||||
"jestConfig": "{projectRoot}/jest.config.ts",
|
||||
"coverage": true,
|
||||
"coverageReporters": ["text-summary"],
|
||||
"coverageReporters": [
|
||||
"text-summary"
|
||||
],
|
||||
"cacheDirectory": "../../.cache/jest/{projectRoot}"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": { "ci": true, "maxWorkers": 3 },
|
||||
"coverage": { "coverageReporters": ["lcov", "text"] },
|
||||
"watch": { "watch": true }
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"maxWorkers": 3
|
||||
},
|
||||
"coverage": {
|
||||
"coverageReporters": [
|
||||
"lcov",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"watch": {
|
||||
"watch": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"test:e2e": { "cache": true, "dependsOn": ["^build"] },
|
||||
"test:e2e": {
|
||||
"cache": true,
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"storybook:build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeTests"
|
||||
],
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.output-dir}"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"storybook:serve:dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "storybook dev",
|
||||
@@ -123,7 +188,9 @@
|
||||
},
|
||||
"storybook:serve:static": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["storybook:build"],
|
||||
"dependsOn": [
|
||||
"storybook:build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx http-server {args.staticDir} -a={args.host} --port={args.port} --silent={args.silent}",
|
||||
@@ -136,8 +203,13 @@
|
||||
"storybook:test": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"outputs": ["{projectRoot}/coverage/storybook"],
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeTests"
|
||||
],
|
||||
"outputs": [
|
||||
"{projectRoot}/coverage/storybook"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
@@ -153,7 +225,10 @@
|
||||
},
|
||||
"storybook:test:no-coverage": {
|
||||
"executor": "nx:run-commands",
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeTests"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
@@ -180,7 +255,11 @@
|
||||
"reporter": "lcov",
|
||||
"checkCoverage": true
|
||||
},
|
||||
"configurations": { "text": { "reporter": "text" } }
|
||||
"configurations": {
|
||||
"text": {
|
||||
"reporter": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storybook:serve-and-test:static": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -206,7 +285,11 @@
|
||||
],
|
||||
"parallel": false
|
||||
},
|
||||
"configurations": { "ci": { "ci": "--exit-zero-on-changes" } }
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": "--exit-zero-on-changes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nx/jest:jest": {
|
||||
"cache": true,
|
||||
@@ -215,8 +298,15 @@
|
||||
"excludeStories",
|
||||
"{workspaceRoot}/jest.preset.js"
|
||||
],
|
||||
"options": { "passWithNoTests": true },
|
||||
"configurations": { "ci": { "ci": true, "codeCoverage": true } }
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nx/eslint:lint": {
|
||||
"cache": true,
|
||||
@@ -226,14 +316,27 @@
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*"
|
||||
]
|
||||
},
|
||||
"@nx/vite:test": { "cache": true, "inputs": ["default", "^default"] },
|
||||
"@nx/vite:test": {
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default"
|
||||
]
|
||||
},
|
||||
"@nx/vite:build": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["default", "^default"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default"
|
||||
]
|
||||
}
|
||||
},
|
||||
"installation": { "version": "21.3.11" },
|
||||
"installation": {
|
||||
"version": "21.3.11"
|
||||
},
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
"application": {
|
||||
@@ -252,12 +355,20 @@
|
||||
"unitTestRunner": "jest",
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"component": { "style": "@emotion/styled" }
|
||||
"component": {
|
||||
"style": "@emotion/styled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tasksRunnerOptions": {
|
||||
"default": { "options": { "cacheableOperations": ["storybook:build"] } }
|
||||
"default": {
|
||||
"options": {
|
||||
"cacheableOperations": [
|
||||
"storybook:build"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"useInferencePlugins": false,
|
||||
"defaultBase": "main"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { type JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest';
|
||||
import { pathsToModuleNameMapper, type JestConfigWithTsJest } from 'ts-jest';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -58,8 +58,8 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 56,
|
||||
lines: 55,
|
||||
statements: 55,
|
||||
lines: 54,
|
||||
functions: 45,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
|
||||
import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState';
|
||||
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
@@ -369,13 +370,26 @@ export const ActivityRichTextEditor = ({
|
||||
|
||||
const recordTitleCellId = getRecordFieldInputInstanceId({
|
||||
recordId: activityId,
|
||||
fieldName: labelIdentifierFieldMetadataItem?.id,
|
||||
prefix: 'activity-rich-text-editor',
|
||||
fieldName: labelIdentifierFieldMetadataItem?.name,
|
||||
// TODO: see comments below, this is a very temporary fix,
|
||||
// it won't work for the breadcrumb title input, but that's ok for now.
|
||||
prefix: RecordTitleCellContainerType.ShowPage,
|
||||
});
|
||||
|
||||
// TODO: Here instead of closing the input, as it was intially planned, we should block if there is anything open,
|
||||
// This information should be derived from the focus stack
|
||||
// The problem with this library is that it takes the focus before anything else and does not prevent the event from bubbling
|
||||
// Because of this, other events listen at the same time, and when we're in luck, the click outside gets triggered,
|
||||
// but this leaves the door open for unpredicted behavior with click handlers conflicts,
|
||||
// we recently had a bug which was deleting what the user typed and closed the right drawer if he used backspace key.
|
||||
// We could maybe use the types of components in the focus stack.
|
||||
const handleBlockEditorFocus = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
// TODO: Here we want to detect anything that is open to avoid conflicts with the library click event
|
||||
// that is not prevented and propagate to other click handlers in the app.
|
||||
// Because that is how we do in the app, for example with stacked dropdowns, we always close what's open before
|
||||
// letting the click being captured by a button or input that can capture it.
|
||||
const isRecordTitleCellOpen = snapshot
|
||||
.getLoadable(isInlineCellInEditModeFamilyState(recordTitleCellId))
|
||||
.getValue();
|
||||
|
||||
+21
-118
@@ -13,7 +13,6 @@ import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/
|
||||
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
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';
|
||||
@@ -40,147 +39,51 @@ import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInpu
|
||||
import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput';
|
||||
import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput';
|
||||
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
||||
import { type FieldInputEvent } from '../types/FieldInputEvent';
|
||||
import { type FieldRichTextV2Metadata } from '../types/FieldMetadata';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
|
||||
type FieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (
|
||||
persist: () => void,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => void;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
export const FieldInput = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onShiftTab,
|
||||
onTab,
|
||||
onClickOutside,
|
||||
isReadOnly,
|
||||
}: FieldInputProps) => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
export const FieldInput = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
<RelationToOneFieldInput />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||
<RelationFromManyFieldInput />
|
||||
) : isFieldPhones(fieldDefinition) ? (
|
||||
<PhonesFieldInput onCancel={onCancel} onClickOutside={onClickOutside} />
|
||||
<PhonesFieldInput />
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<TextFieldInput />
|
||||
) : isFieldEmails(fieldDefinition) ? (
|
||||
<EmailsFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
<EmailsFieldInput />
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<FullNameFieldInput />
|
||||
) : isFieldDateTime(fieldDefinition) ? (
|
||||
<DateTimeFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<DateTimeFieldInput />
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<DateFieldInput />
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<NumberFieldInput />
|
||||
) : isFieldLinks(fieldDefinition) ? (
|
||||
<LinksFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
<LinksFieldInput />
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<CurrencyFieldInput />
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||
<BooleanFieldInput />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||
<RatingFieldInput />
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
<SelectFieldInput />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldInput onCancel={onCancel} />
|
||||
<MultiSelectFieldInput />
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<AddressFieldInput />
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
<RawJsonFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<RawJsonFieldInput />
|
||||
) : isFieldArray(fieldDefinition) ? (
|
||||
<ArrayFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
<ArrayFieldInput />
|
||||
) : isFieldRichTextV2(fieldDefinition) ? (
|
||||
<RichTextFieldInput
|
||||
targetableObject={{
|
||||
id: recordId,
|
||||
targetObjectNameSingular: (
|
||||
fieldDefinition as {
|
||||
metadata: FieldRichTextV2Metadata;
|
||||
}
|
||||
).metadata.objectMetadataNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task,
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
onClickOutside={onClickOutside}
|
||||
onEscape={onEscape}
|
||||
/>
|
||||
<RichTextFieldInput />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type FieldInputEventArgs = {
|
||||
newValue?: unknown;
|
||||
skipPersist?: boolean;
|
||||
};
|
||||
|
||||
export type FieldInputEvent = (args: FieldInputEventArgs) => void;
|
||||
|
||||
export type FieldInputClickOutsideEvent = (args: {
|
||||
newValue?: unknown;
|
||||
skipPersist?: boolean;
|
||||
event?: MouseEvent | TouchEvent;
|
||||
}) => void;
|
||||
|
||||
export type FieldInputEventContextType = {
|
||||
onEnter?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
onEscape?: FieldInputEvent;
|
||||
onSubmit?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
};
|
||||
|
||||
export const FieldInputEventContext = createContext<FieldInputEventContextType>(
|
||||
{} as FieldInputEventContextType,
|
||||
);
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
|
||||
import { responseData } from '@/object-record/hooks/__mocks__/useUpdateOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import {
|
||||
phonesFieldDefinition,
|
||||
relationFieldDefinition,
|
||||
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
import {
|
||||
FieldContext,
|
||||
type RecordUpdateHook,
|
||||
type RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { gql } from '@apollo/client';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { type ReactNode, act } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
|
||||
const query = gql`
|
||||
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
|
||||
updatePerson(id: $idToUpdate, data: $input) {
|
||||
${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const phoneMock = {
|
||||
primaryPhoneNumber: '123 456',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: [],
|
||||
};
|
||||
|
||||
const mockPersonResponse = { ...responseData, id: 'recordId' };
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: {
|
||||
idToUpdate: 'recordId',
|
||||
input: {
|
||||
phones: phoneMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updatePerson: mockPersonResponse,
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: {
|
||||
idToUpdate: 'recordId',
|
||||
input: { companyId: 'companyId' },
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updatePerson: mockPersonResponse,
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const recordId = 'recordId';
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
const getWrapper =
|
||||
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
|
||||
({ children }: { children: ReactNode }) => {
|
||||
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
updateOneRecord?.({
|
||||
idToUpdate: variables.where.id as string,
|
||||
updateOneRecordInput: variables.updateOneRecordInput,
|
||||
});
|
||||
};
|
||||
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
return (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
useUpdateRecord: useUpdateOneRecordMutation,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FieldContext.Provider>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const PhoneWrapper = getWrapper(phonesFieldDefinition);
|
||||
const RelationWrapper = getWrapper(relationFieldDefinition);
|
||||
|
||||
describe('usePersistField', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should work as expected', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const entityFields = useRecoilValue(
|
||||
recordStoreFamilySelector({ recordId, fieldName: 'phones' }),
|
||||
);
|
||||
|
||||
return {
|
||||
persistField: usePersistField(),
|
||||
entityFields,
|
||||
};
|
||||
},
|
||||
{ wrapper: PhoneWrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.persistField({
|
||||
primaryPhoneNumber: '123 456',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: [],
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist relation field', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const entityFields = useRecoilValue(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: 'company',
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
persistField: usePersistField(),
|
||||
entityFields,
|
||||
};
|
||||
},
|
||||
{ wrapper: RelationWrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.persistField({ id: 'companyId' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip persistence value has not changed', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const entityFields = useRecoilValue(
|
||||
recordStoreFamilySelector({ recordId, fieldName: 'phones' }),
|
||||
);
|
||||
|
||||
return {
|
||||
persistField: usePersistField(),
|
||||
entityFields,
|
||||
};
|
||||
},
|
||||
{ wrapper: PhoneWrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.persistField(phoneMock);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[0].result).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip persistence when relation field value has not changed', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const entityFields = useRecoilValue(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: 'company',
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
persistField: usePersistField(),
|
||||
entityFields,
|
||||
};
|
||||
},
|
||||
{ wrapper: RelationWrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.persistField({ id: 'companyId' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[1].result).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+36
-25
@@ -1,8 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import {
|
||||
type FieldMetadata,
|
||||
type FieldRelationMetadata,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
||||
@@ -25,6 +27,8 @@ import { isFieldSelect } from '@/object-record/record-field/types/guards/isField
|
||||
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField';
|
||||
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||
@@ -34,7 +38,6 @@ import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
||||
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||
@@ -48,18 +51,30 @@ import { isFieldRatingValue } from '../types/guards/isFieldRatingValue';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
|
||||
|
||||
export const usePersistField = () => {
|
||||
const {
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
useUpdateRecord = () => [],
|
||||
} = useContext(FieldContext);
|
||||
export const usePersistField = ({
|
||||
objectMetadataItemId,
|
||||
}: {
|
||||
objectMetadataItemId: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const [updateRecord] = useUpdateRecord();
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
|
||||
});
|
||||
|
||||
const persistField = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(valueToPersist: unknown) => {
|
||||
({
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
valueToPersist,
|
||||
}: {
|
||||
recordId: string;
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
valueToPersist: unknown;
|
||||
}) => {
|
||||
const fieldIsRelationToOneObject =
|
||||
isFieldRelationToOneObject(
|
||||
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
|
||||
@@ -183,24 +198,20 @@ export const usePersistField = () => {
|
||||
);
|
||||
|
||||
if (fieldIsRelationToOneObject) {
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: recordId },
|
||||
updateOneRecordInput: {
|
||||
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
||||
valueToPersist?.id ?? null,
|
||||
},
|
||||
updateOneRecord?.({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
||||
valueToPersist?.id ?? null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: recordId },
|
||||
updateOneRecordInput: {
|
||||
[fieldName]: valueToPersist,
|
||||
},
|
||||
updateOneRecord?.({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[fieldName]: valueToPersist,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -213,7 +224,7 @@ export const usePersistField = () => {
|
||||
);
|
||||
}
|
||||
},
|
||||
[recordId, fieldDefinition, updateRecord],
|
||||
[updateOneRecord],
|
||||
);
|
||||
|
||||
return persistField;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const usePersistFieldFromFieldInputContext = () => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItemToFind) =>
|
||||
objectMetadataItemToFind.fields.some(
|
||||
(fieldMetadataItemToFind) =>
|
||||
fieldMetadataItemToFind.id === fieldDefinition.fieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const persistField = usePersistField({
|
||||
objectMetadataItemId: objectMetadataItem?.id ?? '',
|
||||
});
|
||||
|
||||
const persistFieldFromFieldInputContext = (valueToPersist: unknown) => {
|
||||
persistField({
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
valueToPersist,
|
||||
});
|
||||
};
|
||||
|
||||
return { persistFieldFromFieldInputContext };
|
||||
};
|
||||
+11
-3
@@ -1,10 +1,18 @@
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { type FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRecordFieldInput = <FieldValue>() => {
|
||||
const setDraftValue = useSetRecoilComponentState(
|
||||
recordFieldInputDraftValueComponentState,
|
||||
const recordFieldInputDraftValueCallbackState =
|
||||
useRecoilComponentCallbackState(recordFieldInputDraftValueComponentState);
|
||||
|
||||
const setDraftValue = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(newValue: unknown) => {
|
||||
set(recordFieldInputDraftValueCallbackState, newValue);
|
||||
},
|
||||
[recordFieldInputDraftValueCallbackState],
|
||||
);
|
||||
|
||||
const isDraftValueEmpty = (
|
||||
|
||||
-13
@@ -2,14 +2,12 @@ import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { type FieldAddressValue } from '../../types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldAddress } from '../../types/guards/isFieldAddress';
|
||||
@@ -32,16 +30,6 @@ export const useAddressField = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistAddressField = (newValue: FieldAddressValue) => {
|
||||
if (!isFieldAddressValue(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistField(newValue);
|
||||
};
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldAddressValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
@@ -54,6 +42,5 @@ export const useAddressField = () => {
|
||||
setFieldValue,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
persistAddressField,
|
||||
};
|
||||
};
|
||||
|
||||
+9
-13
@@ -1,10 +1,11 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { type FieldArrayValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||
import { arraySchema } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@@ -23,22 +24,17 @@ export const useArrayField = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { setDraftValue } = useRecordFieldInput<FieldArrayValue>();
|
||||
|
||||
const persistArrayField = (nextValue: string[]) => {
|
||||
if (!nextValue) persistField(null);
|
||||
|
||||
try {
|
||||
persistField(arraySchema.parse(nextValue));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
const draftValue = useRecoilComponentValue(
|
||||
recordFieldInputDraftValueComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
fieldValue,
|
||||
fieldDefinition,
|
||||
setFieldValue,
|
||||
persistArrayField,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
};
|
||||
};
|
||||
|
||||
-28
@@ -2,17 +2,14 @@ import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { type FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
||||
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||
|
||||
export const useCurrencyField = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
@@ -32,30 +29,6 @@ export const useCurrencyField = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistCurrencyField = ({
|
||||
amountText,
|
||||
currencyCode,
|
||||
}: {
|
||||
amountText: string;
|
||||
currencyCode: string;
|
||||
}) => {
|
||||
const amount = parseFloat(amountText);
|
||||
|
||||
const newCurrencyValue = {
|
||||
amountMicros: isNaN(amount)
|
||||
? null
|
||||
: convertCurrencyAmountToCurrencyMicros(amount),
|
||||
currencyCode,
|
||||
};
|
||||
|
||||
if (!isFieldCurrencyValue(newCurrencyValue)) {
|
||||
return;
|
||||
}
|
||||
persistField(newCurrencyValue);
|
||||
};
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldCurrencyValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
@@ -70,7 +43,6 @@ export const useCurrencyField = () => {
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
setFieldValue,
|
||||
persistCurrencyField,
|
||||
defaultValue,
|
||||
};
|
||||
};
|
||||
|
||||
-13
@@ -1,11 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { type FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
||||
import { emailsSchema } from '@/object-record/record-field/types/guards/isFieldEmailsValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -34,22 +32,11 @@ export const useEmailsField = () => {
|
||||
recordFieldInputDraftValueComponentState,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistEmailsField = (nextValue: FieldEmailsValue) => {
|
||||
try {
|
||||
persistField(emailsSchema.parse(nextValue));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
setFieldValue,
|
||||
persistEmailsField,
|
||||
};
|
||||
};
|
||||
|
||||
-13
@@ -2,13 +2,11 @@ import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { type FieldFullNameValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@@ -31,16 +29,6 @@ export const useFullNameField = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistFullNameField = (newValue: FieldFullNameValue) => {
|
||||
if (!isFieldFullNameValue(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistField(newValue);
|
||||
};
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldFullNameValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
@@ -53,6 +41,5 @@ export const useFullNameField = () => {
|
||||
setFieldValue,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
persistFullNameField,
|
||||
};
|
||||
};
|
||||
|
||||
-14
@@ -1,7 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { type FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
@@ -32,18 +31,6 @@ export const useJsonField = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistJsonField = (nextValue: string) => {
|
||||
if (!nextValue) persistField(null);
|
||||
|
||||
try {
|
||||
persistField(JSON.parse(nextValue));
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldJsonValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
@@ -62,6 +49,5 @@ export const useJsonField = () => {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
persistJsonField,
|
||||
};
|
||||
};
|
||||
|
||||
-13
@@ -1,11 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { type FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
import { linksSchema } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -34,22 +32,11 @@ export const useLinksField = () => {
|
||||
recordFieldInputDraftValueComponentState,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistLinksField = (nextValue: FieldLinksValue) => {
|
||||
try {
|
||||
persistField(linksSchema.parse(nextValue));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
setFieldValue,
|
||||
persistLinksField,
|
||||
};
|
||||
};
|
||||
|
||||
-3
@@ -2,7 +2,6 @@ import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { type FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
@@ -34,7 +33,6 @@ export const useMultiSelectField = () => {
|
||||
const fieldMultiSelectValues = isFieldMultiSelectValue(fieldValues)
|
||||
? fieldValues
|
||||
: null;
|
||||
const persistField = usePersistField();
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldMultiSelectValue>();
|
||||
|
||||
@@ -45,7 +43,6 @@ export const useMultiSelectField = () => {
|
||||
return {
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
persistField,
|
||||
fieldValues: fieldMultiSelectValues,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
|
||||
-31
@@ -6,16 +6,9 @@ import { type FieldNumberValue } from '@/object-record/record-field/types/FieldM
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
canBeCastAsNumberOrNull,
|
||||
castAsNumberOrNull,
|
||||
} from '~/utils/cast-as-number-or-null';
|
||||
|
||||
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldNumber } from '../../types/guards/isFieldNumber';
|
||||
|
||||
@@ -33,29 +26,6 @@ export const useNumberField = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistNumberField = (newValue: string) => {
|
||||
if (fieldDefinition?.metadata?.settings?.type === 'percentage') {
|
||||
const newValueEscaped = newValue.replaceAll('%', '');
|
||||
if (!canBeCastAsNumberOrNull(newValueEscaped)) {
|
||||
return;
|
||||
}
|
||||
const castedValue = castAsNumberOrNull(newValue);
|
||||
if (!isNull(castedValue)) {
|
||||
persistField(castedValue / 100);
|
||||
return;
|
||||
}
|
||||
persistField(null);
|
||||
return;
|
||||
}
|
||||
if (!canBeCastAsNumberOrNull(newValue)) {
|
||||
return;
|
||||
}
|
||||
const castedValue = castAsNumberOrNull(newValue);
|
||||
persistField(castedValue);
|
||||
};
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldNumberValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
@@ -68,6 +38,5 @@ export const useNumberField = () => {
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
setFieldValue,
|
||||
persistNumberField,
|
||||
};
|
||||
};
|
||||
|
||||
-13
@@ -1,11 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { type FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||
import { phonesSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -34,22 +32,11 @@ export const usePhonesField = () => {
|
||||
recordFieldInputDraftValueComponentState,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistPhonesField = (nextValue: FieldPhonesValue) => {
|
||||
try {
|
||||
persistField(phonesSchema.parse(nextValue));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
setFieldValue,
|
||||
persistPhonesField,
|
||||
};
|
||||
};
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { type 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 { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import type { 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, maxWidth } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
isFieldRichText,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldRichTextValue>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
const fieldRichTextValue = isFieldRichTextValue(fieldValue) ? fieldValue : '';
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldRichTextValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
recordFieldInputDraftValueComponentState,
|
||||
);
|
||||
|
||||
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,
|
||||
persistRichTextField,
|
||||
};
|
||||
};
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import {
|
||||
type FieldRichTextV2Value,
|
||||
type 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 { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import type { PartialBlock } from '@blocknote/core';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
|
||||
export const useRichTextV2Field = () => {
|
||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
isFieldRichTextV2,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldRichTextValue>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
const fieldRichTextV2Value = isFieldRichTextV2Value(fieldValue)
|
||||
? fieldValue
|
||||
: ({ blocknote: null, markdown: null } as FieldRichTextV2Value);
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldRichTextValue>();
|
||||
|
||||
const draftValue = useRecoilComponentValue(
|
||||
recordFieldInputDraftValueComponentState,
|
||||
);
|
||||
|
||||
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: fieldRichTextV2Value,
|
||||
setFieldValue,
|
||||
persistRichTextField,
|
||||
};
|
||||
};
|
||||
-3
@@ -1,7 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@@ -29,7 +28,6 @@ export const useSelectField = () => {
|
||||
);
|
||||
|
||||
const fieldSelectValue = isFieldSelectValue(fieldValue) ? fieldValue : null;
|
||||
const persistField = usePersistField();
|
||||
|
||||
const { setDraftValue } = useRecordFieldInput<FieldSelectValue>();
|
||||
|
||||
@@ -40,7 +38,6 @@ export const useSelectField = () => {
|
||||
return {
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
persistField,
|
||||
fieldValue: fieldSelectValue,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
|
||||
+13
-26
@@ -3,31 +3,17 @@ import { type FieldAddressDraftValue } from '@/object-record/record-field/types/
|
||||
import { AddressInput } from '@/ui/field/input/components/AddressInput';
|
||||
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export type AddressFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const AddressFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: AddressFieldInputProps) => {
|
||||
export const AddressFieldInput = () => {
|
||||
const { draftValue, setDraftValue, fieldDefinition } = useAddressField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { onEnter, onTab, onShiftTab, onEscape, onClickOutside } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const convertToAddress = (
|
||||
newAddress: FieldAddressDraftValue | undefined,
|
||||
@@ -47,27 +33,28 @@ export const AddressFieldInput = ({
|
||||
|
||||
const subFields =
|
||||
settings && 'subFields' in settings ? settings.subFields : undefined;
|
||||
|
||||
const handleEnter = (newAddress: FieldAddressDraftValue) => {
|
||||
onEnter?.(() => persistField(convertToAddress(newAddress)));
|
||||
onEnter?.({ newValue: convertToAddress(newAddress) });
|
||||
};
|
||||
|
||||
const handleTab = (newAddress: FieldAddressDraftValue) => {
|
||||
onTab?.(() => persistField(convertToAddress(newAddress)));
|
||||
onTab?.({ newValue: convertToAddress(newAddress) });
|
||||
};
|
||||
|
||||
const handleShiftTab = (newAddress: FieldAddressDraftValue) => {
|
||||
onShiftTab?.(() => persistField(convertToAddress(newAddress)));
|
||||
onShiftTab?.({ newValue: convertToAddress(newAddress) });
|
||||
};
|
||||
|
||||
const handleEscape = (newAddress: FieldAddressDraftValue) => {
|
||||
onEscape?.(() => persistField(convertToAddress(newAddress)));
|
||||
onEscape?.({ newValue: convertToAddress(newAddress) });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newAddress: FieldAddressDraftValue,
|
||||
) => {
|
||||
onClickOutside?.(() => persistField(convertToAddress(newAddress)), event);
|
||||
onClickOutside?.({ newValue: convertToAddress(newAddress), event });
|
||||
};
|
||||
|
||||
const handleChange = (newAddress: FieldAddressDraftValue) => {
|
||||
|
||||
+33
-18
@@ -1,35 +1,50 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField';
|
||||
import { ArrayFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem';
|
||||
import { MultiItemFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiItemFieldInput';
|
||||
import { useMemo } from 'react';
|
||||
import { arraySchema } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type ArrayFieldInputProps = {
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||
};
|
||||
export const ArrayFieldInput = () => {
|
||||
const { setDraftValue, draftValue, fieldDefinition } = useArrayField();
|
||||
|
||||
export const ArrayFieldInput = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: ArrayFieldInputProps) => {
|
||||
const { persistArrayField, fieldValue, fieldDefinition } = useArrayField();
|
||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
||||
|
||||
const arrayItems = useMemo<Array<string>>(
|
||||
() => (Array.isArray(fieldValue) ? fieldValue : []),
|
||||
[fieldValue],
|
||||
() => (Array.isArray(draftValue) ? draftValue : []),
|
||||
[draftValue],
|
||||
);
|
||||
|
||||
const handleChange = (newValue: any[]) => {
|
||||
if (!isDefined(newValue)) setDraftValue(null);
|
||||
|
||||
const parseResponse = arraySchema.safeParse(newValue);
|
||||
|
||||
if (parseResponse.success) {
|
||||
setDraftValue(parseResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
_newValue: any,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => {
|
||||
onClickOutside?.({ newValue: draftValue, event });
|
||||
};
|
||||
|
||||
const handleEscape = (_newValue: any) => {
|
||||
onEscape?.({ newValue: draftValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiItemFieldInput
|
||||
newItemLabel="Add Item"
|
||||
items={arrayItems}
|
||||
onPersist={persistArrayField}
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(persist, event) => {
|
||||
onClickOutside?.(event);
|
||||
persist();
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onEscape={handleEscape}
|
||||
onClickOutside={handleClickOutside}
|
||||
placeholder="Enter value"
|
||||
fieldMetadataType={FieldMetadataType.ARRAY}
|
||||
renderItem={({ value, index, handleEdit, handleDelete }) => (
|
||||
|
||||
+9
-12
@@ -1,33 +1,30 @@
|
||||
import { BooleanInput } from '@/ui/field/input/components/BooleanInput';
|
||||
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useContext } from 'react';
|
||||
import { useBooleanField } from '../../hooks/useBooleanField';
|
||||
|
||||
export type BooleanFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
readonly?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export const BooleanFieldInput = ({
|
||||
onSubmit,
|
||||
readonly,
|
||||
testId,
|
||||
}: BooleanFieldInputProps) => {
|
||||
export const BooleanFieldInput = ({ testId }: BooleanFieldInputProps) => {
|
||||
const { fieldValue } = useBooleanField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { isRecordFieldReadOnly } = useContext(FieldContext);
|
||||
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const handleToggle = (newValue: boolean) => {
|
||||
onSubmit?.(() => persistField(newValue));
|
||||
onSubmit?.({ newValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<BooleanInput
|
||||
value={fieldValue ?? ''}
|
||||
onToggle={handleToggle}
|
||||
readonly={readonly}
|
||||
readonly={isRecordFieldReadOnly}
|
||||
testId={testId}
|
||||
/>
|
||||
);
|
||||
|
||||
+50
-36
@@ -6,31 +6,21 @@ import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
|
||||
|
||||
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
type CurrencyFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
export const CurrencyFieldInput = () => {
|
||||
const { draftValue, setDraftValue, defaultValue } = useCurrencyField();
|
||||
|
||||
export const CurrencyFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: CurrencyFieldInputProps) => {
|
||||
const { draftValue, persistCurrencyField, setDraftValue, defaultValue } =
|
||||
useCurrencyField();
|
||||
const { onClickOutside, onEnter, onEscape, onShiftTab, onTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
@@ -55,21 +45,44 @@ export const CurrencyFieldInput = ({
|
||||
? defaultCurrencyCodeWithoutSQLQuotes
|
||||
: CurrencyCode.USD;
|
||||
|
||||
const getNewCurrencyValue = ({
|
||||
amountText,
|
||||
currencyCode,
|
||||
}: {
|
||||
amountText: string;
|
||||
currencyCode: string;
|
||||
}) => {
|
||||
const amount = parseFloat(amountText);
|
||||
|
||||
const newCurrencyValue = {
|
||||
amountMicros: isNaN(amount)
|
||||
? null
|
||||
: convertCurrencyAmountToCurrencyMicros(amount),
|
||||
currencyCode,
|
||||
};
|
||||
|
||||
if (!isFieldCurrencyValue(newCurrencyValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return newCurrencyValue;
|
||||
};
|
||||
|
||||
const handleEnter = (newValue: string) => {
|
||||
onEnter?.(() => {
|
||||
persistCurrencyField({
|
||||
onEnter?.({
|
||||
newValue: getNewCurrencyValue({
|
||||
amountText: newValue,
|
||||
currencyCode,
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleEscape = (newValue: string) => {
|
||||
onEscape?.(() => {
|
||||
persistCurrencyField({
|
||||
onEscape?.({
|
||||
newValue: getNewCurrencyValue({
|
||||
amountText: newValue,
|
||||
currencyCode,
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -77,30 +90,31 @@ export const CurrencyFieldInput = ({
|
||||
event: MouseEvent | TouchEvent,
|
||||
newValue: string,
|
||||
) => {
|
||||
onClickOutside?.(() => {
|
||||
persistCurrencyField({
|
||||
onClickOutside?.({
|
||||
newValue: getNewCurrencyValue({
|
||||
amountText: newValue,
|
||||
currencyCode,
|
||||
});
|
||||
}, event);
|
||||
}),
|
||||
event,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTab = (newValue: string) => {
|
||||
onTab?.(() => {
|
||||
persistCurrencyField({
|
||||
onTab?.({
|
||||
newValue: getNewCurrencyValue({
|
||||
amountText: newValue,
|
||||
currencyCode,
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleShiftTab = (newValue: string) => {
|
||||
onShiftTab?.(() =>
|
||||
persistCurrencyField({
|
||||
onShiftTab?.({
|
||||
newValue: getNewCurrencyValue({
|
||||
amountText: newValue,
|
||||
currencyCode,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
|
||||
+15
-29
@@ -1,41 +1,27 @@
|
||||
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
|
||||
import { DateInput } from '@/ui/field/input/components/DateInput';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Nullable } from 'twenty-ui/utilities';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
|
||||
type FieldInputEvent = (persist: () => void) => void;
|
||||
|
||||
type DateFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onClear?: FieldInputEvent;
|
||||
onSubmit?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DateFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onClear,
|
||||
onSubmit,
|
||||
}: DateFieldInputProps) => {
|
||||
export const DateFieldInput = () => {
|
||||
const { fieldValue, setDraftValue } = useDateField();
|
||||
|
||||
const { onEnter, onEscape, onClickOutside, onSubmit } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistDate = (newDate: Nullable<Date>) => {
|
||||
const getDateToPersist = (newDate: Nullable<Date>) => {
|
||||
if (!isDefined(newDate)) {
|
||||
persistField(null);
|
||||
return null;
|
||||
} else {
|
||||
const newDateWithoutTime = `${newDate?.getFullYear()}-${(
|
||||
newDate?.getMonth() + 1
|
||||
@@ -43,27 +29,27 @@ export const DateFieldInput = ({
|
||||
.toString()
|
||||
.padStart(2, '0')}-${newDate?.getDate().toString().padStart(2, '0')}`;
|
||||
|
||||
persistField(newDateWithoutTime);
|
||||
return newDateWithoutTime;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (newDate: Nullable<Date>) => {
|
||||
onEnter?.(() => persistDate(newDate));
|
||||
onEnter?.({ newValue: getDateToPersist(newDate) });
|
||||
};
|
||||
|
||||
const handleSubmit = (newDate: Nullable<Date>) => {
|
||||
onSubmit?.(() => persistDate(newDate));
|
||||
onSubmit?.({ newValue: getDateToPersist(newDate) });
|
||||
};
|
||||
|
||||
const handleEscape = (newDate: Nullable<Date>) => {
|
||||
onEscape?.(() => persistDate(newDate));
|
||||
onEscape?.({ newValue: getDateToPersist(newDate) });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDate: Nullable<Date>,
|
||||
) => {
|
||||
onClickOutside?.(() => persistDate(newDate), event);
|
||||
onClickOutside?.({ newValue: getDateToPersist(newDate), event });
|
||||
};
|
||||
|
||||
const handleChange = (newDate: Nullable<Date>) => {
|
||||
@@ -71,7 +57,7 @@ export const DateFieldInput = ({
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onClear?.(() => persistDate(null));
|
||||
onSubmit?.({ newValue: null });
|
||||
};
|
||||
|
||||
const dateValue = fieldValue ? new Date(fieldValue) : null;
|
||||
|
||||
+15
-29
@@ -1,60 +1,46 @@
|
||||
import { DateInput } from '@/ui/field/input/components/DateInput';
|
||||
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/meta-types/input/components/NumberFieldInput';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
import { type Nullable } from 'twenty-ui/utilities';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
||||
|
||||
export type DateTimeFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onClear?: FieldInputEvent;
|
||||
onSubmit?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DateTimeFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onClear,
|
||||
onSubmit,
|
||||
}: DateTimeFieldInputProps) => {
|
||||
export const DateTimeFieldInput = () => {
|
||||
const { fieldValue, setDraftValue } = useDateTimeField();
|
||||
|
||||
const { onEnter, onEscape, onClickOutside, onSubmit } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistDate = (newDate: Nullable<Date>) => {
|
||||
const getDateToPersist = (newDate: Nullable<Date>) => {
|
||||
if (!newDate) {
|
||||
persistField(null);
|
||||
return null;
|
||||
} else {
|
||||
const newDateISO = newDate?.toISOString();
|
||||
|
||||
persistField(newDateISO);
|
||||
return newDateISO;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (newDate: Nullable<Date>) => {
|
||||
onEnter?.(() => persistDate(newDate));
|
||||
onEnter?.({ newValue: getDateToPersist(newDate) });
|
||||
};
|
||||
|
||||
const handleEscape = (newDate: Nullable<Date>) => {
|
||||
onEscape?.(() => persistDate(newDate));
|
||||
onEscape?.({ newValue: getDateToPersist(newDate) });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDate: Nullable<Date>,
|
||||
) => {
|
||||
onClickOutside?.(() => persistDate(newDate), event);
|
||||
onClickOutside?.({ newValue: getDateToPersist(newDate), event });
|
||||
};
|
||||
|
||||
const handleChange = (newDate: Nullable<Date>) => {
|
||||
@@ -62,11 +48,11 @@ export const DateTimeFieldInput = ({
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onClear?.(() => persistDate(null));
|
||||
onSubmit?.({ newValue: null });
|
||||
};
|
||||
|
||||
const handleSubmit = (newDate: Nullable<Date>) => {
|
||||
onSubmit?.(() => persistDate(newDate));
|
||||
onSubmit?.({ newValue: getDateToPersist(newDate) });
|
||||
};
|
||||
|
||||
const dateValue = fieldValue ? new Date(fieldValue) : null;
|
||||
|
||||
+34
-23
@@ -1,43 +1,46 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
|
||||
import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem';
|
||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState';
|
||||
import { emailsSchema } from '@/object-record/record-field/types/guards/isFieldEmailsValue';
|
||||
import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||
|
||||
type EmailsFieldInputProps = {
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||
};
|
||||
|
||||
export const EmailsFieldInput = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: EmailsFieldInputProps) => {
|
||||
const { persistEmailsField, fieldValue } = useEmailsField();
|
||||
export const EmailsFieldInput = () => {
|
||||
const { setDraftValue, draftValue } = useEmailsField();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
||||
|
||||
const emails = useMemo<string[]>(
|
||||
() =>
|
||||
[
|
||||
fieldValue?.primaryEmail ? fieldValue?.primaryEmail : null,
|
||||
...(fieldValue?.additionalEmails ?? []),
|
||||
draftValue?.primaryEmail ? draftValue?.primaryEmail : null,
|
||||
...(draftValue?.additionalEmails ?? []),
|
||||
].filter(isDefined),
|
||||
[fieldValue?.primaryEmail, fieldValue?.additionalEmails],
|
||||
[draftValue?.primaryEmail, draftValue?.additionalEmails],
|
||||
);
|
||||
|
||||
const handlePersistEmails = (updatedEmails: string[]) => {
|
||||
const handleChange = (updatedEmails: string[]) => {
|
||||
const [nextPrimaryEmail, ...nextAdditionalEmails] = updatedEmails;
|
||||
persistEmailsField({
|
||||
|
||||
const nextValue = {
|
||||
primaryEmail: nextPrimaryEmail ?? '',
|
||||
additionalEmails: nextAdditionalEmails,
|
||||
});
|
||||
};
|
||||
|
||||
const parseResponse = emailsSchema.safeParse(nextValue);
|
||||
|
||||
if (parseResponse.success) {
|
||||
setDraftValue(parseResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const validateInput = useCallback(
|
||||
@@ -64,15 +67,23 @@ export const EmailsFieldInput = ({
|
||||
copyToClipboard(email, t`Email copied to clipboard`);
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
_newValue: any,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => {
|
||||
onClickOutside?.({ newValue: draftValue, event });
|
||||
};
|
||||
|
||||
const handleEscape = (_newValue: any) => {
|
||||
onEscape?.({ newValue: draftValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiItemFieldInput
|
||||
items={emails}
|
||||
onPersist={handlePersistEmails}
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(persist, event) => {
|
||||
onClickOutside?.(event);
|
||||
persist();
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onEscape={handleEscape}
|
||||
onClickOutside={handleClickOutside}
|
||||
placeholder="Email"
|
||||
fieldMetadataType={FieldMetadataType.EMAILS}
|
||||
validateInput={validateInput}
|
||||
|
||||
+13
-28
@@ -6,29 +6,17 @@ import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGER
|
||||
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
|
||||
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
|
||||
type FullNameFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
export const FullNameFieldInput = () => {
|
||||
const { draftValue, setDraftValue } = useFullNameField();
|
||||
|
||||
export const FullNameFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: FullNameFieldInputProps) => {
|
||||
const { draftValue, setDraftValue, persistFullNameField } =
|
||||
useFullNameField();
|
||||
const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const convertToFullName = (newDoubleText: FieldDoubleText) => {
|
||||
return {
|
||||
@@ -46,29 +34,26 @@ export const FullNameFieldInput = ({
|
||||
};
|
||||
|
||||
const handleEnter = (newDoubleText: FieldDoubleText) => {
|
||||
onEnter?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onEnter?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleEscape = (newDoubleText: FieldDoubleText) => {
|
||||
onEscape?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onEscape?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
onClickOutside?.(
|
||||
() => persistFullNameField(convertToFullName(newDoubleText)),
|
||||
event,
|
||||
);
|
||||
onClickOutside?.({ newValue: convertToFullName(newDoubleText), event });
|
||||
};
|
||||
|
||||
const handleTab = (newDoubleText: FieldDoubleText) => {
|
||||
onTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onTab?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
|
||||
onShiftTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onShiftTab?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleChange = (newDoubleText: FieldDoubleText) => {
|
||||
|
||||
+31
-21
@@ -1,40 +1,42 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||
import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem';
|
||||
import { getFieldLinkDefinedLinks } from '@/object-record/record-field/meta-types/input/utils/getFieldLinkDefinedLinks';
|
||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState';
|
||||
import { linksSchema } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useMemo } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { absoluteUrlSchema } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||
|
||||
type LinksFieldInputProps = {
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||
};
|
||||
export const LinksFieldInput = () => {
|
||||
const { draftValue, fieldDefinition, setDraftValue } = useLinksField();
|
||||
|
||||
export const LinksFieldInput = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: LinksFieldInputProps) => {
|
||||
const { persistLinksField, fieldValue, fieldDefinition } = useLinksField();
|
||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
||||
|
||||
const links = useMemo<{ url: string; label: string | null }[]>(
|
||||
() => getFieldLinkDefinedLinks(fieldValue),
|
||||
[fieldValue],
|
||||
() => getFieldLinkDefinedLinks(draftValue),
|
||||
[draftValue],
|
||||
);
|
||||
|
||||
const handlePersistLinks = (
|
||||
const handleChange = (
|
||||
updatedLinks: { url: string | null; label: string | null }[],
|
||||
) => {
|
||||
const nextPrimaryLink = updatedLinks.at(0);
|
||||
const nextSecondaryLinks = updatedLinks.slice(1);
|
||||
|
||||
persistLinksField({
|
||||
const nextValue = {
|
||||
primaryLinkUrl: nextPrimaryLink?.url ?? null,
|
||||
primaryLinkLabel: nextPrimaryLink?.label ?? null,
|
||||
secondaryLinks: nextSecondaryLinks,
|
||||
});
|
||||
};
|
||||
|
||||
const parseResponse = linksSchema.safeParse(nextValue);
|
||||
|
||||
if (parseResponse.success) {
|
||||
setDraftValue(parseResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const getShowPrimaryIcon = (index: number) => index === 0 && links.length > 1;
|
||||
@@ -48,15 +50,23 @@ export const LinksFieldInput = ({
|
||||
setIsFieldInError(hasError && values.length === 0);
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
_newValue: any,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => {
|
||||
onClickOutside?.({ newValue: draftValue, event });
|
||||
};
|
||||
|
||||
const handleEscape = (_newValue: any) => {
|
||||
onEscape?.({ newValue: draftValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiItemFieldInput
|
||||
items={links}
|
||||
onPersist={handlePersistLinks}
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(persist, event) => {
|
||||
onClickOutside?.(event);
|
||||
persist();
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onEscape={handleEscape}
|
||||
onClickOutside={handleClickOutside}
|
||||
placeholder="URL"
|
||||
fieldMetadataType={FieldMetadataType.LINKS}
|
||||
validateInput={(input) => ({
|
||||
|
||||
+21
-18
@@ -7,7 +7,6 @@ import {
|
||||
type MultiItemBaseInputProps,
|
||||
} from '@/object-record/record-field/meta-types/input/components/MultiItemBaseInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { type PhoneRecord } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@@ -25,8 +24,8 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
|
||||
|
||||
type MultiItemFieldInputProps<T> = {
|
||||
items: T[];
|
||||
onPersist: (updatedItems: T[]) => void;
|
||||
onCancel?: () => void;
|
||||
onChange: (newItemsValue: T[]) => void;
|
||||
onEscape?: (newItemsValue: T[]) => void;
|
||||
placeholder: string;
|
||||
validateInput?: (input: string) => { isValid: boolean; errorMessage: string };
|
||||
formatInput?: (input: string) => T;
|
||||
@@ -40,7 +39,7 @@ type MultiItemFieldInputProps<T> = {
|
||||
newItemLabel?: string;
|
||||
fieldMetadataType: FieldMetadataType;
|
||||
renderInput?: MultiItemBaseInputProps['renderInput'];
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onClickOutside?: (newItemsValue: T[], event: MouseEvent | TouchEvent) => void;
|
||||
onError?: (hasError: boolean, values: any[]) => void;
|
||||
};
|
||||
|
||||
@@ -48,8 +47,8 @@ type MultiItemFieldInputProps<T> = {
|
||||
// This should be refactored with a hook instead that exposes those events in a context around this component and its children.
|
||||
export const MultiItemFieldInput = <T,>({
|
||||
items,
|
||||
onPersist,
|
||||
onCancel,
|
||||
onChange,
|
||||
onEscape,
|
||||
placeholder,
|
||||
validateInput,
|
||||
formatInput,
|
||||
@@ -61,8 +60,9 @@ export const MultiItemFieldInput = <T,>({
|
||||
onError,
|
||||
}: MultiItemFieldInputProps<T>) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const handleDropdownClose = () => {
|
||||
onCancel?.();
|
||||
|
||||
const handleEscape = () => {
|
||||
onEscape?.(items);
|
||||
};
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -78,7 +78,8 @@ export const MultiItemFieldInput = <T,>({
|
||||
if (isEditing && isPrimaryItem) {
|
||||
handleSubmitInput();
|
||||
}
|
||||
onClickOutside?.(() => {}, event);
|
||||
|
||||
onClickOutside?.(items, event);
|
||||
},
|
||||
listenerId: instanceId,
|
||||
});
|
||||
@@ -86,8 +87,8 @@ export const MultiItemFieldInput = <T,>({
|
||||
useHotkeysOnFocusedElement({
|
||||
focusId: instanceId,
|
||||
keys: [Key.Escape],
|
||||
callback: handleDropdownClose,
|
||||
dependencies: [handleDropdownClose],
|
||||
callback: handleEscape,
|
||||
dependencies: [handleEscape],
|
||||
});
|
||||
|
||||
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
|
||||
@@ -99,13 +100,15 @@ export const MultiItemFieldInput = <T,>({
|
||||
});
|
||||
const isAddingNewItem = itemToEditIndex === -1;
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
const handleInputChange = (value: string) => {
|
||||
setInputValue(value);
|
||||
|
||||
if (!validateInput) return;
|
||||
|
||||
setErrorData(
|
||||
errorData.isValid ? errorData : { isValid: true, errorMessage: '' },
|
||||
);
|
||||
|
||||
onError?.(false, items);
|
||||
};
|
||||
|
||||
@@ -178,19 +181,19 @@ export const MultiItemFieldInput = <T,>({
|
||||
? [...items, newItem]
|
||||
: toSpliced(items, itemToEditIndex, 1, newItem);
|
||||
|
||||
onPersist(updatedItems);
|
||||
onChange(updatedItems);
|
||||
setIsInputDisplayed(false);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
const handleSetPrimaryItem = (index: number) => {
|
||||
const updatedItems = moveArrayItem(items, { fromIndex: index, toIndex: 0 });
|
||||
onPersist(updatedItems);
|
||||
onChange(updatedItems);
|
||||
};
|
||||
|
||||
const handleDeleteItem = (index: number) => {
|
||||
const updatedItems = toSpliced(items, index, 1);
|
||||
onPersist(updatedItems);
|
||||
onChange(updatedItems);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -219,11 +222,11 @@ export const MultiItemFieldInput = <T,>({
|
||||
value={inputValue}
|
||||
hasError={!errorData.isValid}
|
||||
renderInput={renderInput}
|
||||
onEscape={handleDropdownClose}
|
||||
onEscape={handleEscape}
|
||||
onChange={(value) => {
|
||||
value
|
||||
? handleOnChange(turnIntoEmptyStringIfWhitespacesOnly(value))
|
||||
: handleOnChange('');
|
||||
? handleInputChange(turnIntoEmptyStringIfWhitespacesOnly(value))
|
||||
: handleInputChange('');
|
||||
}}
|
||||
onEnter={handleSubmitInput}
|
||||
hasItem={!!items.length}
|
||||
|
||||
+18
-10
@@ -1,22 +1,30 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
||||
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
|
||||
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
|
||||
type MultiSelectFieldInputProps = {
|
||||
onCancel?: () => void;
|
||||
};
|
||||
export const MultiSelectFieldInput = () => {
|
||||
const { fieldDefinition, draftValue, setDraftValue } = useMultiSelectField();
|
||||
|
||||
export const MultiSelectFieldInput = ({
|
||||
onCancel,
|
||||
}: MultiSelectFieldInputProps) => {
|
||||
const { persistField, fieldDefinition, fieldValues } = useMultiSelectField();
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const handleOptionSelected = (newDraftValue: FieldMultiSelectValue) => {
|
||||
setDraftValue(newDraftValue);
|
||||
};
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const handleCancel = () => {
|
||||
onSubmit?.({ newValue: draftValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelectInput
|
||||
selectableListComponentInstanceId={
|
||||
@@ -24,9 +32,9 @@ export const MultiSelectFieldInput = ({
|
||||
}
|
||||
focusId={instanceId}
|
||||
options={fieldDefinition.metadata.options}
|
||||
onCancel={onCancel}
|
||||
onOptionSelected={persistField}
|
||||
values={fieldValues}
|
||||
onCancel={handleCancel}
|
||||
onOptionSelected={handleOptionSelected}
|
||||
values={draftValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+65
-24
@@ -1,56 +1,97 @@
|
||||
import { TextInput } from '@/ui/field/input/components/TextInput';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
canBeCastAsNumberOrNull,
|
||||
castAsNumberOrNull,
|
||||
} from '~/utils/cast-as-number-or-null';
|
||||
import { useNumberField } from '../../hooks/useNumberField';
|
||||
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
export const NumberFieldInput = () => {
|
||||
const { fieldDefinition, draftValue, setDraftValue } = useNumberField();
|
||||
|
||||
export type NumberFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const NumberFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: NumberFieldInputProps) => {
|
||||
const { fieldDefinition, draftValue, setDraftValue, persistNumberField } =
|
||||
useNumberField();
|
||||
const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const getNumberValueToPersist = (
|
||||
newValue: string,
|
||||
): { success: boolean; value?: any } => {
|
||||
if (fieldDefinition?.metadata?.settings?.type === 'percentage') {
|
||||
const newValueEscaped = newValue.replaceAll('%', '');
|
||||
|
||||
if (!canBeCastAsNumberOrNull(newValueEscaped)) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const castedValue = castAsNumberOrNull(newValue);
|
||||
|
||||
if (!isNull(castedValue)) {
|
||||
return { success: true, value: castedValue / 100 };
|
||||
}
|
||||
|
||||
return { success: true, value: null };
|
||||
}
|
||||
|
||||
if (!canBeCastAsNumberOrNull(newValue)) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const castedValue = castAsNumberOrNull(newValue);
|
||||
|
||||
return { success: true, value: castedValue };
|
||||
};
|
||||
|
||||
const handleEnter = (newText: string) => {
|
||||
onEnter?.(() => persistNumberField(newText));
|
||||
const { success, value } = getNumberValueToPersist(newText);
|
||||
|
||||
const shouldNotPersist = !success;
|
||||
|
||||
onEnter?.({ newValue: value, skipPersist: shouldNotPersist });
|
||||
};
|
||||
|
||||
const handleEscape = (newText: string) => {
|
||||
onEscape?.(() => persistNumberField(newText));
|
||||
const { success, value } = getNumberValueToPersist(newText);
|
||||
|
||||
const shouldNotPersist = !success;
|
||||
|
||||
onEscape?.({ newValue: value, skipPersist: shouldNotPersist });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newText: string,
|
||||
) => {
|
||||
onClickOutside?.(() => persistNumberField(newText), event);
|
||||
const { success, value } = getNumberValueToPersist(newText);
|
||||
|
||||
const shouldNotPersist = !success;
|
||||
|
||||
onClickOutside?.({ newValue: value, skipPersist: shouldNotPersist, event });
|
||||
};
|
||||
|
||||
const handleTab = (newText: string) => {
|
||||
onTab?.(() => persistNumberField(newText));
|
||||
const { success, value } = getNumberValueToPersist(newText);
|
||||
|
||||
const shouldNotPersist = !success;
|
||||
|
||||
onTab?.({ newValue: value, skipPersist: shouldNotPersist });
|
||||
};
|
||||
|
||||
const handleShiftTab = (newText: string) => {
|
||||
onShiftTab?.(() => persistNumberField(newText));
|
||||
const { success, value } = getNumberValueToPersist(newText);
|
||||
|
||||
const shouldNotPersist = !success;
|
||||
|
||||
onShiftTab?.({ newValue: value, skipPersist: shouldNotPersist });
|
||||
};
|
||||
|
||||
const handleChange = (newText: string) => {
|
||||
|
||||
+32
-18
@@ -4,16 +4,18 @@ import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/re
|
||||
import { phoneSchema } from '@/object-record/record-field/validation-schemas/phoneSchema';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { type E164Number, parsePhoneNumber } from 'libphonenumber-js';
|
||||
import { parsePhoneNumber, type E164Number } from 'libphonenumber-js';
|
||||
import ReactPhoneNumberInput from 'react-phone-number-input';
|
||||
import 'react-phone-number-input/style.css';
|
||||
|
||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { createPhonesFromFieldValue } from '@/object-record/record-field/meta-types/input/utils/phonesUtils';
|
||||
import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { phonesSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
|
||||
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
||||
import { css } from '@emotion/react';
|
||||
import { useContext } from 'react';
|
||||
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
@@ -69,24 +71,18 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||
`;
|
||||
|
||||
type PhonesFieldInputProps = {
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
};
|
||||
export const PhonesFieldInput = () => {
|
||||
const { fieldDefinition, setDraftValue, draftValue } = usePhonesField();
|
||||
|
||||
export const PhonesFieldInput = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: PhonesFieldInputProps) => {
|
||||
const { persistPhonesField, fieldValue, fieldDefinition } = usePhonesField();
|
||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
||||
|
||||
const phones = createPhonesFromFieldValue(fieldValue);
|
||||
const phones = createPhonesFromFieldValue(draftValue);
|
||||
|
||||
const defaultCountry = stripSimpleQuotesFromString(
|
||||
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
|
||||
);
|
||||
|
||||
const handlePersistPhones = (
|
||||
const handlePhonesChange = (
|
||||
updatedPhones: {
|
||||
number: string;
|
||||
countryCode: string;
|
||||
@@ -94,12 +90,19 @@ export const PhonesFieldInput = ({
|
||||
}[],
|
||||
) => {
|
||||
const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones;
|
||||
persistPhonesField({
|
||||
|
||||
const newValue = {
|
||||
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
|
||||
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
|
||||
primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '',
|
||||
additionalPhones: nextAdditionalPhones,
|
||||
});
|
||||
};
|
||||
|
||||
const newValidatedPhoneResponse = phonesSchema.safeParse(newValue);
|
||||
|
||||
if (newValidatedPhoneResponse.success) {
|
||||
setDraftValue(newValidatedPhoneResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const validateInput = (input: string) => ({
|
||||
@@ -119,12 +122,23 @@ export const PhonesFieldInput = ({
|
||||
setIsFieldInError(hasError && values.length === 0);
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
_newValue: any,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => {
|
||||
onClickOutside?.({ newValue: draftValue, event });
|
||||
};
|
||||
|
||||
const handleEscape = (_newValue: any) => {
|
||||
onEscape?.({ newValue: draftValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiItemFieldInput
|
||||
items={phones}
|
||||
onPersist={handlePersistPhones}
|
||||
onClickOutside={onClickOutside}
|
||||
onCancel={onCancel}
|
||||
onChange={handlePhonesChange}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEscape={handleEscape}
|
||||
placeholder="Phone"
|
||||
fieldMetadataType={FieldMetadataType.PHONES}
|
||||
validateInput={validateInput}
|
||||
|
||||
+13
-14
@@ -1,28 +1,27 @@
|
||||
import { type FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RatingInput } from '@/ui/field/input/components/RatingInput';
|
||||
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useContext } from 'react';
|
||||
import { useRatingField } from '../../hooks/useRatingField';
|
||||
|
||||
export type RatingFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const RatingFieldInput = ({
|
||||
onSubmit,
|
||||
readonly,
|
||||
}: RatingFieldInputProps) => {
|
||||
export const RatingFieldInput = () => {
|
||||
const { rating } = useRatingField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { isRecordFieldReadOnly } = useContext(FieldContext);
|
||||
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const handleChange = (newRating: FieldRatingValue) => {
|
||||
onSubmit?.(() => persistField(newRating));
|
||||
onSubmit?.({ newValue: newRating });
|
||||
};
|
||||
|
||||
return (
|
||||
<RatingInput value={rating} onChange={handleChange} readonly={readonly} />
|
||||
<RatingInput
|
||||
value={rating}
|
||||
onChange={handleChange}
|
||||
readonly={isRecordFieldReadOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+39
-30
@@ -1,16 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useContext, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { IconPencil } from 'twenty-ui/display';
|
||||
import { CodeEditor, FloatingIconButton } from 'twenty-ui/input';
|
||||
@@ -18,14 +16,6 @@ import { JsonTree, isTwoFirstDepths } from 'twenty-ui/json-visualizer';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { useJsonField } from '../../hooks/useJsonField';
|
||||
|
||||
type RawJsonFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
const CONTAINER_HEIGHT = 300;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -51,47 +41,66 @@ const StyledJsonTreeContainer = styled.div`
|
||||
width: min-content;
|
||||
`;
|
||||
|
||||
export const RawJsonFieldInput = ({
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: RawJsonFieldInputProps) => {
|
||||
export const RawJsonFieldInput = () => {
|
||||
const { t } = useLingui();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const {
|
||||
draftValue,
|
||||
precomputedDraftValue,
|
||||
setDraftValue,
|
||||
persistJsonField,
|
||||
fieldDefinition,
|
||||
} = useJsonField();
|
||||
const { draftValue, precomputedDraftValue, setDraftValue, fieldDefinition } =
|
||||
useJsonField();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const { onEscape, onClickOutside, onTab, onShiftTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const parseJsonField = (
|
||||
nextValue: string,
|
||||
): { success: boolean; value?: any } => {
|
||||
if (!nextValue) {
|
||||
return {
|
||||
success: true,
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return { success: true, value: JSON.parse(nextValue) };
|
||||
} catch {
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (newText: string) => {
|
||||
onEscape?.(() => persistJsonField(newText));
|
||||
const { success, value } = parseJsonField(newText);
|
||||
|
||||
onEscape?.({ newValue: value, skipPersist: !success });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newText: string,
|
||||
) => {
|
||||
onClickOutside?.(() => persistJsonField(newText), event);
|
||||
const { success, value } = parseJsonField(newText);
|
||||
|
||||
onClickOutside?.({ newValue: value, skipPersist: !success, event });
|
||||
};
|
||||
|
||||
const handleTab = (newText: string) => {
|
||||
onTab?.(() => persistJsonField(newText));
|
||||
const { success, value } = parseJsonField(newText);
|
||||
|
||||
onTab?.({ newValue: value, skipPersist: !success });
|
||||
};
|
||||
|
||||
const handleShiftTab = (newText: string) => {
|
||||
onShiftTab?.(() => persistJsonField(newText));
|
||||
const { success, value } = parseJsonField(newText);
|
||||
|
||||
onShiftTab?.({ newValue: value, skipPersist: !success });
|
||||
};
|
||||
|
||||
const handleChange = (newText: string) => {
|
||||
|
||||
+5
-9
@@ -7,13 +7,13 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||
@@ -24,18 +24,14 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RelationFromManyFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const RelationFromManyFieldInput = ({
|
||||
onSubmit,
|
||||
}: RelationFromManyFieldInputProps) => {
|
||||
export const RelationFromManyFieldInput = () => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
const objectMetadataNameSingular =
|
||||
@@ -51,7 +47,7 @@ export const RelationFromManyFieldInput = ({
|
||||
const { fieldValue } = useRelationField();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.(() => {});
|
||||
onSubmit?.({ skipPersist: true });
|
||||
};
|
||||
|
||||
const isRelationFromActivityTargets =
|
||||
|
||||
+5
-13
@@ -1,4 +1,4 @@
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
@@ -6,7 +6,6 @@ import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
@@ -14,21 +13,14 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconForbid } from 'twenty-ui/display';
|
||||
|
||||
export type RelationToOneFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export const RelationToOneFieldInput = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RelationToOneFieldInputProps) => {
|
||||
export const RelationToOneFieldInput = () => {
|
||||
const { fieldDefinition, recordId } = useRelationField<ObjectRecord>();
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { onSubmit, onCancel } = useContext(FieldInputEventContext);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
@@ -36,7 +28,7 @@ export const RelationToOneFieldInput = ({
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => onSubmit?.(() => persistField(selectedRecord?.record ?? null));
|
||||
) => onSubmit?.({ newValue: selectedRecord?.record ?? null });
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
|
||||
+23
-25
@@ -1,17 +1,17 @@
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
|
||||
import { type FieldRichTextV2Metadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Suspense, lazy, useRef } from 'react';
|
||||
import { Suspense, lazy, useContext, useRef } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display';
|
||||
import { FloatingIconButton } from 'twenty-ui/input';
|
||||
@@ -22,13 +22,6 @@ const ActivityRichTextEditor = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
export type RichTextFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onCancel?: () => void;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
width: 480px;
|
||||
@@ -60,29 +53,34 @@ const LoadingSkeleton = () => {
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
export const RichTextFieldInput = ({
|
||||
targetableObject,
|
||||
onClickOutside,
|
||||
onEscape,
|
||||
}: {
|
||||
targetableObject: Pick<ActivityTargetableObject, 'id'> & {
|
||||
targetObjectNameSingular:
|
||||
export const RichTextFieldInput = () => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
|
||||
const targetableObject = {
|
||||
id: recordId,
|
||||
targetObjectNameSingular: (
|
||||
fieldDefinition as {
|
||||
metadata: FieldRichTextV2Metadata;
|
||||
}
|
||||
).metadata.objectMetadataNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task;
|
||||
| CoreObjectNameSingular.Task,
|
||||
};
|
||||
} & RichTextFieldInputProps) => {
|
||||
|
||||
const { editRichText } = useRichTextCommandMenu();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||
onClickOutside?.(() => {}, event);
|
||||
onClickOutside?.({ event, skipPersist: true });
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
onEscape?.(() => {});
|
||||
onEscape?.({ skipPersist: true });
|
||||
};
|
||||
|
||||
useRegisterInputEvents({
|
||||
@@ -106,7 +104,7 @@ export const RichTextFieldInput = ({
|
||||
Icon={IconLayoutSidebarLeftCollapse}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onEscape?.(() => {});
|
||||
onEscape?.({ skipPersist: true });
|
||||
editRichText(
|
||||
targetableObject.id,
|
||||
targetableObject.targetObjectNameSingular,
|
||||
|
||||
+7
-14
@@ -1,27 +1,21 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useClearField } from '@/object-record/record-field/hooks/useClearField';
|
||||
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
|
||||
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { SelectInput } from '@/ui/field/input/components/SelectInput';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useState } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type SelectFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
export const SelectFieldInput = () => {
|
||||
const { fieldDefinition, fieldValue } = useSelectField();
|
||||
|
||||
export const SelectFieldInput = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: SelectFieldInputProps) => {
|
||||
const { persistField, fieldDefinition, fieldValue } = useSelectField();
|
||||
const { onCancel, onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
@@ -44,7 +38,7 @@ export const SelectFieldInput = ({
|
||||
};
|
||||
|
||||
const handleSubmit = (option: SelectOption) => {
|
||||
onSubmit?.(() => persistField(option?.value));
|
||||
onSubmit?.({ newValue: option.value });
|
||||
|
||||
resetSelectedItem();
|
||||
};
|
||||
@@ -76,8 +70,7 @@ export const SelectFieldInput = ({
|
||||
(option) => option.value === itemId,
|
||||
);
|
||||
if (isDefined(option)) {
|
||||
onSubmit?.(() => persistField(option.value));
|
||||
resetSelectedItem();
|
||||
handleSubmit(option);
|
||||
}
|
||||
}}
|
||||
onOptionSelected={handleSubmit}
|
||||
|
||||
+16
-26
@@ -1,60 +1,50 @@
|
||||
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useTextField } from '../../hooks/useTextField';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
|
||||
|
||||
export type TextFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const TextFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: TextFieldInputProps) => {
|
||||
export const TextFieldInput = () => {
|
||||
const { fieldDefinition, draftValue, setDraftValue } = useTextField();
|
||||
|
||||
const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
const handleEnter = (newText: string) => {
|
||||
onEnter?.(() => persistField(newText.trim()));
|
||||
onEnter?.({ newValue: newText.trim() });
|
||||
};
|
||||
|
||||
const handleEscape = (newText: string) => {
|
||||
onEscape?.(() => persistField(newText.trim()));
|
||||
onEscape?.({ newValue: newText.trim() });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newText: string,
|
||||
) => {
|
||||
onClickOutside?.(() => persistField(newText.trim()), event);
|
||||
onClickOutside?.({
|
||||
newValue: newText.trim(),
|
||||
event,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTab = (newText: string) => {
|
||||
onTab?.(() => persistField(newText.trim()));
|
||||
onTab?.({ newValue: newText.trim() });
|
||||
};
|
||||
|
||||
const handleShiftTab = (newText: string) => {
|
||||
onShiftTab?.(() => persistField(newText.trim()));
|
||||
onShiftTab?.({ newValue: newText.trim() });
|
||||
};
|
||||
|
||||
const handleChange = (newText: string) => {
|
||||
|
||||
+13
-44
@@ -1,9 +1,10 @@
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -12,48 +13,34 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ArrayFieldInput } from '../ArrayFieldInput';
|
||||
|
||||
const updateRecord = fn();
|
||||
const { FieldInputEventContextProviderWithJestMocks } =
|
||||
getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const ArrayValueSetterEffect = ({ value }: { value: string[] }) => {
|
||||
const { setFieldValue } = useArrayField();
|
||||
const { setFieldValue, setDraftValue } = useArrayField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
setDraftValue(value);
|
||||
}, [setFieldValue, value, setDraftValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type ArrayFieldValueGaterProps = Pick<
|
||||
ArrayInputWithContextProps,
|
||||
'onCancel' | 'onClickOutside'
|
||||
>;
|
||||
|
||||
const ArrayFieldValueGater = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: ArrayFieldValueGaterProps) => {
|
||||
const ArrayFieldValueGater = () => {
|
||||
const { fieldValue } = useArrayField();
|
||||
|
||||
return (
|
||||
fieldValue && (
|
||||
<ArrayFieldInput onCancel={onCancel} onClickOutside={onClickOutside} />
|
||||
)
|
||||
);
|
||||
return fieldValue && <ArrayFieldInput />;
|
||||
};
|
||||
|
||||
type ArrayInputWithContextProps = {
|
||||
value: string[];
|
||||
recordId?: string;
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||
};
|
||||
|
||||
const ArrayInputWithContext = ({
|
||||
value,
|
||||
recordId = 'record-id',
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: ArrayInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
@@ -99,14 +86,12 @@ const ArrayInputWithContext = ({
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
<ArrayValueSetterEffect value={value} />
|
||||
<ArrayFieldValueGater
|
||||
onCancel={onCancel}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<ArrayValueSetterEffect value={value} />
|
||||
<ArrayFieldValueGater />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
@@ -155,21 +140,5 @@ export const TrimInput: Story = {
|
||||
const tag2Elements = canvas.queryAllByText('tag2');
|
||||
expect(tag2Elements).toHaveLength(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: 'record-id' },
|
||||
updateOneRecordInput: {
|
||||
tags: [
|
||||
'tag1',
|
||||
'tag2',
|
||||
'tag2', // The second tag2 is not trimmed, so it remains as a duplicate
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
+17
-25
@@ -1,5 +1,5 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
@@ -7,13 +7,14 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import {
|
||||
BooleanFieldInput,
|
||||
type BooleanFieldInputProps,
|
||||
} from '../BooleanFieldInput';
|
||||
import { BooleanFieldInput } from '../BooleanFieldInput';
|
||||
|
||||
const { FieldInputEventContextProviderWithJestMocks, handleSubmitMocked } =
|
||||
getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const BooleanFieldValueSetterEffect = ({
|
||||
value,
|
||||
@@ -31,7 +32,7 @@ const BooleanFieldValueSetterEffect = ({
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type BooleanFieldInputWithContextProps = BooleanFieldInputProps & {
|
||||
type BooleanFieldInputWithContextProps = {
|
||||
value: boolean;
|
||||
recordId?: string;
|
||||
};
|
||||
@@ -39,7 +40,6 @@ type BooleanFieldInputWithContextProps = BooleanFieldInputProps & {
|
||||
const BooleanFieldInputWithContext = ({
|
||||
value,
|
||||
recordId,
|
||||
onSubmit,
|
||||
}: BooleanFieldInputWithContextProps) => {
|
||||
return (
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
@@ -69,11 +69,13 @@ const BooleanFieldInputWithContext = ({
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<BooleanFieldValueSetterEffect
|
||||
value={value}
|
||||
recordId={recordId ?? ''}
|
||||
/>
|
||||
<BooleanFieldInput onSubmit={onSubmit} testId="boolean-field-input" />
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<BooleanFieldValueSetterEffect
|
||||
value={value}
|
||||
recordId={recordId ?? ''}
|
||||
/>
|
||||
<BooleanFieldInput testId="boolean-field-input" />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
@@ -92,19 +94,9 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof BooleanFieldInputWithContext>;
|
||||
|
||||
const submitJestFn = fn();
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Toggle: Story = {
|
||||
args: {
|
||||
onSubmit: submitJestFn,
|
||||
},
|
||||
argTypes: {
|
||||
onSubmit: {
|
||||
control: false,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
@@ -114,18 +106,18 @@ export const Toggle: Story = {
|
||||
|
||||
await expect(trueText).toBeInTheDocument();
|
||||
|
||||
await expect(submitJestFn).toHaveBeenCalledTimes(0);
|
||||
await expect(handleSubmitMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
await expect(input).toHaveTextContent('False');
|
||||
|
||||
await expect(submitJestFn).toHaveBeenCalledTimes(1);
|
||||
await expect(handleSubmitMocked).toHaveBeenCalledTimes(1);
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
await expect(input).toHaveTextContent('True');
|
||||
|
||||
await expect(submitJestFn).toHaveBeenCalledTimes(2);
|
||||
await expect(handleSubmitMocked).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
};
|
||||
|
||||
+28
-48
@@ -1,21 +1,27 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||
import { useDateTimeField } from '../../../hooks/useDateTimeField';
|
||||
import {
|
||||
DateTimeFieldInput,
|
||||
type DateTimeFieldInputProps,
|
||||
} from '../DateTimeFieldInput';
|
||||
import { DateTimeFieldInput } from '../DateTimeFieldInput';
|
||||
|
||||
const {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleEscapeMocked,
|
||||
handleClickoutsideMocked,
|
||||
handleEnterMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const formattedDate = new Date(2022, 0, 1, 2, 0, 0);
|
||||
|
||||
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
|
||||
@@ -28,30 +34,13 @@ const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type DateFieldValueGaterProps = Pick<
|
||||
DateTimeFieldInputProps,
|
||||
'onEscape' | 'onEnter' | 'onClickOutside'
|
||||
>;
|
||||
|
||||
const DateFieldValueGater = ({
|
||||
onEscape,
|
||||
onEnter,
|
||||
onClickOutside,
|
||||
}: DateFieldValueGaterProps) => {
|
||||
const DateFieldValueGater = () => {
|
||||
const { fieldValue } = useDateTimeField();
|
||||
|
||||
return (
|
||||
fieldValue && (
|
||||
<DateTimeFieldInput
|
||||
onEscape={onEscape}
|
||||
onEnter={onEnter}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
)
|
||||
);
|
||||
return fieldValue && <DateTimeFieldInput />;
|
||||
};
|
||||
|
||||
type DateFieldInputWithContextProps = DateTimeFieldInputProps & {
|
||||
type DateFieldInputWithContextProps = {
|
||||
value: Date;
|
||||
recordId: string;
|
||||
};
|
||||
@@ -59,9 +48,6 @@ type DateFieldInputWithContextProps = DateTimeFieldInputProps & {
|
||||
const DateFieldInputWithContext = ({
|
||||
value,
|
||||
recordId,
|
||||
onEscape,
|
||||
onEnter,
|
||||
onClickOutside,
|
||||
}: DateFieldInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const instanceId = getRecordFieldInputInstanceId({
|
||||
@@ -104,31 +90,25 @@ const DateFieldInputWithContext = ({
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||
<DateFieldValueSetterEffect value={value} />
|
||||
<DateFieldValueGater
|
||||
onEscape={onEscape}
|
||||
onEnter={onEnter}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||
<DateFieldValueSetterEffect value={value} />
|
||||
<DateFieldValueGater />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
<div data-testid="data-field-input-click-outside-div"></div>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const escapeJestFn = fn();
|
||||
const enterJestFn = fn();
|
||||
const clickOutsideJestFn = fn();
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Input/DateTimeFieldInput',
|
||||
component: DateFieldInputWithContext,
|
||||
args: {
|
||||
value: formattedDate,
|
||||
onEscape: escapeJestFn,
|
||||
onEnter: enterJestFn,
|
||||
onClickOutside: clickOutsideJestFn,
|
||||
onEscape: handleEscapeMocked,
|
||||
onEnter: handleEnterMocked,
|
||||
onClickOutside: handleClickoutsideMocked,
|
||||
},
|
||||
argTypes: {
|
||||
onEscape: {
|
||||
@@ -160,36 +140,36 @@ export const ClickOutside: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
|
||||
await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByText('January');
|
||||
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
||||
await userEvent.click(emptyDiv);
|
||||
|
||||
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
|
||||
await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
export const Escape: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
await expect(escapeJestFn).toHaveBeenCalledTimes(0);
|
||||
await expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('January');
|
||||
await userEvent.keyboard('{escape}');
|
||||
|
||||
await expect(escapeJestFn).toHaveBeenCalledTimes(1);
|
||||
await expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
export const Enter: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
await expect(enterJestFn).toHaveBeenCalledTimes(0);
|
||||
await expect(handleEnterMocked).toHaveBeenCalledTimes(0);
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('January');
|
||||
await userEvent.keyboard('{enter}');
|
||||
|
||||
await expect(enterJestFn).toHaveBeenCalledTimes(1);
|
||||
await expect(handleEnterMocked).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
+14
-41
@@ -1,10 +1,11 @@
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
@@ -18,46 +19,34 @@ import { EmailsFieldInput } from '../EmailsFieldInput';
|
||||
|
||||
const updateRecord = fn();
|
||||
|
||||
const { FieldInputEventContextProviderWithJestMocks } =
|
||||
getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const EmailValueSetterEffect = ({ value }: { value: FieldEmailsValue }) => {
|
||||
const { setFieldValue } = useEmailsField();
|
||||
const { setFieldValue, setDraftValue } = useEmailsField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
setDraftValue(value);
|
||||
}, [setFieldValue, value, setDraftValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type EmailFieldValueGaterProps = Pick<
|
||||
EmailInputWithContextProps,
|
||||
'onCancel' | 'onClickOutside'
|
||||
>;
|
||||
|
||||
const EmailFieldValueGater = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: EmailFieldValueGaterProps) => {
|
||||
const EmailFieldValueGater = () => {
|
||||
const { fieldValue } = useEmailsField();
|
||||
|
||||
return (
|
||||
fieldValue && (
|
||||
<EmailsFieldInput onCancel={onCancel} onClickOutside={onClickOutside} />
|
||||
)
|
||||
);
|
||||
return fieldValue && <EmailsFieldInput />;
|
||||
};
|
||||
|
||||
type EmailInputWithContextProps = {
|
||||
value: FieldEmailsValue;
|
||||
recordId?: string;
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||
};
|
||||
|
||||
const EmailInputWithContext = ({
|
||||
value,
|
||||
recordId = 'record-id',
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: EmailInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const instanceId = getRecordFieldInputInstanceId({
|
||||
@@ -101,11 +90,10 @@ const EmailInputWithContext = ({
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
<EmailValueSetterEffect value={value} />
|
||||
<EmailFieldValueGater
|
||||
onCancel={onCancel}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<EmailValueSetterEffect value={value} />
|
||||
<EmailFieldValueGater />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
@@ -159,21 +147,6 @@ export const TrimInput: Story = {
|
||||
|
||||
const newEmailElement = await canvas.findByText('new.email@example.com');
|
||||
expect(newEmailElement).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: 'record-id' },
|
||||
updateOneRecordInput: {
|
||||
emails: {
|
||||
primaryEmail: 'john@example.com',
|
||||
additionalEmails: ['new.email@example.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+20
-157
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -15,6 +16,12 @@ import { LinksFieldInput } from '../LinksFieldInput';
|
||||
|
||||
const updateRecord = fn();
|
||||
|
||||
const {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleEscapeMocked,
|
||||
handleClickoutsideMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const LinksValueSetterEffect = ({
|
||||
value,
|
||||
}: {
|
||||
@@ -24,11 +31,12 @@ const LinksValueSetterEffect = ({
|
||||
secondaryLinks: Array<{ url: string | null; label: string | null }> | null;
|
||||
};
|
||||
}) => {
|
||||
const { setFieldValue } = useLinksField();
|
||||
const { setFieldValue, setDraftValue } = useLinksField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
setDraftValue(value);
|
||||
}, [setFieldValue, value, setDraftValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -40,33 +48,17 @@ type LinksInputWithContextProps = {
|
||||
secondaryLinks: Array<{ url: string | null; label: string | null }> | null;
|
||||
};
|
||||
recordId: string;
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||
};
|
||||
|
||||
type LinksFieldValueGaterProps = Pick<
|
||||
LinksInputWithContextProps,
|
||||
'onCancel' | 'onClickOutside'
|
||||
>;
|
||||
|
||||
const LinksFieldValueGater = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: LinksFieldValueGaterProps) => {
|
||||
const LinksFieldValueGater = () => {
|
||||
const { fieldValue } = useLinksField();
|
||||
|
||||
return (
|
||||
fieldValue && (
|
||||
<LinksFieldInput onCancel={onCancel} onClickOutside={onClickOutside} />
|
||||
)
|
||||
);
|
||||
return fieldValue && <LinksFieldInput />;
|
||||
};
|
||||
|
||||
const LinksInputWithContext = ({
|
||||
value,
|
||||
recordId,
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: LinksInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const instanceId = getRecordFieldInputInstanceId({
|
||||
@@ -111,11 +103,10 @@ const LinksInputWithContext = ({
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
<LinksValueSetterEffect value={value} />
|
||||
<LinksFieldValueGater
|
||||
onCancel={onCancel}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<LinksValueSetterEffect value={value} />
|
||||
<LinksFieldValueGater />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
<div data-testid="links-field-input-click-outside-div" />
|
||||
@@ -127,9 +118,6 @@ const getPrimaryLinkBookmarkIcon = (canvasElement: HTMLElement) =>
|
||||
// It would be better to use an aria-label on the icon, but we'll do this for now
|
||||
canvasElement.querySelector('svg[class*="tabler-icon-bookmark"]');
|
||||
|
||||
const cancelJestFn = fn();
|
||||
const clickOutsideJestFn = fn();
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Input/LinksFieldInput',
|
||||
component: LinksInputWithContext,
|
||||
@@ -140,8 +128,6 @@ const meta: Meta = {
|
||||
secondaryLinks: null,
|
||||
},
|
||||
recordId: '123',
|
||||
onCancel: cancelJestFn,
|
||||
onClickOutside: clickOutsideJestFn,
|
||||
},
|
||||
argTypes: {
|
||||
onCancel: { control: false },
|
||||
@@ -242,22 +228,6 @@ export const CreatePrimaryLink: Story = {
|
||||
const linkDisplay = await canvas.findByText('twenty.com');
|
||||
expect(linkDisplay).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: 'https://www.twenty.com',
|
||||
primaryLinkLabel: null,
|
||||
secondaryLinks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -272,22 +242,6 @@ export const TrimInput: Story = {
|
||||
const linkDisplay = await canvas.findByText('twenty.com');
|
||||
expect(linkDisplay).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: 'https://www.twenty.com',
|
||||
primaryLinkLabel: null,
|
||||
secondaryLinks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -316,27 +270,6 @@ export const AddSecondaryLink: Story = {
|
||||
|
||||
const secondaryLink = await canvas.findByText('docs.twenty.com');
|
||||
expect(secondaryLink).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: 'https://www.twenty.com',
|
||||
primaryLinkLabel: 'Twenty Website',
|
||||
secondaryLinks: [
|
||||
{
|
||||
url: 'https://docs.twenty.com',
|
||||
label: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -369,22 +302,6 @@ export const DeletePrimaryLink: Story = {
|
||||
const input = await canvas.findByPlaceholderText('URL');
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveValue('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: null,
|
||||
primaryLinkLabel: null,
|
||||
secondaryLinks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -427,22 +344,6 @@ export const DeletePrimaryLinkAndUseSecondaryLinkAsTheNewPrimaryLink: Story = {
|
||||
expect(oldPrimaryLink).not.toBeInTheDocument();
|
||||
|
||||
expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: 'https://docs.twenty.com',
|
||||
primaryLinkLabel: 'Documentation',
|
||||
secondaryLinks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -485,22 +386,6 @@ export const DeleteSecondaryLink: Story = {
|
||||
expect(secondaryLink).not.toBeInTheDocument();
|
||||
|
||||
expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: 'https://www.twenty.com',
|
||||
primaryLinkLabel: 'Twenty Website',
|
||||
secondaryLinks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -511,14 +396,14 @@ export const ClickOutside: Story = {
|
||||
const input = await canvas.findByPlaceholderText('URL');
|
||||
await userEvent.click(input);
|
||||
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const outsideDiv = await canvas.findByTestId(
|
||||
'links-field-input-click-outside-div',
|
||||
);
|
||||
await userEvent.click(outsideDiv);
|
||||
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -526,14 +411,14 @@ export const Cancel: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(cancelJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('URL');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('{escape}');
|
||||
|
||||
expect(cancelJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -602,28 +487,6 @@ export const MakeSecondaryLinkPrimary: Story = {
|
||||
getCanvasElementForDropdownTesting(),
|
||||
).findByText('Set as Primary');
|
||||
await userEvent.click(setPrimaryOption);
|
||||
|
||||
// Documentation should now be the primary link
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: '123' },
|
||||
updateOneRecordInput: {
|
||||
links: {
|
||||
primaryLinkUrl: 'https://docs.twenty.com',
|
||||
primaryLinkLabel: 'Documentation',
|
||||
secondaryLinks: [
|
||||
{
|
||||
url: 'https://www.twenty.com',
|
||||
label: 'Twenty Website',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+38
-46
@@ -1,5 +1,5 @@
|
||||
import { type Decorator, type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
@@ -7,6 +7,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -14,10 +15,16 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { useNumberField } from '../../../hooks/useNumberField';
|
||||
import {
|
||||
NumberFieldInput,
|
||||
type NumberFieldInputProps,
|
||||
} from '../NumberFieldInput';
|
||||
import { NumberFieldInput } from '../NumberFieldInput';
|
||||
|
||||
const {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleEnterMocked,
|
||||
handleEscapeMocked,
|
||||
handleClickoutsideMocked,
|
||||
handleTabMocked,
|
||||
handleShiftTabMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
|
||||
const { setFieldValue } = useNumberField();
|
||||
@@ -29,7 +36,7 @@ const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type NumberFieldInputWithContextProps = NumberFieldInputProps & {
|
||||
type NumberFieldInputWithContextProps = {
|
||||
value: number;
|
||||
recordId: string;
|
||||
};
|
||||
@@ -37,11 +44,6 @@ type NumberFieldInputWithContextProps = NumberFieldInputProps & {
|
||||
const NumberFieldInputWithContext = ({
|
||||
recordId,
|
||||
value,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: NumberFieldInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
@@ -94,15 +96,11 @@ const NumberFieldInputWithContext = ({
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{isReady && <StorybookFieldInputDropdownFocusIdSetterEffect />}
|
||||
<NumberFieldValueSetterEffect value={value} />
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
{isReady && <StorybookFieldInputDropdownFocusIdSetterEffect />}
|
||||
<NumberFieldValueSetterEffect value={value} />
|
||||
<NumberFieldInput />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
{isReady && <div data-testid="is-ready-marker" />}
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
@@ -110,19 +108,13 @@ const NumberFieldInputWithContext = ({
|
||||
);
|
||||
};
|
||||
|
||||
const enterJestFn = fn();
|
||||
const escapeJestfn = fn();
|
||||
const clickOutsideJestFn = fn();
|
||||
const tabJestFn = fn();
|
||||
const shiftTabJestFn = fn();
|
||||
|
||||
const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
if (context.parameters.clearMocks === true) {
|
||||
enterJestFn.mockClear();
|
||||
escapeJestfn.mockClear();
|
||||
clickOutsideJestFn.mockClear();
|
||||
tabJestFn.mockClear();
|
||||
shiftTabJestFn.mockClear();
|
||||
handleEnterMocked.mockClear();
|
||||
handleEscapeMocked.mockClear();
|
||||
handleClickoutsideMocked.mockClear();
|
||||
handleTabMocked.mockClear();
|
||||
handleShiftTabMocked.mockClear();
|
||||
}
|
||||
return <Story />;
|
||||
};
|
||||
@@ -133,11 +125,11 @@ const meta: Meta = {
|
||||
args: {
|
||||
value: 1000,
|
||||
isPositive: true,
|
||||
onEnter: enterJestFn,
|
||||
onEscape: escapeJestfn,
|
||||
onClickOutside: clickOutsideJestFn,
|
||||
onTab: tabJestFn,
|
||||
onShiftTab: shiftTabJestFn,
|
||||
onEnter: handleEnterMocked,
|
||||
onEscape: handleEscapeMocked,
|
||||
onClickOutside: handleClickoutsideMocked,
|
||||
onTab: handleTabMocked,
|
||||
onShiftTab: handleShiftTabMocked,
|
||||
},
|
||||
argTypes: {
|
||||
onEnter: { control: false },
|
||||
@@ -162,13 +154,13 @@ export const Enter: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(enterJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleEnterMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
await userEvent.keyboard('{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(enterJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleEnterMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -177,13 +169,13 @@ export const Escape: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(escapeJestfn).toHaveBeenCalledTimes(0);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
await userEvent.keyboard('{esc}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(escapeJestfn).toHaveBeenCalledTimes(1);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -192,7 +184,7 @@ export const ClickOutside: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
||||
|
||||
@@ -200,7 +192,7 @@ export const ClickOutside: Story = {
|
||||
await userEvent.click(emptyDiv);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -209,13 +201,13 @@ export const Tab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(tabJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleTabMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
await userEvent.keyboard('{tab}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(tabJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleTabMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -224,13 +216,13 @@ export const ShiftTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleShiftTabMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
await userEvent.keyboard('{shift>}{tab}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleShiftTabMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+14
-58
@@ -1,12 +1,12 @@
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePhonesField } from '@/object-record/record-field/meta-types/hooks/usePhonesField';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { type FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -15,48 +15,34 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { PhonesFieldInput } from '../PhonesFieldInput';
|
||||
|
||||
const updateRecord = fn();
|
||||
const { FieldInputEventContextProviderWithJestMocks } =
|
||||
getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const PhoneValueSetterEffect = ({ value }: { value: FieldPhonesValue }) => {
|
||||
const { setFieldValue } = usePhonesField();
|
||||
const { setFieldValue, setDraftValue } = usePhonesField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
setDraftValue(value);
|
||||
}, [setFieldValue, value, setDraftValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type PhoneFieldValueGaterProps = Pick<
|
||||
PhoneInputWithContextProps,
|
||||
'onCancel' | 'onClickOutside'
|
||||
>;
|
||||
|
||||
const PhoneFieldValueGater = ({
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: PhoneFieldValueGaterProps) => {
|
||||
const PhoneFieldValueGater = () => {
|
||||
const { fieldValue } = usePhonesField();
|
||||
|
||||
return (
|
||||
fieldValue && (
|
||||
<PhonesFieldInput onCancel={onCancel} onClickOutside={onClickOutside} />
|
||||
)
|
||||
);
|
||||
return fieldValue && <PhonesFieldInput />;
|
||||
};
|
||||
|
||||
type PhoneInputWithContextProps = {
|
||||
value: FieldPhonesValue;
|
||||
recordId?: string;
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
};
|
||||
|
||||
const PhoneInputWithContext = ({
|
||||
value,
|
||||
recordId = 'record-id',
|
||||
onCancel,
|
||||
onClickOutside,
|
||||
}: PhoneInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const instanceId = getRecordFieldInputInstanceId({
|
||||
@@ -97,14 +83,13 @@ const PhoneInputWithContext = ({
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
useUpdateRecord: () => [() => {}, { loading: false }],
|
||||
}}
|
||||
>
|
||||
<PhoneValueSetterEffect value={value} />
|
||||
<PhoneFieldValueGater
|
||||
onCancel={onCancel}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<PhoneValueSetterEffect value={value} />
|
||||
<PhoneFieldValueGater />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
@@ -173,35 +158,6 @@ export const TrimInput: Story = {
|
||||
|
||||
const newPhoneElement = await canvas.findByText('+33 6 42 64 62 74');
|
||||
expect(newPhoneElement).toBeVisible();
|
||||
|
||||
// Verify the update was called with swapped phones
|
||||
await waitFor(() => {
|
||||
expect(updateRecord).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
where: { id: 'record-id' },
|
||||
updateOneRecordInput: {
|
||||
phones: {
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneNumber: '642646272',
|
||||
additionalPhones: [
|
||||
{
|
||||
countryCode: 'FR',
|
||||
number: '642646273',
|
||||
callingCode: '+33',
|
||||
},
|
||||
{
|
||||
countryCode: 'FR',
|
||||
number: '642646274',
|
||||
callingCode: '+33',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+14
-14
@@ -1,10 +1,11 @@
|
||||
import { type Decorator, type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -12,10 +13,10 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type FieldRatingValue } from '../../../../types/FieldMetadata';
|
||||
import { useRatingField } from '../../../hooks/useRatingField';
|
||||
import {
|
||||
RatingFieldInput,
|
||||
type RatingFieldInputProps,
|
||||
} from '../RatingFieldInput';
|
||||
import { RatingFieldInput } from '../RatingFieldInput';
|
||||
|
||||
const { FieldInputEventContextProviderWithJestMocks, handleSubmitMocked } =
|
||||
getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const RatingFieldValueSetterEffect = ({
|
||||
value,
|
||||
@@ -31,7 +32,7 @@ const RatingFieldValueSetterEffect = ({
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type RatingFieldInputWithContextProps = RatingFieldInputProps & {
|
||||
type RatingFieldInputWithContextProps = {
|
||||
value: FieldRatingValue;
|
||||
recordId: string;
|
||||
};
|
||||
@@ -39,7 +40,6 @@ type RatingFieldInputWithContextProps = RatingFieldInputProps & {
|
||||
const RatingFieldInputWithContext = ({
|
||||
recordId,
|
||||
value,
|
||||
onSubmit,
|
||||
}: RatingFieldInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
@@ -83,17 +83,17 @@ const RatingFieldInputWithContext = ({
|
||||
}}
|
||||
>
|
||||
<RatingFieldValueSetterEffect value={value} />
|
||||
<RatingFieldInput onSubmit={onSubmit} />
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<RatingFieldInput />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const submitJestFn = fn();
|
||||
|
||||
const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
if (context.parameters.clearMocks === true) {
|
||||
submitJestFn.mockClear();
|
||||
handleSubmitMocked.mockClear();
|
||||
}
|
||||
return <Story />;
|
||||
};
|
||||
@@ -103,7 +103,7 @@ const meta: Meta = {
|
||||
component: RatingFieldInputWithContext,
|
||||
args: {
|
||||
value: '3',
|
||||
onSubmit: submitJestFn,
|
||||
onSubmit: handleSubmitMocked,
|
||||
},
|
||||
argTypes: {
|
||||
onSubmit: { control: false },
|
||||
@@ -124,7 +124,7 @@ export const Submit: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleSubmitMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const input = canvas.getByRole('slider', { name: 'Rating' });
|
||||
const firstStar = input.firstElementChild;
|
||||
@@ -136,7 +136,7 @@ export const Submit: Story = {
|
||||
await userEvent.click(firstStar);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleSubmitMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+24
-21
@@ -1,5 +1,5 @@
|
||||
import { type Decorator, type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
@@ -22,10 +22,9 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||
import {
|
||||
RelationToOneFieldInput,
|
||||
type RelationToOneFieldInputProps,
|
||||
} from '../RelationToOneFieldInput';
|
||||
import { RelationToOneFieldInput } from '../RelationToOneFieldInput';
|
||||
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
|
||||
const RelationWorkspaceSetterEffect = () => {
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
@@ -50,15 +49,19 @@ const RelationWorkspaceSetterEffect = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type RelationToOneFieldInputWithContextProps = RelationToOneFieldInputProps & {
|
||||
const {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleSubmitMocked,
|
||||
handleCancelMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
type RelationToOneFieldInputWithContextProps = {
|
||||
value: number;
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
const RelationToOneFieldInputWithContext = ({
|
||||
recordId,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RelationToOneFieldInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
@@ -101,8 +104,11 @@ const RelationToOneFieldInputWithContext = ({
|
||||
instanceId: 'relation-to-one-field-input-123-Relation',
|
||||
}}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
|
||||
<RelationToOneFieldInput />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
</FieldContext.Provider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
@@ -110,13 +116,10 @@ const RelationToOneFieldInputWithContext = ({
|
||||
);
|
||||
};
|
||||
|
||||
const submitJestFn = fn();
|
||||
const cancelJestFn = fn();
|
||||
|
||||
const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
if (context.parameters.clearMocks === true) {
|
||||
submitJestFn.mockClear();
|
||||
cancelJestFn.mockClear();
|
||||
handleSubmitMocked.mockClear();
|
||||
handleCancelMocked.mockClear();
|
||||
}
|
||||
return <Story />;
|
||||
};
|
||||
@@ -126,8 +129,8 @@ const meta: Meta = {
|
||||
component: RelationToOneFieldInputWithContext,
|
||||
args: {
|
||||
useEditButton: true,
|
||||
onSubmit: submitJestFn,
|
||||
onCancel: cancelJestFn,
|
||||
onSubmit: handleSubmitMocked,
|
||||
onCancel: handleCancelMocked,
|
||||
},
|
||||
argTypes: {
|
||||
onSubmit: { control: false },
|
||||
@@ -154,7 +157,7 @@ export const Submit: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(getCanvasElementForDropdownTesting());
|
||||
|
||||
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleSubmitMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const item = await canvas.findByText('Linkedin', undefined, {
|
||||
timeout: 3000,
|
||||
@@ -163,7 +166,7 @@ export const Submit: Story = {
|
||||
await userEvent.click(item);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleSubmitMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -172,12 +175,12 @@ export const Cancel: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(getCanvasElementForDropdownTesting());
|
||||
|
||||
expect(cancelJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleCancelMocked).toHaveBeenCalledTimes(0);
|
||||
await canvas.findByText('Linkedin', undefined, { timeout: 3000 });
|
||||
|
||||
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
||||
|
||||
await userEvent.click(emptyDiv);
|
||||
expect(cancelJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleCancelMocked).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
+20
-30
@@ -1,7 +1,6 @@
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
@@ -14,29 +13,25 @@ import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { RichTextFieldInput } from '../RichTextFieldInput';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
|
||||
const clickOutsideJestFn = fn();
|
||||
const escapeJestFn = fn();
|
||||
const targetableObjectId = 'test-id';
|
||||
|
||||
type RichTextFieldInputWithContextProps = {
|
||||
targetableObjectId?: string;
|
||||
onClickOutside?: typeof clickOutsideJestFn;
|
||||
onEscape?: typeof escapeJestFn;
|
||||
};
|
||||
const {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleEscapeMocked,
|
||||
handleClickoutsideMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
if (context.parameters.clearMocks !== false) {
|
||||
clickOutsideJestFn.mockClear();
|
||||
escapeJestFn.mockClear();
|
||||
handleClickoutsideMocked.mockClear();
|
||||
handleEscapeMocked.mockClear();
|
||||
}
|
||||
return <Story />;
|
||||
};
|
||||
|
||||
const RichTextFieldInputWithContext = ({
|
||||
targetableObjectId = 'test-id',
|
||||
onClickOutside,
|
||||
onEscape,
|
||||
}: RichTextFieldInputWithContextProps) => {
|
||||
const RichTextFieldInputWithContext = () => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const instanceId = getRecordFieldInputInstanceId({
|
||||
@@ -78,14 +73,9 @@ const RichTextFieldInputWithContext = ({
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RichTextFieldInput
|
||||
targetableObject={{
|
||||
id: targetableObjectId,
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Note,
|
||||
}}
|
||||
onClickOutside={onClickOutside}
|
||||
onEscape={onEscape}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<RichTextFieldInput />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
<div data-testid="click-outside-element" />
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
@@ -97,8 +87,8 @@ const meta: Meta = {
|
||||
component: RichTextFieldInputWithContext,
|
||||
args: {
|
||||
targetableObjectId: 'test-id',
|
||||
onClickOutside: clickOutsideJestFn,
|
||||
onEscape: escapeJestFn,
|
||||
onClickOutside: handleClickoutsideMocked,
|
||||
onEscape: handleEscapeMocked,
|
||||
},
|
||||
argTypes: {
|
||||
onClickOutside: { control: false },
|
||||
@@ -124,13 +114,13 @@ export const Default: Story = {};
|
||||
export const Escape: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(escapeJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('click-outside-element');
|
||||
await userEvent.keyboard('{esc}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(escapeJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -138,14 +128,14 @@ export const Escape: Story = {
|
||||
export const ClickOutside: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const outsideElement = await canvas.findByTestId('click-outside-element');
|
||||
|
||||
await userEvent.click(outsideElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+38
-42
@@ -1,9 +1,10 @@
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -13,7 +14,8 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { useTextField } from '../../../hooks/useTextField';
|
||||
import { TextFieldInput, type TextFieldInputProps } from '../TextFieldInput';
|
||||
import { TextFieldInput } from '../TextFieldInput';
|
||||
|
||||
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = useTextField();
|
||||
|
||||
@@ -24,7 +26,16 @@ const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type TextFieldInputWithContextProps = TextFieldInputProps & {
|
||||
const {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleEscapeMocked,
|
||||
handleClickoutsideMocked,
|
||||
handleEnterMocked,
|
||||
handleShiftTabMocked,
|
||||
handleTabMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
type TextFieldInputWithContextProps = {
|
||||
value: string;
|
||||
recordId: string;
|
||||
};
|
||||
@@ -32,11 +43,6 @@ type TextFieldInputWithContextProps = TextFieldInputProps & {
|
||||
const TextFieldInputWithContext = ({
|
||||
recordId,
|
||||
value,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: TextFieldInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
@@ -85,14 +91,10 @@ const TextFieldInputWithContext = ({
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<TextFieldValueSetterEffect value={value} />
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<TextFieldValueSetterEffect value={value} />
|
||||
<TextFieldInput />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</FieldContext.Provider>
|
||||
{isReady && <div data-testid="is-ready-marker" />}
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
@@ -100,19 +102,13 @@ const TextFieldInputWithContext = ({
|
||||
);
|
||||
};
|
||||
|
||||
const enterJestFn = fn();
|
||||
const escapeJestfn = fn();
|
||||
const clickOutsideJestFn = fn();
|
||||
const tabJestFn = fn();
|
||||
const shiftTabJestFn = fn();
|
||||
|
||||
const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
if (context.parameters.clearMocks === true) {
|
||||
enterJestFn.mockClear();
|
||||
escapeJestfn.mockClear();
|
||||
clickOutsideJestFn.mockClear();
|
||||
tabJestFn.mockClear();
|
||||
shiftTabJestFn.mockClear();
|
||||
handleEnterMocked.mockClear();
|
||||
handleEscapeMocked.mockClear();
|
||||
handleClickoutsideMocked.mockClear();
|
||||
handleTabMocked.mockClear();
|
||||
handleShiftTabMocked.mockClear();
|
||||
}
|
||||
return <Story />;
|
||||
};
|
||||
@@ -122,11 +118,11 @@ const meta: Meta = {
|
||||
component: TextFieldInputWithContext,
|
||||
args: {
|
||||
value: 'text',
|
||||
onEnter: enterJestFn,
|
||||
onEscape: escapeJestfn,
|
||||
onClickOutside: clickOutsideJestFn,
|
||||
onTab: tabJestFn,
|
||||
onShiftTab: shiftTabJestFn,
|
||||
onEnter: handleEnterMocked,
|
||||
onEscape: handleEscapeMocked,
|
||||
onClickOutside: handleClickoutsideMocked,
|
||||
onTab: handleTabMocked,
|
||||
onShiftTab: handleShiftTabMocked,
|
||||
},
|
||||
argTypes: {
|
||||
onEnter: { control: false },
|
||||
@@ -151,13 +147,13 @@ export const Enter: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(enterJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleEnterMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
await userEvent.keyboard('{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(enterJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleEnterMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -166,13 +162,13 @@ export const Escape: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(escapeJestfn).toHaveBeenCalledTimes(0);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
await userEvent.keyboard('{esc}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(escapeJestfn).toHaveBeenCalledTimes(1);
|
||||
expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -181,7 +177,7 @@ export const ClickOutside: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
@@ -189,7 +185,7 @@ export const ClickOutside: Story = {
|
||||
await userEvent.click(emptyDiv);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clickOutsideJestFn).toHaveBeenCalled();
|
||||
expect(handleClickoutsideMocked).toHaveBeenCalled();
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -200,12 +196,12 @@ export const Tab: Story = {
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
|
||||
expect(tabJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleTabMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await userEvent.keyboard('{tab}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(tabJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleTabMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -216,12 +212,12 @@ export const ShiftTab: Story = {
|
||||
|
||||
await canvas.findByTestId('is-ready-marker');
|
||||
|
||||
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
|
||||
expect(handleShiftTabMocked).toHaveBeenCalledTimes(0);
|
||||
|
||||
await userEvent.keyboard('{shift>}{tab}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
|
||||
expect(handleShiftTabMocked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { fn } from '@storybook/test';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
type FieldInputEventContextProviderWithJestMocksProps = PropsWithChildren;
|
||||
|
||||
export const getFieldInputEventContextProviderWithJestMocks = () => {
|
||||
const handleSubmitMocked = fn();
|
||||
const handleCancelMocked = fn();
|
||||
const handleClickoutsideMocked = fn();
|
||||
const handleEnterMocked = fn();
|
||||
const handleEscapeMocked = fn();
|
||||
const handleShiftTabMocked = fn();
|
||||
const handleTabMocked = fn();
|
||||
|
||||
const FieldInputEventContextProviderWithJestMocks = ({
|
||||
children,
|
||||
}: FieldInputEventContextProviderWithJestMocksProps) => {
|
||||
return (
|
||||
<FieldInputEventContext.Provider
|
||||
value={{
|
||||
onCancel: handleCancelMocked,
|
||||
onClickOutside: handleClickoutsideMocked,
|
||||
onSubmit: handleSubmitMocked,
|
||||
onEnter: handleEnterMocked,
|
||||
onEscape: handleEscapeMocked,
|
||||
onShiftTab: handleShiftTabMocked,
|
||||
onTab: handleTabMocked,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FieldInputEventContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
FieldInputEventContextProviderWithJestMocks,
|
||||
handleSubmitMocked,
|
||||
handleCancelMocked,
|
||||
handleClickoutsideMocked,
|
||||
handleEnterMocked,
|
||||
handleEscapeMocked,
|
||||
handleShiftTabMocked,
|
||||
handleTabMocked,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
|
||||
export type FieldInputClickOutsideEvent = (
|
||||
persist: () => void,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => void;
|
||||
+71
-36
@@ -5,14 +5,16 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
|
||||
|
||||
import {
|
||||
FieldInputEventContext,
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/hooks/usePersistFieldFromFieldInputContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
@@ -98,13 +100,22 @@ export const RecordInlineCell = ({
|
||||
goBackToPreviousDropdownFocusId,
|
||||
]);
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const { persistFieldFromFieldInputContext } =
|
||||
usePersistFieldFromFieldInputContext();
|
||||
|
||||
const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleSubmit: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleSubmit: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
@@ -112,23 +123,37 @@ export const RecordInlineCell = ({
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
const handleEscape: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
|
||||
const handleClickOutside = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(persistField, event) => {
|
||||
({
|
||||
event,
|
||||
newValue,
|
||||
skipPersist,
|
||||
}: Parameters<FieldInputClickOutsideEvent>[0]) => {
|
||||
const currentDropdownFocusId = snapshot
|
||||
.getLoadable(activeDropdownFocusIdState)
|
||||
.getValue();
|
||||
@@ -142,13 +167,22 @@ export const RecordInlineCell = ({
|
||||
if (currentDropdownFocusId !== expectedDropdownFocusId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
persistField();
|
||||
event?.preventDefault();
|
||||
event?.stopImmediatePropagation();
|
||||
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeInlineCell();
|
||||
},
|
||||
[closeInlineCell, recordId, fieldDefinition.fieldMetadataId],
|
||||
[
|
||||
closeInlineCell,
|
||||
recordId,
|
||||
fieldDefinition.fieldMetadataId,
|
||||
persistFieldFromFieldInputContext,
|
||||
],
|
||||
);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
@@ -163,18 +197,7 @@ export const RecordInlineCell = ({
|
||||
labelWidth: fieldDefinition.labelWidth,
|
||||
showLabel: fieldDefinition.showLabel,
|
||||
isCentered,
|
||||
editModeContent: (
|
||||
<FieldInput
|
||||
onEnter={handleEnter}
|
||||
onCancel={handleCancel}
|
||||
onEscape={handleEscape}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
onClickOutside={handleClickOutside}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
),
|
||||
editModeContent: <FieldInput />,
|
||||
displayModeContent: <FieldDisplay />,
|
||||
isDisplayModeFixHeight: isDisplayModeFixHeight,
|
||||
editModeContentOnly: isFieldInputOnly,
|
||||
@@ -184,10 +207,22 @@ export const RecordInlineCell = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldFocusContextProvider>
|
||||
<RecordInlineCellContext.Provider value={RecordInlineCellContextValue}>
|
||||
<RecordInlineCellContainer />
|
||||
</RecordInlineCellContext.Provider>
|
||||
</FieldFocusContextProvider>
|
||||
<FieldInputEventContext.Provider
|
||||
value={{
|
||||
onCancel: handleCancel,
|
||||
onEnter: handleEnter,
|
||||
onEscape: handleEscape,
|
||||
onClickOutside: handleClickOutside,
|
||||
onShiftTab: handleShiftTab,
|
||||
onSubmit: handleSubmit,
|
||||
onTab: handleTab,
|
||||
}}
|
||||
>
|
||||
<FieldFocusContextProvider>
|
||||
<RecordInlineCellContext.Provider value={RecordInlineCellContextValue}>
|
||||
<RecordInlineCellContainer />
|
||||
</RecordInlineCellContext.Provider>
|
||||
</FieldFocusContextProvider>
|
||||
</FieldInputEventContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -20,10 +20,10 @@ import { RecordDetailDuplicatesSection } from '@/object-record/record-show/recor
|
||||
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
|
||||
type FieldsCardProps = {
|
||||
objectNameSingular: string;
|
||||
|
||||
+4
-4
@@ -17,9 +17,9 @@ import {
|
||||
type RecordUpdateHook,
|
||||
type RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
@@ -113,6 +113,8 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
isRecordFieldReadOnly: parentIsRecordFieldReadOnly,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const {
|
||||
@@ -137,8 +139,6 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
relationObjectMetadataItem.id,
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
@@ -189,7 +189,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
if (!relationFieldMetadataItem?.name) return;
|
||||
|
||||
if (isToOneObject) {
|
||||
persistField(null);
|
||||
onSubmit?.({ newValue: null });
|
||||
} else {
|
||||
updateOneRelationRecord({
|
||||
idToUpdate: relationRecord.id,
|
||||
|
||||
+58
-24
@@ -2,9 +2,15 @@ import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import {
|
||||
FieldInputEventContext,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
|
||||
import { RecordDetailRelationSectionDropdown } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown';
|
||||
@@ -40,6 +46,7 @@ export const RecordDetailRelationSection = ({
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
objectMetadataNameSingular,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
@@ -119,34 +126,61 @@ export const RecordDetailRelationSection = ({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: refactor this when we have refactored columnDefinitions and field definitions because
|
||||
// we should be able to get the objectMetadataItem from a context way more easily
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItemToFind) =>
|
||||
objectMetadataItemToFind.nameSingular === objectMetadataNameSingular,
|
||||
);
|
||||
|
||||
const persistField = usePersistField({
|
||||
objectMetadataItemId: objectMetadataItem?.id ?? '',
|
||||
});
|
||||
|
||||
const handleSubmit: FieldInputEvent = ({ newValue }) => {
|
||||
persistField({
|
||||
recordId: recordId,
|
||||
fieldDefinition,
|
||||
valueToPersist: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
const relationRecordsCount = relationAggregateResult?.id?.COUNT ?? 0;
|
||||
|
||||
return (
|
||||
<RecordDetailSection>
|
||||
<RecordDetailSectionHeader
|
||||
title={fieldDefinition.label}
|
||||
link={
|
||||
isToManyObjects
|
||||
? {
|
||||
to: filterLinkHref,
|
||||
label:
|
||||
relationRecordsCount > 0
|
||||
? t`All (${relationRecordsCount})`
|
||||
: '',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
|
||||
areRecordsAvailable={relationRecords.length > 0}
|
||||
rightAdornment={
|
||||
<RecordDetailRelationSectionDropdown loading={loading} />
|
||||
}
|
||||
/>
|
||||
{relationRecords.length > 0 && (
|
||||
<RecordDetailRelationRecordsList relationRecords={relationRecords} />
|
||||
)}
|
||||
</RecordDetailSection>
|
||||
<FieldInputEventContext.Provider
|
||||
value={{
|
||||
onSubmit: handleSubmit,
|
||||
}}
|
||||
>
|
||||
<RecordDetailSection>
|
||||
<RecordDetailSectionHeader
|
||||
title={fieldDefinition.label}
|
||||
link={
|
||||
isToManyObjects
|
||||
? {
|
||||
to: filterLinkHref,
|
||||
label:
|
||||
relationRecordsCount > 0
|
||||
? t`All (${relationRecordsCount})`
|
||||
: '',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
|
||||
areRecordsAvailable={relationRecords.length > 0}
|
||||
rightAdornment={
|
||||
<RecordDetailRelationSectionDropdown loading={loading} />
|
||||
}
|
||||
/>
|
||||
{relationRecords.length > 0 && (
|
||||
<RecordDetailRelationRecordsList relationRecords={relationRecords} />
|
||||
)}
|
||||
</RecordDetailSection>
|
||||
</FieldInputEventContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
@@ -74,7 +74,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
||||
setSingleRecordPickerSearchFilter('');
|
||||
}, [setSingleRecordPickerSearchFilter]);
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const handleRelationPickerEntitySelected = (
|
||||
selectedRelationEntity?: SingleRecordPickerRecord,
|
||||
@@ -83,7 +83,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
||||
|
||||
if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return;
|
||||
|
||||
persistField(selectedRelationEntity.record);
|
||||
onSubmit?.({ newValue: selectedRelationEntity.record });
|
||||
};
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
|
||||
+50
-29
@@ -1,33 +1,41 @@
|
||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/hooks/usePersistFieldFromFieldInputContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
|
||||
import {
|
||||
FieldInputEventContext,
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
} from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const RecordTableCellFieldInput = () => {
|
||||
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceId(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const { persistFieldFromFieldInputContext } =
|
||||
usePersistFieldFromFieldInputContext();
|
||||
|
||||
const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
onCloseTableCell();
|
||||
onMoveFocus('down');
|
||||
};
|
||||
|
||||
const handleSubmit: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleSubmit: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
onCloseTableCell();
|
||||
};
|
||||
@@ -38,7 +46,7 @@ export const RecordTableCellFieldInput = () => {
|
||||
|
||||
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(persistField, event) => {
|
||||
({ newValue, event, skipPersist }) => {
|
||||
const currentFocusId = snapshot
|
||||
.getLoadable(currentFocusIdSelector)
|
||||
.getValue();
|
||||
@@ -46,44 +54,57 @@ export const RecordTableCellFieldInput = () => {
|
||||
if (currentFocusId !== instanceId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
persistField();
|
||||
event?.preventDefault();
|
||||
event?.stopImmediatePropagation();
|
||||
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
onCloseTableCell();
|
||||
},
|
||||
[onCloseTableCell, instanceId],
|
||||
[onCloseTableCell, instanceId, persistFieldFromFieldInputContext],
|
||||
);
|
||||
|
||||
const handleEscape: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleEscape: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
onCloseTableCell();
|
||||
};
|
||||
|
||||
const handleTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
onCloseTableCell();
|
||||
onMoveFocus('right');
|
||||
};
|
||||
|
||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
onCloseTableCell();
|
||||
onMoveFocus('left');
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInput
|
||||
onCancel={handleCancel}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
<FieldInputEventContext.Provider
|
||||
value={{
|
||||
onCancel: handleCancel,
|
||||
onEnter: handleEnter,
|
||||
onEscape: handleEscape,
|
||||
onClickOutside: handleClickOutside,
|
||||
onShiftTab: handleShiftTab,
|
||||
onSubmit: handleSubmit,
|
||||
onTab: handleTab,
|
||||
}}
|
||||
>
|
||||
<FieldInput />
|
||||
</FieldInputEventContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+56
-29
@@ -1,13 +1,15 @@
|
||||
import { useContext } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
|
||||
import {
|
||||
FieldInputEventContext,
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
} from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/hooks/usePersistFieldFromFieldInputContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
|
||||
import {
|
||||
@@ -19,6 +21,7 @@ import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/com
|
||||
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
|
||||
import { type RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type RecordTitleCellProps = {
|
||||
loading?: boolean;
|
||||
@@ -38,53 +41,77 @@ export const RecordTitleCell = ({
|
||||
|
||||
const { closeRecordTitleCell } = useRecordTitleCell();
|
||||
|
||||
const closeCell = () => {
|
||||
const closeCell = useCallback(() => {
|
||||
closeRecordTitleCell({
|
||||
recordId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
containerType,
|
||||
});
|
||||
};
|
||||
}, [closeRecordTitleCell, containerType, fieldDefinition, recordId]);
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
closeCell();
|
||||
persistField();
|
||||
};
|
||||
const { persistFieldFromFieldInputContext } =
|
||||
usePersistFieldFromFieldInputContext();
|
||||
|
||||
const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
const handleEscape = () => {
|
||||
closeCell();
|
||||
};
|
||||
|
||||
const handleTab: FieldInputEvent = (persistField) => {
|
||||
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
|
||||
() =>
|
||||
({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeCell();
|
||||
},
|
||||
[closeCell, persistFieldFromFieldInputContext],
|
||||
);
|
||||
|
||||
const handleEscape: FieldInputEvent = () => {
|
||||
closeCell();
|
||||
persistField();
|
||||
};
|
||||
|
||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||
const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeCell();
|
||||
persistField();
|
||||
};
|
||||
|
||||
const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => {
|
||||
const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => {
|
||||
if (skipPersist !== true) {
|
||||
persistFieldFromFieldInputContext(newValue);
|
||||
}
|
||||
|
||||
closeCell();
|
||||
persistField();
|
||||
};
|
||||
|
||||
const recordTitleCellContextValue: RecordTitleCellContextProps = {
|
||||
editModeContent: (
|
||||
<RecordTitleCellFieldInput
|
||||
instanceId={getRecordFieldInputInstanceId({
|
||||
recordId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
prefix: containerType,
|
||||
})}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
onClickOutside={handleClickOutside}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
<FieldInputEventContext.Provider
|
||||
value={{
|
||||
onClickOutside: handleClickOutside,
|
||||
onEnter: handleEnter,
|
||||
onEscape: handleEscape,
|
||||
onShiftTab: handleShiftTab,
|
||||
onTab: handleTab,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCellFieldInput
|
||||
instanceId={getRecordFieldInputInstanceId({
|
||||
recordId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
prefix: containerType,
|
||||
})}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
</FieldInputEventContext.Provider>
|
||||
),
|
||||
displayModeContent: (
|
||||
<RecordTitleCellFieldDisplay containerType={containerType} />
|
||||
|
||||
+1
-27
@@ -1,7 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { RecordTitleCellTextFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldInput';
|
||||
@@ -9,25 +8,12 @@ import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell
|
||||
|
||||
type RecordTitleCellFieldInputProps = {
|
||||
instanceId: string;
|
||||
onClickOutside?: (
|
||||
persist: () => void,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => void;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCellFieldInput = ({
|
||||
instanceId,
|
||||
sizeVariant,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onShiftTab,
|
||||
onTab,
|
||||
onClickOutside,
|
||||
}: RecordTitleCellFieldInputProps) => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
@@ -40,22 +26,10 @@ export const RecordTitleCellFieldInput = ({
|
||||
{isFieldText(fieldDefinition) ? (
|
||||
<RecordTitleCellTextFieldInput
|
||||
instanceId={instanceId}
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<RecordTitleFullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
<RecordTitleFullNameFieldInput sizeVariant={sizeVariant} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
+11
-22
@@ -1,33 +1,20 @@
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useTextField } from '@/object-record/record-field/meta-types/hooks/useTextField';
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useRef } from 'react';
|
||||
import { useContext, useRef } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
|
||||
|
||||
type RecordTitleCellTextFieldInputProps = {
|
||||
instanceId: string;
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCellTextFieldInput = ({
|
||||
instanceId,
|
||||
sizeVariant,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: RecordTitleCellTextFieldInputProps) => {
|
||||
const { fieldDefinition, draftValue, setDraftValue } = useTextField();
|
||||
|
||||
@@ -37,26 +24,28 @@ export const RecordTitleCellTextFieldInput = ({
|
||||
setDraftValue(turnIntoUndefinedIfWhitespacesOnly(newText));
|
||||
};
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
useRegisterInputEvents<string>({
|
||||
focusId: instanceId,
|
||||
inputRef: wrapperRef,
|
||||
inputValue: draftValue ?? '',
|
||||
onEnter: (inputValue) => {
|
||||
onEnter?.(() => persistField(inputValue));
|
||||
onEnter?.({ newValue: inputValue });
|
||||
},
|
||||
onEscape: (inputValue) => {
|
||||
onEscape?.(() => persistField(inputValue));
|
||||
onEscape?.({ newValue: inputValue });
|
||||
},
|
||||
onClickOutside: (event, inputValue) => {
|
||||
onClickOutside?.(() => persistField(inputValue), event);
|
||||
onClickOutside?.({ newValue: inputValue, event });
|
||||
},
|
||||
onTab: (inputValue) => {
|
||||
onTab?.(() => persistField(inputValue));
|
||||
onTab?.({ newValue: inputValue });
|
||||
},
|
||||
onShiftTab: (inputValue) => {
|
||||
onShiftTab?.(() => persistField(inputValue));
|
||||
onShiftTab?.({ newValue: inputValue });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+13
-24
@@ -1,33 +1,25 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext';
|
||||
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
|
||||
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
|
||||
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
|
||||
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
|
||||
import { type FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
|
||||
import {
|
||||
type FieldInputClickOutsideEvent,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { RecordTitleDoubleTextInput } from './RecordTitleDoubleTextInput';
|
||||
|
||||
type RecordTitleFullNameFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleFullNameFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
sizeVariant,
|
||||
}: RecordTitleFullNameFieldInputProps) => {
|
||||
const { draftValue, setDraftValue, persistFullNameField } =
|
||||
useFullNameField();
|
||||
const { draftValue, setDraftValue } = useFullNameField();
|
||||
|
||||
const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext(
|
||||
FieldInputEventContext,
|
||||
);
|
||||
|
||||
const convertToFullName = (newDoubleText: FieldDoubleText) => {
|
||||
return {
|
||||
@@ -45,29 +37,26 @@ export const RecordTitleFullNameFieldInput = ({
|
||||
};
|
||||
|
||||
const handleEnter = (newDoubleText: FieldDoubleText) => {
|
||||
onEnter?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onEnter?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleEscape = (newDoubleText: FieldDoubleText) => {
|
||||
onEscape?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onEscape?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
onClickOutside?.(
|
||||
() => persistFullNameField(convertToFullName(newDoubleText)),
|
||||
event,
|
||||
);
|
||||
onClickOutside?.({ newValue: convertToFullName(newDoubleText), event });
|
||||
};
|
||||
|
||||
const handleTab = (newDoubleText: FieldDoubleText) => {
|
||||
onTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onTab?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
|
||||
onShiftTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
onShiftTab?.({ newValue: convertToFullName(newDoubleText) });
|
||||
};
|
||||
|
||||
const handleChange = (newDoubleText: FieldDoubleText) => {
|
||||
|
||||
+7
-3
@@ -150,14 +150,18 @@ export const SettingsDataModelFieldPreview = ({
|
||||
},
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
},
|
||||
isRecordFieldReadOnly: false,
|
||||
isRecordFieldReadOnly:
|
||||
fieldMetadataItem.type === FieldMetadataType.BOOLEAN ||
|
||||
fieldMetadataItem.type === FieldMetadataType.RATING
|
||||
? true
|
||||
: false,
|
||||
disableChipClick: true,
|
||||
}}
|
||||
>
|
||||
{fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? (
|
||||
<BooleanFieldInput readonly />
|
||||
<BooleanFieldInput />
|
||||
) : fieldMetadataItem.type === FieldMetadataType.RATING ? (
|
||||
<RatingFieldInput readonly />
|
||||
<RatingFieldInput />
|
||||
) : (
|
||||
<FieldDisplay />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user