diff --git a/front/package.json b/front/package.json index 9f2d671f7..7b17a8f28 100644 --- a/front/package.json +++ b/front/package.json @@ -167,8 +167,8 @@ "workerDirectory": "public" }, "nyc": { - "statements": 70, - "lines": 70, + "statements": 65, + "lines": 65, "functions": 60, "exclude": [ "src/generated/**/*" diff --git a/front/src/App.tsx b/front/src/App.tsx index 13960c38d..23539e9fe 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -20,7 +20,7 @@ import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks'; import { SignInUp } from './pages/auth/SignInUp'; // TEMP FEATURE FLAG FOR VIEW FIELDS -export const ACTIVATE_VIEW_FIELDS = false; +export const ACTIVATE_VIEW_FIELDS = true; export function App() { return ( diff --git a/front/src/modules/companies/constants/companyFieldMetadataArray.tsx b/front/src/modules/companies/constants/companyFieldMetadataArray.tsx deleted file mode 100644 index 807955061..000000000 --- a/front/src/modules/companies/constants/companyFieldMetadataArray.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { IconBuildingSkyscraper } from '@tabler/icons-react'; - -import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; -import { - ViewFieldChipMetadata, - ViewFieldDefinition, -} from '@/ui/table/types/ViewField'; - -export const companyViewFields: ViewFieldDefinition[] = [ - { - columnLabel: 'Name', - columnIcon: , - columnSize: 150, - type: 'chip', - columnOrder: 1, - metadata: { - urlFieldName: 'domainName', - contentFieldName: 'name', - relationType: Entity.Company, - }, - } as ViewFieldDefinition, -]; diff --git a/front/src/modules/companies/constants/companyViewFields.tsx b/front/src/modules/companies/constants/companyViewFields.tsx new file mode 100644 index 000000000..01f77057e --- /dev/null +++ b/front/src/modules/companies/constants/companyViewFields.tsx @@ -0,0 +1,105 @@ +import { + IconBuildingSkyscraper, + IconCalendarEvent, + IconLink, + IconMap, + IconUser, + IconUsers, +} from '@/ui/icon/index'; +import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; +import { + ViewFieldChipMetadata, + ViewFieldDateMetadata, + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldNumberMetadata, + ViewFieldRelationMetadata, + ViewFieldTextMetadata, + ViewFieldURLMetadata, +} from '@/ui/table/types/ViewField'; + +export const companyViewFields: ViewFieldDefinition[] = [ + { + id: 'name', + columnLabel: 'Name', + columnIcon: , + columnSize: 180, + columnOrder: 1, + metadata: { + type: 'chip', + urlFieldName: 'domainName', + contentFieldName: 'name', + relationType: Entity.Company, + }, + } as ViewFieldDefinition, + { + id: 'domainName', + columnLabel: 'URL', + columnIcon: , + columnSize: 100, + columnOrder: 2, + metadata: { + type: 'url', + fieldName: 'domainName', + placeHolder: 'example.com', + }, + } as ViewFieldDefinition, + { + id: 'accountOwner', + columnLabel: 'Account Owner', + columnIcon: , + columnSize: 150, + columnOrder: 3, + metadata: { + type: 'relation', + fieldName: 'accountOwner', + relationType: Entity.User, + }, + } satisfies ViewFieldDefinition, + { + id: 'createdAt', + columnLabel: 'Creation', + columnIcon: , + columnSize: 150, + columnOrder: 4, + metadata: { + type: 'date', + fieldName: 'createdAt', + }, + } satisfies ViewFieldDefinition, + { + id: 'employees', + columnLabel: 'Employees', + columnIcon: , + columnSize: 150, + columnOrder: 5, + metadata: { + type: 'number', + fieldName: 'employees', + }, + } satisfies ViewFieldDefinition, + { + id: 'linkedin', + columnLabel: 'LinkedIn', + columnIcon: , + columnSize: 170, + columnOrder: 6, + metadata: { + type: 'url', + fieldName: 'linkedinUrl', + placeHolder: 'LinkedIn URL', + }, + } satisfies ViewFieldDefinition, + { + id: 'address', + columnLabel: 'Address', + columnIcon: , + columnSize: 170, + columnOrder: 7, + metadata: { + type: 'text', + fieldName: 'address', + placeHolder: 'Address', + }, + } satisfies ViewFieldDefinition, +]; diff --git a/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx index 89be7946e..2d0293fa6 100644 --- a/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx +++ b/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx @@ -22,8 +22,20 @@ export function CompanyCreatedAtEditableField({ company }: OwnProps) { setInternalValue(company.createdAt); }, [company.createdAt]); + // TODO: refactor change and submit async function handleChange(newValue: string) { setInternalValue(newValue); + + await updateCompany({ + variables: { + where: { + id: company.id, + }, + data: { + createdAt: newValue ?? '', + }, + }, + }); } async function handleSubmit() { diff --git a/front/src/modules/companies/table/components/CompanyTableV2.tsx b/front/src/modules/companies/table/components/CompanyTableV2.tsx index 8abfe3538..6f8e47a2e 100644 --- a/front/src/modules/companies/table/components/CompanyTableV2.tsx +++ b/front/src/modules/companies/table/components/CompanyTableV2.tsx @@ -1,14 +1,14 @@ import { useCallback, useMemo, useState } from 'react'; -import { companyViewFields } from '@/companies/constants/companyFieldMetadataArray'; +import { companyViewFields } from '@/companies/constants/companyViewFields'; import { CompaniesSelectedSortType, defaultOrderBy } from '@/companies/queries'; -import { GenericEntityTableData } from '@/people/components/GenericEntityTableData'; import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause'; import { IconList } from '@/ui/icon'; import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue'; import { EntityTable } from '@/ui/table/components/EntityTableV2'; +import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; import { TableContext } from '@/ui/table/states/TableContext'; import { CompanyOrderByWithRelationInput, diff --git a/front/src/modules/people/constants/peopleFieldMetadataArray.tsx b/front/src/modules/people/constants/peopleFieldMetadataArray.tsx deleted file mode 100644 index 52863b122..000000000 --- a/front/src/modules/people/constants/peopleFieldMetadataArray.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - IconBriefcase, - IconBuildingSkyscraper, - IconMap, -} from '@tabler/icons-react'; - -import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; -import { - ViewFieldDefinition, - ViewFieldRelationMetadata, - ViewFieldTextMetadata, -} from '@/ui/table/types/ViewField'; - -export const peopleViewFields: ViewFieldDefinition[] = [ - { - id: 'city', - columnLabel: 'City', - columnIcon: , - columnSize: 150, - type: 'text', - columnOrder: 1, - metadata: { - fieldName: 'city', - placeHolder: 'City', - }, - } as ViewFieldDefinition, - { - id: 'jobTitle', - columnLabel: 'Job title', - columnIcon: , - columnSize: 150, - type: 'text', - columnOrder: 2, - metadata: { - fieldName: 'jobTitle', - placeHolder: 'Job title', - }, - } as ViewFieldDefinition, - { - id: 'company', - columnLabel: 'Company', - columnIcon: , - columnSize: 150, - type: 'relation', - relationType: Entity.Company, - columnOrder: 3, - metadata: { - fieldName: 'company', - relationType: Entity.Company, - }, - } as ViewFieldDefinition, -]; diff --git a/front/src/modules/people/constants/peopleViewFields.tsx b/front/src/modules/people/constants/peopleViewFields.tsx new file mode 100644 index 000000000..5f78faad8 --- /dev/null +++ b/front/src/modules/people/constants/peopleViewFields.tsx @@ -0,0 +1,122 @@ +import { + IconBrandLinkedin, + IconBriefcase, + IconBuildingSkyscraper, + IconCalendarEvent, + IconMail, + IconMap, + IconPhone, + IconUser, +} from '@/ui/icon/index'; +import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; +import { + ViewFieldDateMetadata, + ViewFieldDefinition, + ViewFieldDoubleTextChipMetadata, + ViewFieldMetadata, + ViewFieldPhoneMetadata, + ViewFieldRelationMetadata, + ViewFieldTextMetadata, + ViewFieldURLMetadata, +} from '@/ui/table/types/ViewField'; + +export const peopleViewFields: ViewFieldDefinition[] = [ + { + id: 'displayName', + columnLabel: 'People', + columnIcon: , + columnSize: 210, + columnOrder: 1, + metadata: { + type: 'double-text-chip', + firstValueFieldName: 'firstName', + secondValueFieldName: 'lastName', + firstValuePlaceholder: 'First name', + secondValuePlaceholder: 'Last name', + entityType: Entity.Person, + }, + } satisfies ViewFieldDefinition, + { + id: 'email', + columnLabel: 'Email', + columnIcon: , + columnSize: 150, + columnOrder: 2, + metadata: { + type: 'text', + fieldName: 'email', + placeHolder: 'Email', + }, + } satisfies ViewFieldDefinition, + { + id: 'company', + columnLabel: 'Company', + columnIcon: , + columnSize: 150, + columnOrder: 3, + metadata: { + type: 'relation', + fieldName: 'company', + relationType: Entity.Company, + }, + } satisfies ViewFieldDefinition, + { + id: 'phone', + columnLabel: 'Phone', + columnIcon: , + columnSize: 150, + columnOrder: 4, + metadata: { + type: 'phone', + fieldName: 'phone', + placeHolder: 'Phone', + }, + } satisfies ViewFieldDefinition, + { + id: 'createdAt', + columnLabel: 'Creation', + columnIcon: , + columnSize: 150, + columnOrder: 5, + metadata: { + type: 'date', + fieldName: 'createdAt', + }, + } satisfies ViewFieldDefinition, + { + id: 'city', + columnLabel: 'City', + columnIcon: , + columnSize: 150, + columnOrder: 6, + metadata: { + type: 'text', + fieldName: 'city', + placeHolder: 'City', + }, + } satisfies ViewFieldDefinition, + { + id: 'jobTitle', + columnLabel: 'Job title', + columnIcon: , + columnSize: 150, + columnOrder: 7, + metadata: { + type: 'text', + fieldName: 'jobTitle', + placeHolder: 'Job title', + }, + } satisfies ViewFieldDefinition, + { + id: 'linkedin', + columnLabel: 'LinkedIn', + columnIcon: , + columnSize: 150, + columnOrder: 8, + metadata: { + type: 'url', + fieldName: 'linkedinUrl', + placeHolder: 'LinkedIn', + }, + } satisfies ViewFieldDefinition, +]; diff --git a/front/src/modules/people/hooks/useUpdateEntityField.ts b/front/src/modules/people/hooks/useUpdateEntityField.ts deleted file mode 100644 index 6c4aa5784..000000000 --- a/front/src/modules/people/hooks/useUpdateEntityField.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect'; -import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext'; -import { viewFieldsState } from '@/ui/table/states/viewFieldsState'; -import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip'; -import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation'; -import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText'; - -export function useUpdateEntityField() { - const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext); - - const [updateEntity] = useUpdateEntityMutation(); - - const viewFields = useRecoilValue(viewFieldsState); - - return function updatePeopleField( - currentEntityId: string, - viewFieldId: string, - newFieldValue: unknown, - ) { - const viewField = viewFields.find( - (metadata) => metadata.id === viewFieldId, - ); - - if (!viewField) { - throw new Error(`View field not found for id ${viewFieldId}`); - } - - // TODO: improve type narrowing here with validation maybe ? Also validate the newFieldValue with linked type guards - if (isViewFieldRelation(viewField)) { - const newSelectedEntity = newFieldValue as EntityForSelect | null; - - const fieldName = viewField.metadata.fieldName; - - if (!newSelectedEntity) { - updateEntity({ - variables: { - where: { id: currentEntityId }, - data: { - [fieldName]: { - disconnect: true, - }, - }, - }, - }); - } else { - updateEntity({ - variables: { - where: { id: currentEntityId }, - data: { - [fieldName]: { - connect: { id: newSelectedEntity.id }, - }, - }, - }, - }); - } - } else if (isViewFieldChip(viewField)) { - const newContent = newFieldValue as string; - - updateEntity({ - variables: { - where: { id: currentEntityId }, - data: { [viewField.metadata.contentFieldName]: newContent }, - }, - }); - } else if (isViewFieldText(viewField)) { - const newContent = newFieldValue as string; - - updateEntity({ - variables: { - where: { id: currentEntityId }, - data: { [viewField.metadata.fieldName]: newContent }, - }, - }); - } - }; -} diff --git a/front/src/modules/people/table/components/PeopleTableV2.tsx b/front/src/modules/people/table/components/PeopleTableV2.tsx index 00e79b9a6..a2edb3599 100644 --- a/front/src/modules/people/table/components/PeopleTableV2.tsx +++ b/front/src/modules/people/table/components/PeopleTableV2.tsx @@ -1,8 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; import { defaultOrderBy } from '@/companies/queries'; -import { GenericEntityTableData } from '@/people/components/GenericEntityTableData'; -import { peopleViewFields } from '@/people/constants/peopleFieldMetadataArray'; +import { peopleViewFields } from '@/people/constants/peopleViewFields'; import { PeopleSelectedSortType } from '@/people/queries'; import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; @@ -10,6 +9,7 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn import { IconList } from '@/ui/icon'; import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue'; import { EntityTable } from '@/ui/table/components/EntityTableV2'; +import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; import { TableContext } from '@/ui/table/states/TableContext'; import { PersonOrderByWithRelationInput, diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx index 8e612c2c6..5e95d31de 100644 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx +++ b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, useMemo, useState } from 'react'; import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode'; -import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; +import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; import { debounce } from '~/utils/debounce'; import { BoardCardEditableField } from './BoardCardEditableField'; diff --git a/front/src/modules/ui/display/component/InplaceInputDateDisplayMode.tsx b/front/src/modules/ui/display/component/InplaceInputDateDisplayMode.tsx index 6f7dac2f4..be67cd640 100644 --- a/front/src/modules/ui/display/component/InplaceInputDateDisplayMode.tsx +++ b/front/src/modules/ui/display/component/InplaceInputDateDisplayMode.tsx @@ -1,7 +1,7 @@ import { formatToHumanReadableDate } from '~/utils'; type OwnProps = { - value: Date | null; + value: Date | string | null; }; export function InplaceInputDateDisplayMode({ value }: OwnProps) { diff --git a/front/src/modules/ui/display/component/InplaceInputURLDisplayMode.tsx b/front/src/modules/ui/display/component/InplaceInputURLDisplayMode.tsx new file mode 100644 index 000000000..4511e43bc --- /dev/null +++ b/front/src/modules/ui/display/component/InplaceInputURLDisplayMode.tsx @@ -0,0 +1,36 @@ +import { MouseEvent } from 'react'; +import styled from '@emotion/styled'; + +import { RawLink } from '@/ui/link/components/RawLink'; + +const StyledRawLink = styled(RawLink)` + overflow: hidden; + + a { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +`; + +type OwnProps = { + value: string; +}; + +export function InplaceInputURLDisplayMode({ value }: OwnProps) { + function handleClick(event: MouseEvent) { + event.stopPropagation(); + } + + const absoluteUrl = value + ? value.startsWith('http') + ? value + : 'https://' + value + : ''; + + return ( + + {value} + + ); +} diff --git a/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts b/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts index d73f70d5b..b9671aa1d 100644 --- a/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts +++ b/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts @@ -3,7 +3,7 @@ import { useRecoilCallback } from 'recoil'; import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState'; -const DEBUG_HOTKEY_SCOPE = false; +const DEBUG_HOTKEY_SCOPE = true; export function useScopedHotkeyCallback() { return useRecoilCallback( diff --git a/front/src/modules/ui/inplace-input/components/InplaceInputDoubleText.tsx b/front/src/modules/ui/inplace-input/components/InplaceInputDoubleText.tsx index fb042284a..cc3543446 100644 --- a/front/src/modules/ui/inplace-input/components/InplaceInputDoubleText.tsx +++ b/front/src/modules/ui/inplace-input/components/InplaceInputDoubleText.tsx @@ -1,7 +1,7 @@ import { ChangeEvent } from 'react'; import styled from '@emotion/styled'; -import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; +import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; type OwnProps = { firstValue: string; @@ -11,7 +11,7 @@ type OwnProps = { onChange: (firstValue: string, secondValue: string) => void; }; -const StyledContainer = styled.div` +export const StyledDoubleTextContainer = styled.div` align-items: center; display: flex; justify-content: space-between; @@ -30,7 +30,7 @@ export function InplaceInputDoubleText({ onChange, }: OwnProps) { return ( - + - + ); } diff --git a/front/src/modules/ui/inplace-input/components/InplaceInputDoubleTextCellEditMode.tsx b/front/src/modules/ui/inplace-input/components/InplaceInputDoubleTextCellEditMode.tsx new file mode 100644 index 000000000..c3beb2474 --- /dev/null +++ b/front/src/modules/ui/inplace-input/components/InplaceInputDoubleTextCellEditMode.tsx @@ -0,0 +1,79 @@ +import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import styled from '@emotion/styled'; + +import { textInputStyle } from '@/ui/themes/effects'; + +import { useRegisterCloseCellHandlers } from '../../table/editable-cell/hooks/useRegisterCloseCellHandlers'; + +import { StyledDoubleTextContainer } from './InplaceInputDoubleText'; + +export const StyledInput = styled.input` + margin: 0; + width: 100%; + ${textInputStyle} +`; + +type OwnProps = { + firstValuePlaceholder?: string; + secondValuePlaceholder?: string; + firstValue: string; + secondValue: string; + onSubmit: (newFirstValue: string, newSecondValue: string) => void; +}; + +export function InplaceInputDoubleTextCellEditMode({ + firstValue, + secondValue, + firstValuePlaceholder, + secondValuePlaceholder, + onSubmit, +}: OwnProps) { + const [internalFirstValue, setInternalFirstValue] = useState(firstValue); + const [internalSecondValue, setInternalSecondValue] = useState(secondValue); + + const wrapperRef = useRef(null); + + function handleSubmit() { + onSubmit(internalFirstValue, internalSecondValue); + } + + function handleCancel() { + setInternalFirstValue(firstValue); + setInternalSecondValue(secondValue); + } + + function handleFirstValueChange(event: ChangeEvent) { + setInternalFirstValue(event.target.value); + } + + function handleSecondValueChange(event: ChangeEvent) { + setInternalSecondValue(event.target.value); + } + + useEffect(() => { + setInternalFirstValue(firstValue); + }, [firstValue]); + + useEffect(() => { + setInternalSecondValue(secondValue); + }, [secondValue]); + + useRegisterCloseCellHandlers(wrapperRef, handleSubmit, handleCancel); + + return ( + + + + + ); +} diff --git a/front/src/modules/ui/inplace-input/components/InplaceInputTextEditMode.tsx b/front/src/modules/ui/inplace-input/components/InplaceInputTextCellEditMode.tsx similarity index 96% rename from front/src/modules/ui/inplace-input/components/InplaceInputTextEditMode.tsx rename to front/src/modules/ui/inplace-input/components/InplaceInputTextCellEditMode.tsx index 87f54d51e..a3930af29 100644 --- a/front/src/modules/ui/inplace-input/components/InplaceInputTextEditMode.tsx +++ b/front/src/modules/ui/inplace-input/components/InplaceInputTextCellEditMode.tsx @@ -18,7 +18,7 @@ type OwnProps = { onSubmit: (newText: string) => void; }; -export function InplaceInputTextEditMode({ +export function InplaceInputTextCellEditMode({ placeholder, autoFocus, value, diff --git a/front/src/modules/ui/table/components/ColumnHead.tsx b/front/src/modules/ui/table/components/ColumnHead.tsx index 4a3b3d585..3309caccb 100644 --- a/front/src/modules/ui/table/components/ColumnHead.tsx +++ b/front/src/modules/ui/table/components/ColumnHead.tsx @@ -11,13 +11,18 @@ const StyledTitle = styled.div` display: flex; flex-direction: row; font-weight: ${({ theme }) => theme.font.weight.medium}; + gap: ${({ theme }) => theme.spacing(1)}; height: ${({ theme }) => theme.spacing(8)}; padding-left: ${({ theme }) => theme.spacing(2)}; `; const StyledIcon = styled.div` display: flex; - margin-right: ${({ theme }) => theme.spacing(1)}; + + & > svg { + height: ${({ theme }) => theme.icon.size.md}px; + width: ${({ theme }) => theme.icon.size.md}px; + } `; export function ColumnHead({ viewName, viewIcon }: OwnProps) { diff --git a/front/src/modules/ui/table/components/EntityTableCellV2.tsx b/front/src/modules/ui/table/components/EntityTableCellV2.tsx index 9f16c1a79..d4ee44e32 100644 --- a/front/src/modules/ui/table/components/EntityTableCellV2.tsx +++ b/front/src/modules/ui/table/components/EntityTableCellV2.tsx @@ -25,9 +25,9 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) { }); } - const entityFieldMetadata = useContext(ViewFieldContext); + const viewField = useContext(ViewFieldContext); - if (!entityFieldMetadata) { + if (!viewField) { return null; } @@ -37,12 +37,12 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) { handleContextMenu(event)} style={{ - width: entityFieldMetadata.columnSize, - minWidth: entityFieldMetadata.columnSize, - maxWidth: entityFieldMetadata.columnSize, + width: viewField.columnSize, + minWidth: viewField.columnSize, + maxWidth: viewField.columnSize, }} > - + diff --git a/front/src/modules/ui/table/components/EntityTableHeaderV2.tsx b/front/src/modules/ui/table/components/EntityTableHeaderV2.tsx index bb18d69de..a21705991 100644 --- a/front/src/modules/ui/table/components/EntityTableHeaderV2.tsx +++ b/front/src/modules/ui/table/components/EntityTableHeaderV2.tsx @@ -1,12 +1,12 @@ import { useRecoilValue } from 'recoil'; -import { viewFieldsState } from '../states/viewFieldsState'; +import { viewFieldsFamilyState } from '../states/viewFieldsState'; import { ColumnHead } from './ColumnHead'; import { SelectAllCheckbox } from './SelectAllCheckbox'; export function EntityTableHeader() { - const viewFields = useRecoilValue(viewFieldsState); + const viewFields = useRecoilValue(viewFieldsFamilyState); return ( diff --git a/front/src/modules/ui/table/components/EntityTableRowV2.tsx b/front/src/modules/ui/table/components/EntityTableRowV2.tsx index 5e17ffb0c..d95a21aa4 100644 --- a/front/src/modules/ui/table/components/EntityTableRowV2.tsx +++ b/front/src/modules/ui/table/components/EntityTableRowV2.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { ViewFieldContext } from '../states/ViewFieldContext'; -import { viewFieldsState } from '../states/viewFieldsState'; +import { viewFieldsFamilyState } from '../states/viewFieldsState'; import { CheckboxCell } from './CheckboxCell'; import { EntityTableCell } from './EntityTableCellV2'; @@ -13,18 +13,18 @@ const StyledRow = styled.tr<{ selected: boolean }>` `; export function EntityTableRow({ rowId }: { rowId: string }) { - const entityFieldMetadataArray = useRecoilValue(viewFieldsState); + const viewFields = useRecoilValue(viewFieldsFamilyState); return ( - {entityFieldMetadataArray.map((entityFieldMetadata, columnIndex) => { + {viewFields.map((viewField, columnIndex) => { return ( diff --git a/front/src/modules/ui/table/components/GenericEditableCell.tsx b/front/src/modules/ui/table/components/GenericEditableCell.tsx index ac72008c5..483beff35 100644 --- a/front/src/modules/ui/table/components/GenericEditableCell.tsx +++ b/front/src/modules/ui/table/components/GenericEditableCell.tsx @@ -1,37 +1,54 @@ -import { ViewFieldDefinition } from '@/ui/table/types/ViewField'; +import { + ViewFieldDefinition, + ViewFieldMetadata, +} from '@/ui/table/types/ViewField'; import { isViewFieldChip } from '../types/guards/isViewFieldChip'; +import { isViewFieldDate } from '../types/guards/isViewFieldDate'; +import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText'; +import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip'; +import { isViewFieldNumber } from '../types/guards/isViewFieldNumber'; +import { isViewFieldPhone } from '../types/guards/isViewFieldPhone'; import { isViewFieldRelation } from '../types/guards/isViewFieldRelation'; import { isViewFieldText } from '../types/guards/isViewFieldText'; +import { isViewFieldURL } from '../types/guards/isViewFieldURL'; import { GenericEditableChipCell } from './GenericEditableChipCell'; +import { GenericEditableDateCell } from './GenericEditableDateCell'; +import { GenericEditableDoubleTextCell } from './GenericEditableDoubleTextCell'; +import { GenericEditableDoubleTextChipCell } from './GenericEditableDoubleTextChipCell'; +import { GenericEditableNumberCell } from './GenericEditableNumberCell'; +import { GenericEditablePhoneCell } from './GenericEditablePhoneCell'; import { GenericEditableRelationCell } from './GenericEditableRelationCell'; import { GenericEditableTextCell } from './GenericEditableTextCell'; +import { GenericEditableURLCell } from './GenericEditableURLCell'; type OwnProps = { - fieldDefinition: ViewFieldDefinition; + viewField: ViewFieldDefinition; }; -export function GenericEditableCell({ fieldDefinition }: OwnProps) { +export function GenericEditableCell({ viewField: fieldDefinition }: OwnProps) { if (isViewFieldText(fieldDefinition)) { - return ( - - ); + return ; } else if (isViewFieldRelation(fieldDefinition)) { return ; + } else if (isViewFieldDoubleTextChip(fieldDefinition)) { + return ; + } else if (isViewFieldDoubleText(fieldDefinition)) { + return ; + } else if (isViewFieldPhone(fieldDefinition)) { + return ; + } else if (isViewFieldURL(fieldDefinition)) { + return ; + } else if (isViewFieldDate(fieldDefinition)) { + return ; + } else if (isViewFieldNumber(fieldDefinition)) { + return ; } else if (isViewFieldChip(fieldDefinition)) { - return ( - - ); + return ; } else { console.warn( - `Unknown field type: ${fieldDefinition.type} in GenericEditableCell`, + `Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableCell`, ); return <>; } diff --git a/front/src/modules/ui/table/components/GenericEditableChipCell.tsx b/front/src/modules/ui/table/components/GenericEditableChipCell.tsx index 0754f2ec0..8ce55dbd6 100644 --- a/front/src/modules/ui/table/components/GenericEditableChipCell.tsx +++ b/front/src/modules/ui/table/components/GenericEditableChipCell.tsx @@ -3,7 +3,7 @@ import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; import { ViewFieldChipMetadata, ViewFieldDefinition } from '../types/ViewField'; import { GenericEditableChipCellDisplayMode } from './GenericEditableChipCellDisplayMode'; -import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMode'; +import { GenericEditableChipCellEditMode } from './GenericEditableChipCellEditMode'; type OwnProps = { viewField: ViewFieldDefinition; @@ -14,17 +14,12 @@ type OwnProps = { export function GenericEditableChipCell({ viewField, editModeHorizontalAlign, - placeholder, }: OwnProps) { return ( + } nonEditModeContent={ diff --git a/front/src/modules/ui/table/components/GenericEditableChipCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableChipCellEditMode.tsx new file mode 100644 index 000000000..d76bd1922 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableChipCellEditMode.tsx @@ -0,0 +1,45 @@ +import { useRecoilState } from 'recoil'; + +import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { ViewFieldChipMetadata, ViewFieldDefinition } from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableChipCellEditMode({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.contentFieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newText: string) { + if (newText === fieldValue) return; + + setFieldValue(newText); + + if (currentRowEntityId && updateField) { + updateField(currentRowEntityId, viewField, newText); + } + } + + return ( + + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableDateCell.tsx b/front/src/modules/ui/table/components/GenericEditableDateCell.tsx new file mode 100644 index 000000000..10034a660 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDateCell.tsx @@ -0,0 +1,39 @@ +import { useRecoilValue } from 'recoil'; + +import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode'; +import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { ViewFieldDateMetadata, ViewFieldDefinition } from '../types/ViewField'; + +import { GenericEditableDateCellEditMode } from './GenericEditableDateCellEditMode'; + +type OwnProps = { + viewField: ViewFieldDefinition; + editModeHorizontalAlign?: 'left' | 'right'; +}; + +export function GenericEditableDateCell({ + viewField, + editModeHorizontalAlign, +}: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + const fieldValue = useRecoilValue( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + return ( + + } + nonEditModeContent={} + > + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableDateCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableDateCellEditMode.tsx new file mode 100644 index 000000000..f0f4feb28 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDateCellEditMode.tsx @@ -0,0 +1,50 @@ +import { DateTime } from 'luxon'; +import { useRecoilState } from 'recoil'; + +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { EditableCellDateEditMode } from '../editable-cell/types/EditableCellDateEditMode'; +import { ViewFieldDateMetadata, ViewFieldDefinition } from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableDateCellEditMode({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newDate: Date) { + const fieldValueDate = fieldValue + ? DateTime.fromISO(fieldValue).toJSDate() + : null; + + const newDateISO = DateTime.fromJSDate(newDate).toISO(); + + if (newDate === fieldValueDate || !newDateISO) return; + + setFieldValue(newDateISO); + + if (currentRowEntityId && updateField && newDateISO) { + updateField(currentRowEntityId, viewField, newDateISO); + } + } + + return ( + + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableDoubleTextCell.tsx b/front/src/modules/ui/table/components/GenericEditableDoubleTextCell.tsx new file mode 100644 index 000000000..d26fd626d --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDoubleTextCell.tsx @@ -0,0 +1,48 @@ +import { useRecoilValue } from 'recoil'; + +import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode'; +import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { + ViewFieldDefinition, + ViewFieldDoubleTextMetadata, +} from '../types/ViewField'; + +import { GenericEditableDoubleTextCellEditMode } from './GenericEditableDoubleTextCellEditMode'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableDoubleTextCell({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + const firstValue = useRecoilValue( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.firstValueFieldName, + }), + ); + + const secondValue = useRecoilValue( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.secondValueFieldName, + }), + ); + + const displayName = `${firstValue ?? ''} ${secondValue ?? ''}`; + + return ( + + } + nonEditModeContent={ + {displayName} + } + > + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableDoubleTextCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableDoubleTextCellEditMode.tsx new file mode 100644 index 000000000..491ea4497 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDoubleTextCellEditMode.tsx @@ -0,0 +1,60 @@ +import { useRecoilState } from 'recoil'; + +import { InplaceInputDoubleTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputDoubleTextCellEditMode'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { + ViewFieldDefinition, + ViewFieldDoubleTextMetadata, +} from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableDoubleTextCellEditMode({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [firstValue, setFirstValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.firstValueFieldName, + }), + ); + + const [secondValue, setSecondValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.firstValueFieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newFirstValue: string, newSecondValue: string) { + if (newFirstValue === firstValue && newSecondValue === secondValue) return; + + setFirstValue(newFirstValue); + setSecondValue(newSecondValue); + + if (currentRowEntityId && updateField) { + updateField(currentRowEntityId, viewField, { + firstValue: newFirstValue, + secondValue: newSecondValue, + }); + } + } + + return ( + + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCell.tsx b/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCell.tsx new file mode 100644 index 000000000..5c380815d --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCell.tsx @@ -0,0 +1,28 @@ +import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; + +import { TableHotkeyScope } from '../types/TableHotkeyScope'; +import { + ViewFieldDefinition, + ViewFieldDoubleTextChipMetadata, +} from '../types/ViewField'; + +import { GenericEditableDoubleTextChipCellDisplayMode } from './GenericEditableDoubleTextChipCellDisplayMode'; +import { GenericEditableDoubleTextChipCellEditMode } from './GenericEditableDoubleTextChipCellEditMode'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableDoubleTextChipCell({ viewField }: OwnProps) { + return ( + + } + nonEditModeContent={ + + } + > + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCellDisplayMode.tsx b/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCellDisplayMode.tsx new file mode 100644 index 000000000..983c90572 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCellDisplayMode.tsx @@ -0,0 +1,51 @@ +import { useRecoilState } from 'recoil'; + +import { CompanyChip } from '@/companies/components/CompanyChip'; +import { PersonChip } from '@/people/components/PersonChip'; +import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; +import { + ViewFieldDefinition, + ViewFieldDoubleTextChipMetadata, +} from '@/ui/table/types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableDoubleTextChipCellDisplayMode({ + viewField, +}: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + const [firstValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.firstValueFieldName, + }), + ); + + const [secondValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.secondValueFieldName, + }), + ); + + const displayName = `${firstValue} ${secondValue}`; + + switch (viewField.metadata.entityType) { + case Entity.Company: { + return ; + } + case Entity.Person: { + return ; + } + default: + console.warn( + `Unknown relation type: "${viewField.metadata.entityType}" in GenericEditableDoubleTextChipCellDisplayMode`, + ); + return <> ; + } +} diff --git a/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCellEditMode.tsx new file mode 100644 index 000000000..926a1e053 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableDoubleTextChipCellEditMode.tsx @@ -0,0 +1,72 @@ +import { useRecoilState } from 'recoil'; + +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { EditableCellDoubleTextEditMode } from '../editable-cell/types/EditableCellDoubleTextEditMode'; +import { + ViewFieldDefinition, + ViewFieldDoubleTextChipMetadata, +} from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableDoubleTextChipCellEditMode({ + viewField, +}: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [firstValue, setFirstValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.firstValueFieldName, + }), + ); + + const [secondValue, setSecondValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.secondValueFieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newFirstValue: string, newSecondValue: string) { + const firstValueChanged = newFirstValue !== firstValue; + const secondValueChanged = newSecondValue !== secondValue; + + if (firstValueChanged) { + setFirstValue(newFirstValue); + } + + if (secondValueChanged) { + setSecondValue(newSecondValue); + } + + if ( + currentRowEntityId && + updateField && + (firstValueChanged || secondValueChanged) + ) { + updateField(currentRowEntityId, viewField, { + firstValue: firstValueChanged ? newFirstValue : firstValue, + secondValue: secondValueChanged ? newSecondValue : secondValue, + }); + } + } + + return ( + + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableNumberCell.tsx b/front/src/modules/ui/table/components/GenericEditableNumberCell.tsx new file mode 100644 index 000000000..4ef180f28 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableNumberCell.tsx @@ -0,0 +1,41 @@ +import { useRecoilValue } from 'recoil'; + +import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { + ViewFieldDefinition, + ViewFieldNumberMetadata, +} from '../types/ViewField'; + +import { GenericEditableNumberCellEditMode } from './GenericEditableNumberCellEditMode'; + +type OwnProps = { + viewField: ViewFieldDefinition; + editModeHorizontalAlign?: 'left' | 'right'; +}; + +export function GenericEditableNumberCell({ + viewField, + editModeHorizontalAlign, +}: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + const fieldValue = useRecoilValue( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + return ( + + } + nonEditModeContent={<>{fieldValue}} + > + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableNumberCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableNumberCellEditMode.tsx new file mode 100644 index 000000000..91655ce18 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableNumberCellEditMode.tsx @@ -0,0 +1,66 @@ +import { useRecoilState } from 'recoil'; + +import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { + ViewFieldDefinition, + ViewFieldNumberMetadata, +} from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableNumberCellEditMode({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newText: string) { + if (newText === fieldValue) return; + + try { + const numberValue = parseInt(newText); + + if (isNaN(numberValue)) { + throw new Error('Not a number'); + } + + // TODO: find a way to store this better in DB + if (numberValue > 2000000000) { + throw new Error('Number too big'); + } + + console.log({ numberValue }); + + setFieldValue(numberValue.toString()); + + if (currentRowEntityId && updateField) { + updateField(currentRowEntityId, viewField, numberValue); + } + } catch (error) { + console.warn( + `In GenericEditableNumberCellEditMode, Invalid number: ${newText}, ${error}`, + ); + } + } + + return ( + + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditablePhoneCell.tsx b/front/src/modules/ui/table/components/GenericEditablePhoneCell.tsx new file mode 100644 index 000000000..cdbe56150 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditablePhoneCell.tsx @@ -0,0 +1,42 @@ +import { useRecoilValue } from 'recoil'; + +import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode'; +import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { + ViewFieldDefinition, + ViewFieldPhoneMetadata, +} from '../types/ViewField'; + +import { GenericEditablePhoneCellEditMode } from './GenericEditablePhoneCellEditMode'; + +type OwnProps = { + viewField: ViewFieldDefinition; + editModeHorizontalAlign?: 'left' | 'right'; +}; + +export function GenericEditablePhoneCell({ + viewField, + editModeHorizontalAlign, +}: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + const fieldValue = useRecoilValue( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + return ( + + } + nonEditModeContent={} + > + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditablePhoneCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditablePhoneCellEditMode.tsx new file mode 100644 index 000000000..5ef3cab6b --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditablePhoneCellEditMode.tsx @@ -0,0 +1,48 @@ +import { useRecoilState } from 'recoil'; + +import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { + ViewFieldDefinition, + ViewFieldPhoneMetadata, +} from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditablePhoneCellEditMode({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newText: string) { + if (newText === fieldValue) return; + + setFieldValue(newText); + + if (currentRowEntityId && updateField) { + updateField(currentRowEntityId, viewField, newText); + } + } + + return ( + + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx b/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx index 663306810..1c743f808 100644 --- a/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx +++ b/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx @@ -24,9 +24,7 @@ export function GenericEditableRelationCell({ editModeHorizontalAlign={editModeHorizontalAlign} editHotkeyScope={{ scope: RelationPickerHotkeyScope.RelationPicker }} editModeContent={ - + } nonEditModeContent={ ( tableEntityFieldFamilySelector({ entityId: currentRowEntityId ?? '', @@ -38,6 +40,14 @@ export function GenericEditableRelationCellDisplayMode({ /> ); } + case Entity.User: { + return ( + + ); + } default: console.warn( `Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationCellEditMode`, diff --git a/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx index 4eb245182..4d5d1b7e9 100644 --- a/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx +++ b/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx @@ -1,24 +1,23 @@ import { useRecoilState } from 'recoil'; import { CompanyPickerCell } from '@/companies/components/CompanyPickerCell'; -import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField'; import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect'; import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; import { ViewFieldDefinition, ViewFieldRelationMetadata, } from '@/ui/table/types/ViewField'; +import { UserPicker } from '@/users/components/UserPicker'; type OwnProps = { - viewFieldDefinition: ViewFieldDefinition; + viewField: ViewFieldDefinition; }; -export function GenericEditableRelationCellEditMode({ - viewFieldDefinition, -}: OwnProps) { +export function GenericEditableRelationCellEditMode({ viewField }: OwnProps) { const currentRowEntityId = useCurrentRowEntityId(); const { closeEditableCell } = useEditableCell(); @@ -26,7 +25,7 @@ export function GenericEditableRelationCellEditMode({ const [fieldValueEntity] = useRecoilState( tableEntityFieldFamilySelector({ entityId: currentRowEntityId ?? '', - fieldName: viewFieldDefinition.metadata.fieldName, + fieldName: viewField.metadata.fieldName, }), ); @@ -38,11 +37,7 @@ export function GenericEditableRelationCellEditMode({ currentRowEntityId && updateEntityField ) { - updateEntityField( - currentRowEntityId, - viewFieldDefinition.id, - newFieldEntity, - ); + updateEntityField(currentRowEntityId, viewField, newFieldEntity); } closeEditableCell(); @@ -52,7 +47,7 @@ export function GenericEditableRelationCellEditMode({ closeEditableCell(); } - switch (viewFieldDefinition.metadata.relationType) { + switch (viewField.metadata.relationType) { case Entity.Company: { return ( ); } + case Entity.User: { + return ( + + ); + } default: console.warn( - `Unknown relation type: "${viewFieldDefinition.metadata.relationType}" in GenericEditableRelationCellEditMode`, + `Unknown relation type: "${viewField.metadata.relationType}" in GenericEditableRelationCellEditMode`, ); return <>; } diff --git a/front/src/modules/ui/table/components/GenericEditableTextCell.tsx b/front/src/modules/ui/table/components/GenericEditableTextCell.tsx index 71f921192..716586ae7 100644 --- a/front/src/modules/ui/table/components/GenericEditableTextCell.tsx +++ b/front/src/modules/ui/table/components/GenericEditableTextCell.tsx @@ -12,13 +12,11 @@ import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMo type OwnProps = { viewField: ViewFieldDefinition; editModeHorizontalAlign?: 'left' | 'right'; - placeholder?: string; }; export function GenericEditableTextCell({ viewField, editModeHorizontalAlign, - placeholder, }: OwnProps) { const currentRowEntityId = useCurrentRowEntityId(); @@ -33,11 +31,7 @@ export function GenericEditableTextCell({ + } nonEditModeContent={ {fieldValue} diff --git a/front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx index c0da2f8b6..db5e8747f 100644 --- a/front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx +++ b/front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx @@ -1,28 +1,24 @@ import { useRecoilState } from 'recoil'; -import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField'; -import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; +import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; +import { ViewFieldDefinition, ViewFieldTextMetadata } from '../types/ViewField'; + type OwnProps = { - fieldName: string; - viewFieldId: string; - placeholder?: string; + viewField: ViewFieldDefinition; }; -export function GenericEditableTextCellEditMode({ - fieldName, - viewFieldId, - placeholder, -}: OwnProps) { +export function GenericEditableTextCellEditMode({ viewField }: OwnProps) { const currentRowEntityId = useCurrentRowEntityId(); // TODO: we could use a hook that would return the field value with the right type const [fieldValue, setFieldValue] = useRecoilState( tableEntityFieldFamilySelector({ entityId: currentRowEntityId ?? '', - fieldName, + fieldName: viewField.metadata.fieldName, }), ); @@ -34,13 +30,13 @@ export function GenericEditableTextCellEditMode({ setFieldValue(newText); if (currentRowEntityId && updateField) { - updateField(currentRowEntityId, viewFieldId, newText); + updateField(currentRowEntityId, viewField, newText); } } return ( - ; + editModeHorizontalAlign?: 'left' | 'right'; +}; + +export function GenericEditableURLCell({ + viewField, + editModeHorizontalAlign, +}: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + const fieldValue = useRecoilValue( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + return ( + } + nonEditModeContent={} + > + ); +} diff --git a/front/src/modules/ui/table/components/GenericEditableURLCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableURLCellEditMode.tsx new file mode 100644 index 000000000..25f575e20 --- /dev/null +++ b/front/src/modules/ui/table/components/GenericEditableURLCellEditMode.tsx @@ -0,0 +1,45 @@ +import { useRecoilState } from 'recoil'; + +import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; +import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; +import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; +import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector'; + +import { ViewFieldDefinition, ViewFieldURLMetadata } from '../types/ViewField'; + +type OwnProps = { + viewField: ViewFieldDefinition; +}; + +export function GenericEditableURLCellEditMode({ viewField }: OwnProps) { + const currentRowEntityId = useCurrentRowEntityId(); + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.fieldName, + }), + ); + + const updateField = useUpdateEntityField(); + + function handleSubmit(newText: string) { + if (newText === fieldValue) return; + + setFieldValue(newText); + + if (currentRowEntityId && updateField) { + updateField(currentRowEntityId, viewField, newText); + } + } + + return ( + + ); +} diff --git a/front/src/modules/people/components/GenericEntityTableData.tsx b/front/src/modules/ui/table/components/GenericEntityTableData.tsx similarity index 71% rename from front/src/modules/people/components/GenericEntityTableData.tsx rename to front/src/modules/ui/table/components/GenericEntityTableData.tsx index cd148b82f..af81edf61 100644 --- a/front/src/modules/people/components/GenericEntityTableData.tsx +++ b/front/src/modules/ui/table/components/GenericEntityTableData.tsx @@ -1,8 +1,11 @@ import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; -import { ViewFieldDefinition } from '@/ui/table/types/ViewField'; +import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData'; +import { + ViewFieldDefinition, + ViewFieldMetadata, +} from '@/ui/table/types/ViewField'; -import { useSetEntityTableData } from '../hooks/useSetEntityTableData'; -import { defaultOrderBy } from '../queries'; +import { defaultOrderBy } from '../../../people/queries'; export function GenericEntityTableData({ useGetRequest, @@ -16,7 +19,7 @@ export function GenericEntityTableData({ getRequestResultKey: string; orderBy?: any; whereFilters?: any; - viewFields: ViewFieldDefinition[]; + viewFields: ViewFieldDefinition[]; filterDefinitionArray: FilterDefinition[]; }) { const setEntityTableData = useSetEntityTableData(); diff --git a/front/src/modules/ui/table/editable-cell/types/EditableCellDate.tsx b/front/src/modules/ui/table/editable-cell/types/EditableCellDate.tsx index 169065f18..6af62c29c 100644 --- a/front/src/modules/ui/table/editable-cell/types/EditableCellDate.tsx +++ b/front/src/modules/ui/table/editable-cell/types/EditableCellDate.tsx @@ -20,7 +20,7 @@ export function EditableCellDate({ + } nonEditModeContent={} editHotkeyScope={{ scope: TableHotkeyScope.CellDateEditMode }} diff --git a/front/src/modules/ui/table/editable-cell/types/EditableCellDateEditMode.tsx b/front/src/modules/ui/table/editable-cell/types/EditableCellDateEditMode.tsx index 914c221bd..20df294ef 100644 --- a/front/src/modules/ui/table/editable-cell/types/EditableCellDateEditMode.tsx +++ b/front/src/modules/ui/table/editable-cell/types/EditableCellDateEditMode.tsx @@ -16,17 +16,17 @@ const EditableCellDateEditModeContainer = styled.div` export type EditableDateProps = { value: Date; - onChange: (date: Date) => void; + onSubmit: (date: Date) => void; }; export function EditableCellDateEditMode({ value, - onChange, + onSubmit, }: EditableDateProps) { const { closeEditableCell } = useEditableCell(); function handleDateChange(newDate: Date) { - onChange(newDate); + onSubmit(newDate); closeEditableCell(); } diff --git a/front/src/modules/ui/table/editable-cell/types/EditableCellDoubleTextEditMode.tsx b/front/src/modules/ui/table/editable-cell/types/EditableCellDoubleTextEditMode.tsx index da8ac851a..09ede873d 100644 --- a/front/src/modules/ui/table/editable-cell/types/EditableCellDoubleTextEditMode.tsx +++ b/front/src/modules/ui/table/editable-cell/types/EditableCellDoubleTextEditMode.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { Key } from 'ts-key-enum'; import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys'; -import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; +import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; diff --git a/front/src/modules/ui/table/editable-cell/types/EditableCellPhone.tsx b/front/src/modules/ui/table/editable-cell/types/EditableCellPhone.tsx index 2f97b8962..580d3da3f 100644 --- a/front/src/modules/ui/table/editable-cell/types/EditableCellPhone.tsx +++ b/front/src/modules/ui/table/editable-cell/types/EditableCellPhone.tsx @@ -1,5 +1,5 @@ import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode'; -import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; +import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode'; import { EditableCell } from '../components/EditableCell'; @@ -13,7 +13,7 @@ export function EditableCellPhone({ value, placeholder, onSubmit }: OwnProps) { return ( ( newEntityArray: T[], - viewFields: ViewFieldDefinition[], + viewFields: ViewFieldDefinition[], filters: FilterDefinition[], ) => { for (const entity of newEntityArray) { @@ -54,7 +56,7 @@ export function useSetEntityTableData() { set(availableFiltersScopedState(tableContextScopeId), filters); - set(viewFieldsState, viewFields); + set(viewFieldsFamilyState, viewFields); set(isFetchingEntityTableDataState, false); }, diff --git a/front/src/modules/ui/table/hooks/useUpdateEntityField.ts b/front/src/modules/ui/table/hooks/useUpdateEntityField.ts new file mode 100644 index 000000000..e070763ac --- /dev/null +++ b/front/src/modules/ui/table/hooks/useUpdateEntityField.ts @@ -0,0 +1,228 @@ +import { useContext } from 'react'; + +import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext'; +import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip'; +import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation'; +import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText'; + +import { isViewFieldChipValue } from '../types/guards/isViewFieldChipValue'; +import { isViewFieldDate } from '../types/guards/isViewFieldDate'; +import { isViewFieldDateValue } from '../types/guards/isViewFieldDateValue'; +import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText'; +import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip'; +import { isViewFieldDoubleTextChipValue } from '../types/guards/isViewFieldDoubleTextChipValue'; +import { isViewFieldDoubleTextValue } from '../types/guards/isViewFieldDoubleTextValue'; +import { isViewFieldNumber } from '../types/guards/isViewFieldNumber'; +import { isViewFieldNumberValue } from '../types/guards/isViewFieldNumberValue'; +import { isViewFieldPhone } from '../types/guards/isViewFieldPhone'; +import { isViewFieldPhoneValue } from '../types/guards/isViewFieldPhoneValue'; +import { isViewFieldRelationValue } from '../types/guards/isViewFieldRelationValue'; +import { isViewFieldTextValue } from '../types/guards/isViewFieldTextValue'; +import { isViewFieldURL } from '../types/guards/isViewFieldURL'; +import { isViewFieldURLValue } from '../types/guards/isViewFieldURLValue'; +import { + ViewFieldChipMetadata, + ViewFieldChipValue, + ViewFieldDateMetadata, + ViewFieldDateValue, + ViewFieldDefinition, + ViewFieldDoubleTextChipMetadata, + ViewFieldDoubleTextChipValue, + ViewFieldDoubleTextMetadata, + ViewFieldDoubleTextValue, + ViewFieldMetadata, + ViewFieldNumberMetadata, + ViewFieldNumberValue, + ViewFieldPhoneMetadata, + ViewFieldPhoneValue, + ViewFieldRelationMetadata, + ViewFieldRelationValue, + ViewFieldTextMetadata, + ViewFieldTextValue, + ViewFieldURLMetadata, + ViewFieldURLValue, +} from '../types/ViewField'; + +export function useUpdateEntityField() { + const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext); + + const [updateEntity] = useUpdateEntityMutation(); + + return function updatePeopleField< + MetadataType extends ViewFieldMetadata, + ValueType extends MetadataType extends ViewFieldDoubleTextMetadata + ? ViewFieldDoubleTextValue + : MetadataType extends ViewFieldTextMetadata + ? ViewFieldTextValue + : MetadataType extends ViewFieldPhoneMetadata + ? ViewFieldPhoneValue + : MetadataType extends ViewFieldURLMetadata + ? ViewFieldURLValue + : MetadataType extends ViewFieldNumberMetadata + ? ViewFieldNumberValue + : MetadataType extends ViewFieldDateMetadata + ? ViewFieldDateValue + : MetadataType extends ViewFieldChipMetadata + ? ViewFieldChipValue + : MetadataType extends ViewFieldDoubleTextChipMetadata + ? ViewFieldDoubleTextChipValue + : MetadataType extends ViewFieldRelationMetadata + ? ViewFieldRelationValue + : unknown, + >( + currentEntityId: string, + viewField: ViewFieldDefinition, + newFieldValue: ValueType, + ) { + const newFieldValueUnknown = newFieldValue as unknown; + // TODO: improve type guards organization, maybe with a common typeguard for all view fields + // taking an object of options as parameter ? + // + // The goal would be to check that the view field value not only is valid, + // but also that it is validated against the corresponding view field type + + // Relation + if ( + isViewFieldRelation(viewField) && + isViewFieldRelationValue(newFieldValueUnknown) + ) { + const newSelectedEntity = newFieldValueUnknown; + + const fieldName = viewField.metadata.fieldName; + + if (!newSelectedEntity) { + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { + [fieldName]: { + disconnect: true, + }, + }, + }, + }); + } else { + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { + [fieldName]: { + connect: { id: newSelectedEntity.id }, + }, + }, + }, + }); + } + // Chip + } else if ( + isViewFieldChip(viewField) && + isViewFieldChipValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { [viewField.metadata.contentFieldName]: newContent }, + }, + }); + // Text + } else if ( + isViewFieldText(viewField) && + isViewFieldTextValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { [viewField.metadata.fieldName]: newContent }, + }, + }); + // Double text + } else if ( + isViewFieldDoubleText(viewField) && + isViewFieldDoubleTextValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { + [viewField.metadata.firstValueFieldName]: newContent.firstValue, + [viewField.metadata.secondValueFieldName]: newContent.secondValue, + }, + }, + }); + // Double Text Chip + } else if ( + isViewFieldDoubleTextChip(viewField) && + isViewFieldDoubleTextChipValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { + [viewField.metadata.firstValueFieldName]: newContent.firstValue, + [viewField.metadata.secondValueFieldName]: newContent.secondValue, + }, + }, + }); + // Phone + } else if ( + isViewFieldPhone(viewField) && + isViewFieldPhoneValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { [viewField.metadata.fieldName]: newContent }, + }, + }); + // URL + } else if ( + isViewFieldURL(viewField) && + isViewFieldURLValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { [viewField.metadata.fieldName]: newContent }, + }, + }); + // Number + } else if ( + isViewFieldNumber(viewField) && + isViewFieldNumberValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { [viewField.metadata.fieldName]: newContent }, + }, + }); + // Date + } else if ( + isViewFieldDate(viewField) && + isViewFieldDateValue(newFieldValueUnknown) + ) { + const newContent = newFieldValueUnknown; + + updateEntity({ + variables: { + where: { id: currentEntityId }, + data: { [viewField.metadata.fieldName]: newContent }, + }, + }); + } + }; +} diff --git a/front/src/modules/ui/table/states/ViewFieldContext.ts b/front/src/modules/ui/table/states/ViewFieldContext.ts index 175907e41..5c0502fd3 100644 --- a/front/src/modules/ui/table/states/ViewFieldContext.ts +++ b/front/src/modules/ui/table/states/ViewFieldContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import { ViewFieldDefinition } from '../types/ViewField'; +import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField'; export const ViewFieldContext = - createContext | null>(null); + createContext | null>(null); diff --git a/front/src/modules/ui/table/states/viewFieldsState.ts b/front/src/modules/ui/table/states/viewFieldsState.ts index 42ba0bf55..b1f7a73d0 100644 --- a/front/src/modules/ui/table/states/viewFieldsState.ts +++ b/front/src/modules/ui/table/states/viewFieldsState.ts @@ -1,8 +1,10 @@ import { atom } from 'recoil'; -import { ViewFieldDefinition } from '../types/ViewField'; +import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField'; -export const viewFieldsState = atom[]>({ - key: 'viewFieldsState', +export const viewFieldsFamilyState = atom< + ViewFieldDefinition[] +>({ + key: 'viewFieldsFamilyState', default: [], }); diff --git a/front/src/modules/ui/table/types/ViewField.ts b/front/src/modules/ui/table/types/ViewField.ts index ff4021888..2cf0d197c 100644 --- a/front/src/modules/ui/table/types/ViewField.ts +++ b/front/src/modules/ui/table/types/ViewField.ts @@ -1,36 +1,114 @@ +import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect'; import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect'; -export type ViewFieldType = 'text' | 'relation' | 'chip'; +export type ViewFieldType = + | 'text' + | 'relation' + | 'chip' + | 'double-text-chip' + | 'double-text' + | 'number' + | 'date' + | 'phone' + | 'url'; export type ViewFieldTextMetadata = { + type: 'text'; placeHolder: string; fieldName: string; }; +export type ViewFieldPhoneMetadata = { + type: 'phone'; + placeHolder: string; + fieldName: string; +}; + +export type ViewFieldURLMetadata = { + type: 'url'; + placeHolder: string; + fieldName: string; +}; + +export type ViewFieldDateMetadata = { + type: 'date'; + fieldName: string; +}; + +export type ViewFieldNumberMetadata = { + type: 'number'; + fieldName: string; +}; + export type ViewFieldRelationMetadata = { + type: 'relation'; relationType: Entity; fieldName: string; }; export type ViewFieldChipMetadata = { + type: 'chip'; relationType: Entity; contentFieldName: string; urlFieldName: string; + placeHolder: string; }; -export type ViewFieldDefinition< - T extends - | ViewFieldTextMetadata - | ViewFieldRelationMetadata - | ViewFieldChipMetadata - | unknown, -> = { +export type ViewFieldDoubleTextMetadata = { + type: 'double-text'; + firstValueFieldName: string; + firstValuePlaceholder: string; + secondValueFieldName: string; + secondValuePlaceholder: string; +}; + +export type ViewFieldDoubleTextChipMetadata = { + type: 'double-text-chip'; + firstValueFieldName: string; + firstValuePlaceholder: string; + secondValueFieldName: string; + secondValuePlaceholder: string; + entityType: Entity; +}; + +export type ViewFieldMetadata = { type: ViewFieldType } & ( + | ViewFieldTextMetadata + | ViewFieldRelationMetadata + | ViewFieldChipMetadata + | ViewFieldDoubleTextChipMetadata + | ViewFieldDoubleTextMetadata + | ViewFieldPhoneMetadata + | ViewFieldURLMetadata + | ViewFieldNumberMetadata + | ViewFieldDateMetadata +); + +export type ViewFieldDefinition = { id: string; columnLabel: string; columnSize: number; columnOrder: number; columnIcon?: JSX.Element; filterIcon?: JSX.Element; - type: ViewFieldType; metadata: T; }; + +export type ViewFieldTextValue = string; + +export type ViewFieldChipValue = string; +export type ViewFieldDateValue = string; +export type ViewFieldPhoneValue = string; +export type ViewFieldURLValue = string; +export type ViewFieldNumberValue = number; + +export type ViewFieldDoubleTextValue = { + firstValue: string; + secondValue: string; +}; + +export type ViewFieldDoubleTextChipValue = { + firstValue: string; + secondValue: string; +}; + +export type ViewFieldRelationValue = EntityForSelect | null; diff --git a/front/src/modules/ui/table/types/guards/isViewFieldChip.ts b/front/src/modules/ui/table/types/guards/isViewFieldChip.ts index 25868ec2b..2faf40dde 100644 --- a/front/src/modules/ui/table/types/guards/isViewFieldChip.ts +++ b/front/src/modules/ui/table/types/guards/isViewFieldChip.ts @@ -1,7 +1,11 @@ -import { ViewFieldChipMetadata, ViewFieldDefinition } from '../ViewField'; +import { + ViewFieldChipMetadata, + ViewFieldDefinition, + ViewFieldMetadata, +} from '../ViewField'; export function isViewFieldChip( - field: ViewFieldDefinition, + field: ViewFieldDefinition, ): field is ViewFieldDefinition { - return field.type === 'chip'; + return field.metadata.type === 'chip'; } diff --git a/front/src/modules/ui/table/types/guards/isViewFieldChipValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldChipValue.ts new file mode 100644 index 000000000..95b9c2950 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldChipValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldChipValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldChipValue( + fieldValue: unknown, +): fieldValue is ViewFieldChipValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'string' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldDate.ts b/front/src/modules/ui/table/types/guards/isViewFieldDate.ts new file mode 100644 index 000000000..87d5e00b8 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldDate.ts @@ -0,0 +1,11 @@ +import { + ViewFieldDateMetadata, + ViewFieldDefinition, + ViewFieldMetadata, +} from '../ViewField'; + +export function isViewFieldDate( + field: ViewFieldDefinition, +): field is ViewFieldDefinition { + return field.metadata.type === 'date'; +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldDateValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldDateValue.ts new file mode 100644 index 000000000..2e47964e3 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldDateValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldDateValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldDateValue( + fieldValue: unknown, +): fieldValue is ViewFieldDateValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'string' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldDoubleText.ts b/front/src/modules/ui/table/types/guards/isViewFieldDoubleText.ts new file mode 100644 index 000000000..c37826b92 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldDoubleText.ts @@ -0,0 +1,11 @@ +import { + ViewFieldDefinition, + ViewFieldDoubleTextMetadata, + ViewFieldMetadata, +} from '../ViewField'; + +export function isViewFieldDoubleText( + field: ViewFieldDefinition, +): field is ViewFieldDefinition { + return field.metadata.type === 'double-text'; +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextChip.ts b/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextChip.ts new file mode 100644 index 000000000..45b59c668 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextChip.ts @@ -0,0 +1,11 @@ +import { + ViewFieldDefinition, + ViewFieldDoubleTextChipMetadata, + ViewFieldMetadata, +} from '../ViewField'; + +export function isViewFieldDoubleTextChip( + field: ViewFieldDefinition, +): field is ViewFieldDefinition { + return field.metadata.type === 'double-text-chip'; +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextChipValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextChipValue.ts new file mode 100644 index 000000000..4161d2d9f --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextChipValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldDoubleTextChipValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldDoubleTextChipValue( + fieldValue: unknown, +): fieldValue is ViewFieldDoubleTextChipValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'object' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextValue.ts new file mode 100644 index 000000000..3af8083e9 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldDoubleTextValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldDoubleTextValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldDoubleTextValue( + fieldValue: unknown, +): fieldValue is ViewFieldDoubleTextValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'object' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldNumber.ts b/front/src/modules/ui/table/types/guards/isViewFieldNumber.ts new file mode 100644 index 000000000..c47ce696f --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldNumber.ts @@ -0,0 +1,11 @@ +import { + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldNumberMetadata, +} from '../ViewField'; + +export function isViewFieldNumber( + field: ViewFieldDefinition, +): field is ViewFieldDefinition { + return field.metadata.type === 'number'; +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldNumberValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldNumberValue.ts new file mode 100644 index 000000000..c4e6a494b --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldNumberValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldNumberValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldNumberValue( + fieldValue: unknown, +): fieldValue is ViewFieldNumberValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'number' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldPhone.ts b/front/src/modules/ui/table/types/guards/isViewFieldPhone.ts new file mode 100644 index 000000000..516438952 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldPhone.ts @@ -0,0 +1,11 @@ +import { + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldPhoneMetadata, +} from '../ViewField'; + +export function isViewFieldPhone( + field: ViewFieldDefinition, +): field is ViewFieldDefinition { + return field.metadata.type === 'phone'; +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldPhoneValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldPhoneValue.ts new file mode 100644 index 000000000..2224bd091 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldPhoneValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldPhoneValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldPhoneValue( + fieldValue: unknown, +): fieldValue is ViewFieldPhoneValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'string' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldRelation.ts b/front/src/modules/ui/table/types/guards/isViewFieldRelation.ts index 1642f9c04..cd0f61b09 100644 --- a/front/src/modules/ui/table/types/guards/isViewFieldRelation.ts +++ b/front/src/modules/ui/table/types/guards/isViewFieldRelation.ts @@ -1,7 +1,11 @@ -import { ViewFieldDefinition, ViewFieldRelationMetadata } from '../ViewField'; +import { + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldRelationMetadata, +} from '../ViewField'; export function isViewFieldRelation( - field: ViewFieldDefinition, + field: ViewFieldDefinition, ): field is ViewFieldDefinition { - return field.type === 'relation'; + return field.metadata.type === 'relation'; } diff --git a/front/src/modules/ui/table/types/guards/isViewFieldRelationValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldRelationValue.ts new file mode 100644 index 000000000..62f80f816 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldRelationValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldRelationValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldRelationValue( + fieldValue: unknown, +): fieldValue is ViewFieldRelationValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'object' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldText.ts b/front/src/modules/ui/table/types/guards/isViewFieldText.ts index d51bae806..b914477b0 100644 --- a/front/src/modules/ui/table/types/guards/isViewFieldText.ts +++ b/front/src/modules/ui/table/types/guards/isViewFieldText.ts @@ -1,7 +1,11 @@ -import { ViewFieldDefinition, ViewFieldTextMetadata } from '../ViewField'; +import { + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldTextMetadata, +} from '../ViewField'; export function isViewFieldText( - field: ViewFieldDefinition, + field: ViewFieldDefinition, ): field is ViewFieldDefinition { - return field.type === 'text'; + return field.metadata.type === 'text'; } diff --git a/front/src/modules/ui/table/types/guards/isViewFieldTextValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldTextValue.ts new file mode 100644 index 000000000..d0e32095d --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldTextValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldTextValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldTextValue( + fieldValue: unknown, +): fieldValue is ViewFieldTextValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'string' + ); +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldURL.ts b/front/src/modules/ui/table/types/guards/isViewFieldURL.ts new file mode 100644 index 000000000..fb99732f3 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldURL.ts @@ -0,0 +1,11 @@ +import { + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldURLMetadata, +} from '../ViewField'; + +export function isViewFieldURL( + field: ViewFieldDefinition, +): field is ViewFieldDefinition { + return field.metadata.type === 'url'; +} diff --git a/front/src/modules/ui/table/types/guards/isViewFieldURLValue.ts b/front/src/modules/ui/table/types/guards/isViewFieldURLValue.ts new file mode 100644 index 000000000..4e924c385 --- /dev/null +++ b/front/src/modules/ui/table/types/guards/isViewFieldURLValue.ts @@ -0,0 +1,12 @@ +import { ViewFieldURLValue } from '../ViewField'; + +// TODO: add yup +export function isViewFieldURLValue( + fieldValue: unknown, +): fieldValue is ViewFieldURLValue { + return ( + fieldValue !== null && + fieldValue !== undefined && + typeof fieldValue === 'string' + ); +} diff --git a/front/src/modules/users/components/UserPicker.tsx b/front/src/modules/users/components/UserPicker.tsx index 5ad2b5a56..902c3624a 100644 --- a/front/src/modules/users/components/UserPicker.tsx +++ b/front/src/modules/users/components/UserPicker.tsx @@ -8,7 +8,7 @@ import { useSearchUserQuery } from '~/generated/graphql'; export type OwnProps = { userId: string; - onSubmit: (newUserId: string) => void; + onSubmit: (newUser: EntityForSelect | null) => void; onCancel?: () => void; }; @@ -23,7 +23,7 @@ export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) { const users = useFilteredSearchEntityQuery({ queryHook: useSearchUserQuery, - selectedIds: [userId], + selectedIds: userId ? [userId] : [], searchFilter: searchFilter, mappingFunction: (user) => ({ entityType: Entity.User, @@ -39,7 +39,7 @@ export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) { async function handleEntitySelected( selectedUser: UserForSelect | null | undefined, ) { - onSubmit(selectedUser?.id ?? ''); + onSubmit(selectedUser ?? null); } return ( diff --git a/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx b/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx index 6cacd3e0c..e2f9b6e04 100644 --- a/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx +++ b/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx @@ -34,11 +34,11 @@ export const FilterByName: Story = { const filterButton = await canvas.findByText('Filter'); await userEvent.click(filterButton); - const nameFilterButton = canvas - .queryAllByTestId('dropdown-menu-item') - .find((item) => { - return item.textContent === 'Name'; - }); + const nameFilterButton = ( + await canvas.findAllByTestId('dropdown-menu-item') + ).find((item) => { + return item.textContent === 'Name'; + }); assert(nameFilterButton); @@ -49,7 +49,7 @@ export const FilterByName: Story = { delay: 200, }); - await sleep(1000); + await sleep(50); expect(await canvas.findByText('Airbnb')).toBeInTheDocument(); expect(await canvas.findByText('Aircall')).toBeInTheDocument(); @@ -88,11 +88,11 @@ export const FilterByAccountOwner: Story = { await sleep(1000); - const charlesChip = canvas - .getAllByTestId('dropdown-menu-item') - .find((item) => { - return item.textContent?.includes('Charles Test'); - }); + const charlesChip = ( + await canvas.findAllByTestId('dropdown-menu-item') + ).find((item) => { + return item.textContent?.includes('Charles Test'); + }); assert(charlesChip); diff --git a/front/src/pages/people/__stories__/People.filterBy.stories.tsx b/front/src/pages/people/__stories__/People.filterBy.stories.tsx index 47aed55b3..3f28b210d 100644 --- a/front/src/pages/people/__stories__/People.filterBy.stories.tsx +++ b/front/src/pages/people/__stories__/People.filterBy.stories.tsx @@ -34,22 +34,23 @@ export const Email: Story = { const filterButton = await canvas.findByText('Filter'); await userEvent.click(filterButton); - const emailFilterButton = canvas - .getAllByTestId('dropdown-menu-item') - .find((item) => { - return item.textContent?.includes('Email'); - }); + const emailFilterButton = ( + await canvas.findAllByTestId('dropdown-menu-item') + ).find((item) => { + return item.textContent?.includes('Email'); + }); assert(emailFilterButton); await userEvent.click(emailFilterButton); const emailInput = canvas.getByPlaceholderText('Email'); + await userEvent.type(emailInput, 'al', { delay: 200, }); - await sleep(1000); + await sleep(50); expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument(); await expect(canvas.queryAllByText('John Doe')).toStrictEqual([]); @@ -68,11 +69,11 @@ export const CompanyName: Story = { const filterButton = await canvas.findByText('Filter'); await userEvent.click(filterButton); - const companyFilterButton = canvas - .getAllByTestId('dropdown-menu-item') - .find((item) => { - return item.textContent?.includes('Company'); - }); + const companyFilterButton = ( + await canvas.findAllByTestId('dropdown-menu-item') + ).find((item) => { + return item.textContent?.includes('Company'); + }); assert(companyFilterButton); @@ -85,11 +86,11 @@ export const CompanyName: Story = { await sleep(500); - const qontoChip = canvas - .getAllByTestId('dropdown-menu-item') - .find((item) => { + const qontoChip = (await canvas.findAllByTestId('dropdown-menu-item')).find( + (item) => { return item.textContent?.includes('Qonto'); - }); + }, + ); expect(qontoChip).toBeInTheDocument(); diff --git a/front/src/pages/people/__stories__/People.inputs.stories.tsx b/front/src/pages/people/__stories__/People.inputs.stories.tsx index b9a30ee02..0804acdf9 100644 --- a/front/src/pages/people/__stories__/People.inputs.stories.tsx +++ b/front/src/pages/people/__stories__/People.inputs.stories.tsx @@ -191,7 +191,7 @@ export const EditRelation: Story = { await step('Click on second row company cell', async () => { const secondRowCompanyCell = await canvas.findByText( - mockedPeopleData[1].company.name, + mockedPeopleData[2].company.name, ); await userEvent.click( @@ -262,11 +262,24 @@ export const SelectRelationWithKeys: Story = { }); await userEvent.type(relationInput, '{arrowdown}'); + + await sleep(50); + await userEvent.type(relationInput, '{arrowup}'); + + await sleep(50); + await userEvent.type(relationInput, '{arrowdown}'); + + await sleep(50); + await userEvent.type(relationInput, '{arrowdown}'); + + await sleep(50); + await userEvent.type(relationInput, '{enter}'); - sleep(25); + + await sleep(50); const allAirbns = await canvas.findAllByText('Aircall'); expect(allAirbns.length).toBe(1);