@ -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(
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
14
front/src/modules/ui/view-bar/contexts/ViewBarContext.ts
Normal file
14
front/src/modules/ui/view-bar/contexts/ViewBarContext.ts
Normal 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>;
|
||||
}>({});
|
||||
37
front/src/modules/ui/view-bar/hooks/useRemoveView.ts
Normal file
37
front/src/modules/ui/view-bar/hooks/useRemoveView.ts
Normal 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 };
|
||||
};
|
||||
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user