FieldDisplay & FieldInput (#1708)

* Removed view field duplicate types

* wip

* wip 2

* wip 3

* Unified state for fields

* Renaming

* Wip

* Post merge

* Post post merge

* wip

* Delete unused file

* Boolean and Probability

* Finished InlineCell

* Renamed EditableCell to TableCell

* Finished double texts

* Finished MoneyField

* Fixed bug inline cell click outside

* Fixed hotkey scope

* Final fixes

* Phone

* Fix url and number input validation

* Fix

* Fix position

* wip refactor activity editor

* Fixed activity editor

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-09-27 18:18:02 +02:00
committed by GitHub
parent d9feabbc63
commit cbadcba188
290 changed files with 3152 additions and 4481 deletions

View File

@ -1,5 +1,5 @@
import { useCurrentCellEditMode } from '../editable-cell/hooks/useCurrentCellEditMode';
import { useEditableCell } from '../editable-cell/hooks/useEditableCell';
import { useCurrentTableCellEditMode } from '../editable-cell/hooks/useCurrentTableCellEditMode';
import { useTableCell } from '../editable-cell/hooks/useTableCell';
import { useMoveSoftFocus } from './useMoveSoftFocus';
@ -10,8 +10,9 @@ export const useCellInputEventHandlers = <T>({
onSubmit?: (newValue: T) => void;
onCancel?: () => void;
}) => {
const { closeEditableCell } = useEditableCell();
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const { closeTableCell: closeEditableCell } = useTableCell();
const { isCurrentTableCellInEditMode: isCurrentCellInEditMode } =
useCurrentTableCellEditMode();
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
return {

View File

@ -1,15 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
export const useCloseCurrentCellInEditMode = () =>
useRecoilCallback(({ set, snapshot }) => {
return async () => {
const currentCellInEditModePosition = await snapshot.getPromise(
currentCellInEditModePositionState,
);
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
};
}, []);

View File

@ -0,0 +1,18 @@
import { useRecoilCallback } from 'recoil';
import { currentTableCellInEditModePositionState } from '../states/currentTableCellInEditModePositionState';
import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditModeFamilyState';
export const useCloseCurrentTableCellInEditMode = () =>
useRecoilCallback(({ set, snapshot }) => {
return async () => {
const currentTableCellInEditModePosition = await snapshot.getPromise(
currentTableCellInEditModePositionState,
);
set(
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
false,
);
};
}, []);

View File

@ -1,7 +1,7 @@
import { useRecoilCallback } from 'recoil';
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
import { isSoftFocusOnTableCellFamilyState } from '../states/isSoftFocusOnTableCellFamilyState';
import { softFocusPositionState } from '../states/softFocusPositionState';
export const useDisableSoftFocus = () =>
@ -13,6 +13,6 @@ export const useDisableSoftFocus = () =>
set(isSoftFocusActiveState, false);
set(isSoftFocusOnCellFamilyState(currentPosition), false);
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
};
}, []);

View File

@ -5,12 +5,12 @@ import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/c
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { TableHotkeyScope } from '../types/TableHotkeyScope';
import { useCloseCurrentCellInEditMode } from './useClearCellInEditMode';
import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode';
import { useDisableSoftFocus } from './useDisableSoftFocus';
export const useLeaveTableFocus = () => {
const disableSoftFocus = useDisableSoftFocus();
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode();
return useRecoilCallback(
({ snapshot }) =>

View File

@ -1,20 +1,23 @@
import { useRecoilCallback } from 'recoil';
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
import { CellPosition } from '../types/CellPosition';
import { currentTableCellInEditModePositionState } from '../states/currentTableCellInEditModePositionState';
import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditModeFamilyState';
import { TableCellPosition } from '../types/TableCellPosition';
export const useMoveEditModeToCellPosition = () =>
export const useMoveEditModeToTableCellPosition = () =>
useRecoilCallback(({ set, snapshot }) => {
return (newPosition: CellPosition) => {
const currentCellInEditModePosition = snapshot
.getLoadable(currentCellInEditModePositionState)
return (newPosition: TableCellPosition) => {
const currentTableCellInEditModePosition = snapshot
.getLoadable(currentTableCellInEditModePositionState)
.valueOrThrow();
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
set(
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
false,
);
set(currentCellInEditModePositionState, newPosition);
set(currentTableCellInEditModePositionState, newPosition);
set(isCellInEditModeFamilyState(newPosition), true);
set(isTableCellInEditModeFamilyState(newPosition), true);
};
}, []);

View File

@ -1,8 +1,8 @@
import { useRecoilCallback } from 'recoil';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState';
@ -27,11 +27,11 @@ export const useSetEntityTableData = () => {
) => {
for (const entity of newEntityArray) {
const currentEntity = snapshot
.getLoadable(tableEntitiesFamilyState(entity.id))
.getLoadable(entityFieldsFamilyState(entity.id))
.valueOrThrow();
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
set(tableEntitiesFamilyState(entity.id), entity);
set(entityFieldsFamilyState(entity.id), entity);
}
}

View File

@ -1,23 +1,23 @@
import { useRecoilCallback } from 'recoil';
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
import { isSoftFocusOnTableCellFamilyState } from '../states/isSoftFocusOnTableCellFamilyState';
import { softFocusPositionState } from '../states/softFocusPositionState';
import { CellPosition } from '../types/CellPosition';
import { TableCellPosition } from '../types/TableCellPosition';
export const useSetSoftFocusPosition = () =>
useRecoilCallback(({ set, snapshot }) => {
return (newPosition: CellPosition) => {
return (newPosition: TableCellPosition) => {
const currentPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
set(isSoftFocusActiveState, true);
set(isSoftFocusOnCellFamilyState(currentPosition), false);
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
set(softFocusPositionState, newPosition);
set(isSoftFocusOnCellFamilyState(newPosition), true);
set(isSoftFocusOnTableCellFamilyState(newPosition), true);
};
}, []);

View File

@ -1,16 +1,16 @@
import { useCallback, useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { ViewFieldForVisibility } from '@/ui/view-bar/types/ViewFieldForVisibility';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { TableContext } from '../contexts/TableContext';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../states/savedTableColumnsFamilyState';
import { tableColumnsByKeyScopedSelector } from '../states/selectors/tableColumnsByKeyScopedSelector';
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
import { ColumnDefinition } from '../types/ColumnDefinition';
@ -28,15 +28,11 @@ export const useTableColumns = () => {
tableColumnsScopedState,
TableRecoilScopeContext,
);
const tableColumnsByKey = useRecoilScopedValue(
tableColumnsByKeyScopedSelector,
TableRecoilScopeContext,
);
const { handleColumnMove } = useMoveViewColumns();
const handleColumnsChange = useCallback(
async (columns: ColumnDefinition<ViewFieldMetadata>[]) => {
async (columns: ColumnDefinition<FieldMetadata>[]) => {
setSavedTableColumns(columns);
setTableColumns(columns);
@ -46,7 +42,7 @@ export const useTableColumns = () => {
);
const handleColumnReorder = useCallback(
async (columns: ColumnDefinition<ViewFieldMetadata>[]) => {
async (columns: ColumnDefinition<FieldMetadata>[]) => {
const updatedColumns = columns.map((column, index) => ({
...column,
index,
@ -58,27 +54,20 @@ export const useTableColumns = () => {
);
const handleColumnVisibilityChange = useCallback(
async (column: ColumnDefinition<ViewFieldMetadata>) => {
const nextColumns = tableColumnsByKey[column.key]
? tableColumns.map((previousColumn) =>
previousColumn.key === column.key
? { ...previousColumn, isVisible: !column.isVisible }
: previousColumn,
)
: [...tableColumns, { ...column, isVisible: true }].sort(
(columnA, columnB) => columnA.index - columnB.index,
);
async (column: ViewFieldForVisibility) => {
const nextColumns = tableColumns.map((previousColumn) =>
previousColumn.key === column.key
? { ...previousColumn, isVisible: !column.isVisible }
: previousColumn,
);
await handleColumnsChange(nextColumns);
},
[tableColumnsByKey, tableColumns, handleColumnsChange],
[tableColumns, handleColumnsChange],
);
const handleMoveTableColumn = useCallback(
(
direction: 'left' | 'right',
column: ColumnDefinition<ViewFieldMetadata>,
) => {
(direction: 'left' | 'right', column: ColumnDefinition<FieldMetadata>) => {
const currentColumnArrayIndex = tableColumns.findIndex(
(tableColumn) => tableColumn.key === column.key,
);

View File

@ -1,179 +0,0 @@
import { useContext } from 'react';
import { isViewFieldBoolean } from '@/ui/editable-field/types/guards/isViewFieldBoolean';
import { isViewFieldBooleanValue } from '@/ui/editable-field/types/guards/isViewFieldBooleanValue';
import { isViewFieldChip } from '@/ui/editable-field/types/guards/isViewFieldChip';
import { isViewFieldChipValue } from '@/ui/editable-field/types/guards/isViewFieldChipValue';
import { isViewFieldDate } from '@/ui/editable-field/types/guards/isViewFieldDate';
import { isViewFieldDateValue } from '@/ui/editable-field/types/guards/isViewFieldDateValue';
import { isViewFieldDoubleText } from '@/ui/editable-field/types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChip';
import { isViewFieldDoubleTextChipValue } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChipValue';
import { isViewFieldDoubleTextValue } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextValue';
import { isViewFieldEmail } from '@/ui/editable-field/types/guards/isViewFieldEmail';
import { isViewFieldEmailValue } from '@/ui/editable-field/types/guards/isViewFieldEmailValue';
import { isViewFieldMoney } from '@/ui/editable-field/types/guards/isViewFieldMoney';
import { isViewFieldMoneyValue } from '@/ui/editable-field/types/guards/isViewFieldMoneyValue';
import { isViewFieldNumber } from '@/ui/editable-field/types/guards/isViewFieldNumber';
import { isViewFieldNumberValue } from '@/ui/editable-field/types/guards/isViewFieldNumberValue';
import { isViewFieldPhone } from '@/ui/editable-field/types/guards/isViewFieldPhone';
import { isViewFieldPhoneValue } from '@/ui/editable-field/types/guards/isViewFieldPhoneValue';
import { isViewFieldRelation } from '@/ui/editable-field/types/guards/isViewFieldRelation';
import { isViewFieldRelationValue } from '@/ui/editable-field/types/guards/isViewFieldRelationValue';
import { isViewFieldText } from '@/ui/editable-field/types/guards/isViewFieldText';
import { isViewFieldTextValue } from '@/ui/editable-field/types/guards/isViewFieldTextValue';
import { isViewFieldURL } from '@/ui/editable-field/types/guards/isViewFieldURL';
import { isViewFieldURLValue } from '@/ui/editable-field/types/guards/isViewFieldURLValue';
import {
ViewFieldChipMetadata,
ViewFieldChipValue,
ViewFieldDateMetadata,
ViewFieldDateValue,
ViewFieldDoubleTextChipMetadata,
ViewFieldDoubleTextChipValue,
ViewFieldDoubleTextMetadata,
ViewFieldDoubleTextValue,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldNumberValue,
ViewFieldPhoneMetadata,
ViewFieldPhoneValue,
ViewFieldRelationMetadata,
ViewFieldRelationValue,
ViewFieldTextMetadata,
ViewFieldTextValue,
ViewFieldURLMetadata,
ViewFieldURLValue,
} from '@/ui/editable-field/types/ViewField';
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
import { ColumnDefinition } from '../types/ColumnDefinition';
export const useUpdateEntityField = () => {
const updateEntity = useContext(EntityUpdateMutationContext);
const updateEntityField = <
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,
columnDefinition: ColumnDefinition<MetadataType>,
newFieldValue: ValueType | null,
) => {
// 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
if (
// Relation
isViewFieldRelation(columnDefinition) &&
isViewFieldRelationValue(newFieldValue)
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[columnDefinition.metadata.fieldName]:
!newFieldValue || newFieldValue.id === ''
? { disconnect: true }
: { connect: { id: newFieldValue.id } },
},
},
});
return;
}
if (
// Chip
isViewFieldChip(columnDefinition) &&
isViewFieldChipValue(newFieldValue)
) {
const newContent = newFieldValue;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [columnDefinition.metadata.contentFieldName]: newContent },
},
});
return;
}
if (
// Text
(isViewFieldText(columnDefinition) &&
isViewFieldTextValue(newFieldValue)) ||
// Phone
(isViewFieldPhone(columnDefinition) &&
isViewFieldPhoneValue(newFieldValue)) ||
// Email
(isViewFieldEmail(columnDefinition) &&
isViewFieldEmailValue(newFieldValue)) ||
// URL
(isViewFieldURL(columnDefinition) &&
isViewFieldURLValue(newFieldValue)) ||
// Number
(isViewFieldNumber(columnDefinition) &&
isViewFieldNumberValue(newFieldValue)) ||
// Boolean
(isViewFieldBoolean(columnDefinition) &&
isViewFieldBooleanValue(newFieldValue)) ||
// Money
(isViewFieldMoney(columnDefinition) &&
isViewFieldMoneyValue(newFieldValue)) ||
// Date
(isViewFieldDate(columnDefinition) && isViewFieldDateValue(newFieldValue))
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [columnDefinition.metadata.fieldName]: newFieldValue },
},
});
return;
}
if (
// Double text
(isViewFieldDoubleText(columnDefinition) &&
isViewFieldDoubleTextValue(newFieldValue)) ||
// Double Text Chip
(isViewFieldDoubleTextChip(columnDefinition) &&
isViewFieldDoubleTextChipValue(newFieldValue))
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[columnDefinition.metadata.firstValueFieldName]:
newFieldValue.firstValue,
[columnDefinition.metadata.secondValueFieldName]:
newFieldValue.secondValue,
},
},
});
}
};
return updateEntityField;
};

View File

@ -1,17 +1,17 @@
import { useRecoilCallback } from 'recoil';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
export const useUpsertEntityTableItem = () =>
useRecoilCallback(
({ set, snapshot }) =>
<T extends { id: string }>(entity: T) => {
const currentEntity = snapshot
.getLoadable(tableEntitiesFamilyState(entity.id))
.getLoadable(entityFieldsFamilyState(entity.id))
.valueOrThrow();
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
set(tableEntitiesFamilyState(entity.id), entity);
set(entityFieldsFamilyState(entity.id), entity);
}
},
[],

View File

@ -1,6 +1,6 @@
import { useRecoilCallback } from 'recoil';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
export const useUpsertEntityTableItems = () =>
useRecoilCallback(
@ -14,7 +14,7 @@ export const useUpsertEntityTableItems = () =>
// Filter out entities that are already the same in the state.
const entitiesToUpdate = entities.filter((entity) => {
const currentEntity = snapshot
.getLoadable(tableEntitiesFamilyState(entity.id))
.getLoadable(entityFieldsFamilyState(entity.id))
.valueMaybe();
return (
@ -26,7 +26,7 @@ export const useUpsertEntityTableItems = () =>
// Batch set state for the filtered entities.
for (const entity of entitiesToUpdate) {
set(tableEntitiesFamilyState(entity.id), entity);
set(entityFieldsFamilyState(entity.id), entity);
}
},
[],