mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
✨ Add generic menu for tiles
This commit is contained in:
@@ -2,7 +2,14 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"ok": "Okay"
|
"ok": "Okay",
|
||||||
|
"edit": "Edit",
|
||||||
|
"changePosition": "Change position",
|
||||||
|
"remove": "Remove"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"settings": "Settings",
|
||||||
|
"dangerZone": "Danger zone"
|
||||||
},
|
},
|
||||||
"tip": "Tip: ",
|
"tip": "Tip: ",
|
||||||
"time": {
|
"time": {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useState } from 'react';
|
|||||||
import PingComponent from '../../modules/ping/PingModule';
|
import PingComponent from '../../modules/ping/PingModule';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { serviceItem } from '../../tools/types';
|
import { serviceItem } from '../../tools/types';
|
||||||
import { TileMenu } from '../Dashboard/Menu/TileMenu';
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
item: {
|
item: {
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { ActionIcon, Menu, Text } from '@mantine/core';
|
|
||||||
import { openContextModal } from '@mantine/modals';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { IconCheck, IconEdit, IconMenu, IconTrash } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useConfigContext } from '../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../config/store';
|
|
||||||
import { useColorTheme } from '../../../tools/color';
|
|
||||||
import { ServiceType } from '../../../types/service';
|
|
||||||
|
|
||||||
interface TileMenuProps {
|
|
||||||
service: ServiceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TileMenu = ({ service }: TileMenuProps) => {
|
|
||||||
const { secondaryColor } = useColorTheme();
|
|
||||||
const { t } = useTranslation('layout/app-shelf-menu');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { name: configName } = useConfigContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu withinPortal width={150} shadow="xl" withArrow radius="md" position="right">
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon>
|
|
||||||
<IconMenu />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
|
||||||
<Menu.Item
|
|
||||||
color={secondaryColor}
|
|
||||||
icon={<IconEdit />}
|
|
||||||
onClick={() =>
|
|
||||||
openContextModal({
|
|
||||||
modal: 'changeTilePosition',
|
|
||||||
innerProps: {
|
|
||||||
type: 'service',
|
|
||||||
tile: service,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('menu.actions.edit')}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
|
||||||
<Menu.Item
|
|
||||||
color="red"
|
|
||||||
onClick={(e: any) => {
|
|
||||||
if (!configName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateConfig(configName, (previousConfig) => ({
|
|
||||||
...previousConfig,
|
|
||||||
services: previousConfig.services.filter((x) => x.id !== service.id),
|
|
||||||
})).then(() => {
|
|
||||||
showNotification({
|
|
||||||
autoClose: 5000,
|
|
||||||
title: (
|
|
||||||
<Text>
|
|
||||||
Service <b>{service.name}</b> removed successfully!
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
color: 'green',
|
|
||||||
icon: <IconCheck />,
|
|
||||||
message: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={<IconTrash />}
|
|
||||||
>
|
|
||||||
{t('menu.actions.delete')}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Center, Stack, Text, Title } from '@mantine/core';
|
import { Center, Stack, Text, Title } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useSetSafeInterval } from '../../../../tools/hooks/useSetSafeInterval';
|
import { useSetSafeInterval } from '../../../../tools/hooks/useSetSafeInterval';
|
||||||
|
|||||||
57
src/components/Dashboard/Tiles/GenericTileMenu.tsx
Normal file
57
src/components/Dashboard/Tiles/GenericTileMenu.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { ActionIcon, Menu } from '@mantine/core';
|
||||||
|
import { IconDots, IconLayoutKanban, IconPencil, IconTrash } from '@tabler/icons';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useEditModeStore } from '../Views/useEditModeStore';
|
||||||
|
|
||||||
|
interface GenericTileMenuProps {
|
||||||
|
handleClickEdit: () => void;
|
||||||
|
handleClickChangePosition: () => void;
|
||||||
|
handleClickDelete: () => void;
|
||||||
|
displayEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenericTileMenu = ({
|
||||||
|
handleClickEdit,
|
||||||
|
handleClickChangePosition,
|
||||||
|
handleClickDelete,
|
||||||
|
displayEdit,
|
||||||
|
}: GenericTileMenuProps) => {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||||
|
|
||||||
|
if (!isEditMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu withinPortal>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon pos="absolute" top={4} right={4}>
|
||||||
|
<IconDots />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown w={250}>
|
||||||
|
<Menu.Label>{t('sections.settings')}</Menu.Label>
|
||||||
|
{displayEdit && (
|
||||||
|
<Menu.Item icon={<IconPencil size={16} stroke={1.5} />} onClick={handleClickEdit}>
|
||||||
|
{t('actions.edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
icon={<IconLayoutKanban size={16} stroke={1.5} />}
|
||||||
|
onClick={handleClickChangePosition}
|
||||||
|
>
|
||||||
|
{t('actions.changePosition')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Label>{t('sections.dangerZone')}</Menu.Label>
|
||||||
|
<Menu.Item
|
||||||
|
color="red"
|
||||||
|
icon={<IconTrash size={16} stroke={1.5} color="red" />}
|
||||||
|
onClick={handleClickDelete}
|
||||||
|
>
|
||||||
|
{t('actions.remove')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
@@ -10,14 +9,10 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
import { ContextModalProps } from '@mantine/modals';
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { ClockIntegrationType, IntegrationsType } from '../../../types/integration';
|
|
||||||
import { integrationModuleTranslationsMap } from './IntegrationsEditModal';
|
|
||||||
import { TileBaseType } from '../../../types/tile';
|
|
||||||
import {
|
import {
|
||||||
IconArrowsUpDown,
|
IconArrowsUpDown,
|
||||||
IconCalendarTime,
|
IconCalendarTime,
|
||||||
@@ -25,11 +20,14 @@ import {
|
|||||||
IconCloudRain,
|
IconCloudRain,
|
||||||
IconFileDownload,
|
IconFileDownload,
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
import { ServiceIcon } from './Service/ServiceIcon';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { Tiles } from './tilesDefinitions';
|
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../config/store';
|
||||||
|
import { IntegrationsType } from '../../../types/integration';
|
||||||
|
import { TileBaseType } from '../../../types/tile';
|
||||||
|
import { integrationModuleTranslationsMap } from './IntegrationsEditModal';
|
||||||
|
import { ServiceIcon } from './Service/ServiceIcon';
|
||||||
|
import { Tiles } from './tilesDefinitions';
|
||||||
|
|
||||||
export type IntegrationChangePositionModalInnerProps = {
|
export type IntegrationChangePositionModalInnerProps = {
|
||||||
integration: keyof IntegrationsType;
|
integration: keyof IntegrationsType;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ActionIcon, Menu, Title } from '@mantine/core';
|
import { Title } from '@mantine/core';
|
||||||
import { IconDots, IconLayoutKanban, IconPencil, IconTrash } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
||||||
import { IntegrationsType } from '../../../../types/integration';
|
import { IntegrationsType } from '../../../../types/integration';
|
||||||
import { TileBaseType } from '../../../../types/tile';
|
import { TileBaseType } from '../../../../types/tile';
|
||||||
|
import { GenericTileMenu } from '../GenericTileMenu';
|
||||||
import { IntegrationChangePositionModalInnerProps } from '../IntegrationChangePositionModal';
|
import { IntegrationChangePositionModalInnerProps } from '../IntegrationChangePositionModal';
|
||||||
import { IntegrationRemoveModalInnerProps } from '../IntegrationRemoveModal';
|
import { IntegrationRemoveModalInnerProps } from '../IntegrationRemoveModal';
|
||||||
import {
|
import {
|
||||||
@@ -65,34 +65,11 @@ export const IntegrationsMenu = <TIntegrationKey extends keyof IntegrationsType>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu withinPortal>
|
<GenericTileMenu
|
||||||
<Menu.Target>
|
handleClickEdit={handleEditClick}
|
||||||
<ActionIcon pos="absolute" top={4} right={4}>
|
handleClickChangePosition={handleChangeSizeClick}
|
||||||
<IconDots />
|
handleClickDelete={handleDeleteClick}
|
||||||
</ActionIcon>
|
displayEdit={options !== undefined}
|
||||||
</Menu.Target>
|
/>
|
||||||
<Menu.Dropdown w={250}>
|
|
||||||
<Menu.Label>Settings</Menu.Label>
|
|
||||||
{options && (
|
|
||||||
<Menu.Item icon={<IconPencil size={16} stroke={1.5} />} onClick={handleEditClick}>
|
|
||||||
Edit
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
<Menu.Item
|
|
||||||
icon={<IconLayoutKanban size={16} stroke={1.5} />}
|
|
||||||
onClick={handleChangeSizeClick}
|
|
||||||
>
|
|
||||||
Change position
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Label>Danger zone</Menu.Label>
|
|
||||||
<Menu.Item
|
|
||||||
color="red"
|
|
||||||
icon={<IconTrash size={16} stroke={1.5} color="red" />}
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
39
src/components/Dashboard/Tiles/Service/ServiceMenu.tsx
Normal file
39
src/components/Dashboard/Tiles/Service/ServiceMenu.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
||||||
|
import { ServiceType } from '../../../../types/service';
|
||||||
|
import { GenericTileMenu } from '../GenericTileMenu';
|
||||||
|
|
||||||
|
interface TileMenuProps {
|
||||||
|
service: ServiceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServiceMenu = ({ service }: TileMenuProps) => {
|
||||||
|
const handleClickEdit = () => {
|
||||||
|
openContextModalGeneric<{ service: ServiceType }>({
|
||||||
|
modal: 'editService',
|
||||||
|
size: 'xl',
|
||||||
|
innerProps: {
|
||||||
|
service,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickChangePosition = () => {
|
||||||
|
openContextModalGeneric({
|
||||||
|
modal: 'changeTilePosition',
|
||||||
|
innerProps: {
|
||||||
|
tile: service,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickDelete = () => {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericTileMenu
|
||||||
|
handleClickEdit={handleClickEdit}
|
||||||
|
handleClickChangePosition={handleClickChangePosition}
|
||||||
|
handleClickDelete={handleClickDelete}
|
||||||
|
displayEdit
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ActionIcon, Card, Center, Text, UnstyledButton } from '@mantine/core';
|
import { Card, Center, Text, UnstyledButton } from '@mantine/core';
|
||||||
import { NextLink } from '@mantine/next';
|
import { NextLink } from '@mantine/next';
|
||||||
import { createStyles } from '@mantine/styles';
|
import { createStyles } from '@mantine/styles';
|
||||||
import { IconDots } from '@tabler/icons';
|
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { ServiceType } from '../../../../types/service';
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { BaseTileProps } from '../type';
|
import { BaseTileProps } from '../type';
|
||||||
|
import { ServiceMenu } from './ServiceMenu';
|
||||||
|
|
||||||
interface ServiceTileProps extends BaseTileProps {
|
interface ServiceTileProps extends BaseTileProps {
|
||||||
service: ServiceType;
|
service: ServiceType;
|
||||||
@@ -26,6 +26,7 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
|||||||
{service.name}
|
{service.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Center style={{ height: '75%', flex: 1 }}>
|
<Center style={{ height: '75%', flex: 1 }}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img className={classes.image} src={service.appearance.iconUrl} alt="" />
|
<img className={classes.image} src={service.appearance.iconUrl} alt="" />
|
||||||
</Center>
|
</Center>
|
||||||
</>
|
</>
|
||||||
@@ -34,6 +35,11 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
|||||||
return (
|
return (
|
||||||
<Card className={cx(className, cardClass)} withBorder radius="lg" shadow="md">
|
<Card className={cx(className, cardClass)} withBorder radius="lg" shadow="md">
|
||||||
{/* TODO: add service menu */}
|
{/* TODO: add service menu */}
|
||||||
|
|
||||||
|
<div style={{ position: 'absolute', top: 10, right: 10 }}>
|
||||||
|
<ServiceMenu service={service} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{!service.url || isEditMode ? (
|
{!service.url || isEditMode ? (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
@@ -57,8 +63,7 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles((theme, _params, getRef) => {
|
const useStyles = createStyles((theme, _params, getRef) => ({
|
||||||
return {
|
|
||||||
image: {
|
image: {
|
||||||
ref: getRef('image'),
|
ref: getRef('image'),
|
||||||
maxHeight: '80%',
|
maxHeight: '80%',
|
||||||
@@ -84,5 +89,4 @@ const useStyles = createStyles((theme, _params, getRef) => {
|
|||||||
// TODO: add styles for service name when hovering card
|
// TODO: add styles for service name when hovering card
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Box, createStyles, Group, Header as MantineHeader, Text } from '@mantine/core';
|
import { Box, createStyles, Group, Header as MantineHeader } from '@mantine/core';
|
||||||
import { openContextModal } from '@mantine/modals';
|
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Logo';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
@@ -23,21 +22,6 @@ export function Header(props: any) {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</Box>
|
</Box>
|
||||||
<Group position="right" style={{ maxWidth: 'none' }} noWrap>
|
<Group position="right" style={{ maxWidth: 'none' }} noWrap>
|
||||||
<Text
|
|
||||||
onClick={() => {
|
|
||||||
openContextModal({
|
|
||||||
modal: 'changeTilePosition',
|
|
||||||
title: 'Change tile position',
|
|
||||||
innerProps: {
|
|
||||||
tile: config?.services[0],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
variant="link"
|
|
||||||
>
|
|
||||||
Test
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Search />
|
<Search />
|
||||||
<AddElementAction />
|
<AddElementAction />
|
||||||
<ToggleEditModeAction />
|
<ToggleEditModeAction />
|
||||||
|
|||||||
Reference in New Issue
Block a user