mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
🔀 Merge branch 'dev' into next-13
This commit is contained in:
@@ -45,7 +45,14 @@ export const ChangePositionModal = ({
|
||||
const width = parseInt(form.values.width, 10);
|
||||
const height = parseInt(form.values.height, 10);
|
||||
|
||||
if (!form.values.x || !form.values.y || Number.isNaN(width) || Number.isNaN(height)) return;
|
||||
if (
|
||||
form.values.x === null ||
|
||||
form.values.y === null ||
|
||||
Number.isNaN(width) ||
|
||||
Number.isNaN(height)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(form.values.x, form.values.y, width, height);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
||||
import { Autocomplete, createStyles, Flex, Tabs } from '@mantine/core';
|
||||
import { UseFormReturnType } from '@mantine/form';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppType } from '../../../../../../types/app';
|
||||
import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
|
||||
@@ -18,16 +19,21 @@ export const AppearanceTab = ({
|
||||
}: AppearanceTabProps) => {
|
||||
const { t } = useTranslation('layout/modals/add-app');
|
||||
const { classes } = useStyles();
|
||||
const { isLoading, error, data } = useQuery({
|
||||
queryKey: ['autocompleteLocale'],
|
||||
queryFn: () => fetch('/api/getLocalImages').then((res) => res.json()),
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs.Panel value="appearance" pt="lg">
|
||||
<Flex gap={5}>
|
||||
<TextInput
|
||||
<Autocomplete
|
||||
className={classes.textInput}
|
||||
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
|
||||
label={t('appearance.icon.label')}
|
||||
description={t('appearance.icon.description')}
|
||||
variant="default"
|
||||
data={data?.files ?? []}
|
||||
withAsterisk
|
||||
required
|
||||
{...form.getInputProps('appearance.iconUrl')}
|
||||
|
||||
@@ -31,7 +31,7 @@ interface IconSelectorProps {
|
||||
}
|
||||
|
||||
export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSelectorProps) => {
|
||||
const { t } = useTranslation('layout/tools');
|
||||
const { t } = useTranslation('layout/modals/icon-picker');
|
||||
|
||||
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
||||
|
||||
@@ -50,7 +50,7 @@ export const AvailableElementTypes = ({
|
||||
{
|
||||
id: uuidv4(),
|
||||
// Thank you ChatGPT ;)
|
||||
position: previousConfig.wrappers.length + 1,
|
||||
position: previousConfig.categories.length + 1,
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Group, Title } from '@mantine/core';
|
||||
import { Accordion, Title } from '@mantine/core';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { CategoryType } from '../../../../types/category';
|
||||
import { HomarrCardWrapper } from '../../Tiles/HomarrCardWrapper';
|
||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||
import { useGridstack } from '../gridstack/use-gridstack';
|
||||
import { WrapperContent } from '../WrapperContent';
|
||||
@@ -13,20 +15,47 @@ interface DashboardCategoryProps {
|
||||
export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
|
||||
const { refs, apps, widgets } = useGridstack('category', category.id);
|
||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||
const { config } = useConfigContext();
|
||||
const { classes: cardClasses } = useCardStyles(true);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
return (
|
||||
<HomarrCardWrapper pt={10} mx={10} isCategory>
|
||||
<Group position="apart" align="center">
|
||||
<Title order={3}>{category.name}</Title>
|
||||
{isEditMode ? <CategoryEditMenu category={category} /> : null}
|
||||
</Group>
|
||||
<div
|
||||
className="grid-stack grid-stack-category"
|
||||
data-category={category.id}
|
||||
ref={refs.wrapper}
|
||||
>
|
||||
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
||||
</div>
|
||||
</HomarrCardWrapper>
|
||||
<Accordion
|
||||
classNames={{
|
||||
item: cardClasses.card,
|
||||
}}
|
||||
mx={10}
|
||||
chevronPosition="left"
|
||||
multiple
|
||||
value={isEditMode ? categoryList : toggledCategories}
|
||||
variant="separated"
|
||||
radius="lg"
|
||||
onChange={(state) => {
|
||||
// Cancel if edit mode is on
|
||||
if (isEditMode) return;
|
||||
setToggledCategories([...state]);
|
||||
}}
|
||||
>
|
||||
<Accordion.Item value={category.name}>
|
||||
<Accordion.Control icon={isEditMode && <CategoryEditMenu category={category} />}>
|
||||
<Title order={3}>{category.name}</Title>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<div
|
||||
className="grid-stack grid-stack-category"
|
||||
data-category={category.id}
|
||||
ref={refs.wrapper}
|
||||
>
|
||||
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
||||
</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
||||
useCategoryActions(configName, category);
|
||||
|
||||
return (
|
||||
<Menu withinPortal position="left-start" withArrow>
|
||||
<Menu withinPortal withArrow>
|
||||
<Menu.Target>
|
||||
<ActionIcon>
|
||||
<IconDots />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
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 useCategoryActions = (configName: string | undefined, category: CategoryType) => {
|
||||
@@ -185,29 +187,75 @@ export const useCategoryActions = (configName: string | undefined, category: Cat
|
||||
const currentItem = previous.categories.find((x) => x.id === category.id);
|
||||
if (!currentItem) return previous;
|
||||
// Find the main wrapper
|
||||
const mainWrapper = previous.wrappers.find((x) => x.position === 1);
|
||||
const mainWrapper = previous.wrappers.find((x) => x.position === 0);
|
||||
const mainWrapperId = mainWrapper?.id ?? 'default';
|
||||
|
||||
// Check that the app has an area.type or "category" and that the area.id is the current category
|
||||
const appsToMove = previous.apps.filter(
|
||||
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
|
||||
);
|
||||
appsToMove.forEach((x) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
|
||||
});
|
||||
const isAppAffectedFilter = (app: AppType): boolean => {
|
||||
if (!app.area) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const widgetsToMove = previous.widgets.filter(
|
||||
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
|
||||
);
|
||||
if (app.area.type !== 'category') {
|
||||
return false;
|
||||
}
|
||||
|
||||
widgetsToMove.forEach((x) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
|
||||
});
|
||||
if (app.area.properties.id === mainWrapperId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return app.area.properties.id === currentItem.id;
|
||||
};
|
||||
|
||||
const isWidgetAffectedFilter = (widget: IWidget<string, any>): boolean => {
|
||||
if (!widget.area) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (widget.area.type !== 'category') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (widget.area.properties.id === mainWrapperId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return widget.area.properties.id === currentItem.id;
|
||||
};
|
||||
|
||||
return {
|
||||
...previous,
|
||||
apps: previous.apps,
|
||||
apps: [
|
||||
...previous.apps.filter((x) => !isAppAffectedFilter(x)),
|
||||
...previous.apps
|
||||
.filter((x) => isAppAffectedFilter(x))
|
||||
.map((app): AppType => ({
|
||||
...app,
|
||||
area: {
|
||||
...app.area,
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
...app.area.properties,
|
||||
id: mainWrapperId,
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
widgets: [
|
||||
...previous.widgets.filter((widget) => !isWidgetAffectedFilter(widget)),
|
||||
...previous.widgets
|
||||
.filter((widget) => isWidgetAffectedFilter(widget))
|
||||
.map((widget): IWidget<string, any> => ({
|
||||
...widget,
|
||||
area: {
|
||||
...widget.area,
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
...widget.area.properties,
|
||||
id: mainWrapperId,
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
categories: previous.categories.filter((x) => x.id !== category.id),
|
||||
wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user