mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
✨ Add propagation for service name to service icon
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
|
import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { ContextModalProps } from '@mantine/modals';
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
|
||||||
import {
|
import {
|
||||||
IconAccessPoint,
|
IconAccessPoint,
|
||||||
IconAdjustments,
|
IconAdjustments,
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
IconPlug,
|
IconPlug,
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Image from 'next/image';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
@@ -31,11 +29,13 @@ export const EditServiceModal = ({
|
|||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<{ service: ServiceType }>) => {
|
}: ContextModalProps<{ service: ServiceType; allowServiceNamePropagation: boolean }>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { classes } = useStyles();
|
|
||||||
const { name: configName, config } = useConfigContext();
|
const { name: configName, config } = useConfigContext();
|
||||||
const updateConfig = useConfigStore((store) => store.updateConfig);
|
const updateConfig = useConfigStore((store) => store.updateConfig);
|
||||||
|
const [allowServiceNamePropagation, setAllowServiceNamePropagation] = useState<boolean>(
|
||||||
|
innerProps.allowServiceNamePropagation
|
||||||
|
);
|
||||||
|
|
||||||
const form = useForm<ServiceType>({
|
const form = useForm<ServiceType>({
|
||||||
initialValues: innerProps.service,
|
initialValues: innerProps.service,
|
||||||
@@ -94,29 +94,7 @@ export const EditServiceModal = ({
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general');
|
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general');
|
||||||
|
|
||||||
const tryCloseModal = () => {
|
const closeModal = () => {
|
||||||
if (form.isDirty()) {
|
|
||||||
showNotification({
|
|
||||||
id: 'unsaved-edit-service-modal-changes',
|
|
||||||
title: 'You have unsaved changes',
|
|
||||||
message: (
|
|
||||||
<Stack>
|
|
||||||
<Text color="dimmed">If you close, your changes will be discarded and not saved.</Text>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
context.closeModal(id);
|
|
||||||
hideNotification('unsaved-edit-service-modal-changes');
|
|
||||||
}}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
Close anyway
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,12 +174,16 @@ export const EditServiceModal = ({
|
|||||||
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
|
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
|
||||||
<BehaviourTab form={form} />
|
<BehaviourTab form={form} />
|
||||||
<NetworkTab form={form} />
|
<NetworkTab form={form} />
|
||||||
<AppearanceTab form={form} />
|
<AppearanceTab
|
||||||
|
form={form}
|
||||||
|
disallowServiceNameProgagation={() => setAllowServiceNamePropagation(false)}
|
||||||
|
allowServiceNamePropagation={allowServiceNamePropagation}
|
||||||
|
/>
|
||||||
<IntegrationTab form={form} />
|
<IntegrationTab form={form} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Group position="right" mt={100}>
|
<Group position="right" mt={100}>
|
||||||
<Button onClick={tryCloseModal} px={50} variant="light" color="gray">
|
<Button onClick={closeModal} px={50} variant="light" color="gray">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={!form.isValid()} px={50} type="submit">
|
<Button disabled={!form.isValid()} px={50} type="submit">
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ import { IconSelector } from './IconSelector/IconSelector';
|
|||||||
|
|
||||||
interface AppearanceTabProps {
|
interface AppearanceTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
||||||
|
disallowServiceNameProgagation: () => void;
|
||||||
|
allowServiceNamePropagation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppearanceTab = ({ form }: AppearanceTabProps) => {
|
export const AppearanceTab = ({
|
||||||
|
form,
|
||||||
|
disallowServiceNameProgagation,
|
||||||
|
allowServiceNamePropagation,
|
||||||
|
}: AppearanceTabProps) => {
|
||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
@@ -28,13 +34,16 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
|
|||||||
{...form.getInputProps('appearance.iconUrl')}
|
{...form.getInputProps('appearance.iconUrl')}
|
||||||
/>
|
/>
|
||||||
<IconSelector
|
<IconSelector
|
||||||
onChange={(item) =>
|
onChange={(item) => {
|
||||||
form.setValues({
|
form.setValues({
|
||||||
appearance: {
|
appearance: {
|
||||||
iconUrl: item.url,
|
iconUrl: item.url,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
disallowServiceNameProgagation();
|
||||||
|
}}
|
||||||
|
allowServiceNamePropagation={allowServiceNamePropagation}
|
||||||
|
form={form}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|||||||
@@ -13,22 +13,32 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { IconSearch, IconX } from '@tabler/icons';
|
import { IconSearch, IconX } from '@tabler/icons';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants';
|
import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants';
|
||||||
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
|
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
|
||||||
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
|
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
|
||||||
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
|
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
|
||||||
|
import { ServiceType } from '../../../../../../../types/service';
|
||||||
|
|
||||||
interface IconSelectorProps {
|
interface IconSelectorProps {
|
||||||
|
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
||||||
onChange: (icon: IconSelectorItem) => void;
|
onChange: (icon: IconSelectorItem) => void;
|
||||||
|
allowServiceNamePropagation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconSelector = ({ onChange }: IconSelectorProps) => {
|
export const IconSelector = ({
|
||||||
|
onChange,
|
||||||
|
allowServiceNamePropagation,
|
||||||
|
form,
|
||||||
|
}: IconSelectorProps) => {
|
||||||
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||||
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
||||||
converter: (item) => ({
|
converter: (item) => ({
|
||||||
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`,
|
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`,
|
||||||
|
fileName: item.name,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,8 +49,7 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => {
|
|||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceCharacters = (value: string) =>
|
const replaceCharacters = (value: string) => value.toLowerCase().replaceAll('', '-');
|
||||||
value.toLowerCase().replaceAll(' ', '').replaceAll('-', '');
|
|
||||||
|
|
||||||
const filteredItems = searchTerm
|
const filteredItems = searchTerm
|
||||||
? data.filter((x) => replaceCharacters(x.url).includes(replaceCharacters(searchTerm)))
|
? data.filter((x) => replaceCharacters(x.url).includes(replaceCharacters(searchTerm)))
|
||||||
@@ -49,6 +58,24 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => {
|
|||||||
const isTruncated =
|
const isTruncated =
|
||||||
slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length;
|
slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length;
|
||||||
|
|
||||||
|
const [debouncedValue] = useDebouncedValue(form.values.name, 500);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (allowServiceNamePropagation !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingDebouncedIcon = data.find(
|
||||||
|
(x) => replaceCharacters(x.fileName.split('.')[0]) === replaceCharacters(debouncedValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingDebouncedIcon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setFieldValue('appearance.iconUrl', matchingDebouncedIcon.url);
|
||||||
|
}, [debouncedValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover width={310}>
|
<Popover width={310}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Group, Tabs, Text, TextInput } from '@mantine/core';
|
import { Tabs, Text, TextInput } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconCursorText, IconLink } from '@tabler/icons';
|
import { IconCursorText, IconLink } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const AvailableElementTypes = ({
|
|||||||
name="Service"
|
name="Service"
|
||||||
icon={<IconBox size={40} strokeWidth={1.3} />}
|
icon={<IconBox size={40} strokeWidth={1.3} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openContextModalGeneric<{ service: ServiceType }>({
|
openContextModalGeneric<{ service: ServiceType; allowServiceNamePropagation: boolean }>(
|
||||||
|
{
|
||||||
modal: 'editService',
|
modal: 'editService',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service: {
|
service: {
|
||||||
@@ -62,9 +63,11 @@ export const AvailableElementTypes = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowServiceNamePropagation: true,
|
||||||
},
|
},
|
||||||
size: 'xl',
|
size: 'xl',
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ElementItem
|
<ElementItem
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ interface TileMenuProps {
|
|||||||
|
|
||||||
export const ServiceMenu = ({ service }: TileMenuProps) => {
|
export const ServiceMenu = ({ service }: TileMenuProps) => {
|
||||||
const handleClickEdit = () => {
|
const handleClickEdit = () => {
|
||||||
openContextModalGeneric<{ service: ServiceType }>({
|
openContextModalGeneric<{ service: ServiceType; allowServiceNamePropagation: boolean }>({
|
||||||
modal: 'editService',
|
modal: 'editService',
|
||||||
size: 'xl',
|
size: 'xl',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service,
|
service,
|
||||||
|
allowServiceNamePropagation: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export interface IconSelectorItem {
|
export interface IconSelectorItem {
|
||||||
url: string;
|
url: string;
|
||||||
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user