Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9154f7a4c2 | ||
|
|
1ce64f5028 | ||
|
|
176e81cd76 | ||
|
|
1ce4da5b67 | ||
|
|
ef66d6b337 | ||
|
|
bcca5d0002 | ||
|
|
e0630b8653 | ||
|
|
61a27984e8 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.8.0-canary.6",
|
||||
"version": "0.8.0-canary.7",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "0.8.0-canary.6",
|
||||
"version": "0.8.0-canary.7",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Lêeretiket"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "تسمية الملف"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Etiqueta del fitxer"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Štítek souboru"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Fil-etiket"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Dateibezeichnung"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Ετικέτα αρχείου"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6887,6 +6887,12 @@ msgstr "File field not found for attachment object"
|
||||
msgid "File label"
|
||||
msgstr "File label"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr "File no longer exists - {0}"
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Etiqueta del archivo"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Tiedoston otsikko"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Étiquette du fichier"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "תווית קובץ"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr "Fájlmező nem található a melléklet objektumhoz"
|
||||
msgid "File label"
|
||||
msgstr "Fájlcímke"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Etichetta del file"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "ファイルのラベル"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "파일 라벨"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Bestandslabel"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Etikett for fil"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Etykieta pliku"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6887,6 +6887,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Rótulo do arquivo"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Rótulo do arquivo"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Eticheta fișierului"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
Binary file not shown.
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Ознака датотеке"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Filens etikett"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Dosya etiketi"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Мітка файлу"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "Nhãn tệp"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "文件标签"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
@@ -6892,6 +6892,12 @@ msgstr ""
|
||||
msgid "File label"
|
||||
msgstr "檔案標籤"
|
||||
|
||||
#. js-lingui-id: QWWgBD
|
||||
#. placeholder {0}: file.label
|
||||
#: src/modules/ui/field/display/components/FileChip.tsx
|
||||
msgid "File no longer exists - {0}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ncBVqE
|
||||
#: src/modules/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile.ts
|
||||
msgid "File upload failed"
|
||||
|
||||
+35
-3
@@ -1,9 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const EventFieldDiffValueEffect = ({
|
||||
@@ -22,13 +26,39 @@ export const EventFieldDiffValueEffect = ({
|
||||
diffArtificialRecordStoreId,
|
||||
);
|
||||
|
||||
const { recordId } = useContext(TimelineActivityContext);
|
||||
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(diffRecord)) return;
|
||||
|
||||
let fieldValue = diffRecord;
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.FILES &&
|
||||
isDefined(recordStore) &&
|
||||
Array.isArray(diffRecord)
|
||||
) {
|
||||
const currentFiles = Array.isArray(recordStore[fieldMetadataItem.name])
|
||||
? (recordStore[fieldMetadataItem.name] as FieldFilesValue[])
|
||||
: [];
|
||||
const currentFileMap = new Map(
|
||||
currentFiles.map((file) => [file.fileId, file]),
|
||||
);
|
||||
|
||||
fieldValue = (diffRecord as FieldFilesValue[]).map((file) => {
|
||||
const currentFile = currentFileMap.get(file.fileId);
|
||||
if (isDefined(currentFile)) {
|
||||
return { ...file, url: currentFile.url };
|
||||
}
|
||||
return { ...file, isDeleted: true, url: undefined };
|
||||
});
|
||||
}
|
||||
|
||||
const forgedObjectRecord = {
|
||||
__typename: mainObjectMetadataItem.nameSingular,
|
||||
id: diffArtificialRecordStoreId,
|
||||
[fieldMetadataItem.name]: diffRecord,
|
||||
[fieldMetadataItem.name]: fieldValue,
|
||||
};
|
||||
|
||||
setRecordStore(forgedObjectRecord);
|
||||
@@ -36,8 +66,10 @@ export const EventFieldDiffValueEffect = ({
|
||||
diffRecord,
|
||||
diffArtificialRecordStoreId,
|
||||
fieldMetadataItem.name,
|
||||
fieldMetadataItem.type,
|
||||
mainObjectMetadataItem.nameSingular,
|
||||
setRecordStore,
|
||||
recordStore,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
||||
+17
-15
@@ -56,29 +56,29 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
type FormFieldInputProps = {
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
|
||||
defaultValue: JsonValue;
|
||||
error?: string;
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
|
||||
onChange: (value: JsonValue) => void;
|
||||
onClear?: () => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
onError?: (error: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
timeZone?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormFieldInput = ({
|
||||
field,
|
||||
defaultValue,
|
||||
error,
|
||||
field,
|
||||
onChange,
|
||||
onClear,
|
||||
VariablePicker,
|
||||
readonly,
|
||||
placeholder,
|
||||
error,
|
||||
onError,
|
||||
placeholder,
|
||||
readonly,
|
||||
timeZone,
|
||||
VariablePicker,
|
||||
}: FormFieldInputProps) => {
|
||||
return isFieldNumber(field) || field.type === FieldMetadataType.NUMERIC ? (
|
||||
<FormNumberFieldInput
|
||||
@@ -168,21 +168,23 @@ export const FormFieldInput = ({
|
||||
/>
|
||||
) : isFieldDate(field) ? (
|
||||
<FormDateFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | undefined}
|
||||
error={error}
|
||||
label={field.label}
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
onError={onError}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldDateTime(field) ? (
|
||||
<FormDateTimeFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | undefined}
|
||||
label={field.label}
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
timeZone={timeZone}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldMultiSelect(field) ? (
|
||||
<FormMultiSelectFieldInput
|
||||
|
||||
+58
-103
@@ -1,17 +1,16 @@
|
||||
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
|
||||
import { VariableChipStandalone } from '@/object-record/record-field/ui/form-types/components/VariableChipStandalone';
|
||||
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
|
||||
import { InputHint } from '@/ui/input/components/InputHint';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { DatePicker } from '@/ui/input/components/internal/date/components/DatePicker';
|
||||
import { DatePickerInput } from '@/ui/input/components/internal/date/components/DatePickerInput';
|
||||
import {
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||
} from '@/ui/input/components/internal/date/components/DateTimePicker';
|
||||
import { useParseDateInputStringToPlainDate } from '@/ui/input/components/internal/date/hooks/useParseDateInputStringToPlainDate';
|
||||
import { useParsePlainDateToDateInputString } from '@/ui/input/components/internal/date/hooks/useParsePlainDateToDateInputString';
|
||||
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
@@ -20,25 +19,21 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
import { useId, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type Nullable } from 'twenty-ui/utilities';
|
||||
import { getDateFormatStringForDatePickerInputMask } from '~/utils/date-utils';
|
||||
|
||||
const StyledInputContainerWrapper = styled.div`
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputAbsoluteContainer = styled.div`
|
||||
@@ -46,31 +41,11 @@ const StyledDateInputAbsoluteContainer = styled.div`
|
||||
top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledDateInput = styled.input<{ hasError?: boolean }>`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: ${({ hasError }) =>
|
||||
hasError
|
||||
? themeCssVariables.color.red
|
||||
: themeCssVariables.font.color.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
outline: none;
|
||||
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
}
|
||||
|
||||
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
|
||||
const StyledDateInputTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputContainer = styled.div`
|
||||
@@ -90,29 +65,31 @@ type DraftValue =
|
||||
};
|
||||
|
||||
type FormDateFieldInputProps = {
|
||||
label?: string;
|
||||
defaultValue: string | undefined;
|
||||
error?: string;
|
||||
label?: string;
|
||||
onChange: (value: string | null) => void;
|
||||
onError?: (error: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
readonly?: boolean;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormDateFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
error: errorFromProps,
|
||||
label,
|
||||
onChange,
|
||||
VariablePicker,
|
||||
onError,
|
||||
placeholder: _placeholder,
|
||||
readonly,
|
||||
placeholder,
|
||||
VariablePicker,
|
||||
}: FormDateFieldInputProps) => {
|
||||
const instanceId = useId();
|
||||
const { dateFormat } = useDateTimeFormat();
|
||||
|
||||
const { parsePlainDateToDateInputString } =
|
||||
useParsePlainDateToDateInputString();
|
||||
const { parseDateInputStringToPlainDate } =
|
||||
useParseDateInputStringToPlainDate();
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const [draftValue, setDraftValue] = useState<DraftValue>(
|
||||
isStandaloneVariableString(defaultValue)
|
||||
@@ -139,12 +116,6 @@ export const FormDateFieldInput = ({
|
||||
|
||||
const datePickerWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [inputDate, setInputDate] = useState(
|
||||
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
|
||||
? parsePlainDateToDateInputString(draftValueAsDate)
|
||||
: '',
|
||||
);
|
||||
|
||||
const persistDate = (newDate: Nullable<string>) => {
|
||||
if (!isDefined(newDate)) {
|
||||
onChange(null);
|
||||
@@ -159,11 +130,6 @@ export const FormDateFieldInput = ({
|
||||
const displayDatePicker =
|
||||
draftValue.type === 'static' && draftValue.mode === 'edit';
|
||||
|
||||
const defaultPlaceHolder =
|
||||
getDateFormatStringForDatePickerInputMask(dateFormat);
|
||||
|
||||
const placeholderToDisplay = placeholder ?? defaultPlaceHolder;
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [datePickerWrapperRef],
|
||||
listenerId: 'FormDateTimeFieldInputBase',
|
||||
@@ -182,16 +148,15 @@ export const FormDateFieldInput = ({
|
||||
});
|
||||
|
||||
const handlePickerChange = (newDate: Nullable<string>) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
mode: 'edit',
|
||||
value: newDate ?? null,
|
||||
});
|
||||
|
||||
setInputDate(
|
||||
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
|
||||
);
|
||||
|
||||
setPickerDate(newDate);
|
||||
|
||||
persistDate(newDate);
|
||||
@@ -216,6 +181,9 @@ export const FormDateFieldInput = ({
|
||||
};
|
||||
|
||||
const handlePickerClear = () => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: null,
|
||||
@@ -224,12 +192,13 @@ export const FormDateFieldInput = ({
|
||||
|
||||
setPickerDate(null);
|
||||
|
||||
setInputDate('');
|
||||
|
||||
persistDate(null);
|
||||
};
|
||||
|
||||
const handlePickerMouseSelect = (newDate: Nullable<string>) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: newDate ?? null,
|
||||
@@ -238,10 +207,6 @@ export const FormDateFieldInput = ({
|
||||
|
||||
setPickerDate(newDate);
|
||||
|
||||
setInputDate(
|
||||
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
|
||||
);
|
||||
|
||||
persistDate(newDate);
|
||||
};
|
||||
|
||||
@@ -253,55 +218,42 @@ export const FormDateFieldInput = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputDate(event.target.value);
|
||||
};
|
||||
const handleMaskedDateChange = (newPlainDate: string | null) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputDateTime = inputDate.trim();
|
||||
|
||||
if (inputDateTime === '') {
|
||||
if (!isDefined(newPlainDate)) {
|
||||
handlePickerClear();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedInputPlainDate = parseDateInputStringToPlainDate(inputDateTime);
|
||||
|
||||
if (!isDefined(parsedInputPlainDate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let validatedDate = parsedInputPlainDate;
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: validatedDate,
|
||||
mode: 'edit',
|
||||
value: newPlainDate,
|
||||
});
|
||||
|
||||
setPickerDate(validatedDate);
|
||||
setPickerDate(newPlainDate);
|
||||
|
||||
setInputDate(parsePlainDateToDateInputString(validatedDate));
|
||||
|
||||
persistDate(validatedDate);
|
||||
persistDate(newPlainDate);
|
||||
};
|
||||
|
||||
const handleVariableTagInsert = (variableName: string) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'variable',
|
||||
value: variableName,
|
||||
});
|
||||
|
||||
setInputDate('');
|
||||
|
||||
onChange(variableName);
|
||||
};
|
||||
|
||||
const handleUnlinkVariable = () => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: null,
|
||||
@@ -313,6 +265,8 @@ export const FormDateFieldInput = ({
|
||||
onChange(null);
|
||||
};
|
||||
|
||||
const error = errorMessage ?? errorFromProps;
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: handlePickerEscape,
|
||||
@@ -332,16 +286,15 @@ export const FormDateFieldInput = ({
|
||||
>
|
||||
{draftValue.type === 'static' ? (
|
||||
<>
|
||||
<StyledDateInput
|
||||
type="text"
|
||||
placeholder={placeholderToDisplay}
|
||||
value={inputDate}
|
||||
onFocus={handleInputFocus}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeydown}
|
||||
disabled={readonly}
|
||||
/>
|
||||
|
||||
<StyledDateInputTextContainer>
|
||||
<DatePickerInput
|
||||
date={pickerDate ?? null}
|
||||
hasError={isDefined(error)}
|
||||
onChange={handleMaskedDateChange}
|
||||
onFocus={handleInputFocus}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</StyledDateInputTextContainer>
|
||||
{draftValue.mode === 'edit' ? (
|
||||
<StyledDateInputContainer>
|
||||
<StyledDateInputAbsoluteContainer>
|
||||
@@ -377,6 +330,8 @@ export const FormDateFieldInput = ({
|
||||
/>
|
||||
) : null}
|
||||
</FormFieldInputRowContainer>
|
||||
|
||||
{error ? <InputHint danger>{error}</InputHint> : null}
|
||||
</FormFieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+10
-4
@@ -28,10 +28,13 @@ import { type Nullable } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledInputContainerWrapper = styled.div`
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputAbsoluteContainer = styled.div`
|
||||
@@ -42,6 +45,8 @@ const StyledDateInputAbsoluteContainer = styled.div`
|
||||
const StyledDateInputTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputContainer = styled.div`
|
||||
@@ -61,22 +66,22 @@ type DraftValue =
|
||||
};
|
||||
|
||||
type FormDateTimeFieldInputProps = {
|
||||
label?: string;
|
||||
defaultValue: string | undefined;
|
||||
label?: string;
|
||||
onChange: (value: string | null) => void;
|
||||
placeholder?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
readonly?: boolean;
|
||||
timeZone?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormDateTimeFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
label,
|
||||
onChange,
|
||||
VariablePicker,
|
||||
readonly,
|
||||
timeZone,
|
||||
VariablePicker,
|
||||
}: FormDateTimeFieldInputProps) => {
|
||||
const instanceId = useId();
|
||||
|
||||
@@ -259,6 +264,7 @@ export const FormDateTimeFieldInput = ({
|
||||
<StyledDateInputTextContainer>
|
||||
<DateTimePickerInput
|
||||
date={dateValue}
|
||||
fullWidth
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleInputFocus}
|
||||
readonly={readonly}
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
|
||||
const StyledFormFieldInputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
|
||||
+4
-2
@@ -23,8 +23,10 @@ const StyledFormFieldInputInnerContainer = styled.div<
|
||||
border-bottom-left-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-bottom-right-radius: ${({ multiline, hasRightElement }) =>
|
||||
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
|
||||
border-right: ${({ multiline, hasRightElement }) =>
|
||||
multiline || !hasRightElement ? 'auto' : 'none'};
|
||||
border-right: ${({ hasRightElement }) =>
|
||||
hasRightElement
|
||||
? 'none'
|
||||
: `1px solid ${themeCssVariables.border.color.medium}`};
|
||||
border-top-left-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-top-right-radius: ${({ multiline, hasRightElement }) =>
|
||||
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
|
||||
|
||||
+3
-1
@@ -9,14 +9,16 @@ const StyledFormFieldInputRowContainer = styled.div<{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: ${({ multiline }) => (multiline ? 'auto' : '32px')};
|
||||
|
||||
line-height: ${({ multiline }) =>
|
||||
multiline ? `${LINE_HEIGHT}px` : 'normal'};
|
||||
max-height: ${({ multiline, maxHeight }) =>
|
||||
multiline ? `${maxHeight ?? 5 * LINE_HEIGHT}px` : 'none'};
|
||||
|
||||
min-height: ${({ multiline }) =>
|
||||
multiline ? `${3 * LINE_HEIGHT}px` : 'auto'};
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FormFieldInputRowContainer = StyledFormFieldInputRowContainer;
|
||||
|
||||
@@ -335,4 +335,5 @@ export type FieldFilesValue = {
|
||||
extension?: string;
|
||||
url?: string;
|
||||
fileCategory?: FileCategory;
|
||||
isDeleted?: boolean;
|
||||
};
|
||||
|
||||
+32
-1
@@ -100,10 +100,13 @@ describe('useSidePanelHistory', () => {
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenuHistory.goBackFromSidePanel();
|
||||
result.current.sidePanelCloseAnimationCompleteCleanup.sidePanelCloseAnimationCompleteCleanup();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toEqual([]);
|
||||
|
||||
act(() => {
|
||||
result.current.sidePanelCloseAnimationCompleteCleanup.sidePanelCloseAnimationCompleteCleanup();
|
||||
});
|
||||
expect(jotaiStore.get(sidePanelPageState.atom)).toBe(
|
||||
SidePanelPages.CommandMenuDisplay,
|
||||
);
|
||||
@@ -115,6 +118,34 @@ describe('useSidePanelHistory', () => {
|
||||
expect(jotaiStore.get(isSidePanelOpenedState.atom)).toBe(false);
|
||||
});
|
||||
|
||||
it('should clear navigation stack immediately when closeSidePanelMenu is called', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateSidePanelMenu({
|
||||
page: SidePanelPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toEqual([
|
||||
{
|
||||
page: SidePanelPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '1',
|
||||
},
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.closeSidePanelMenu();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should navigate to a page in history', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ export const useSidePanelHistory = () => {
|
||||
const newNavigationStack = currentNavigationStack.slice(0, -1);
|
||||
const lastNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
store.set(sidePanelNavigationStackState.atom, newNavigationStack);
|
||||
|
||||
if (!isDefined(lastNavigationStackItem)) {
|
||||
closeSidePanelMenu();
|
||||
return;
|
||||
@@ -80,8 +82,6 @@ export const useSidePanelHistory = () => {
|
||||
instanceId: lastNavigationStackItem.pageId,
|
||||
});
|
||||
|
||||
store.set(sidePanelNavigationStackState.atom, newNavigationStack);
|
||||
|
||||
store.set(hasUserSelectedSidePanelListItemState.atom, false);
|
||||
}, [cleanupCurrentPage, closeSidePanelMenu, store]);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-i
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
@@ -28,6 +29,7 @@ export const useSidePanelMenu = () => {
|
||||
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
|
||||
|
||||
if (isSidePanelOpened) {
|
||||
store.set(sidePanelNavigationStackState.atom, []);
|
||||
store.set(isSidePanelOpenedState.atom, false);
|
||||
store.set(isSidePanelClosingState.atom, true);
|
||||
closeAnyOpenDropdown();
|
||||
|
||||
+4
@@ -30,9 +30,13 @@ import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { FileIcon } from '@/file/components/FileIcon';
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
@@ -24,7 +25,8 @@ export const FileChip = ({
|
||||
onClick,
|
||||
forceDisableClick,
|
||||
}: FileChipProps) => {
|
||||
const isClickable = forceDisableClick !== true;
|
||||
const isDeleted = file.isDeleted === true;
|
||||
const isClickable = forceDisableClick !== true && !isDeleted;
|
||||
|
||||
const handleMouseDown = (event: React.MouseEvent): void => {
|
||||
if (!isClickable) {
|
||||
@@ -45,17 +47,24 @@ export const FileChip = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledClickableContainer
|
||||
clickable={isClickable}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<Chip
|
||||
label={file.label ?? ''}
|
||||
maxWidth={MAX_WIDTH}
|
||||
leftComponent={fileIcon}
|
||||
variant={ChipVariant.Highlighted}
|
||||
<>
|
||||
<StyledClickableContainer
|
||||
clickable={isClickable}
|
||||
/>
|
||||
</StyledClickableContainer>
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<Chip
|
||||
label={file.label}
|
||||
alwaysShowTooltip={isDeleted}
|
||||
tooltipLabel={
|
||||
isDeleted ? t`File no longer exists - ${file.label}` : undefined
|
||||
}
|
||||
disabled={isDeleted}
|
||||
maxWidth={MAX_WIDTH}
|
||||
leftComponent={fileIcon}
|
||||
variant={isDeleted ? ChipVariant.Static : ChipVariant.Highlighted}
|
||||
clickable={isClickable}
|
||||
/>
|
||||
</StyledClickableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+38
-10
@@ -22,6 +22,7 @@ const StyledInputContainer = styled.div`
|
||||
border-top-right-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@@ -32,19 +33,34 @@ const StyledInput = styled.input<{ hasError?: boolean }>`
|
||||
hasError
|
||||
? themeCssVariables.color.red
|
||||
: themeCssVariables.font.color.primary};
|
||||
flex: 1 1 auto;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
padding: 4px 8px 4px 8px;
|
||||
width: 100%;
|
||||
|
||||
&:disabled {
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
type DatePickerInputProps = {
|
||||
onChange?: (date: string | null) => void;
|
||||
date: string | null;
|
||||
hasError?: boolean;
|
||||
onChange?: (date: string | null) => void;
|
||||
onFocus?: () => void;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
|
||||
export const DatePickerInput = ({
|
||||
date,
|
||||
hasError,
|
||||
onChange,
|
||||
onFocus,
|
||||
readonly,
|
||||
}: DatePickerInputProps) => {
|
||||
const { dateFormat } = useDateTimeFormat();
|
||||
|
||||
const [internalDate, setInternalDate] = useState(date);
|
||||
@@ -71,15 +87,17 @@ export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
|
||||
? (parsePlainDateToDateInputString(internalDate) ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const { ref, setValue, value } = useIMask(
|
||||
const { ref, setValue } = useIMask(
|
||||
{
|
||||
mask: Date,
|
||||
pattern,
|
||||
blocks,
|
||||
min: MIN_DATE,
|
||||
max: MAX_DATE,
|
||||
format: (date: any) =>
|
||||
isDefined(date) ? parseIMaskJSDateIMaskDateInputString(date) : '',
|
||||
format: (dateValue: unknown) =>
|
||||
isDefined(dateValue) && dateValue instanceof Date
|
||||
? parseIMaskJSDateIMaskDateInputString(dateValue)
|
||||
: '',
|
||||
parse: parseIMaskDateInputStringToJSDate,
|
||||
lazy: false,
|
||||
autofix: true,
|
||||
@@ -89,25 +107,35 @@ export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
|
||||
onComplete: (newValue) => {
|
||||
const parsedDate = parseDateInputStringToPlainDate(newValue);
|
||||
|
||||
onChange?.(parsedDate);
|
||||
onChange?.(isDefined(parsedDate) ? parsedDate : null);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(date) && internalDate !== date) {
|
||||
setInternalDate(date);
|
||||
if (date === internalDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInternalDate(date);
|
||||
|
||||
if (isDefined(date)) {
|
||||
setValue(parsePlainDateToDateInputString(date));
|
||||
} else {
|
||||
setValue('');
|
||||
}
|
||||
}, [date, internalDate, parsePlainDateToDateInputString, setValue]);
|
||||
|
||||
const shouldDisplayReadOnly = readonly === true;
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
disabled={shouldDisplayReadOnly}
|
||||
hasError={hasError}
|
||||
type="text"
|
||||
ref={ref as any}
|
||||
value={value}
|
||||
onChange={() => {}} // Prevent React warning
|
||||
onFocus={!shouldDisplayReadOnly ? onFocus : undefined}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
);
|
||||
|
||||
+11
-6
@@ -20,30 +20,33 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
const StyledInputContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
align-items: center;
|
||||
|
||||
border-top-left-radius: ${themeCssVariables.border.radius.md};
|
||||
border-top-right-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
min-width: ${({ fullWidth }) => (fullWidth === true ? '0' : 'auto')};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<{ hasError?: boolean }>`
|
||||
const StyledInput = styled.input<{ fullWidth?: boolean; hasError?: boolean }>`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
flex: ${({ fullWidth }) => (fullWidth === true ? '1 1 auto' : 'none')};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: 500;
|
||||
min-width: ${({ fullWidth }) => (fullWidth === true ? '0' : 'auto')};
|
||||
outline: none;
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
width: 140px;
|
||||
width: ${({ fullWidth }) => (fullWidth === true ? '100%' : '140px')};
|
||||
`;
|
||||
|
||||
type DateTimePickerInputProps = {
|
||||
onChange?: (date: Temporal.ZonedDateTime | null) => void;
|
||||
date: Temporal.ZonedDateTime | null;
|
||||
fullWidth?: boolean;
|
||||
onChange?: (date: Temporal.ZonedDateTime | null) => void;
|
||||
onFocus?: () => void;
|
||||
readonly?: boolean;
|
||||
timeZone?: string;
|
||||
@@ -51,6 +54,7 @@ type DateTimePickerInputProps = {
|
||||
|
||||
export const DateTimePickerInput = ({
|
||||
date,
|
||||
fullWidth = false,
|
||||
onChange,
|
||||
onFocus,
|
||||
readonly,
|
||||
@@ -167,9 +171,10 @@ export const DateTimePickerInput = ({
|
||||
internalDate?.toInstant() ?? Temporal.Now.instant();
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInputContainer fullWidth={fullWidth}>
|
||||
<StyledInput
|
||||
disabled={shouldDisplayReadOnly}
|
||||
fullWidth={fullWidth}
|
||||
type="text"
|
||||
ref={ref as any}
|
||||
onFocus={!shouldDisplayReadOnly ? onFocus : undefined}
|
||||
|
||||
+3
@@ -13,17 +13,20 @@ const StyledWorkflowStepBody = styled.div<{
|
||||
gridTemplateRows?: string;
|
||||
}>`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
box-sizing: border-box;
|
||||
display: ${({ display }) => display ?? 'flex'};
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
grid-template-rows: ${({ gridTemplateRows }) => gridTemplateRows ?? 'none'};
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: ${({ overflow }) => overflow ?? 'hidden scroll'};
|
||||
padding-block: ${({ paddingBlock }) =>
|
||||
paddingBlock ?? themeCssVariables.spacing[4]};
|
||||
padding-inline: ${({ paddingInline }) =>
|
||||
paddingInline ?? themeCssVariables.spacing[3]};
|
||||
row-gap: ${({ rowGap }) => rowGap ?? themeCssVariables.spacing[4]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const WorkflowStepBody = ({
|
||||
|
||||
+57
-43
@@ -17,8 +17,8 @@ import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-
|
||||
import { styled } from '@linaria/react';
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -83,6 +83,8 @@ const StyledTrashButtonContainer = styled.div`
|
||||
|
||||
const StyledFormFieldInputContainerWrapper = styled.div`
|
||||
grid-area: input;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledOpenedSettingsContainer = styled.div`
|
||||
@@ -93,11 +95,13 @@ const StyledFieldContainer = styled.div<{
|
||||
readonly?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: ${({ readonly }) => (readonly ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
font-family: inherit;
|
||||
min-height: 0;
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
padding-right: ${themeCssVariables.spacing[2]};
|
||||
|
||||
@@ -116,10 +120,19 @@ const StyledPlaceholderContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAddFieldButtonContainer = styled.div`
|
||||
padding-left: ${themeCssVariables.spacing[7]};
|
||||
padding-right: ${themeCssVariables.spacing[7]};
|
||||
const StyledAddFieldButtonOuterGrid = styled.div`
|
||||
column-gap: ${themeCssVariables.spacing[1]};
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr 24px;
|
||||
min-width: 0;
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAddFieldButtonGridCenter = styled.div`
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAddFieldButtonContentContainer = styled.div`
|
||||
@@ -323,8 +336,7 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
>
|
||||
<StyledPlaceholderContainer>
|
||||
<FormFieldPlaceholder>
|
||||
{isDefined(field.placeholder) &&
|
||||
isNonEmptyString(field.placeholder)
|
||||
{isNonEmptyString(field.placeholder)
|
||||
? field.placeholder
|
||||
: getDefaultFormFieldSettings(field.type)
|
||||
.placeholder}
|
||||
@@ -391,47 +403,49 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
/>
|
||||
|
||||
{!actionOptions.readonly && (
|
||||
<StyledAddFieldButtonContainer>
|
||||
<FormFieldInputContainer>
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId="add-field-button"
|
||||
hasRightElement={false}
|
||||
onClick={() => {
|
||||
const { label, name } = getDefaultFormFieldSettings(
|
||||
FieldMetadataType.TEXT,
|
||||
);
|
||||
<StyledAddFieldButtonOuterGrid>
|
||||
<StyledAddFieldButtonGridCenter>
|
||||
<FormFieldInputContainer>
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId="add-field-button"
|
||||
hasRightElement={false}
|
||||
onClick={() => {
|
||||
const { label, name } = getDefaultFormFieldSettings(
|
||||
FieldMetadataType.TEXT,
|
||||
);
|
||||
|
||||
const newField: WorkflowFormActionField = {
|
||||
id: v4(),
|
||||
name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label,
|
||||
};
|
||||
const newField: WorkflowFormActionField = {
|
||||
id: v4(),
|
||||
name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label,
|
||||
};
|
||||
|
||||
setFormData([...formData, newField]);
|
||||
setFormData([...formData, newField]);
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: [...action.settings.input, newField],
|
||||
},
|
||||
});
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: [...action.settings.input, newField],
|
||||
},
|
||||
});
|
||||
|
||||
setSelectedField(newField.id);
|
||||
}}
|
||||
>
|
||||
<StyledFieldContainer>
|
||||
<StyledAddFieldButtonContentContainer>
|
||||
<IconPlus size={theme.icon.size.sm} />
|
||||
{t`Add Field`}
|
||||
</StyledAddFieldButtonContentContainer>
|
||||
</StyledFieldContainer>
|
||||
</FormFieldInputInnerContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
</FormFieldInputContainer>
|
||||
</StyledAddFieldButtonContainer>
|
||||
setSelectedField(newField.id);
|
||||
}}
|
||||
>
|
||||
<StyledFieldContainer>
|
||||
<StyledAddFieldButtonContentContainer>
|
||||
<IconPlus size={theme.icon.size.sm} />
|
||||
{t`Add Field`}
|
||||
</StyledAddFieldButtonContentContainer>
|
||||
</StyledFieldContainer>
|
||||
</FormFieldInputInnerContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
</FormFieldInputContainer>
|
||||
</StyledAddFieldButtonGridCenter>
|
||||
</StyledAddFieldButtonOuterGrid>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
|
||||
+89
-64
@@ -1,22 +1,44 @@
|
||||
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
|
||||
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
|
||||
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
|
||||
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
const StyledWorkflowFormFillerRoot = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledWorkflowFormFillerFields = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type WorkflowEditActionFormFillerProps = {
|
||||
action: WorkflowFormAction;
|
||||
actionOptions: {
|
||||
@@ -99,86 +121,89 @@ export const WorkflowEditActionFormFiller = ({
|
||||
}, [saveAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWorkflowFormFillerRoot>
|
||||
<WorkflowStepBody>
|
||||
{formData.map((field) => {
|
||||
if (field.type === 'RECORD') {
|
||||
const objectNameSingular = field.settings?.objectName;
|
||||
<StyledWorkflowFormFillerFields>
|
||||
{formData.map((field) => {
|
||||
if (field.type === 'RECORD') {
|
||||
const objectNameSingular = field.settings?.objectName;
|
||||
|
||||
if (!isDefined(objectNameSingular)) {
|
||||
return null;
|
||||
if (!isDefined(objectNameSingular)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recordId = field.value?.id;
|
||||
|
||||
return (
|
||||
<FormSingleRecordPicker
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
defaultValue={recordId}
|
||||
onChange={(recordId) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value: {
|
||||
id: recordId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const recordId = field.value?.id;
|
||||
if (field.type === 'SELECT' || field.type === 'MULTI_SELECT') {
|
||||
const selectedFieldId = field.settings?.selectedFieldId;
|
||||
|
||||
return (
|
||||
<FormSingleRecordPicker
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
defaultValue={recordId}
|
||||
onChange={(recordId) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value: {
|
||||
id: recordId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!isDefined(selectedFieldId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (field.type === 'SELECT' || field.type === 'MULTI_SELECT') {
|
||||
const selectedFieldId = field.settings?.selectedFieldId;
|
||||
|
||||
if (!isDefined(selectedFieldId)) {
|
||||
return null;
|
||||
return (
|
||||
<WorkflowFormFieldInput
|
||||
key={field.id}
|
||||
fieldMetadataId={selectedFieldId}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowFormFieldInput
|
||||
<FormFieldInput
|
||||
key={field.id}
|
||||
fieldMetadataId={selectedFieldId}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
field={{
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
metadata: {} as FieldMetadata,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
onError={(error) => {
|
||||
setError(error);
|
||||
}}
|
||||
placeholder={
|
||||
isNonEmptyString(field.placeholder)
|
||||
? field.placeholder
|
||||
: getDefaultFormFieldSettings(field.type).placeholder
|
||||
}
|
||||
readonly={actionOptions.readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={field.id}
|
||||
field={{
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
metadata: {} as FieldMetadata,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
placeholder={
|
||||
field.placeholder ??
|
||||
getDefaultFormFieldSettings(field.type).placeholder
|
||||
}
|
||||
onError={(error) => {
|
||||
setError(error);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</StyledWorkflowFormFillerFields>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && (
|
||||
<SidePanelFooter
|
||||
@@ -191,6 +216,6 @@ export const WorkflowEditActionFormFiller = ({
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</StyledWorkflowFormFillerRoot>
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -69,7 +69,6 @@ const mockAction: WorkflowFormAction = {
|
||||
name: 'date',
|
||||
label: 'Date',
|
||||
type: FieldMetadataType.DATE,
|
||||
placeholder: 'mm/dd/yyyy',
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-sdk",
|
||||
"version": "0.8.0-canary.6",
|
||||
"version": "0.8.0-canary.7",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/sdk/index.d.ts",
|
||||
|
||||
-3
@@ -3,7 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
import { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command';
|
||||
@@ -66,7 +65,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
@@ -84,7 +82,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
|
||||
+5
-5
@@ -87,7 +87,7 @@ const isRelationTargetAvailable = (
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:backfill-page-layouts-and-fields-widget-view-fields',
|
||||
name: 'upgrade:1-21:backfill-page-layouts-and-fields-widget-view-fields',
|
||||
description:
|
||||
'Backfill RECORD_PAGE page layouts, sync FIELDS_WIDGET view fields, create FIELD widgets, and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
})
|
||||
@@ -594,7 +594,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID → objectMetadata map
|
||||
// Build object ID -> objectMetadata map
|
||||
const objectById = new Map<string, FlatObjectMetadata>();
|
||||
|
||||
for (const obj of Object.values(
|
||||
@@ -605,7 +605,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID → RECORD_PAGE layout map
|
||||
// Build object ID -> RECORD_PAGE layout map
|
||||
const recordPageLayoutByObjectId = new Map<
|
||||
string,
|
||||
{ id: string; universalIdentifier: string }
|
||||
@@ -626,7 +626,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
|
||||
}
|
||||
}
|
||||
|
||||
// Build pageLayoutId → home tab map
|
||||
// Build pageLayoutId -> home tab map
|
||||
const homeTabByPageLayoutId = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -655,7 +655,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
|
||||
// Group fields by objectMetadataId
|
||||
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
|
||||
|
||||
// Map morphId → all field IDs sharing that morphId (for dedup)
|
||||
// Map morphId -> all field IDs sharing that morphId (for dedup)
|
||||
const fieldIdsByMorphId = new Map<string, string[]>();
|
||||
|
||||
for (const field of Object.values(
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheModule,
|
||||
ApplicationModule,
|
||||
WorkspaceMigrationModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand],
|
||||
exports: [BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand],
|
||||
})
|
||||
export class V1_21_UpgradeVersionCommandModule {}
|
||||
+2
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { V1_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module';
|
||||
import { V1_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { CoreEngineVersionModule } from 'src/engine/core-engine-version/core-engine-version.module';
|
||||
import { CoreMigrationRunnerModule } from 'src/database/commands/core-migration-runner/core-migration-runner.module';
|
||||
@@ -13,6 +14,7 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
V1_20_UpgradeVersionCommandModule,
|
||||
V1_21_UpgradeVersionCommandModule,
|
||||
DataSourceModule,
|
||||
CoreEngineVersionModule,
|
||||
CoreMigrationRunnerModule,
|
||||
|
||||
+9
-3
@@ -12,7 +12,7 @@ import {
|
||||
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration-runner/services/core-migration-runner.service';
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command';
|
||||
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
@@ -60,13 +60,15 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
|
||||
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
|
||||
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
|
||||
protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
|
||||
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
|
||||
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
|
||||
protected readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand,
|
||||
protected readonly updateStandardIndexViewNamesCommand: UpdateStandardIndexViewNamesCommand,
|
||||
protected readonly makeWorkflowSearchableCommand: MakeWorkflowSearchableCommand,
|
||||
|
||||
// 1.21 Commands
|
||||
protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -92,7 +94,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.migrateRichTextToTextCommand,
|
||||
this.deleteOrphanNavigationMenuItemsCommand,
|
||||
this.backfillCommandMenuItemsCommand,
|
||||
this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
this.seedCliApplicationRegistrationCommand,
|
||||
this.migrateMessagingInfrastructureToMetadataCommand,
|
||||
this.backfillSelectFieldOptionIdsCommand,
|
||||
@@ -100,9 +101,14 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.makeWorkflowSearchableCommand,
|
||||
];
|
||||
|
||||
const commands_1210: VersionCommands = [
|
||||
this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
'1.19.0': [],
|
||||
'1.20.0': commands_1200,
|
||||
'1.21.0': commands_1210,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -14,6 +14,9 @@ export const makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_da8ffd3c24b4a819430a861067" ON "core"."permissionFlag" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT IF EXISTS "FK_b26a9d39a88d0e72373c677c6c5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag" ADD CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
+3
@@ -14,6 +14,9 @@ export const makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueri
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_c5ea53618b32558fe24e495f21" ON "core"."objectPermission" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" DROP CONSTRAINT IF EXISTS "FK_f2ecee1066fd43800dbc85f87e4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ADD CONSTRAINT "FK_f2ecee1066fd43800dbc85f87e4" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
+3
@@ -14,6 +14,9 @@ export const makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQuerie
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_0dedb90c717e179ef653c512b9" ON "core"."fieldPermission" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT IF EXISTS "FK_71cc60c4a1c9f8a7c434d91d38c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_71cc60c4a1c9f8a7c434d91d38c" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
+3
@@ -7,6 +7,9 @@ export const makeNavigationMenuItemTypeNotNullQueries = async (
|
||||
`ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" SET NOT NULL`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT IF EXISTS "CHK_navigation_menu_item_type_fields"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "CHK_navigation_menu_item_type_fields" CHECK (
|
||||
("type" = 'FOLDER')
|
||||
|
||||
+5
-1
@@ -1,4 +1,8 @@
|
||||
export const UPGRADE_COMMAND_SUPPORTED_VERSIONS = ['1.19.0', '1.20.0'] as const;
|
||||
export const UPGRADE_COMMAND_SUPPORTED_VERSIONS = [
|
||||
'1.19.0',
|
||||
'1.20.0',
|
||||
'1.21.0',
|
||||
] as const;
|
||||
|
||||
export type UpgradeCommandVersion =
|
||||
(typeof UPGRADE_COMMAND_SUPPORTED_VERSIONS)[number];
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Repository, type DataSource, type QueryRunner } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { MAX_WORKSPACES_WITHOUT_ENTERPRISE_KEY } from 'src/engine/core-modules/auth/constants/max-workspaces-without-enterprise-key.constants';
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import {
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
compareHash,
|
||||
hashPassword,
|
||||
} from 'src/engine/core-modules/auth/auth.util';
|
||||
import { MAX_WORKSPACES_WITHOUT_ENTERPRISE_KEY } from 'src/engine/core-modules/auth/constants/max-workspaces-without-enterprise-key.constants';
|
||||
import {
|
||||
type AuthProviderWithPasswordType,
|
||||
type ExistingUserOrPartialUserWithPicture,
|
||||
@@ -593,16 +593,18 @@ export class SignInUpService {
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
return { user, workspace };
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
if (queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-78
@@ -1,21 +1,14 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
import { S3Driver } from 'src/engine/core-modules/file-storage/drivers/s3.driver';
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
const mockSend = jest.fn();
|
||||
|
||||
jest.mock('@aws-sdk/client-s3', () => {
|
||||
const actual = jest.requireActual('@aws-sdk/client-s3');
|
||||
|
||||
return {
|
||||
...actual,
|
||||
S3: jest.fn().mockImplementation(() => ({
|
||||
send: mockSend,
|
||||
})),
|
||||
S3: jest.fn().mockImplementation(() => ({})),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -23,76 +16,6 @@ jest.mock('@aws-sdk/s3-request-presigner', () => ({
|
||||
getSignedUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('S3Driver.readFile', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should retry once on ExpiredToken and succeed', async () => {
|
||||
const expiredError = new Error('The provided token has expired.');
|
||||
|
||||
Object.defineProperty(expiredError, 'name', { value: 'ExpiredToken' });
|
||||
|
||||
const readableBody = Readable.from(Buffer.from('file-content'));
|
||||
|
||||
mockSend
|
||||
.mockRejectedValueOnce(expiredError)
|
||||
.mockResolvedValueOnce({ Body: readableBody });
|
||||
|
||||
const driver = new S3Driver({
|
||||
bucketName: 'test-bucket',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
|
||||
const result = await driver.readFile({ filePath: 'some/file.js' });
|
||||
|
||||
expect(result).toBeInstanceOf(Readable);
|
||||
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should throw NoSuchKey as FileStorageException without retrying', async () => {
|
||||
const noSuchKeyError = new Error('Not found');
|
||||
|
||||
Object.defineProperty(noSuchKeyError, 'name', { value: 'NoSuchKey' });
|
||||
|
||||
mockSend.mockRejectedValueOnce(noSuchKeyError);
|
||||
|
||||
const driver = new S3Driver({
|
||||
bucketName: 'test-bucket',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
|
||||
await expect(
|
||||
driver.readFile({ filePath: 'missing/file.js' }),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||
});
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw non-retryable errors immediately', async () => {
|
||||
const accessDeniedError = new Error('Access denied');
|
||||
|
||||
Object.defineProperty(accessDeniedError, 'name', {
|
||||
value: 'AccessDenied',
|
||||
});
|
||||
|
||||
mockSend.mockRejectedValueOnce(accessDeniedError);
|
||||
|
||||
const driver = new S3Driver({
|
||||
bucketName: 'test-bucket',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
|
||||
await expect(
|
||||
driver.readFile({ filePath: 'some/file.js' }),
|
||||
).rejects.toThrow('Access denied');
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S3Driver.getPresignedUrl', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
@@ -37,14 +37,6 @@ export interface S3DriverOptions extends S3ClientConfig {
|
||||
presignEndpoint?: string;
|
||||
}
|
||||
|
||||
// AWS error names that indicate expired/invalid temporary credentials
|
||||
// and are recoverable by retrying (which triggers credential refresh)
|
||||
const RETRYABLE_CREDENTIAL_ERROR_NAMES = [
|
||||
'ExpiredToken',
|
||||
'ExpiredTokenException',
|
||||
'RequestExpired',
|
||||
];
|
||||
|
||||
export class S3Driver implements StorageDriver {
|
||||
private s3Client: S3;
|
||||
private presignClient: S3 | undefined;
|
||||
@@ -79,10 +71,6 @@ export class S3Driver implements StorageDriver {
|
||||
return this.s3Client;
|
||||
}
|
||||
|
||||
private isRetryableCredentialError(error: { name?: string }): boolean {
|
||||
return RETRYABLE_CREDENTIAL_ERROR_NAMES.includes(error.name ?? '');
|
||||
}
|
||||
|
||||
async readFile(params: { filePath: string }): Promise<Readable> {
|
||||
const command = new GetObjectCommand({
|
||||
Key: params.filePath,
|
||||
@@ -105,25 +93,6 @@ export class S3Driver implements StorageDriver {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isRetryableCredentialError(error)) {
|
||||
this.logger.warn(
|
||||
`S3 credential error (${error.name}), retrying once to trigger credential refresh`,
|
||||
);
|
||||
|
||||
const retryCommand = new GetObjectCommand({
|
||||
Key: params.filePath,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
|
||||
const file = await this.s3Client.send(retryCommand);
|
||||
|
||||
if (!file || !file.Body || !(file.Body instanceof Readable)) {
|
||||
throw new Error('Unable to get file stream');
|
||||
}
|
||||
|
||||
return Readable.from(file.Body);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ export type ChipProps = {
|
||||
disabled?: boolean;
|
||||
clickable?: boolean;
|
||||
label: string;
|
||||
tooltipLabel?: string;
|
||||
alwaysShowTooltip?: boolean;
|
||||
isLabelHidden?: boolean;
|
||||
isBold?: boolean;
|
||||
maxWidth?: number;
|
||||
@@ -172,6 +174,8 @@ const renderRightComponent = (
|
||||
export const Chip = ({
|
||||
size = ChipSize.Small,
|
||||
label,
|
||||
tooltipLabel,
|
||||
alwaysShowTooltip = false,
|
||||
isLabelHidden = false,
|
||||
isBold = false,
|
||||
disabled = false,
|
||||
@@ -200,7 +204,12 @@ export const Chip = ({
|
||||
>
|
||||
{leftComponent}
|
||||
{!isLabelHidden && isDefined(label) && isNonEmptyString(label) ? (
|
||||
<OverflowingTextWithTooltip size={size} text={label} />
|
||||
<OverflowingTextWithTooltip
|
||||
size={size}
|
||||
text={label}
|
||||
tooltipContent={tooltipLabel}
|
||||
alwaysShowTooltip={alwaysShowTooltip}
|
||||
/>
|
||||
) : !forceEmptyText && !isLabelHidden ? (
|
||||
<StyledDiv>{emptyLabel}</StyledDiv>
|
||||
) : (
|
||||
|
||||
@@ -3,8 +3,8 @@ import { type ReactNode, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AppTooltip, TooltipDelay } from './AppTooltip';
|
||||
|
||||
const spacing4 = themeCssVariables.spacing[4];
|
||||
@@ -64,6 +64,7 @@ type OverflowingTextWithTooltipProps = {
|
||||
isTooltipMultiline?: boolean;
|
||||
displayedMaxRows?: number;
|
||||
tooltipDelay?: TooltipDelay;
|
||||
alwaysShowTooltip?: boolean;
|
||||
} & (
|
||||
| {
|
||||
text: string | null | undefined;
|
||||
@@ -82,6 +83,7 @@ export const OverflowingTextWithTooltip = ({
|
||||
displayedMaxRows,
|
||||
tooltipContent,
|
||||
tooltipDelay = TooltipDelay.mediumDelay,
|
||||
alwaysShowTooltip = false,
|
||||
}: OverflowingTextWithTooltipProps) => {
|
||||
const textElementId = `title-id-${+new Date()}`;
|
||||
|
||||
@@ -146,7 +148,7 @@ export const OverflowingTextWithTooltip = ({
|
||||
)}
|
||||
|
||||
{shouldRenderTooltip &&
|
||||
isTitleOverflowing &&
|
||||
(isTitleOverflowing || alwaysShowTooltip) &&
|
||||
isDefined(tooltipText) &&
|
||||
createPortal(
|
||||
<div onClick={handleTooltipClick}>
|
||||
|
||||
Reference in New Issue
Block a user