diff --git a/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx b/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx index 7eb11d9ba..2edea1680 100644 --- a/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx +++ b/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx @@ -1,24 +1,23 @@ -import { Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core'; +import { Alert, Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core'; import { useForm } from '@mantine/form'; import { ContextModalProps } from '@mantine/modals'; import { hideNotification, showNotification } from '@mantine/notifications'; -import { - IconAccessPoint, - IconAdjustments, - IconBrush, - IconClick, - IconDeviceFloppy, - IconDoorExit, - IconPlug, -} from '@tabler/icons'; +import { IconAccessPoint, IconAdjustments, IconBrush, IconClick, IconPlug } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import Image from 'next/image'; +import { useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; import { ServiceType } from '../../../../types/service'; import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab'; import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab'; import { GeneralTab } from './Tabs/GeneralTab/GeneralTab'; import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab'; import { NetworkTab } from './Tabs/NetworkTab/NetworkTab'; +import { EditServiceModalTab } from './Tabs/type'; + +const serviceUrlRegex = + '(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})'; export const EditServiceModal = ({ context, @@ -27,16 +26,55 @@ export const EditServiceModal = ({ }: ContextModalProps<{ service: ServiceType }>) => { const { t } = useTranslation(); const { classes } = useStyles(); + const { name: configName, config } = useConfigContext(); + const updateConfig = useConfigStore((store) => store.updateConfig); const form = useForm({ initialValues: innerProps.service, + validate: { + name: (name) => (!name ? 'Name is required' : null), + url: (url) => { + if (!url) { + return 'Url is required'; + } + + if (!url.match(serviceUrlRegex)) { + return 'Value is not a valid url'; + } + + return null; + }, + appearance: (appearance) => (!appearance.iconUrl ? 'Icon is required' : null), + behaviour: (behaviour) => { + if (behaviour.onClickUrl === undefined || behaviour.onClickUrl.length < 1) { + return null; + } + + if (!behaviour.onClickUrl?.match(serviceUrlRegex)) { + return 'Uri override is not a valid uri'; + } + + return null; + }, + }, + validateInputOnChange: true, }); const onSubmit = (values: ServiceType) => { - console.log('form submitted'); console.log(values); + + if (!configName) { + return; + } + + updateConfig(configName, (previousConfig) => ({ + ...previousConfig, + services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values], + })); }; + const [activeTab, setActiveTab] = useState('general'); + const tryCloseModal = () => { if (form.isDirty()) { showNotification({ @@ -65,6 +103,13 @@ export const EditServiceModal = ({ return ( <> + {configName === undefined || + (config === undefined && ( + + There was an unexpected problem loading the configuration. Functionality might be + restricted. Please report this incident. + + ))} {form.values.appearance.iconUrl ? ( // disabled because image target is too dynamic for next image cache @@ -85,7 +130,11 @@ export const EditServiceModal = ({
- + setActiveTab(tab as EditServiceModalTab)} + defaultValue="general" + > }> General @@ -104,7 +153,7 @@ export const EditServiceModal = ({ - + setActiveTab(targetTab)} /> @@ -112,16 +161,10 @@ export const EditServiceModal = ({ - - diff --git a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx index 048c2ed8f..c7c496d0e 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx @@ -31,6 +31,7 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => { className={classes.textInput} icon={} label="Service Icon" + description="Logo of your service displayed in your dashboard. Must return a body content containg an image" variant="default" withAsterisk required diff --git a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx index e6cbc82e9..bac318863 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx @@ -1,6 +1,7 @@ /* eslint-disable @next/next/no-img-element */ import { ActionIcon, + Button, createStyles, Divider, Flex, @@ -12,7 +13,7 @@ import { TextInput, Title, } from '@mantine/core'; -import { IconFlame, IconSearch, IconX } from '@tabler/icons'; +import { IconSearch, IconX } from '@tabler/icons'; import { useState } from 'react'; import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants'; import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery'; @@ -38,7 +39,12 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => { return ; } - const filteredItems = searchTerm ? data.filter((x) => x.url.includes(searchTerm)) : data; + const replaceCharacters = (value: string) => + value.toLowerCase().replaceAll(' ', '').replaceAll('-', ''); + + const filteredItems = searchTerm + ? data.filter((x) => replaceCharacters(x.url).includes(replaceCharacters(searchTerm))) + : data; const slicedFilteredItems = filteredItems.slice(0, ICON_PICKER_SLICE_LIMIT); const isTruncated = slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length; @@ -46,9 +52,13 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => { return ( - - - + @@ -76,7 +86,6 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => { {isTruncated && ( - Search is limited to {ICON_PICKER_SLICE_LIMIT} icons diff --git a/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx index 19e39aed0..4c760ee06 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx @@ -15,13 +15,17 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => { } label="On click url" - placeholder="Override the default service url when clicking on the service" + description="Overrides the service URL when clicking on the service" + placeholder="URL that should be opened instead when clicking on the service" variant="default" mb="md" {...form.getInputProps('behaviour.onClickUrl')} /> - + ); }; diff --git a/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx index 4971058c5..69e819403 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx @@ -1,32 +1,46 @@ -import { Tabs, TextInput } from '@mantine/core'; +import { Group, Tabs, Text, TextInput } from '@mantine/core'; import { UseFormReturnType } from '@mantine/form'; import { IconCursorText, IconLink } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { ServiceType } from '../../../../../../types/service'; +import { EditServiceModalTab } from '../type'; interface GeneralTabProps { form: UseFormReturnType ServiceType>; + openTab: (tab: EditServiceModalTab) => void; } -export const GeneralTab = ({ form }: GeneralTabProps) => { +export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { const { t } = useTranslation(''); return ( } label="Service name" + description="Used for displaying the service on the dashboard" placeholder="My example service" variant="default" mb="md" - required + withAsterisk {...form.getInputProps('name')} /> } label="Service url" + description={ + + + URL that will be opened when clicking on the service. Can be overwritten using + + openTab('behaviour')} variant="link"> + on click URL + + when using external URLs to enhance security. + + } placeholder="https://google.com" variant="default" - required + withAsterisk {...form.getInputProps('url')} /> diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx index 0e2176d07..4fa58bd6c 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx @@ -62,11 +62,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { return ( <> - - -