Merge commit 'cd3a32e55503dc1e6b9873d812dd401bf7d51045' into context-menu-vertical

This commit is contained in:
brendanlaschke
2023-08-14 22:00:49 +02:00
179 changed files with 1971 additions and 3230 deletions

View File

@ -1,24 +0,0 @@
import { ReactElement } from 'react';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { BoardCardFieldContext } from '../states/BoardCardFieldContext';
import { BoardCardEditableFieldInternal } from './BoardCardEditableFieldInternal';
type OwnProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
editHotkeyScope?: HotkeyScope;
};
export function BoardCardEditableField(props: OwnProps) {
return (
<RecoilScope SpecificContext={BoardCardFieldContext}>
<BoardCardEditableFieldInternal {...props} />
</RecoilScope>
);
}

View File

@ -1,39 +0,0 @@
import { useMemo, useState } from 'react';
import { DateInputDisplay } from '@/ui/input/date/components/DateInputDisplay';
import { debounce } from '~/utils/debounce';
import { BoardCardEditableField } from './BoardCardEditableField';
import { BoardCardEditableFieldDateEditMode } from './BoardCardEditableFieldDateEditMode';
type OwnProps = {
value: Date;
onChange: (newValue: Date) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
export function BoardCardEditableFieldDate({
value,
onChange,
editModeHorizontalAlign,
}: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
const debouncedOnChange = useMemo(() => {
return debounce(onChange, 200);
}, [onChange]);
return (
<BoardCardEditableField
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<BoardCardEditableFieldDateEditMode
value={internalValue}
onChange={(date: Date) => {
setInternalValue(date);
debouncedOnChange(date);
}}
/>
}
nonEditModeContent={<DateInputDisplay value={value} />}
></BoardCardEditableField>
);
}

View File

@ -1,17 +0,0 @@
import { DateInputEdit } from '@/ui/input/date/components/DateInputEdit';
type OwnProps = {
value: Date;
onChange: (newValue: Date) => void;
};
export function BoardCardEditableFieldDateEditMode({
value,
onChange,
}: OwnProps) {
function handleDateChange(newDate: Date) {
onChange(newDate);
}
return <DateInputEdit value={value} onChange={handleDateChange} />;
}

View File

@ -1,32 +0,0 @@
import styled from '@emotion/styled';
export const BoardCardFieldDisplayModeOuterContainer = styled.div`
align-items: center;
display: flex;
height: 100%;
overflow: hidden;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(1)};
width: 100%;
`;
export const BoardCardFieldDisplayModeInnerContainer = styled.div`
align-items: center;
display: flex;
height: 100%;
overflow: hidden;
width: 100%;
`;
export function BoardCardEditableFieldDisplayMode({
children,
}: React.PropsWithChildren<unknown>) {
return (
<BoardCardFieldDisplayModeOuterContainer>
<BoardCardFieldDisplayModeInnerContainer>
{children}
</BoardCardFieldDisplayModeInnerContainer>
</BoardCardFieldDisplayModeOuterContainer>
);
}

View File

@ -1,81 +0,0 @@
import { ReactElement, useRef } from 'react';
import styled from '@emotion/styled';
import { overlayBackground } from '@/ui/theme/constants/effects';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope';
export const BoardCardFieldEditModeContainer = styled.div<
Omit<OwnProps, 'onExit'>
>`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
left: ${(props) =>
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
margin-left: -2px;
min-height: 100%;
min-width: calc(100% + 20px);
position: absolute;
right: ${(props) =>
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
z-index: 1;
${overlayBackground}
`;
type OwnProps = {
children: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
onExit: () => void;
};
export function BoardCardEditableFieldEditMode({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
onExit,
}: OwnProps) {
const wrapperRef = useRef(null);
useListenClickOutside({
refs: [wrapperRef],
callback: () => {
onExit();
},
});
useScopedHotkeys(
'enter',
() => {
onExit();
},
BoardCardFieldHotkeyScope.BoardCardFieldEditMode,
[onExit],
);
useScopedHotkeys(
'esc',
() => {
onExit();
},
BoardCardFieldHotkeyScope.BoardCardFieldEditMode,
[onExit],
);
return (
<BoardCardFieldEditModeContainer
data-testid="editable-cell-edit-mode-container"
ref={wrapperRef}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{children}
</BoardCardFieldEditModeContainer>
);
}

View File

@ -1,82 +0,0 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useBoardCardField } from '../hooks/useBoardCardField';
import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope';
import { BoardCardEditableFieldDisplayMode } from './BoardCardEditableFieldDisplayMode';
import { BoardCardEditableFieldEditMode } from './BoardCardEditableFieldEditMode';
export const BoardCardFieldContainer = styled.div`
align-items: center;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 32px;
position: relative;
user-select: none;
width: 100%;
`;
type OwnProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
editHotkeyScope?: HotkeyScope;
};
export function BoardCardEditableFieldInternal({
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
editHotkeyScope,
}: OwnProps) {
const { openBoardCardField, isBoardCardFieldInEditMode } =
useBoardCardField();
const { closeBoardCardField } = useBoardCardField();
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
function handleOnClick() {
if (!isBoardCardFieldInEditMode) {
openBoardCardField();
setHotkeyScopeAndMemorizePreviousScope(
editHotkeyScope?.scope ??
BoardCardFieldHotkeyScope.BoardCardFieldEditMode,
editHotkeyScope?.customScopes ?? {},
);
}
}
function handleEditModeExit() {
goBackToPreviousHotkeyScope();
closeBoardCardField();
}
return (
<BoardCardFieldContainer onClick={handleOnClick}>
{isBoardCardFieldInEditMode ? (
<BoardCardEditableFieldEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
onExit={handleEditModeExit}
>
{editModeContent}
</BoardCardEditableFieldEditMode>
) : (
<BoardCardEditableFieldDisplayMode>
{nonEditModeContent}
</BoardCardEditableFieldDisplayMode>
)}
</BoardCardFieldContainer>
);
}

View File

@ -1,46 +0,0 @@
import { ChangeEvent, useMemo, useState } from 'react';
import { TextInputDisplay } from '@/ui/input/text/components/TextInputDisplay';
import { StyledInput } from '@/ui/table/editable-cell/type/components/TextCellEdit';
import { debounce } from '~/utils/debounce';
import { BoardCardEditableField } from './BoardCardEditableField';
type OwnProps = {
placeholder?: string;
value: string;
onChange: (newValue: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
export function BoardCardEditableFieldText({
value,
placeholder,
onChange,
editModeHorizontalAlign,
}: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
const debouncedOnChange = useMemo(() => {
return debounce(onChange, 200);
}, [onChange]);
return (
<BoardCardEditableField
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledInput
placeholder={placeholder || ''}
autoFocus
value={internalValue}
autoComplete="off"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInternalValue(event.target.value);
debouncedOnChange(event.target.value);
}}
/>
}
nonEditModeContent={<TextInputDisplay>{value}</TextInputDisplay>}
></BoardCardEditableField>
);
}

View File

@ -1,26 +0,0 @@
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { BoardCardFieldContext } from '../states/BoardCardFieldContext';
import { isBoardCardFieldInEditModeScopedState } from '../states/isBoardCardFieldInEditModeScopedState';
export function useBoardCardField() {
const [isBoardCardFieldInEditMode, setIsBoardCardFieldInEditMode] =
useRecoilScopedState(
isBoardCardFieldInEditModeScopedState,
BoardCardFieldContext,
);
function openBoardCardField() {
setIsBoardCardFieldInEditMode(true);
}
function closeBoardCardField() {
setIsBoardCardFieldInEditMode(false);
}
return {
isBoardCardFieldInEditMode,
openBoardCardField,
closeBoardCardField,
};
}

View File

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const BoardCardFieldContext = createContext<string | null>(null);

View File

@ -1,9 +0,0 @@
import { atomFamily } from 'recoil';
export const isBoardCardFieldInEditModeScopedState = atomFamily<
boolean,
string
>({
key: 'isBoardCardFieldInEditModeScopedState',
default: false,
});

View File

@ -1,3 +0,0 @@
export enum BoardCardFieldHotkeyScope {
BoardCardFieldEditMode = 'board-card-field-edit-mode',
}

View File

@ -1,53 +1,40 @@
import { useRecoilCallback } from 'recoil';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { GET_PIPELINES } from '@/pipeline/queries';
import { ActionBarEntry } from '@/ui/action-bar/components/ActionBarEntry';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { selectedBoardCardIdsState } from '@/ui/board/states/selectedBoardCardIdsState';
import { IconTrash } from '@/ui/icon/index';
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
export function BoardActionBarButtonDeleteBoardCard({
onDelete,
}: {
onDelete: (deletedCardIds: string[]) => void;
}) {
const deleteBoardCardIds = useRecoilCallback(
({ set, snapshot }) =>
() => {
const boardCardIdsToDelete = snapshot
.getLoadable(selectedBoardCardIdsState)
.getValue();
import { useRemoveCardIds } from '../hooks/useRemoveCardIds';
import { selectedCardIdsSelector } from '../states/selectedCardIdsSelector';
const boardColumns = snapshot.getLoadable(boardColumnsState).getValue();
export function BoardActionBarButtonDeleteBoardCard() {
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
const removeCardIds = useRemoveCardIds();
for (const boardColumn of boardColumns) {
const boardColumnCardIds = snapshot
.getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id))
.getValue();
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
const newBoardColumnCardIds = boardColumnCardIds.filter(
(cardId) => !boardCardIdsToDelete.includes(cardId),
);
if (newBoardColumnCardIds.length !== boardColumnCardIds.length) {
set(
boardCardIdsByColumnIdFamilyState(boardColumn.id),
newBoardColumnCardIds,
);
}
}
set(selectedBoardCardIdsState, []);
return boardCardIdsToDelete;
async function handleDelete() {
await deletePipelineProgress({
variables: {
ids: selectedCardIds,
},
[],
);
async function handleDeleteClick() {
const deletedCardIds = deleteBoardCardIds();
onDelete(deletedCardIds);
optimisticResponse: {
__typename: 'Mutation',
deleteManyPipelineProgress: {
count: selectedCardIds.length,
},
},
update: (cache) => {
removeCardIds(selectedCardIds);
selectedCardIds.forEach((id) => {
cache.evict({ id: `PipelineProgress:${id}` });
});
},
});
}
return (
@ -55,7 +42,7 @@ export function BoardActionBarButtonDeleteBoardCard({
label="Delete"
icon={<IconTrash size={16} />}
type="danger"
onClick={handleDeleteClick}
onClick={handleDelete}
/>
);
}

View File

@ -4,16 +4,14 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { IconList } from '@tabler/icons-react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useRecoilState } from 'recoil';
import { CompanyBoardContext } from '@/companies/states/CompanyBoardContext';
import { GET_PIPELINE_PROGRESS } from '@/pipeline/queries';
import { BoardHeader } from '@/ui/board/components/BoardHeader';
import { StyledBoard } from '@/ui/board/components/StyledBoard';
import { useUpdateBoardCardIds } from '@/ui/board/hooks/useUpdateBoardCardIds';
import { BoardColumnIdContext } from '@/ui/board/states/BoardColumnIdContext';
import { SelectedSortType } from '@/ui/filter-n-sort/types/interface';
import { actionBarOpenState } from '@/ui/table/states/ActionBarIsOpenState';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import {
@ -23,9 +21,10 @@ import {
useUpdateOnePipelineProgressStageMutation,
} from '~/generated/graphql';
import { useSetCardSelected } from '../hooks/useSetCardSelected';
import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds';
import { BoardColumnContext } from '../states/BoardColumnContext';
import { boardColumnsState } from '../states/boardColumnsState';
import { selectedBoardCardIdsState } from '../states/selectedBoardCardIdsState';
import { BoardOptions } from '../types/BoardOptions';
import { EntityBoardColumn } from './EntityBoardColumn';
@ -49,6 +48,7 @@ export function EntityBoard({
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
}) {
const [boardColumns] = useRecoilState(boardColumnsState);
const setCardSelected = useSetCardSelected();
const theme = useTheme();
const [updatePipelineProgressStage] =
@ -105,21 +105,6 @@ export function EntityBoard({
});
const boardRef = useRef(null);
const [selectedBoardCards, setSelectedBoardCards] = useRecoilState(
selectedBoardCardIdsState,
);
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
function setRowSelectedState(boardCardId: string, selected: boolean) {
if (selected && !selectedBoardCards.includes(boardCardId)) {
setSelectedBoardCards([...selectedBoardCards, boardCardId ?? '']);
setActionBarOpenState(true);
} else if (!selected && selectedBoardCards.includes(boardCardId)) {
setSelectedBoardCards(
selectedBoardCards.filter((id) => id !== boardCardId),
);
}
}
return (boardColumns?.length ?? 0) > 0 ? (
<StyledBoardWithHeader>
@ -147,7 +132,7 @@ export function EntityBoard({
</StyledBoard>
<DragSelect
dragSelectable={boardRef}
onDragSelectionChange={setRowSelectedState}
onDragSelectionChange={setCardSelected}
/>
</StyledBoardWithHeader>
) : (

View File

@ -3,9 +3,9 @@ import { useRecoilValue } from 'recoil';
import { ActionBar } from '@/ui/action-bar/components/ActionBar';
import { selectedBoardCardIdsState } from '../states/selectedBoardCardIdsState';
import { selectedCardIdsSelector } from '../states/selectedCardIdsSelector';
export function EntityBoardActionBar() {
const selectedBoardCards = useRecoilValue(selectedBoardCardIdsState);
const selectedBoardCards = useRecoilValue(selectedCardIdsSelector);
return <ActionBar selectedIds={selectedBoardCards}></ActionBar>;
}

View File

@ -1,53 +0,0 @@
// TODO: refactor this test with Recoil
describe('getOptimisticlyUpdatedBoard', () => {
it('should return a new board with the updated cell', () => {
// const initialColumn1: string[] = ['item-1', 'item-2', 'item-3'];
// const initialColumn2: string[] = ['item-4', 'item-5'];
// const finalColumn1: string[] = ['item-2', 'item-3'];
// const finalColumn2: string[] = ['item-4', 'item-1', 'item-5'];
// const dropResult = {
// source: {
// droppableId: 'column-1',
// index: 0,
// },
// destination: {
// droppableId: 'column-2',
// index: 1,
// },
// } as DropResult;
// const initialBoard = [
// {
// id: 'column-1',
// title: 'My Column',
// pipelineStageId: 'column-1',
// pipelineProgressIds: initialColumn1,
// },
// {
// id: 'column-2',
// title: 'My Column',
// pipelineStageId: 'column-2',
// pipelineProgressIds: initialColumn2,
// },
// ];
// const updatedBoard = u(
// initialBoard,
// dropResult,
// );
// const finalBoard = [
// {
// id: 'column-1',
// title: 'My Column',
// pipelineStageId: 'column-1',
// pipelineProgressIds: finalColumn1,
// },
// {
// id: 'column-2',
// title: 'My Column',
// pipelineStageId: 'column-2',
// pipelineProgressIds: finalColumn2,
// },
// ];
// expect(updatedBoard).toEqual(finalBoard);
// expect(updatedBoard).not.toBe(initialBoard);
});
});

View File

@ -1,30 +1,13 @@
import { getOperationName } from '@apollo/client/utilities';
import { useSetRecoilState } from 'recoil';
import { GET_PIPELINES } from '@/pipeline/queries';
import { actionBarEntriesState } from '@/ui/table/states/ActionBarEntriesState';
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
import { BoardActionBarButtonDeleteBoardCard } from '../components/BoardActionBarButtonDeleteBoardCard';
export function useOpenActionBar() {
const setActionBarEntries = useSetRecoilState(actionBarEntriesState);
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
async function handleDelete(cardIdsToDelete: string[]) {
await deletePipelineProgress({
variables: {
ids: cardIdsToDelete,
},
});
}
return () => {
setActionBarEntries([
<BoardActionBarButtonDeleteBoardCard onDelete={handleDelete} />,
]);
setActionBarEntries([<BoardActionBarButtonDeleteBoardCard />]);
};
}

View File

@ -0,0 +1,28 @@
import { useContext } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { BoardCardIdContext } from '../states/BoardCardIdContext';
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
export function useCurrentCardSelected() {
const currentCardId = useContext(BoardCardIdContext);
const [isCardSelected] = useRecoilState(
isCardSelectedFamilyState(currentCardId ?? ''),
);
const setCurrentCardSelected = useRecoilCallback(
({ set }) =>
(selected: boolean) => {
if (!currentCardId) return;
set(isCardSelectedFamilyState(currentCardId), selected);
},
[],
);
return {
currentCardSelected: isCardSelected,
setCurrentCardSelected,
};
}

View File

@ -0,0 +1,27 @@
// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { useRecoilCallback } from 'recoil';
import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '../states/boardColumnsState';
export function useRemoveCardIds() {
return useRecoilCallback(
({ snapshot, set }) =>
(cardIdToRemove: string[]) => {
const boardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
boardColumns.forEach((boardColumn) => {
const columnCardIds = snapshot
.getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id))
.valueOrThrow();
set(
boardCardIdsByColumnIdFamilyState(boardColumn.id),
columnCardIds.filter((cardId) => !cardIdToRemove.includes(cardId)),
);
});
},
[],
);
}

View File

@ -0,0 +1,27 @@
import { useRecoilCallback } from 'recoil';
import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '../states/boardColumnsState';
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
export function useResetCardSelection() {
return useRecoilCallback(
({ snapshot, set }) =>
() => {
const boardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
const cardIds = boardColumns.flatMap((boardColumn) =>
snapshot
.getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id))
.valueOrThrow(),
);
for (const cardId of cardIds) {
set(isCardSelectedFamilyState(cardId), false);
}
},
[],
);
}

View File

@ -0,0 +1,9 @@
import { useRecoilCallback } from 'recoil';
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
export function useSetCardSelected() {
return useRecoilCallback(({ set }) => (cardId: string, selected: boolean) => {
set(isCardSelectedFamilyState(cardId), selected);
});
}

View File

@ -1,7 +1,10 @@
import { createContext } from 'react';
import { FieldDefinition } from '@/ui/editable-field/types/FieldDefinition';
import { FieldMetadata } from '@/ui/editable-field/types/FieldMetadata';
import {
FieldMetadata,
FieldType,
} from '@/ui/editable-field/types/FieldMetadata';
export const FieldDefinitionContext = createContext<
FieldDefinition<FieldMetadata>
@ -9,6 +12,6 @@ export const FieldDefinitionContext = createContext<
id: '',
label: '',
icon: undefined,
type: '',
type: 'unknown' satisfies FieldType,
metadata: {} as FieldMetadata,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const isCardSelectedFamilyState = atomFamily<boolean, string>({
key: 'isCardSelectedFamilyState',
default: false,
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const selectedBoardCardIdsState = atom<string[]>({
key: 'selectedBoardCardIdsState',
default: [],
});

View File

@ -0,0 +1,22 @@
import { selector } from 'recoil';
import { boardCardIdsByColumnIdFamilyState } from './boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from './boardColumnsState';
import { isCardSelectedFamilyState } from './isCardSelectedFamilyState';
export const selectedCardIdsSelector = selector<string[]>({
key: 'selectedCardIdsSelector',
get: ({ get }) => {
const boardColumns = get(boardColumnsState);
const cardIds = boardColumns.flatMap((boardColumn) =>
get(boardCardIdsByColumnIdFamilyState(boardColumn.id)),
);
const selectedCardIds = cardIds.filter(
(cardId) => get(isCardSelectedFamilyState(cardId)) === true,
);
return selectedCardIds;
},
});

View File

@ -18,5 +18,5 @@ export const DropdownMenu = styled.div<{
overflow: hidden;
width: ${({ width }) => width ?? 160}px;
width: ${({ width }) => (width && width > 160 ? width : 160)}px;
`;

View File

@ -4,7 +4,6 @@ import { motion } from 'framer-motion';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useBindFieldHotkeyScope } from '../hooks/useBindFieldHotkeyScope';
import { useEditableField } from '../hooks/useEditableField';
import { EditableFieldDisplayMode } from './EditableFieldDisplayMode';
@ -74,7 +73,6 @@ type OwnProps = {
displayModeContentOnly?: boolean;
disableHoverEffect?: boolean;
displayModeContent: React.ReactNode;
parentHotkeyScope?: HotkeyScope;
customEditHotkeyScope?: HotkeyScope;
isDisplayModeContentEmpty?: boolean;
isDisplayModeFixHeight?: boolean;
@ -89,7 +87,6 @@ export function EditableField({
useEditButton,
editModeContent,
displayModeContent,
parentHotkeyScope,
customEditHotkeyScope,
disableHoverEffect,
isDisplayModeContentEmpty,
@ -100,11 +97,6 @@ export function EditableField({
}: OwnProps) {
const [isHovered, setIsHovered] = useState(false);
useBindFieldHotkeyScope({
customEditHotkeyScope,
parentHotkeyScope,
});
function handleContainerMouseEnter() {
setIsHovered(true);
}
@ -116,7 +108,7 @@ export function EditableField({
const { isFieldInEditMode, openEditableField } = useEditableField();
function handleDisplayModeClick() {
openEditableField();
openEditableField(customEditHotkeyScope);
}
const showEditButton = !isFieldInEditMode && isHovered && useEditButton;

View File

@ -1,64 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconMap } from '@/ui/icon';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
type OwnProps = {
company: Pick<Company, 'id' | 'address'>;
};
export function CompanyEditableFieldAddress({ company }: OwnProps) {
const [internalValue, setInternalValue] = useState(company.address);
const [updateCompany] = useUpdateOneCompanyMutation();
useEffect(() => {
setInternalValue(company.address);
}, [company.address]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
address: internalValue ?? '',
},
},
});
}
async function handleCancel() {
setInternalValue(company.address);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={<IconMap />}
editModeContent={
<TextInputEdit
placeholder={'Address'}
autoFocus
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue !== '' ? internalValue : 'No address'}
/>
</RecoilScope>
);
}

View File

@ -3,12 +3,18 @@ import { useContext } from 'react';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
import { GenericEditableDateField } from './GenericEditableDateField';
import { GenericEditableNumberField } from './GenericEditableNumberField';
import { GenericEditablePhoneField } from './GenericEditablePhoneField';
import { GenericEditableRelationField } from './GenericEditableRelationField';
import { GenericEditableTextField } from './GenericEditableTextField';
import { GenericEditableURLField } from './GenericEditableURLField';
import { ProbabilityEditableField } from './ProbabilityEditableField';
export function GenericEditableField() {
@ -22,9 +28,15 @@ export function GenericEditableField() {
return <GenericEditableNumberField />;
} else if (isFieldProbability(fieldDefinition)) {
return <ProbabilityEditableField />;
} else if (isFieldURL(fieldDefinition)) {
return <GenericEditableURLField />;
} else if (isFieldText(fieldDefinition)) {
return <GenericEditableTextField />;
} else if (isFieldPhone(fieldDefinition)) {
return <GenericEditablePhoneField />;
} else {
console.warn(
`Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableCell`,
`Unknown field metadata type: ${fieldDefinition.type} in GenericEditableField`,
);
return <></>;
}

View File

@ -69,6 +69,7 @@ export function GenericEditableNumberFieldEditMode() {
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue ? internalValue.toString() : ''}
onChange={(newValue: string) => {
handleChange(newValue);

View File

@ -0,0 +1,43 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { PhoneInputDisplay } from '@/ui/input/phone/components/PhoneInputDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldPhoneMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditablePhoneFieldEditMode } from './GenericEditablePhoneFieldEditMode';
export function GenericEditablePhoneField() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldPhoneMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
useEditButton
iconLabel={currentEditableFieldDefinition.icon}
editModeContent={<GenericEditablePhoneFieldEditMode />}
displayModeContent={<PhoneInputDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,72 @@
import { useContext, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldPhoneMetadata } from '../types/FieldMetadata';
export function GenericEditablePhoneFieldEditMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldPhoneMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const [internalValue, setInternalValue] = useState(fieldValue);
const updateField = useUpdateGenericEntityField();
const wrapperRef = useRef(null);
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
function handleSubmit() {
if (internalValue === fieldValue) return;
setFieldValue(internalValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
internalValue,
);
}
}
function onCancel() {
setFieldValue(fieldValue);
}
function handleChange(newValue: string) {
setInternalValue(newValue);
}
return (
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
</div>
);
}

View File

@ -34,7 +34,7 @@ export function GenericEditableRelationField() {
<RecoilScope SpecificContext={FieldContext}>
<RecoilScope>
<EditableField
useEditButton
useEditButton={currentEditableFieldDefinition.metadata.useEditButton}
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}

View File

@ -1,8 +1,11 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { CompanyChip } from '@/companies/components/CompanyChip';
import { PersonChip } from '@/people/components/PersonChip';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { UserChip } from '@/users/components/UserChip';
import { getLogoUrlFromDomainName } from '~/utils';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
@ -35,6 +38,28 @@ export function GenericEditableRelationFieldDisplayMode() {
/>
);
}
case Entity.User: {
return (
<UserChip
id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''}
pictureUrl={fieldValue?.avatarUrl ?? ''}
/>
);
}
case Entity.Company: {
return (
<CompanyChip
id={fieldValue?.id ?? ''}
name={fieldValue?.name ?? ''}
pictureUrl={
fieldValue?.domainName
? getLogoUrlFromDomainName(fieldValue.domainName)
: ''
}
/>
);
}
default:
console.warn(
`Unknown relation type: "${currentEditableFieldDefinition.metadata.relationType}"

View File

@ -2,10 +2,11 @@ import { useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { CompanyPicker } from '@/companies/components/CompanyPicker';
import { PeoplePicker } from '@/people/components/PeoplePicker';
import { ViewFieldRelationValue } from '@/ui/editable-field/types/ViewField';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { UserPicker } from '@/users/components/UserPicker';
import { useEditableField } from '../hooks/useEditableField';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
@ -13,7 +14,10 @@ import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitio
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldRelationMetadata } from '../types/FieldMetadata';
import {
FieldRelationMetadata,
FieldRelationValue,
} from '../types/FieldMetadata';
const RelationPickerContainer = styled.div`
left: 0px;
@ -28,7 +32,7 @@ function RelationPicker({
handleCancel,
}: {
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
fieldValue: ViewFieldRelationValue;
fieldValue: FieldRelationValue;
handleEntitySubmit: (newRelationId: EntityForSelect | null) => void;
handleCancel: () => void;
}) {
@ -36,7 +40,25 @@ function RelationPicker({
case Entity.Person: {
return (
<PeoplePicker
personId={fieldValue?.id ?? null}
personId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
case Entity.User: {
return (
<UserPicker
userId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
case Entity.Company: {
return (
<CompanyPicker
companyId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
@ -46,7 +68,7 @@ function RelationPicker({
console.warn(
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`,
);
return <> </>;
return <></>;
}
}

View File

@ -0,0 +1,41 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableTextFieldEditMode } from './GenericEditableTextFieldEditMode';
export function GenericEditableTextField() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
editModeContent={<GenericEditableTextFieldEditMode />}
displayModeContent={fieldValue}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,72 @@
import { useContext, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldTextMetadata } from '../types/FieldMetadata';
export function GenericEditableTextFieldEditMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldTextMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const [internalValue, setInternalValue] = useState(fieldValue);
const updateField = useUpdateGenericEntityField();
const wrapperRef = useRef(null);
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
function handleSubmit() {
if (internalValue === fieldValue) return;
setFieldValue(internalValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
internalValue,
);
}
}
function onCancel() {
setFieldValue(fieldValue);
}
function handleChange(newValue: string) {
setInternalValue(newValue);
}
return (
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { FieldDisplayURL } from './FieldDisplayURL';
import { GenericEditableURLFieldEditMode } from './GenericEditableURLFieldEditMode';
export function GenericEditableURLField() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
useEditButton
iconLabel={currentEditableFieldDefinition.icon}
editModeContent={<GenericEditableURLFieldEditMode />}
displayModeContent={<FieldDisplayURL URL={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,74 @@
import { useContext, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldURLMetadata } from '../types/FieldMetadata';
// This one is very similar to GenericEditableTextFieldEditMode
// We could probably merge them since FieldURLMetadata is basically a FieldTextMetadata
export function GenericEditableURLFieldEditMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldURLMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const [internalValue, setInternalValue] = useState(fieldValue);
const updateField = useUpdateGenericEntityField();
const wrapperRef = useRef(null);
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
function handleSubmit() {
if (internalValue === fieldValue) return;
setFieldValue(internalValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
internalValue,
);
}
}
function onCancel() {
setFieldValue(fieldValue);
}
function handleChange(newValue: string) {
setInternalValue(newValue);
}
return (
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
</div>
);
}

View File

@ -1,75 +0,0 @@
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { isSameHotkeyScope } from '@/ui/utilities/hotkey/utils/isSameHotkeyScope';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { getSnapshotScopedState } from '@/ui/utilities/recoil-scope/utils/getSnapshotScopedState';
import { getSnapshotState } from '@/ui/utilities/recoil-scope/utils/getSnapshotState';
import { customEditHotkeyScopeForFieldScopedState } from '../states/customEditHotkeyScopeForFieldScopedState';
import { FieldContext } from '../states/FieldContext';
import { parentHotkeyScopeForFieldScopedState } from '../states/parentHotkeyScopeForFieldScopedState';
export function useBindFieldHotkeyScope({
customEditHotkeyScope,
parentHotkeyScope,
}: {
customEditHotkeyScope?: HotkeyScope;
parentHotkeyScope?: HotkeyScope;
}) {
const [customEditHotkeyScopeForField, setCustomEditHotkeyScopeForField] =
useRecoilScopedState(
customEditHotkeyScopeForFieldScopedState,
FieldContext,
);
const fieldContextScopeId = useContextScopeId(FieldContext);
useEffect(() => {
if (
customEditHotkeyScope &&
!isSameHotkeyScope(customEditHotkeyScope, customEditHotkeyScopeForField)
) {
setCustomEditHotkeyScopeForField(customEditHotkeyScope);
}
}, [
customEditHotkeyScope,
customEditHotkeyScopeForField,
setCustomEditHotkeyScopeForField,
]);
const setParentHotkeyScopeForField = useRecoilCallback(
({ snapshot, set }) =>
(parentHotkeyScopeToSet: HotkeyScope | null | undefined) => {
const currentHotkeyScope = getSnapshotState({
snapshot,
state: currentHotkeyScopeState,
});
const parentHotkeyScopeForField = getSnapshotScopedState({
snapshot,
state: parentHotkeyScopeForFieldScopedState,
contextScopeId: fieldContextScopeId,
});
if (!parentHotkeyScopeToSet) {
set(
parentHotkeyScopeForFieldScopedState(fieldContextScopeId),
currentHotkeyScope,
);
} else if (
!isSameHotkeyScope(parentHotkeyScopeToSet, parentHotkeyScopeForField)
) {
setParentHotkeyScopeForField(parentHotkeyScopeToSet);
}
},
[fieldContextScopeId],
);
useEffect(() => {
setParentHotkeyScopeForField(parentHotkeyScope);
}, [parentHotkeyScope, setParentHotkeyScopeForField]);
}

View File

@ -1,10 +1,9 @@
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { customEditHotkeyScopeForFieldScopedState } from '../states/customEditHotkeyScopeForFieldScopedState';
import { FieldContext } from '../states/FieldContext';
import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedState';
import { parentHotkeyScopeForFieldScopedState } from '../states/parentHotkeyScopeForFieldScopedState';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
export function useEditableField() {
@ -13,39 +12,29 @@ export function useEditableField() {
FieldContext,
);
const [customEditHotkeyScopeForField] = useRecoilScopedState(
customEditHotkeyScopeForFieldScopedState,
FieldContext,
);
const [parentHotkeyScopeForField] = useRecoilScopedState(
parentHotkeyScopeForFieldScopedState,
FieldContext,
);
const setHotkeyScope = useSetHotkeyScope();
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
function closeEditableField() {
setIsFieldInEditMode(false);
if (parentHotkeyScopeForField) {
setHotkeyScope(
parentHotkeyScopeForField.scope,
parentHotkeyScopeForField.customScopes,
);
}
goBackToPreviousHotkeyScope();
}
function openEditableField() {
function openEditableField(customEditHotkeyScopeForField?: HotkeyScope) {
setIsFieldInEditMode(true);
if (customEditHotkeyScopeForField) {
setHotkeyScope(
setHotkeyScopeAndMemorizePreviousScope(
customEditHotkeyScopeForField.scope,
customEditHotkeyScopeForField.customScopes,
);
} else {
setHotkeyScope(EditableFieldHotkeyScope.EditableField);
setHotkeyScopeAndMemorizePreviousScope(
EditableFieldHotkeyScope.EditableField,
);
}
}

View File

@ -1,7 +1,5 @@
import { useContext } from 'react';
import { isFieldChip } from '@/ui/editable-field/types/guards/isFieldChip';
import { EditableFieldMutationContext } from '../states/EditableFieldMutationContext';
import { FieldDefinition } from '../types/FieldDefinition';
import {
@ -27,6 +25,7 @@ import {
FieldURLMetadata,
FieldURLValue,
} from '../types/FieldMetadata';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldChipValue } from '../types/guards/isFieldChipValue';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDateValue } from '../types/guards/isFieldDateValue';
@ -53,31 +52,30 @@ export function useUpdateGenericEntityField() {
const [updateEntity] = useUpdateEntityMutation();
return function updateEntityField<
MetadataType extends FieldMetadata,
ValueType extends MetadataType extends FieldDoubleTextMetadata
ValueType extends FieldMetadata extends FieldDoubleTextMetadata
? FieldDoubleTextValue
: MetadataType extends FieldTextMetadata
: FieldMetadata extends FieldTextMetadata
? FieldTextValue
: MetadataType extends FieldPhoneMetadata
: FieldMetadata extends FieldPhoneMetadata
? FieldPhoneValue
: MetadataType extends FieldURLMetadata
: FieldMetadata extends FieldURLMetadata
? FieldURLValue
: MetadataType extends FieldNumberMetadata
: FieldMetadata extends FieldNumberMetadata
? FieldNumberValue
: MetadataType extends FieldDateMetadata
: FieldMetadata extends FieldDateMetadata
? FieldDateValue
: MetadataType extends FieldChipMetadata
: FieldMetadata extends FieldChipMetadata
? FieldChipValue
: MetadataType extends FieldDoubleTextChipMetadata
: FieldMetadata extends FieldDoubleTextChipMetadata
? FieldDoubleTextChipValue
: MetadataType extends FieldRelationMetadata
: FieldMetadata extends FieldRelationMetadata
? FieldRelationValue
: MetadataType extends FieldProbabilityMetadata
: FieldMetadata extends FieldProbabilityMetadata
? FieldProbabilityValue
: unknown,
>(
currentEntityId: string,
field: FieldDefinition<MetadataType>,
field: FieldDefinition<FieldMetadata>,
newFieldValue: ValueType,
) {
const newFieldValueUnknown = newFieldValue as unknown;

View File

@ -1,58 +0,0 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
const StyledPropertyBoxItem = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
svg {
align-items: center;
display: flex;
height: 16px;
justify-content: center;
width: 16px;
}
`;
const StyledValueContainer = styled.div`
align-content: flex-start;
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex: 1 0 0;
flex-wrap: wrap;
`;
const StyledLabelAndIconContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export function PropertyBoxItem({
icon,
label,
value,
}: {
icon: ReactNode;
label?: string;
value: ReactNode;
}) {
return (
<StyledPropertyBoxItem>
<StyledLabelAndIconContainer>
<StyledIconContainer>{icon}</StyledIconContainer>
{label}
</StyledLabelAndIconContainer>
<StyledValueContainer>{value}</StyledValueContainer>
</StyledPropertyBoxItem>
);
}

View File

@ -1,8 +1,8 @@
import { createContext } from 'react';
import { FieldDefinition } from '../types/FieldDefinition';
import { ViewFieldMetadata } from '../types/ViewField';
import { FieldMetadata } from '../types/FieldMetadata';
export const EditableFieldDefinitionContext = createContext<
FieldDefinition<ViewFieldMetadata>
>({} as FieldDefinition<ViewFieldMetadata>);
FieldDefinition<FieldMetadata>
>({} as FieldDefinition<FieldMetadata>);

View File

@ -1,9 +1,9 @@
import { FieldMetadata } from './FieldMetadata';
import { FieldMetadata, FieldType } from './FieldMetadata';
export type FieldDefinition<T extends FieldMetadata | unknown> = {
id: string;
label: string;
icon?: JSX.Element;
type: string;
type: FieldType;
metadata: T;
};

View File

@ -2,6 +2,7 @@ import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelec
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
export type FieldType =
| 'unknown'
| 'text'
| 'relation'
| 'chip'
@ -14,41 +15,36 @@ export type FieldType =
| 'probability';
export type FieldTextMetadata = {
type: 'text';
placeHolder: string;
fieldName: string;
};
export type FieldPhoneMetadata = {
type: 'phone';
placeHolder: string;
fieldName: string;
};
export type FieldURLMetadata = {
type: 'url';
placeHolder: string;
fieldName: string;
};
export type FieldDateMetadata = {
type: 'date';
fieldName: string;
};
export type FieldNumberMetadata = {
type: 'number';
fieldName: string;
placeHolder: string;
};
export type FieldRelationMetadata = {
type: 'relation';
relationType: Entity;
fieldName: string;
useEditButton?: boolean;
};
export type FieldChipMetadata = {
type: 'chip';
relationType: Entity;
contentFieldName: string;
urlFieldName: string;
@ -56,7 +52,6 @@ export type FieldChipMetadata = {
};
export type FieldDoubleTextMetadata = {
type: 'double-text';
firstValueFieldName: string;
firstValuePlaceholder: string;
secondValueFieldName: string;
@ -64,7 +59,6 @@ export type FieldDoubleTextMetadata = {
};
export type FieldDoubleTextChipMetadata = {
type: 'double-text-chip';
firstValueFieldName: string;
firstValuePlaceholder: string;
secondValueFieldName: string;
@ -74,11 +68,10 @@ export type FieldDoubleTextChipMetadata = {
};
export type FieldProbabilityMetadata = {
type: 'probability';
fieldName: string;
};
export type FieldMetadata = { type: FieldType } & (
export type FieldMetadata =
| FieldTextMetadata
| FieldRelationMetadata
| FieldChipMetadata
@ -88,8 +81,7 @@ export type FieldMetadata = { type: FieldType } & (
| FieldURLMetadata
| FieldNumberMetadata
| FieldDateMetadata
| FieldProbabilityMetadata
);
| FieldProbabilityMetadata;
export type FieldTextValue = string;

View File

@ -46,6 +46,7 @@ export type ViewFieldRelationMetadata = {
type: 'relation';
relationType: Entity;
fieldName: string;
useEditButton?: boolean;
};
export type ViewFieldChipMetadata = {

View File

@ -1,64 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { OverflowingTextWithTooltip } from '../../../tooltip/OverflowingTextWithTooltip';
type OwnProps = {
icon?: React.ReactNode;
placeholder?: string;
value: string | null | undefined;
onSubmit?: (newValue: string) => void;
};
export function TextEditableField({
icon,
placeholder,
value,
onSubmit,
}: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
if (!internalValue) return;
onSubmit?.(internalValue);
}
async function handleCancel() {
setInternalValue(value);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={icon}
editModeContent={
<TextInputEdit
placeholder={placeholder ?? ''}
autoFocus
value={internalValue ?? ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={<OverflowingTextWithTooltip text={internalValue} />}
isDisplayModeContentEmpty={!(internalValue !== '')}
/>
</RecoilScope>
);
}

View File

@ -1,31 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconUser } from '@tabler/icons-react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { TextEditableField } from '../TextEditableField';
const meta: Meta<typeof TextEditableField> = {
title: 'UI/EditableField/TextEditableField',
component: TextEditableField,
decorators: [ComponentDecorator],
argTypes: {
icon: {
type: 'boolean',
mapping: {
true: <IconUser />,
false: undefined,
},
},
},
args: {
value: 'John Doe',
icon: true,
placeholder: 'Name',
},
};
export default meta;
type Story = StoryObj<typeof TextEditableField>;
export const Default: Story = {};

View File

@ -28,7 +28,7 @@ export function DropdownMenuContainer({
});
return (
<StyledDropdownMenuContainer>
<StyledDropdownMenuContainer data-select-disable>
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu>
</StyledDropdownMenuContainer>
);

View File

@ -1,4 +1,5 @@
import { CommentableType, PipelineProgressableType } from '~/generated/graphql';
import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity';
import { PipelineProgressableType } from '~/generated/graphql';
export enum Entity {
Company = 'Company',
@ -7,6 +8,6 @@ export enum Entity {
}
export type EntityTypeForSelect =
| CommentableType
| ActivityTargetableEntityType
| PipelineProgressableType
| Entity;

View File

@ -30,16 +30,25 @@ export function SocialLink({ children, href, onClick, type }: OwnProps) {
let displayValue = children;
if (type === 'linkedin') {
const splitUrl = href.split('/');
const splitName = splitUrl[4].split('-');
displayValue = splitName[2]
? `${splitName[0]}-${splitName[1]}`
: splitName[0];
const matches = href.match(
/(?:https?:\/\/)?(?:www.)?linkedin.com\/(?:in|company)\/([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
);
if (matches && matches[1]) {
displayValue = matches[1];
} else {
displayValue = 'LinkedIn';
}
}
if (type === 'twitter') {
const splitUrl = href.split('/');
displayValue = `@${splitUrl[3]}`;
const matches = href.match(
/(?:https?:\/\/)?(?:www.)?twitter.com\/([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
);
if (matches && matches[1]) {
displayValue = `@${matches[1]}`;
} else {
displayValue = '@twitter';
}
}
return (

View File

@ -1,16 +1,19 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
import { tableRowIdsState } from '../states/tableRowIdsState';
import { EntityUpdateMutationContext } from '../states/EntityUpdateMutationHookContext';
import { TableHeader } from '../table-header/components/TableHeader';
import { EntityTableBody } from './EntityTableBody';
@ -25,6 +28,8 @@ const StyledTable = styled.table`
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
table-layout: fixed;
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
th {
border: 1px solid ${({ theme }) => theme.border.color.light};
border-collapse: collapse;
@ -90,28 +95,24 @@ type OwnProps<SortField> = {
viewName: string;
viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onRowSelectionChange?: (rowSelection: string[]) => void;
useUpdateEntityMutation: any;
updateEntityMutation: any;
};
export function EntityTable<SortField>({
viewName,
viewIcon,
availableSorts,
onColumnsChange,
onSortsUpdate,
useUpdateEntityMutation,
updateEntityMutation,
}: OwnProps<SortField>) {
const tableBodyRef = useRef<HTMLDivElement>(null);
const rowIds = useRecoilValue(tableRowIdsState);
const setRowSelectedState = useSetRowSelectedState();
function resetSelections() {
for (const rowId of rowIds) {
setRowSelectedState(rowId, false);
}
}
const resetTableRowSelection = useResetTableRowSelection();
useMapKeyboardToSoftFocus();
@ -125,28 +126,29 @@ export function EntityTable<SortField>({
});
return (
<EntityUpdateMutationHookContext.Provider value={useUpdateEntityMutation}>
<EntityUpdateMutationContext.Provider value={updateEntityMutation}>
<StyledTableWithHeader>
<StyledTableContainer ref={tableBodyRef}>
<TableHeader
viewName={viewName}
viewIcon={viewIcon}
availableSorts={availableSorts}
onColumnsChange={onColumnsChange}
onSortsUpdate={onSortsUpdate}
/>
<StyledTableWrapper>
<StyledTable>
<EntityTableHeader />
<EntityTableHeader onColumnsChange={onColumnsChange} />
<EntityTableBody />
</StyledTable>
</StyledTableWrapper>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={resetSelections}
onDragSelectionStart={resetTableRowSelection}
onDragSelectionChange={setRowSelectedState}
/>
</StyledTableContainer>
</StyledTableWithHeader>
</EntityUpdateMutationHookContext.Provider>
</EntityUpdateMutationContext.Provider>
);
}

View File

@ -1,6 +1,7 @@
import { cloneElement, ComponentProps, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
@ -9,32 +10,27 @@ import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMen
import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
import { hiddenTableColumnsState } from '../states/tableColumnsState';
const StyledColumnMenu = styled(DropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
type EntityTableColumnMenuProps = {
onAddViewField: (
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => void;
onAddColumn: (columnId: string) => void;
onClickOutside?: () => void;
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
} & ComponentProps<'div'>;
export const EntityTableColumnMenu = ({
onAddViewField,
onAddColumn,
onClickOutside = () => undefined,
viewFieldDefinitions,
...props
}: EntityTableColumnMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme();
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
useListenClickOutside({
refs: [ref],
callback: onClickOutside,
@ -43,21 +39,21 @@ export const EntityTableColumnMenu = ({
return (
<StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer>
{viewFieldDefinitions.map((viewFieldDefinition) => (
{hiddenColumns.map((column) => (
<DropdownMenuItem
key={viewFieldDefinition.id}
key={column.id}
actions={
<IconButton
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddViewField(viewFieldDefinition)}
onClick={() => onAddColumn(column.id)}
/>
}
>
{viewFieldDefinition.columnIcon &&
cloneElement(viewFieldDefinition.columnIcon, {
{column.columnIcon &&
cloneElement(column.columnIcon, {
size: theme.icon.size.md,
})}
{viewFieldDefinition.columnLabel}
{column.columnLabel}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>

View File

@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
@ -11,21 +10,14 @@ import type {
} from '@/ui/editable-field/types/ViewField';
import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
useCreateViewFieldMutation,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { toViewFieldInput } from '../hooks/useLoadViewFields';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import {
addableViewFieldDefinitionsState,
columnWidthByViewFieldIdState,
viewFieldsState,
visibleViewFieldsState,
} from '../states/viewFieldsState';
hiddenTableColumnsState,
tableColumnsByIdState,
tableColumnsState,
visibleTableColumnsState,
} from '../states/tableColumnsState';
import { ColumnHead } from './ColumnHead';
import { EntityTableColumnMenu } from './EntityTableColumnMenu';
@ -86,17 +78,18 @@ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
export function EntityTableHeader() {
export type EntityTableHeaderProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
};
export function EntityTableHeader({ onColumnsChange }: EntityTableHeaderProps) {
const theme = useTheme();
const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState);
const currentViewId = useRecoilValue(currentViewIdState);
const viewFields = useRecoilValue(visibleViewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue(
addableViewFieldDefinitionsState,
);
const [columns, setColumns] = useRecoilState(tableColumnsState);
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
const columnsById = useRecoilValue(tableColumnsByIdState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const visibleColumns = useRecoilValue(visibleTableColumnsState);
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
@ -104,9 +97,6 @@ export function EntityTableHeader() {
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const [createViewFieldMutation] = useCreateViewFieldMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const handleResizeHandlerStart = useCallback((positionX: number) => {
setInitialPointerPositionX(positionX);
}, []);
@ -126,37 +116,28 @@ export function EntityTableHeader() {
const nextWidth = Math.round(
Math.max(
columnWidths[resizedFieldId] +
columnsById[resizedFieldId].columnSize +
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
COLUMN_MIN_WIDTH,
),
);
if (nextWidth !== columnWidths[resizedFieldId]) {
// Optimistic update to avoid "bouncing width" visual effect on resize.
setViewFieldsState((previousState) => ({
...previousState,
viewFields: previousState.viewFields.map((viewField) =>
viewField.id === resizedFieldId
? { ...viewField, columnSize: nextWidth }
: viewField,
),
}));
if (nextWidth !== columnsById[resizedFieldId].columnSize) {
const nextColumns = columns.map((column) =>
column.id === resizedFieldId
? { ...column, columnSize: nextWidth }
: column,
);
updateViewFieldMutation({
variables: {
data: { sizeInPx: nextWidth },
where: { id: resizedFieldId },
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
}
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldId(null);
},
[resizedFieldId, columnWidths, setResizedFieldId],
[resizedFieldId, columnsById, setResizedFieldId],
);
useTrackPointer({
@ -170,26 +151,18 @@ export function EntityTableHeader() {
setIsColumnMenuOpen((previousValue) => !previousValue);
}, []);
const handleAddViewField = useCallback(
(viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>) => {
const handleAddColumn = useCallback(
(columnId: string) => {
setIsColumnMenuOpen(false);
if (!objectName) return;
const nextColumns = columns.map((column) =>
column.id === columnId ? { ...column, isVisible: true } : column,
);
createViewFieldMutation({
variables: {
data: {
...toViewFieldInput(objectName, {
...viewFieldDefinition,
columnOrder: viewFields.length + 1,
}),
view: { connect: { id: currentViewId } },
},
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
},
[createViewFieldMutation, currentViewId, objectName, viewFields.length],
[columns, onColumnsChange, setColumns],
);
return (
@ -205,31 +178,31 @@ export function EntityTableHeader() {
<SelectAllCheckbox />
</th>
{viewFields.map((viewField) => (
{visibleColumns.map((column) => (
<StyledColumnHeaderCell
key={viewField.id}
isResizing={resizedFieldId === viewField.id}
key={column.id}
isResizing={resizedFieldId === column.id}
columnWidth={Math.max(
columnWidths[viewField.id] +
(resizedFieldId === viewField.id ? offset : 0),
columnsById[column.id].columnSize +
(resizedFieldId === column.id ? offset : 0),
COLUMN_MIN_WIDTH,
)}
>
<ColumnHead
viewName={viewField.columnLabel}
viewIcon={viewField.columnIcon}
viewName={column.columnLabel}
viewIcon={column.columnIcon}
/>
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldId(viewField.id);
setResizedFieldId(column.id);
}}
/>
</StyledColumnHeaderCell>
))}
<th>
{addableViewFieldDefinitions.length > 0 && (
{hiddenColumns.length > 0 && (
<StyledAddIconButtonWrapper>
<StyledAddIconButton
size="large"
@ -238,9 +211,8 @@ export function EntityTableHeader() {
/>
{isColumnMenuOpen && (
<StyledEntityTableColumnMenu
onAddViewField={handleAddViewField}
onAddColumn={handleAddColumn}
onClickOutside={toggleColumnMenu}
viewFieldDefinitions={addableViewFieldDefinitions}
/>
)}
</StyledAddIconButtonWrapper>

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { visibleTableColumnsState } from '../states/tableColumnsState';
import { ViewFieldContext } from '../states/ViewFieldContext';
import { visibleViewFieldsState } from '../states/viewFieldsState';
import { CheckboxCell } from './CheckboxCell';
import { EntityTableCell } from './EntityTableCell';
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
`;
export function EntityTableRow({ rowId }: { rowId: string }) {
const viewFields = useRecoilValue(visibleViewFieldsState);
const columns = useRecoilValue(visibleTableColumnsState);
return (
<StyledRow
@ -24,9 +24,9 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
<td>
<CheckboxCell />
</td>
{viewFields.map((viewField, columnIndex) => {
{columns.map((column, columnIndex) => {
return (
<ViewFieldContext.Provider value={viewField} key={viewField.id}>
<ViewFieldContext.Provider value={column} key={column.id}>
<EntityTableCell cellIndex={columnIndex} />
</ViewFieldContext.Provider>
);

View File

@ -1,34 +1,21 @@
import { defaultOrderBy } from '@/people/queries';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import { useLoadViewFields } from '../hooks/useLoadViewFields';
export function GenericEntityTableData({
objectName,
useGetRequest,
getRequestResultKey,
orderBy = defaultOrderBy,
whereFilters,
viewFieldDefinitions,
filterDefinitionArray,
}: {
objectName: 'company' | 'person';
useGetRequest: any;
getRequestResultKey: string;
orderBy?: any;
whereFilters?: any;
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
filterDefinitionArray: FilterDefinition[];
}) {
const setEntityTableData = useSetEntityTableData();
useLoadViewFields({ objectName, viewFieldDefinitions });
useGetRequest({
variables: { orderBy, where: whereFilters },
onCompleted: (data: any) => {

View File

@ -1,19 +0,0 @@
import styled from '@emotion/styled';
export const HoverableMenuItem = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 100%;
position: relative;
transition: background 0.1s ease;
user-select: none;
width: 100%;
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;

View File

@ -1,5 +1,6 @@
import { useContext } from 'react';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@ -17,17 +18,20 @@ export function useEditableCell() {
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
function closeEditableCell() {
setDragSelectionStartEnabled(true);
closeCurrentCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
}
function openEditableCell() {
setDragSelectionStartEnabled(false);
setCurrentCellInEditMode();
if (customCellHotkeyScope) {

View File

@ -2,11 +2,6 @@ import { useContext } from 'react';
import { RowIdContext } from '../states/RowIdContext';
export type TableDimensions = {
numberOfColumns: number;
numberOfRows: number;
};
export function useCurrentRowEntityId() {
const currentEntityId = useContext(RowIdContext);

View File

@ -1,35 +0,0 @@
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { tableRowIdsState } from '../states/tableRowIdsState';
import { useResetTableRowSelection } from './useResetTableRowSelection';
export type TableDimensions = {
numberOfColumns: number;
numberOfRows: number;
};
export function useInitializeEntityTable({
numberOfColumns,
}: {
numberOfColumns: number;
}) {
const resetTableRowSelection = useResetTableRowSelection();
const tableRowIds = useRecoilValue(tableRowIdsState);
useEffect(() => {
resetTableRowSelection();
}, [resetTableRowSelection]);
const [, setTableDimensions] = useRecoilState(entityTableDimensionsState);
useEffect(() => {
setTableDimensions({
numberOfColumns,
numberOfRows: tableRowIds?.length,
});
}, [tableRowIds, numberOfColumns, setTableDimensions]);
}

View File

@ -1,22 +0,0 @@
import { useEffect } from 'react';
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '../states/TableContext';
export function useInitializeEntityTableFilters({
availableFilters,
}: {
availableFilters: FilterDefinition[];
}) {
const [, setAvailableFilters] = useRecoilScopedState(
availableFiltersScopedState,
TableContext,
);
useEffect(() => {
setAvailableFilters(availableFilters);
}, [setAvailableFilters, availableFilters]);
}

View File

@ -1,96 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
} from '~/generated/graphql';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState';
const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
type: 'text',
placeHolder: '',
fieldName: '',
};
export const toViewFieldInput = (
objectName: 'company' | 'person',
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: viewFieldDefinition.isVisible ?? true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
});
export const useLoadViewFields = ({
objectName,
viewFieldDefinitions,
}: {
objectName: 'company' | 'person';
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
}) => {
const currentViewId = useRecoilValue(currentViewIdState);
const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState,
);
const setViewFieldsState = useSetRecoilState(viewFieldsState);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
useGetViewFieldsQuery({
variables: {
orderBy: { index: SortOrder.Asc },
where: {
objectName: { equals: objectName },
viewId: { equals: currentViewId ?? null },
},
},
onCompleted: (data) => {
if (data.viewFields.length) {
const viewFields = data.viewFields.map<
ViewFieldDefinition<ViewFieldMetadata>
>((viewField) => ({
...(viewFieldDefinitions.find(
({ columnLabel }) => viewField.fieldName === columnLabel,
) || { metadata: DEFAULT_VIEW_FIELD_METADATA }),
id: viewField.id,
columnLabel: viewField.fieldName,
columnOrder: viewField.index,
columnSize: viewField.sizeInPx,
isVisible: viewField.isVisible,
}));
setViewFieldsState({ objectName, viewFields });
setEntityTableDimensions((prevState) => ({
...prevState,
numberOfColumns: data.viewFields.length,
}));
return;
}
// Populate if empty
createViewFieldsMutation({
variables: {
data: viewFieldDefinitions.map((viewFieldDefinition) => ({
...toViewFieldInput(objectName, viewFieldDefinition),
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
});
};

View File

@ -1,8 +1,8 @@
import { useRecoilCallback } from 'recoil';
import { numberOfTableColumnsSelectorState } from '../states/numberOfTableColumnsSelectorState';
import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState';
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
import { softFocusPositionState } from '../states/softFocusPositionState';
import { numberOfTableColumnsState } from '../states/tableColumnsState';
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
@ -39,7 +39,7 @@ export function useMoveSoftFocus() {
.valueOrThrow();
const numberOfTableRows = snapshot
.getLoadable(numberOfTableRowsSelectorState)
.getLoadable(numberOfTableRowsState)
.valueOrThrow();
let newRowNumber = softFocusPosition.row + 1;
@ -64,11 +64,11 @@ export function useMoveSoftFocus() {
.valueOrThrow();
const numberOfTableColumns = snapshot
.getLoadable(numberOfTableColumnsSelectorState)
.getLoadable(numberOfTableColumnsState)
.valueOrThrow();
const numberOfTableRows = snapshot
.getLoadable(numberOfTableRowsSelectorState)
.getLoadable(numberOfTableRowsState)
.valueOrThrow();
const currentColumnNumber = softFocusPosition.column;
@ -112,7 +112,7 @@ export function useMoveSoftFocus() {
.valueOrThrow();
const numberOfTableColumns = snapshot
.getLoadable(numberOfTableColumnsSelectorState)
.getLoadable(numberOfTableColumnsState)
.valueOrThrow();
const currentColumnNumber = softFocusPosition.column;

View File

@ -3,13 +3,14 @@ import { useRecoilCallback } from 'recoil';
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState';
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
import { TableContext } from '@/ui/table/states/TableContext';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState';
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
export function useSetEntityTableData() {
const resetTableRowSelection = useResetTableRowSelection();
@ -43,10 +44,7 @@ export function useSetEntityTableData() {
resetTableRowSelection();
set(entityTableDimensionsState, (prevState) => ({
...prevState,
numberOfRows: entityIds.length,
}));
set(numberOfTableRowsState, entityIds.length);
set(availableFiltersScopedState(tableContextScopeId), filters);

View File

@ -0,0 +1,19 @@
import { useRecoilCallback } from 'recoil';
import { tableRowIdsState } from '../states/tableRowIdsState';
export function useSetTableRowIds() {
return useRecoilCallback(
({ set, snapshot }) =>
(rowIds: string[]) => {
const currentRowIds = snapshot
.getLoadable(tableRowIdsState)
.valueOrThrow();
if (JSON.stringify(rowIds) !== JSON.stringify(currentRowIds)) {
set(tableRowIdsState, rowIds);
}
},
[],
);
}

View File

@ -17,7 +17,6 @@ import { isViewFieldText } from '@/ui/editable-field/types/guards/isViewFieldTex
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 { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { isViewFieldChipValue } from '../../editable-field/types/guards/isViewFieldChipValue';
import {
@ -42,11 +41,10 @@ import {
ViewFieldURLMetadata,
ViewFieldURLValue,
} from '../../editable-field/types/ViewField';
import { EntityUpdateMutationContext } from '../states/EntityUpdateMutationHookContext';
export function useUpdateEntityField() {
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
const [updateEntity] = useUpdateEntityMutation();
const updateEntity = useContext(EntityUpdateMutationContext);
return function updatePeopleField<
MetadataType extends ViewFieldMetadata,

View File

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

View File

@ -0,0 +1,17 @@
import { useRecoilCallback } from 'recoil';
import { tableRowIdsState } from '../states/tableRowIdsState';
export function useUpsertTableRowId() {
return useRecoilCallback(
({ set, snapshot }) =>
(rowId: string) => {
const currentRowIds = snapshot
.getLoadable(tableRowIdsState)
.valueOrThrow();
set(tableRowIdsState, Array.from(new Set([rowId, ...currentRowIds])));
},
[],
);
}

View File

@ -0,0 +1,136 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
import {
hiddenTableColumnsState,
tableColumnsState,
visibleTableColumnsState,
} from '@/ui/table/states/tableColumnsState';
import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
HotkeyScope: FiltersHotkeyScope;
};
enum Option {
Properties = 'Properties',
}
export const TableOptionsDropdownButton = ({
onColumnsChange,
HotkeyScope,
}: TableOptionsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined,
);
const [columns, setColumns] = useRecoilState(tableColumnsState);
const visibleColumns = useRecoilValue(visibleTableColumnsState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const handleColumnVisibilityChange = useCallback(
(columnId: string, nextIsVisible: boolean) => {
const nextColumns = columns.map((column) =>
column.id === columnId
? { ...column, isVisible: nextIsVisible }
: column,
);
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
},
[columns, onColumnsChange, setColumns],
);
const renderFieldActions = useCallback(
(column: ViewFieldDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column
!column.isVisible || visibleColumns.length > 1 ? (
<IconButton
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleColumnVisibilityChange(column.id, !column.isVisible)
}
/>
) : undefined,
[handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
);
const resetSelectedOption = useCallback(() => {
setSelectedOption(undefined);
}, []);
return (
<DropdownButton
label="Options"
isActive={false}
isUnfolded={isUnfolded}
onIsUnfoldedChange={setIsUnfolded}
HotkeyScope={HotkeyScope}
>
{!selectedOption && (
<>
<DropdownMenuHeader>View settings</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<DropdownMenuItem
onClick={() => setSelectedOption(Option.Properties)}
>
<IconTag size={theme.icon.size.md} />
Properties
</DropdownMenuItem>
</DropdownMenuItemsContainer>
</>
)}
{selectedOption === Option.Properties && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
onClick={resetSelectedOption}
>
Properties
</DropdownMenuHeader>
<DropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
title="Visible"
columns={visibleColumns}
/>
{hiddenColumns.length > 0 && (
<>
<DropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
title="Hidden"
columns={hiddenColumns}
/>
</>
)}
</>
)}
</DropdownButton>
);
};

View File

@ -0,0 +1,46 @@
import { cloneElement } from 'react';
import { useTheme } from '@emotion/react';
import {
DropdownMenuItem,
DropdownMenuItemProps,
} from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSubheader } from '@/ui/dropdown/components/DropdownMenuSubheader';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
type TableOptionsDropdownSectionProps = {
renderActions: (
column: ViewFieldDefinition<ViewFieldMetadata>,
) => DropdownMenuItemProps['actions'];
title: string;
columns: ViewFieldDefinition<ViewFieldMetadata>[];
};
export const TableOptionsDropdownSection = ({
renderActions,
title,
columns,
}: TableOptionsDropdownSectionProps) => {
const theme = useTheme();
return (
<>
<DropdownMenuSubheader>{title}</DropdownMenuSubheader>
<DropdownMenuItemsContainer>
{columns.map((column) => (
<DropdownMenuItem key={column.id} actions={renderActions(column)}>
{column.columnIcon &&
cloneElement(column.columnIcon, {
size: theme.icon.size.md,
})}
{column.columnLabel}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,3 +1,3 @@
import { createContext } from 'react';
export const EntityUpdateMutationHookContext = createContext<any | null>(null);
export const EntityUpdateMutationContext = createContext<any | null>(null);

View File

@ -2,13 +2,13 @@ import { selector } from 'recoil';
import { AllRowsSelectedStatus } from '../types/AllRowSelectedStatus';
import { numberOfTableRowsSelectorState } from './numberOfTableRowsSelectorState';
import { numberOfTableRowsState } from './numberOfTableRowsState';
import { selectedRowIdsSelector } from './selectedRowIdsSelector';
export const allRowsSelectedStatusSelector = selector<AllRowsSelectedStatus>({
key: 'allRowsSelectedStatusSelector',
get: ({ get }) => {
const numberOfRows = get(numberOfTableRowsSelectorState);
const numberOfRows = get(numberOfTableRowsState);
const selectedRowIds = get(selectedRowIdsSelector);

View File

@ -1,11 +0,0 @@
import { atom } from 'recoil';
import { TableDimensions } from '../hooks/useInitializeEntityTable';
export const entityTableDimensionsState = atom<TableDimensions>({
key: 'entityTableDimensionsState',
default: {
numberOfRows: 0,
numberOfColumns: 0,
},
});

View File

@ -1,12 +0,0 @@
import { selector } from 'recoil';
import { entityTableDimensionsState } from './entityTableDimensionsState';
export const numberOfTableColumnsSelectorState = selector<number>({
key: 'numberOfTableColumnsState',
get: ({ get }) => {
const { numberOfColumns } = get(entityTableDimensionsState);
return numberOfColumns;
},
});

View File

@ -1,12 +0,0 @@
import { selector } from 'recoil';
import { entityTableDimensionsState } from './entityTableDimensionsState';
export const numberOfTableRowsSelectorState = selector<number>({
key: 'numberOfTableRowsState',
get: ({ get }) => {
const { numberOfRows } = get(entityTableDimensionsState);
return numberOfRows;
},
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const numberOfTableRowsState = atom<number>({
key: 'numberOfTableRowsState',
default: 0,
});

View File

@ -0,0 +1,37 @@
import { atom, selector } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
export const tableColumnsState = atom<ViewFieldDefinition<ViewFieldMetadata>[]>(
{
key: 'tableColumnsState',
default: [],
},
);
export const tableColumnsByIdState = selector({
key: 'tableColumnsByIdState',
get: ({ get }) =>
get(tableColumnsState).reduce<
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
>((result, column) => ({ ...result, [column.id]: column }), {}),
});
export const numberOfTableColumnsState = selector<number>({
key: 'numberOfTableColumnsState',
get: ({ get }) => get(tableColumnsState).length,
});
export const visibleTableColumnsState = selector({
key: 'visibleTableColumnsState',
get: ({ get }) => get(tableColumnsState).filter((column) => column.isVisible),
});
export const hiddenTableColumnsState = selector({
key: 'hiddenTableColumnsState',
get: ({ get }) =>
get(tableColumnsState).filter((column) => !column.isVisible),
});

View File

@ -1,61 +0,0 @@
import { atom, selector } from 'recoil';
import { companyViewFields } from '@/companies/constants/companyViewFields';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
export const viewFieldsState = atom<{
objectName: 'company' | 'person' | '';
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
}>({
key: 'viewFieldsState',
default: { objectName: '', viewFields: [] },
});
export const columnWidthByViewFieldIdState = selector({
key: 'columnWidthByViewFieldIdState',
get: ({ get }) =>
get(viewFieldsState).viewFields.reduce<Record<string, number>>(
(result, viewField) => ({
...result,
[viewField.id]: viewField.columnSize,
}),
{},
),
});
export const addableViewFieldDefinitionsState = selector({
key: 'addableViewFieldDefinitionsState',
get: ({ get }) => {
const { objectName, viewFields } = get(viewFieldsState);
if (!objectName) return [];
const existingColumnLabels = viewFields.map(
(viewField) => viewField.columnLabel,
);
const viewFieldDefinitions =
objectName === 'company' ? companyViewFields : peopleViewFields;
return viewFieldDefinitions.filter(
(viewFieldDefinition) =>
!existingColumnLabels.includes(viewFieldDefinition.columnLabel),
);
},
});
export const visibleViewFieldsState = selector({
key: 'visibleViewFieldsState',
get: ({ get }) =>
get(viewFieldsState).viewFields.filter((viewField) => viewField.isVisible),
});
export const hiddenViewFieldsState = selector({
key: 'hiddenViewFieldsState',
get: ({ get }) =>
get(viewFieldsState).viewFields.filter((viewField) => !viewField.isVisible),
});

View File

@ -1,15 +1,19 @@
import { ReactNode, useCallback } from 'react';
import styled from '@emotion/styled';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
import { sortScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableOptionsDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { OptionsDropdownButton } from '@/views/components/OptionsDropdownButton';
import { TableContext } from '../../states/TableContext';
@ -17,6 +21,7 @@ type OwnProps<SortField> = {
viewName: string;
viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
};
@ -34,6 +39,7 @@ export function TableHeader<SortField>({
viewName,
viewIcon,
availableSorts,
onColumnsChange,
onSortsUpdate,
}: OwnProps<SortField>) {
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
@ -79,7 +85,8 @@ export function TableHeader<SortField>({
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/>
<OptionsDropdownButton
<TableOptionsDropdownButton
onColumnsChange={onColumnsChange}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/>
</>

View File

@ -47,11 +47,6 @@ export function OverflowingTextWithTooltip({
event.preventDefault();
}
function handleTooltipMouseUp(event: React.MouseEvent<HTMLDivElement>) {
event.stopPropagation();
event.preventDefault();
}
return (
<>
<StyledOverflowingText
@ -64,12 +59,11 @@ export function OverflowingTextWithTooltip({
</StyledOverflowingText>
{isTitleOverflowing &&
createPortal(
<div onMouseUp={handleTooltipMouseUp} onClick={handleTooltipClick}>
<div onClick={handleTooltipClick}>
<AppTooltip
anchorSelect={`#${textElementId}`}
content={text ?? ''}
clickable
delayHide={100}
delayHide={0}
offset={5}
noArrow
place="bottom"

View File

@ -4,6 +4,8 @@ import {
useSelectionContainer,
} from '@air/react-drag-to-select';
import { useDragSelect } from '../hooks/useDragSelect';
type OwnProps = {
dragSelectable: RefObject<HTMLElement>;
onDragSelectionChange: (id: string, selected: boolean) => void;
@ -15,8 +17,12 @@ export function DragSelect({
onDragSelectionChange,
onDragSelectionStart,
}: OwnProps) {
const { isDragSelectionStartEnabled } = useDragSelect();
const { DragSelection } = useSelectionContainer({
shouldStartSelecting: (target) => {
if (!isDragSelectionStartEnabled()) {
return false;
}
if (target instanceof HTMLElement) {
let el = target;
while (el.parentElement && !el.dataset.selectDisable) {

View File

@ -0,0 +1,28 @@
import { useRecoilCallback, useRecoilState } from 'recoil';
import { isDragSelectionStartEnabledState } from '../states/internal/isDragSelectionStartEnabledState';
export function useDragSelect() {
const [, setIsDragSelectionStartEnabledInternal] = useRecoilState(
isDragSelectionStartEnabledState,
);
function setDragSelectionStartEnabled(isEnabled: boolean) {
setIsDragSelectionStartEnabledInternal(isEnabled);
}
const isDragSelectionStartEnabled = useRecoilCallback(
({ snapshot }) =>
() => {
return snapshot
.getLoadable(isDragSelectionStartEnabledState)
.getValue();
},
[],
);
return {
isDragSelectionStartEnabled,
setDragSelectionStartEnabled,
};
}

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isDragSelectionStartEnabledState = atom({
key: 'drag-select/isDragSelectionStartEnabledState',
default: true,
});