mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 23:45:48 +01:00
feat: add form error icon for tabs
This commit is contained in:
20
package.json
20
package.json
@@ -32,16 +32,16 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.0",
|
"@dnd-kit/utilities": "^3.2.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@mantine/carousel": "^5.9.0",
|
"@mantine/carousel": "^5.9.2",
|
||||||
"@mantine/core": "^5.9.0",
|
"@mantine/core": "^5.9.2",
|
||||||
"@mantine/dates": "^5.9.0",
|
"@mantine/dates": "^5.9.2",
|
||||||
"@mantine/dropzone": "^5.9.0",
|
"@mantine/dropzone": "^5.9.2",
|
||||||
"@mantine/form": "^5.9.0",
|
"@mantine/form": "^5.9.2",
|
||||||
"@mantine/hooks": "^5.9.0",
|
"@mantine/hooks": "^5.9.2",
|
||||||
"@mantine/modals": "^5.9.0",
|
"@mantine/modals": "^5.9.2",
|
||||||
"@mantine/next": "^5.9.0",
|
"@mantine/next": "^5.9.2",
|
||||||
"@mantine/notifications": "^5.9.0",
|
"@mantine/notifications": "^5.9.2",
|
||||||
"@mantine/prism": "^5.9.0",
|
"@mantine/prism": "^5.9.2",
|
||||||
"@nivo/core": "^0.79.0",
|
"@nivo/core": "^0.79.0",
|
||||||
"@nivo/line": "^0.79.1",
|
"@nivo/line": "^0.79.1",
|
||||||
"@tabler/icons": "^1.106.0",
|
"@tabler/icons": "^1.106.0",
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { Alert, Button, createStyles, Group, Stack, Tabs, Text } 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 { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import { IconAccessPoint, IconAdjustments, IconBrush, IconClick, IconPlug } from '@tabler/icons';
|
import {
|
||||||
|
IconAccessPoint,
|
||||||
|
IconAdjustments,
|
||||||
|
IconAlertTriangle,
|
||||||
|
IconBrush,
|
||||||
|
IconClick,
|
||||||
|
IconPlug,
|
||||||
|
} from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -44,17 +51,27 @@ export const EditServiceModal = ({
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appearance: (appearance) => (!appearance.iconUrl ? 'Icon is required' : null),
|
appearance: {
|
||||||
behaviour: (behaviour) => {
|
iconUrl: (url: string) => {
|
||||||
if (behaviour.onClickUrl === undefined || behaviour.onClickUrl.length < 1) {
|
if (url.length < 1) {
|
||||||
|
return 'This field is required';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
behaviour: {
|
||||||
|
onClickUrl: (url: string) => {
|
||||||
|
if (url === undefined || url.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!behaviour.onClickUrl?.match(serviceUrlRegex)) {
|
if (!url.match(serviceUrlRegex)) {
|
||||||
return 'Uri override is not a valid uri';
|
return 'Uri override is not a valid uri';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validateInputOnChange: true,
|
validateInputOnChange: true,
|
||||||
@@ -102,6 +119,18 @@ export const EditServiceModal = ({
|
|||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validationErrors = Object.keys(form.errors);
|
||||||
|
|
||||||
|
const ValidationErrorIndicator = ({ keys }: { keys: string[] }) => {
|
||||||
|
const relevantErrors = validationErrors.filter((x) => keys.includes(x));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeIcon opacity={relevantErrors.length === 0 ? 0 : 1} color="red" variant="light">
|
||||||
|
<IconAlertTriangle size={15} />
|
||||||
|
</ThemeIcon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{configName === undefined ||
|
{configName === undefined ||
|
||||||
@@ -130,6 +159,7 @@ export const EditServiceModal = ({
|
|||||||
{form.values.name ?? 'New Service'}
|
{form.values.name ?? 'New Service'}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
@@ -137,19 +167,39 @@ export const EditServiceModal = ({
|
|||||||
defaultValue="general"
|
defaultValue="general"
|
||||||
>
|
>
|
||||||
<Tabs.List grow>
|
<Tabs.List grow>
|
||||||
<Tabs.Tab value="general" icon={<IconAdjustments size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={['name', 'url']} />}
|
||||||
|
icon={<IconAdjustments size={14} />}
|
||||||
|
value="general"
|
||||||
|
>
|
||||||
General
|
General
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="behaviour" icon={<IconClick size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />}
|
||||||
|
icon={<IconClick size={14} />}
|
||||||
|
value="behaviour"
|
||||||
|
>
|
||||||
Behaviour
|
Behaviour
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="network" icon={<IconAccessPoint size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={[]} />}
|
||||||
|
icon={<IconAccessPoint size={14} />}
|
||||||
|
value="network"
|
||||||
|
>
|
||||||
Network
|
Network
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="appearance" icon={<IconBrush size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />}
|
||||||
|
icon={<IconBrush size={14} />}
|
||||||
|
value="appearance"
|
||||||
|
>
|
||||||
Appearance
|
Appearance
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="integration" icon={<IconPlug size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={[]} />}
|
||||||
|
icon={<IconPlug size={14} />}
|
||||||
|
value="integration"
|
||||||
|
>
|
||||||
Integration
|
Integration
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
@@ -162,10 +212,10 @@ export const EditServiceModal = ({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Group position="right" mt={100}>
|
<Group position="right" mt={100}>
|
||||||
<Button px={50} variant="light" color="gray" onClick={tryCloseModal}>
|
<Button onClick={tryCloseModal} px={50} variant="light" color="gray">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" px={50}>
|
<Button disabled={!form.isValid()} px={50} type="submit">
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconPhoto } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { ServiceType } from '../../../../../../types/service';
|
||||||
import { IconSelector } from './IconSelector/IconSelector';
|
import { IconSelector } from './IconSelector/IconSelector';
|
||||||
@@ -13,14 +13,22 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
|
|||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
const Image = () => {
|
const PreviewImage = () => {
|
||||||
if (form.values.appearance.iconUrl !== undefined) {
|
if (form.values.appearance.iconUrl !== undefined && form.values.appearance.iconUrl.length > 0) {
|
||||||
// disabled due to too many dynamic targets for next image cache
|
// disabled due to too many dynamic targets for next image cache
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
return <img className={classes.iconImage} src={form.values.appearance.iconUrl} alt="jife" />;
|
return <img className={classes.iconImage} src={form.values.appearance.iconUrl} alt="" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <IconPhoto />;
|
return (
|
||||||
|
<Image
|
||||||
|
src="/imgs/logo/logo.png"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
objectFit="contain"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +37,7 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
defaultValue={form.values.appearance.iconUrl}
|
defaultValue={form.values.appearance.iconUrl}
|
||||||
className={classes.textInput}
|
className={classes.textInput}
|
||||||
icon={<Image />}
|
icon={<PreviewImage />}
|
||||||
label="Service Icon"
|
label="Service Icon"
|
||||||
description="Logo of your service displayed in your dashboard. Must return a body content containg an image"
|
description="Logo of your service displayed in your dashboard. Must return a body content containg an image"
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => {
|
|||||||
<Flex gap={4} wrap="wrap" pr={15}>
|
<Flex gap={4} wrap="wrap" pr={15}>
|
||||||
{slicedFilteredItems.map((item) => (
|
{slicedFilteredItems.map((item) => (
|
||||||
<ActionIcon onClick={() => onChange(item)} size={40} p={3}>
|
<ActionIcon onClick={() => onChange(item)} size={40} p={3}>
|
||||||
<img className={classes.icon} src={item.url} alt="icon from repository" />
|
<img className={classes.icon} src={item.url} alt="" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
Reference in New Issue
Block a user