feat: persist board card fields (#1566)

Closes #1538
This commit is contained in:
Thaïs
2023-09-15 00:06:15 +02:00
committed by GitHub
parent 6462505a86
commit 2461a387ce
27 changed files with 541 additions and 342 deletions

View File

@ -1,46 +1,101 @@
import type { ComponentProps } from 'react';
import { useContext } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewBar, type ViewBarProps } from '@/ui/view-bar/components/ViewBar';
import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState';
import { canPersistBoardCardFieldsScopedFamilySelector } from '../states/selectors/canPersistBoardCardFieldsScopedFamilySelector';
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
import { BoardOptionsDropdown } from './BoardOptionsDropdown';
export type BoardHeaderProps = ComponentProps<'div'> & {
export type BoardHeaderProps = {
className?: string;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
} & Pick<
ViewBarProps,
'defaultViewName' | 'onViewsChange' | 'onViewSubmit' | 'scopeContext'
>;
} & Pick<ViewBarProps, 'scopeContext'>;
export function BoardHeader({
className,
onStageAdd,
onViewsChange,
onViewSubmit,
scopeContext,
defaultViewName,
}: BoardHeaderProps) {
const { onCurrentViewSubmit, ...viewBarContextProps } =
useContext(ViewBarContext);
const tableScopeId = useContextScopeId(scopeContext);
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
scopeContext,
);
const canPersistBoardCardFields = useRecoilValue(
canPersistBoardCardFieldsScopedFamilySelector([
tableScopeId,
currentViewId,
]),
);
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState,
scopeContext,
);
const [savedBoardCardFields, setSavedBoardCardFields] = useRecoilState(
savedBoardCardFieldsFamilyState(currentViewId),
);
const handleViewBarReset = () => setBoardCardFields(savedBoardCardFields);
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const savedBoardCardFields = await snapshot.getPromise(
savedBoardCardFieldsFamilyState(viewId),
);
set(boardCardFieldsScopedState(tableScopeId), savedBoardCardFields);
},
[tableScopeId],
);
const handleCurrentViewSubmit = async () => {
if (canPersistBoardCardFields) {
setSavedBoardCardFields(boardCardFields);
}
await onCurrentViewSubmit?.();
};
return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<ViewBar
defaultViewName={defaultViewName}
onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit}
optionsDropdownButton={
<BoardOptionsDropdown
customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
onStageAdd={onStageAdd}
onViewsChange={onViewsChange}
scopeContext={scopeContext}
/>
}
optionsDropdownKey={BoardOptionsDropdownKey}
scopeContext={scopeContext}
/>
<ViewBarContext.Provider
value={{
...viewBarContextProps,
canPersistViewFields: canPersistBoardCardFields,
onCurrentViewSubmit: handleCurrentViewSubmit,
onViewBarReset: handleViewBarReset,
onViewSelect: handleViewSelect,
}}
>
<ViewBar
className={className}
optionsDropdownButton={
<BoardOptionsDropdown
customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
onStageAdd={onStageAdd}
scopeContext={scopeContext}
/>
}
optionsDropdownKey={BoardOptionsDropdownKey}
scopeContext={scopeContext}
/>
</ViewBarContext.Provider>
</RecoilScope>
);
}

View File

@ -10,20 +10,22 @@ import {
type BoardOptionsDropdownProps = Pick<
BoardOptionsDropdownContentProps,
'customHotkeyScope' | 'onStageAdd' | 'onViewsChange' | 'scopeContext'
'customHotkeyScope' | 'onStageAdd' | 'scopeContext'
>;
export function BoardOptionsDropdown({
customHotkeyScope,
...props
onStageAdd,
scopeContext,
}: BoardOptionsDropdownProps) {
return (
<DropdownButton
buttonComponents={<BoardOptionsDropdownButton />}
dropdownComponents={
<BoardOptionsDropdownContent
{...props}
customHotkeyScope={customHotkeyScope}
onStageAdd={onStageAdd}
scopeContext={scopeContext}
/>
}
dropdownHotkeyScope={customHotkeyScope}

View File

@ -1,7 +1,7 @@
import { type Context, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { v4 } from 'uuid';
@ -23,15 +23,17 @@ import { MenuItemNavigate } from '@/ui/menu-item/components/MenuItemNavigate';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection';
import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView';
import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import type { View } from '@/ui/view-bar/types/View';
import { useBoardCardFields } from '../hooks/useBoardCardFields';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { boardColumnsState } from '../states/boardColumnsState';
import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState';
import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector';
import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector';
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
@ -40,7 +42,6 @@ import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
export type BoardOptionsDropdownContentProps = {
customHotkeyScope: HotkeyScope;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
onViewsChange?: (views: View[]) => void | Promise<void>;
scopeContext: Context<string | null>;
};
@ -60,10 +61,10 @@ type ColumnForCreate = {
export function BoardOptionsDropdownContent({
customHotkeyScope,
onStageAdd,
onViewsChange,
scopeContext,
}: BoardOptionsDropdownContentProps) {
const theme = useTheme();
const scopeId = useContextScopeId(scopeContext);
const stageInputRef = useRef<HTMLInputElement>(null);
const viewEditInputRef = useRef<HTMLInputElement>(null);
@ -106,15 +107,24 @@ export function BoardOptionsDropdownContent({
onStageAdd?.(columnToCreate);
};
const { upsertView } = useUpsertView({
onViewsChange,
scopeContext,
});
const { upsertView } = useUpsertView({ scopeContext });
const handleViewNameSubmit = async () => {
const name = viewEditInputRef.current?.value;
await upsertView(name);
};
const handleViewNameSubmit = useRecoilCallback(
({ set, snapshot }) =>
async () => {
const boardCardFields = await snapshot.getPromise(
boardCardFieldsScopedState(scopeId),
);
const isCreateMode = viewEditMode.mode === 'create';
const name = viewEditInputRef.current?.value;
const view = await upsertView(name);
if (view && isCreateMode) {
set(savedBoardCardFieldsFamilyState(view.id), boardCardFields);
}
},
[scopeId, upsertView, viewEditMode.mode],
);
const resetMenu = () => setCurrentMenu(undefined);

View File

@ -6,10 +6,7 @@ import { useRecoilState } from 'recoil';
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import {
BoardHeader,
BoardHeaderProps,
} from '@/ui/board/components/BoardHeader';
import { BoardHeader } from '@/ui/board/components/BoardHeader';
import { StyledBoard } from '@/ui/board/components/StyledBoard';
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
@ -39,10 +36,7 @@ export type EntityBoardProps = {
onColumnDelete?: (boardColumnId: string) => void;
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
scopeContext: Context<string | null>;
} & Pick<
BoardHeaderProps,
'defaultViewName' | 'onViewsChange' | 'onViewSubmit'
>;
};
const StyledWrapper = styled.div`
display: flex;
@ -57,12 +51,9 @@ const StyledBoardHeader = styled(BoardHeader)`
export function EntityBoard({
boardOptions,
defaultViewName,
onColumnAdd,
onColumnDelete,
onEditColumnTitle,
onViewsChange,
onViewSubmit,
scopeContext,
}: EntityBoardProps) {
const [boardColumns] = useRecoilState(boardColumnsState);
@ -139,13 +130,7 @@ export function EntityBoard({
return (boardColumns?.length ?? 0) > 0 ? (
<StyledWrapper>
<StyledBoardHeader
defaultViewName={defaultViewName}
onStageAdd={onColumnAdd}
onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit}
scopeContext={scopeContext}
/>
<StyledBoardHeader onStageAdd={onColumnAdd} scopeContext={scopeContext} />
<ScrollWrapper>
<StyledBoard ref={boardRef}>
<DragDropContext onDragEnd={onDragEnd}>

View File

@ -0,0 +1,14 @@
import { atomFamily } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
export const savedBoardCardFieldsFamilyState = atomFamily<
ViewFieldDefinition<ViewFieldMetadata>[],
string | undefined
>({
key: 'savedBoardCardFieldsFamilyState',
default: [],
});

View File

@ -0,0 +1,17 @@
import { selectorFamily } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
export const canPersistBoardCardFieldsScopedFamilySelector = selectorFamily({
key: 'canPersistBoardCardFieldsScopedFamilySelector',
get:
([scopeId, viewId]: [string, string | undefined]) =>
({ get }) =>
!isDeeplyEqual(
get(savedBoardCardFieldsFamilyState(viewId)),
get(boardCardFieldsScopedState(scopeId)),
),
});

View File

@ -0,0 +1,18 @@
import { selectorFamily } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
key: 'savedBoardCardFieldsByKeyFamilySelector',
get:
(viewId: string | undefined) =>
({ get }) =>
get(savedBoardCardFieldsFamilyState(viewId)).reduce<
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
>((result, field) => ({ ...result, [field.key]: field }), {}),
});