♻️ Migrate category toggle and actions

This commit is contained in:
Meier Lukas
2023-10-08 22:19:53 +02:00
parent f8dfc3b23b
commit afe4ae54fa
13 changed files with 330 additions and 226 deletions

View File

@@ -1,5 +1,12 @@
{
"header": {
"customize": "Customize board"
},
"category": {
"actions": {
"add": "Add category",
"addAbove": "Add category above",
"addBelow": "Add category below"
}
}
}

View File

@@ -17,9 +17,6 @@
},
"menu": {
"moveUp": "Move up",
"moveDown": "Move down",
"addCategory": "Add category {{location}}",
"addAbove": "above",
"addBelow": "below"
"moveDown": "Move down"
}
}

View File

@@ -0,0 +1,177 @@
import { ActionIcon, List, Menu, Stack, Text, createStyles } from '@mantine/core';
import { modals } from '@mantine/modals';
import {
IconDotsVertical,
IconEdit,
IconRowInsertBottom,
IconRowInsertTop,
IconSettings,
IconShare3,
IconTransitionBottom,
IconTransitionTop,
IconTrash,
TablerIconsProps,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useCallback, useMemo } from 'react';
import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore';
import { useCategoryActionHelper } from '~/components/Dashboard/Wrappers/Category/useCategoryActions';
import { AppItem, CategorySection } from '../../context';
type CategoryMenuProps = {
category: CategorySection;
};
export const CategoryMenu = ({ category }: CategoryMenuProps) => {
const isEditMode = useEditModeStore((x) => x.enabled);
const editModeActions = useEditModeActions(category);
const nonEditModeActions = useNonEditModeActions(category);
const { t } = useTranslation(['layout/common', 'boards/common', 'common']);
const Icon = isEditMode ? IconSettings : IconDotsVertical;
const actions = isEditMode ? editModeActions : nonEditModeActions;
return (
<Menu withArrow withinPortal>
<Menu.Target>
<ActionIcon mr="md">
<Icon />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{actions.map((action) => (
<>
{action.group && <Menu.Label>{t(action.group)}</Menu.Label>}
<Menu.Item
key={action.label}
icon={<action.icon size="1rem" />}
onClick={action.onClick}
color={action.color}
>
{t(action.label)}
</Menu.Item>
</>
))}
</Menu.Dropdown>
</Menu>
);
};
const useEditModeActions = (category: CategorySection): ActionDefinition[] => {
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
useCategoryActionHelper(category);
return [
{
icon: IconEdit,
label: 'common:edit',
onClick: edit,
},
{
icon: IconTrash,
color: 'red',
label: 'common:remove',
onClick: remove,
},
{
group: 'common:changePosition',
icon: IconTransitionTop,
label: 'menu.moveUp',
onClick: moveCategoryUp,
},
{
icon: IconTransitionBottom,
label: 'menu.moveDown',
onClick: moveCategoryDown,
},
{
group: 'boards/common:category.actions.add',
icon: IconRowInsertTop,
label: 'boards/common:category.actions.addAbove',
onClick: addCategoryAbove,
},
{
icon: IconRowInsertBottom,
label: 'boards/common:category.actions.addBelow',
onClick: addCategoryBelow,
},
];
};
const useNonEditModeActions = (category: CategorySection): ActionDefinition[] => {
const openAllApps = useOpenAllApps();
const apps = useMemo(
() => category.items.filter((x): x is AppItem => x.type === 'app'),
[category.items.length]
);
return [
{
icon: IconShare3,
label: 'actions.category.openAllInNewTab',
onClick: openAllApps(apps),
},
];
};
type ActionDefinition = {
icon: (props: TablerIconsProps) => JSX.Element;
label: string;
onClick: () => void;
color?: string;
group?: string;
};
const useStyles = createStyles(() => ({
listItem: {
'& div': {
maxWidth: 'calc(100% - 23px)',
},
},
}));
const useOpenAllApps = () => {
const { classes } = useStyles();
const { t } = useTranslation(['layout/common', 'common']);
return useCallback((apps: AppItem[]) => {
return () => {
for (let i = 0; i < apps.length; i += 1) {
const app = apps[i];
const popUp = window.open(app.externalUrl ?? app.internalUrl, app.id);
if (popUp !== null) continue;
modals.openConfirmModal({
title: <Text weight="bold">{t('modals.blockedPopups.title')}</Text>,
children: (
<Stack maw="100%">
<Text>{t('modals.blockedPopups.text')}</Text>
<List>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.browserPermission')}
</List.Item>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.adBlockers')}
</List.Item>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.otherBrowser')}
</List.Item>
</List>
</Stack>
),
labels: {
confirm: t('common:close'),
cancel: '',
},
cancelProps: {
display: 'none',
},
closeOnClickOutside: false,
});
break;
}
};
}, []);
};

View File

@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { v4 } from 'uuid';
import { api } from '~/utils/api';
import { type CategorySection, type EmptySection } from './context';
import { type CategorySection, type EmptySection } from '../../context';
type AddCategory = {
name: string;

View File

@@ -0,0 +1,115 @@
import { Accordion, Group, List, Stack, Text, Title, createStyles } from '@mantine/core';
import { modals } from '@mantine/modals';
import { useTranslation } from 'next-i18next';
import { useCallback, useMemo } from 'react';
import { AppItem, CategorySection } from '~/components/Board/context';
import { useEditModeStore } from '../../Dashboard/Views/useEditModeStore';
import { WrapperContent } from '../../Dashboard/Wrappers/WrapperContent';
import { useGridstack } from '../../Dashboard/Wrappers/gridstack/use-gridstack';
import { useCardStyles } from '../../layout/Common/useCardStyles';
import { CategoryMenu } from './Category/CategoryMenu';
interface DashboardCategoryProps {
section: CategorySection;
isOpened: boolean;
toggle: (categoryId: string) => void;
}
export const BoardCategorySection = ({ section, isOpened, toggle }: DashboardCategoryProps) => {
const { refs } = useGridstack({ section });
const isEditMode = useEditModeStore((x) => x.enabled);
const { classes: cardClasses, cx } = useCardStyles(true);
const { t } = useTranslation(['layout/common', 'common']);
const openAllApps = useOpenAllApps();
const apps = useMemo(
() => section.items.filter((x): x is AppItem => x.type === 'app'),
[section.items.length]
);
return (
<Accordion
classNames={{
item: cx(cardClasses.card, 'dashboard-gs-category'),
}}
mx={10}
chevronPosition="left"
multiple
variant="separated"
radius="lg"
value={isOpened || isEditMode ? [section.id] : []}
onChange={() => !isEditMode && toggle(section.id)}
>
<Accordion.Item value={section.id}>
<Group noWrap align="center">
<Accordion.Control>
<Title order={3}>{section.name}</Title>
</Accordion.Control>
<CategoryMenu category={section} />
</Group>
<Accordion.Panel>
<div
className="grid-stack grid-stack-category"
data-category={section.id}
ref={refs.wrapper}
>
<WrapperContent items={section.items} refs={refs} />
</div>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
};
const useStyles = createStyles(() => ({
listItem: {
'& div': {
maxWidth: 'calc(100% - 23px)',
},
},
}));
const useOpenAllApps = () => {
const { classes } = useStyles();
const { t } = useTranslation(['layout/common', 'common']);
return useCallback((apps: AppItem[]) => {
return () => {
for (let i = 0; i < apps.length; i += 1) {
const app = apps[i];
const popUp = window.open(app.externalUrl ?? app.internalUrl, app.id);
if (popUp !== null) continue;
modals.openConfirmModal({
title: <Text weight="bold">{t('modals.blockedPopups.title')}</Text>,
children: (
<Stack maw="100%">
<Text>{t('modals.blockedPopups.text')}</Text>
<List>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.browserPermission')}
</List.Item>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.adBlockers')}
</List.Item>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.otherBrowser')}
</List.Item>
</List>
</Stack>
),
labels: {
confirm: t('common:close'),
cancel: '',
},
cancelProps: {
display: 'none',
},
closeOnClickOutside: false,
});
break;
}
};
}, []);
};

View File

@@ -11,7 +11,7 @@ interface EmptySectionWrapperProps {
const defaultClasses = 'grid-stack grid-stack-empty min-row';
export const EmptySectionWrapper = ({ section }: EmptySectionWrapperProps) => {
export const BoardEmptySection = ({ section }: EmptySectionWrapperProps) => {
const { refs } = useGridstack({ section });
const isEditMode = useEditModeStore((x) => x.enabled);

View File

@@ -1,10 +1,7 @@
import { MobileRibbons } from './Mobile/Ribbon/MobileRibbon';
import { BoardView } from './Views/DashboardView';
import { useEditModeStore } from './Views/useEditModeStore';
export const Board = () => {
const isEditMode = useEditModeStore((x) => x.enabled);
return (
<>
{/* The following elemens are splitted because gridstack doesn't reinitialize them when using same item. */}

View File

@@ -1,5 +1,6 @@
import { Box, Group, LoadingOverlay, Stack } from '@mantine/core';
import { useEffect, useRef } from 'react';
import { useLocalStorage } from '@mantine/hooks';
import { useCallback, useEffect, useRef } from 'react';
import {
CategorySection,
EmptySection,
@@ -9,17 +10,32 @@ import {
import { useResize } from '~/hooks/use-resize';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { EmptySectionWrapper } from '../../Board/Sections/EmptySection';
import { DashboardCategory } from '../Wrappers/Category/Category';
import { BoardCategorySection } from '../../Board/Sections/CategorySection';
import { BoardEmptySection } from '../../Board/Sections/EmptySection';
import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar';
import { useGridstackStore } from '../Wrappers/gridstack/store';
export const BoardView = () => {
const boardName = useRequiredBoard().name;
const stackedSections = useStackedSections();
const sidebarsVisible = useSidebarVisibility();
const { isReady, mainAreaRef } = usePrepareGridstack();
const leftSidebarSection = useSidebarSection('left');
const rightSidebarSection = useSidebarSection('right');
const [toggledCategories, setToggledCategories] = useLocalStorage({
key: `${boardName}-category-section-toggled`,
// This is a bit of a hack to toggle the categories on the first load, return a string[] of the categories
defaultValue: stackedSections.filter((s) => s.type === 'category').map((s) => s.id),
});
const toggleCategory = useCallback((categoryId: string) => {
setToggledCategories((current) => {
console.log('toggle', current, categoryId);
if (current.includes(categoryId)) {
return current.filter((x) => x !== categoryId);
}
return [...current, categoryId];
});
}, []);
return (
<Box h="100%" pos="relative">
@@ -41,9 +57,14 @@ export const BoardView = () => {
<Stack ref={mainAreaRef} mx={-10} style={{ flexGrow: 1 }}>
{stackedSections.map((item) =>
item.type === 'category' ? (
<DashboardCategory key={item.id} section={item} />
<BoardCategorySection
key={item.id}
section={item}
isOpened={toggledCategories.includes(item.id)}
toggle={toggleCategory}
/>
) : (
<EmptySectionWrapper key={item.id} section={item} />
<BoardEmptySection key={item.id} section={item} />
)
)}
</Stack>

View File

@@ -1,3 +0,0 @@
import { BoardView } from './DashboardView';
export const DashboardDetailView = () => <BoardView />;

View File

@@ -1,3 +0,0 @@
import { BoardView } from './DashboardView';
export const DashboardEditView = () => <BoardView />;

View File

@@ -1,142 +0,0 @@
import {
Accordion,
ActionIcon,
Box,
List,
Menu,
Stack,
Text,
Title,
createStyles,
} from '@mantine/core';
import { modals } from '@mantine/modals';
import { IconDotsVertical, IconShare3 } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { CategorySection } from '~/components/Board/context';
import { useCardStyles } from '../../../layout/Common/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { WrapperContent } from '../WrapperContent';
import { useGridstack } from '../gridstack/use-gridstack';
import { CategoryEditMenu } from './CategoryEditMenu';
interface DashboardCategoryProps {
section: CategorySection;
}
export const DashboardCategory = ({ section }: DashboardCategoryProps) => {
const { refs } = useGridstack({ section });
const isEditMode = useEditModeStore((x) => x.enabled);
const { classes: cardClasses, cx } = useCardStyles(true);
const { classes } = useStyles();
const { t } = useTranslation(['layout/common', 'common']);
//const categoryList = config?.categories.map((x) => x.name) ?? [];
/*const [toggledCategories, setToggledCategories] = useLocalStorage({
key: `${config?.configProperties.name}-app-shelf-toggled`,
// This is a bit of a hack to toggle the categories on the first load, return a string[] of the categories
defaultValue: categoryList,
});*/
/*const handleMenuClick = () => {
for (let i = 0; i < apps.length; i += 1) {
const app = apps[i];
const popUp = window.open(app.url, app.id);
if (popUp === null) {
modals.openConfirmModal({
title: <Text weight="bold">{t('modals.blockedPopups.title')}</Text>,
children: (
<Stack maw="100%">
<Text>{t('modals.blockedPopups.text')}</Text>
<List>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.browserPermission')}
</List.Item>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.adBlockers')}
</List.Item>
<List.Item className={classes.listItem}>
{t('modals.blockedPopups.list.otherBrowser')}
</List.Item>
</List>
</Stack>
),
labels: {
confirm: t('common:close'),
cancel: '',
},
cancelProps: {
display: 'none',
},
closeOnClickOutside: false,
});
break;
}
}
};
// value={isEditMode ? categoryList : toggledCategories}
/*
onChange={(state) => {
// Cancel if edit mode is on
if (isEditMode) return;
setToggledCategories([...state]);
}}
*/
return (
<Accordion
classNames={{
item: cx(cardClasses.card, 'dashboard-gs-category'),
}}
mx={10}
chevronPosition="left"
multiple
variant="separated"
radius="lg"
>
<Accordion.Item value={section.name}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Accordion.Control icon={isEditMode && <CategoryEditMenu category={section} />}>
<Title order={3}>{section.name}</Title>
</Accordion.Control>
{!isEditMode && (
<Menu withArrow withinPortal>
<Menu.Target>
<ActionIcon variant="light" mr="md">
<IconDotsVertical />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={/*handleMenuClick*/ undefined}
icon={<IconShare3 size="1rem" />}
>
{t('actions.category.openAllInNewTab')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Box>
<Accordion.Panel>
<div
className="grid-stack grid-stack-category"
data-category={section.id}
ref={refs.wrapper}
>
<WrapperContent items={section.items} refs={refs} />
</div>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
};
const useStyles = createStyles(() => ({
listItem: {
'& div': {
maxWidth: 'calc(100% - 23px)',
},
},
}));

View File

@@ -1,58 +0,0 @@
import { ActionIcon, Menu } from '@mantine/core';
import {
IconEdit,
IconRowInsertBottom,
IconRowInsertTop,
IconSettings,
IconTransitionBottom,
IconTransitionTop,
IconTrash,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { CategoryType } from '~/types/category';
import { useCategoryActionHelper } from './useCategoryActions';
interface CategoryEditMenuProps {
category: CategoryType;
}
export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
const { name: configName } = useConfigContext();
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
useCategoryActionHelper(configName, category);
const { t } = useTranslation(['layout/common', 'common']);
return (
<Menu withinPortal withArrow>
<Menu.Target>
<ActionIcon>
<IconSettings />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
{t('common:edit')}
</Menu.Item>
<Menu.Item icon={<IconTrash size={20} />} onClick={remove}>
{t('common:remove')}
</Menu.Item>
<Menu.Label>{t('common:changePosition')}</Menu.Label>
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
{t('menu.moveUp')}
</Menu.Item>
<Menu.Item icon={<IconTransitionBottom size={20} />} onClick={moveCategoryDown}>
{t('menu.moveDown')}
</Menu.Item>
<Menu.Label>{t('menu.addCategory', { location: '' })}</Menu.Label>
<Menu.Item icon={<IconRowInsertTop size={20} />} onClick={addCategoryAbove}>
{t('menu.addCategory', { location: t('menu.addAbove') })}
</Menu.Item>
<Menu.Item icon={<IconRowInsertBottom size={20} />} onClick={addCategoryBelow}>
{t('menu.addCategory', { location: t('menu.addBelow') })}
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};

View File

@@ -1,16 +1,12 @@
import { v4 as uuidv4 } from 'uuid';
import { useCategoryActions } from '~/components/Board/category-actions';
import { useCategoryActions } from '~/components/Board/Sections/Category/category-actions';
import { useRequiredBoard } from '~/components/Board/context';
import { useConfigStore } from '~/config/store';
import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions';
import { AppType } from '~/types/app';
import { CategoryType } from '~/types/category';
import { WrapperType } from '~/types/wrapper';
import { IWidget } from '~/widgets/widgets';
import { CategoryEditModalInnerProps } from './CategoryEditModal';
export const useCategoryActionHelper = (configName: string | undefined, category: CategoryType) => {
export const useCategoryActionHelper = (category: CategoryType) => {
const boardName = useRequiredBoard().name;
const { addCategory, moveCategory, removeCategory, renameCategory } = useCategoryActions({
boardName,