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,4 +1,4 @@
import { type Context, useCallback, useState } from 'react';
import { type Context, useCallback, useContext, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
@ -21,6 +21,8 @@ import { canPersistSortsScopedFamilySelector } from '@/ui/view-bar/states/select
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import { ViewBarContext } from '../contexts/ViewBarContext';
const StyledContainer = styled.div`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
@ -28,22 +30,20 @@ const StyledContainer = styled.div`
`;
export type UpdateViewButtonGroupProps = {
canPersistViewFields?: boolean;
hotkeyScope: string;
onViewEditModeChange?: () => void;
onViewSubmit?: () => void | Promise<void>;
scopeContext: Context<string | null>;
};
export const UpdateViewButtonGroup = ({
canPersistViewFields,
hotkeyScope,
onViewEditModeChange,
onViewSubmit,
scopeContext,
}: UpdateViewButtonGroupProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { canPersistViewFields, onCurrentViewSubmit } =
useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext);
const currentViewId = useRecoilScopedValue(
@ -89,7 +89,7 @@ export const UpdateViewButtonGroup = ({
if (canPersistFilters) setSavedFilters(filters);
if (canPersistSorts) setSavedSorts(sorts);
await onViewSubmit?.();
await onCurrentViewSubmit?.();
};
useScopedHotkeys(

View File

@ -1,4 +1,4 @@
import type { ComponentProps, Context, ReactNode } from 'react';
import type { Context, ReactNode } from 'react';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar';
@ -8,38 +8,22 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { FilterDropdownButton } from './FilterDropdownButton';
import { SortDropdownButton } from './SortDropdownButton';
import {
UpdateViewButtonGroup,
type UpdateViewButtonGroupProps,
} from './UpdateViewButtonGroup';
import ViewBarDetails, { type ViewBarDetailsProps } from './ViewBarDetails';
import {
ViewsDropdownButton,
type ViewsDropdownButtonProps,
} from './ViewsDropdownButton';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import ViewBarDetails from './ViewBarDetails';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = ComponentProps<'div'> & {
export type ViewBarProps = {
className?: string;
optionsDropdownButton: ReactNode;
optionsDropdownKey: string;
scopeContext: Context<string | null>;
} & Pick<
ViewsDropdownButtonProps,
'defaultViewName' | 'onViewsChange' | 'onViewSelect'
> &
Pick<ViewBarDetailsProps, 'canPersistViewFields' | 'onReset'> &
Pick<UpdateViewButtonGroupProps, 'onViewSubmit'>;
};
export const ViewBar = ({
canPersistViewFields,
defaultViewName,
onReset,
onViewsChange,
onViewSelect,
onViewSubmit,
className,
optionsDropdownButton,
optionsDropdownKey,
scopeContext,
...props
}: ViewBarProps) => {
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
dropdownId: optionsDropdownKey,
@ -47,13 +31,10 @@ export const ViewBar = ({
return (
<TopBar
{...props}
className={className}
leftComponent={
<ViewsDropdownButton
defaultViewName={defaultViewName}
onViewEditModeChange={openOptionsDropdownButton}
onViewsChange={onViewsChange}
onViewSelect={onViewSelect}
hotkeyScope={ViewsHotkeyScope.ListDropdown}
scopeContext={scopeContext}
/>
@ -75,15 +56,11 @@ export const ViewBar = ({
}
bottomComponent={
<ViewBarDetails
canPersistViewFields={canPersistViewFields}
context={scopeContext}
hasFilterButton
onReset={onReset}
rightComponent={
<UpdateViewButtonGroup
canPersistViewFields={canPersistViewFields}
onViewEditModeChange={openOptionsDropdownButton}
onViewSubmit={onViewSubmit}
hotkeyScope={ViewsHotkeyScope.CreateDropdown}
scopeContext={scopeContext}
/>

View File

@ -1,4 +1,4 @@
import type { Context, ReactNode } from 'react';
import { type Context, type ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@ -7,6 +7,7 @@ import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextS
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
@ -23,10 +24,8 @@ import { AddFilterFromDropdownButton } from './AddFilterFromDetailsButton';
import SortOrFilterChip from './SortOrFilterChip';
export type ViewBarDetailsProps = {
canPersistViewFields?: boolean;
context: Context<string | null>;
hasFilterButton?: boolean;
onReset?: () => void;
rightComponent?: ReactNode;
};
@ -99,12 +98,11 @@ const StyledAddFilterContainer = styled.div`
`;
function ViewBarDetails({
canPersistViewFields,
context,
hasFilterButton = false,
onReset,
rightComponent,
}: ViewBarDetailsProps) {
const { canPersistViewFields, onViewBarReset } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(context);
const currentViewId = useRecoilScopedValue(currentViewIdScopedState, context);
@ -155,7 +153,7 @@ function ViewBarDetails({
const removeFilter = useRemoveFilter(context);
function handleCancelClick() {
onReset?.();
onViewBarReset?.();
setFilters(savedFilters);
setSorts(savedSorts);
}

View File

@ -2,6 +2,7 @@ import {
type Context,
type MouseEvent,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
@ -22,7 +23,6 @@ import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
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 DropdownButton from '@/ui/view-bar/components/DropdownButton';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
@ -33,10 +33,12 @@ import { currentViewScopedSelector } from '@/ui/view-bar/states/selectors/curren
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import { viewsScopedState } from '@/ui/view-bar/states/viewsScopedState';
import type { View } from '@/ui/view-bar/types/View';
import { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope';
import { assertNotNull } from '~/utils/assert';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { useRemoveView } from '../hooks/useRemoveView';
const StyledBoldDropdownMenuItemsContainer = styled(
StyledDropdownMenuItemsContainer,
)`
@ -71,39 +73,28 @@ const StyledViewName = styled.span`
`;
export type ViewsDropdownButtonProps = {
defaultViewName: string;
hotkeyScope: ViewsHotkeyScope;
onViewEditModeChange?: () => void;
onViewsChange?: (views: View[]) => void | Promise<void>;
onViewSelect?: (viewId: string) => void | Promise<void>;
scopeContext: Context<string | null>;
};
export const ViewsDropdownButton = ({
defaultViewName,
hotkeyScope,
onViewEditModeChange,
onViewsChange,
onViewSelect,
scopeContext,
}: ViewsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const { defaultViewName, onViewSelect } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext);
const [, setCurrentViewId] = useRecoilScopedState(
currentViewIdScopedState,
scopeContext,
);
const [isUnfolded, setIsUnfolded] = useState(false);
const currentView = useRecoilScopedValue(
currentViewScopedSelector,
scopeContext,
);
const [views, setViews] = useRecoilScopedState(
viewsScopedState,
scopeContext,
);
const views = useRecoilScopedValue(viewsScopedState, scopeContext);
const setViewEditMode = useSetRecoilState(viewEditModeState);
const {
@ -146,20 +137,17 @@ export const ViewsDropdownButton = ({
[setViewEditMode, onViewEditModeChange],
);
const handleDeleteViewButtonClick = useCallback(
async (event: MouseEvent<HTMLButtonElement>, viewId: string) => {
event.stopPropagation();
const { removeView } = useRemoveView({ scopeContext });
if (currentView?.id === viewId) setCurrentViewId(undefined);
const handleDeleteViewButtonClick = async (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
const nextViews = views.filter((view) => view.id !== viewId);
setViews(nextViews);
await onViewsChange?.(nextViews);
setIsUnfolded(false);
},
[currentView?.id, onViewsChange, setCurrentViewId, setViews, views],
);
await removeView(viewId);
setIsUnfolded(false);
};
useEffect(() => {
isUnfolded

View File

@ -0,0 +1,14 @@
import { createContext } from 'react';
import type { View } from '../types/View';
export const ViewBarContext = createContext<{
canPersistViewFields?: boolean;
defaultViewName?: string;
onCurrentViewSubmit?: () => void | Promise<void>;
onViewBarReset?: () => void;
onViewCreate?: (view: View) => void | Promise<void>;
onViewEdit?: (view: View) => void | Promise<void>;
onViewRemove?: (viewId: string) => void | Promise<void>;
onViewSelect?: (viewId: string) => void | Promise<void>;
}>({});

View File

@ -0,0 +1,37 @@
import { type Context, useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { viewsScopedState } from '../states/viewsScopedState';
export const useRemoveView = ({
scopeContext,
}: {
scopeContext: Context<string | null>;
}) => {
const { onViewRemove } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext);
const removeView = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const currentViewId = await snapshot.getPromise(
currentViewIdScopedState(recoilScopeId),
);
if (currentViewId === viewId)
set(currentViewIdScopedState(recoilScopeId), undefined);
set(viewsScopedState(recoilScopeId), (previousViews) =>
previousViews.filter((view) => view.id !== viewId),
);
await onViewRemove?.(viewId);
},
[onViewRemove, recoilScopeId],
);
return { removeView };
};

View File

@ -1,37 +1,30 @@
import { Context, useCallback } from 'react';
import { type Context, useCallback, useContext } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState';
import { savedSortsFamilyState } from '../states/savedSortsFamilyState';
import { viewsByIdScopedSelector } from '../states/selectors/viewsByIdScopedSelector';
import { sortsScopedState } from '../states/sortsScopedState';
import { viewEditModeState } from '../states/viewEditModeState';
import { viewsScopedState } from '../states/viewsScopedState';
import type { View } from '../types/View';
export const useUpsertView = ({
onViewsChange,
scopeContext,
}: {
onViewsChange?: (views: View[]) => void | Promise<void>;
scopeContext: Context<string | null>;
}) => {
const { onViewCreate, onViewEdit } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext);
const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
const sorts = useRecoilScopedValue(sortsScopedState, scopeContext);
const [, setCurrentViewId] = useRecoilScopedState(
currentViewIdScopedState,
scopeContext,
);
const [views, setViews] = useRecoilScopedState(
viewsScopedState,
scopeContext,
);
const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState);
const resetViewEditMode = useCallback(
@ -40,44 +33,63 @@ export const useUpsertView = ({
);
const upsertView = useRecoilCallback(
({ set }) =>
({ set, snapshot }) =>
async (name?: string) => {
if (!viewEditMode.mode || !name) return resetViewEditMode();
if (!viewEditMode.mode || !name) {
resetViewEditMode();
return;
}
if (viewEditMode.mode === 'create') {
const viewToCreate = { id: v4(), name };
const nextViews = [...views, viewToCreate];
const createdView = { id: v4(), name };
set(savedFiltersFamilyState(viewToCreate.id), filters);
set(savedSortsFamilyState(viewToCreate.id), sorts);
set(savedFiltersFamilyState(createdView.id), filters);
set(savedSortsFamilyState(createdView.id), sorts);
setViews(nextViews);
await onViewsChange?.(nextViews);
set(viewsScopedState(recoilScopeId), (previousViews) => [
...previousViews,
createdView,
]);
setCurrentViewId(viewToCreate.id);
await onViewCreate?.(createdView);
resetViewEditMode();
set(currentViewIdScopedState(recoilScopeId), createdView.id);
return createdView;
}
if (viewEditMode.mode === 'edit') {
const nextViews = views.map((view) =>
view.id === viewEditMode.viewId ? { ...view, name } : view,
if (viewEditMode.mode === 'edit' && viewEditMode.viewId) {
const viewsById = await snapshot.getPromise(
viewsByIdScopedSelector(recoilScopeId),
);
const editedView = { ...viewsById[viewEditMode.viewId], name };
set(viewsScopedState(recoilScopeId), (previousViews) =>
previousViews.map((previousView) =>
previousView.id === viewEditMode.viewId
? editedView
: previousView,
),
);
setViews(nextViews);
await onViewsChange?.(nextViews);
}
await onViewEdit?.(editedView);
return resetViewEditMode();
resetViewEditMode();
return editedView;
}
},
[
filters,
onViewsChange,
onViewCreate,
onViewEdit,
recoilScopeId,
resetViewEditMode,
setCurrentViewId,
setViews,
sorts,
viewEditMode.mode,
viewEditMode.viewId,
views,
],
);