Form updates

This commit is contained in:
Sean Edmunds
2025-06-13 12:53:17 -05:00
parent 29514d3290
commit c3530a23a4
10 changed files with 472 additions and 436 deletions

26
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@mantine/form": "^8.0.2",
"@mantine/hooks": "^8.0.2", "@mantine/hooks": "^8.0.2",
"@mantine/notifications": "^8.0.2", "@mantine/notifications": "^8.0.2",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.80.6",
@@ -943,6 +944,19 @@
"react-dom": "^18.x || ^19.x" "react-dom": "^18.x || ^19.x"
} }
}, },
"node_modules/@mantine/form": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.1.0.tgz",
"integrity": "sha512-bCLdeWRbuYfG7l5b7nSKYEdWEWLOl55g/dyifRjFEr9pOK9ZNCj7oCOtXp0Lk1QJGqopZoaKxb5mvajEwDFXoQ==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"klona": "^2.0.6"
},
"peerDependencies": {
"react": "^18.x || ^19.x"
}
},
"node_modules/@mantine/hooks": { "node_modules/@mantine/hooks": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.2.tgz",
@@ -2713,8 +2727,7 @@
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"dev": true
}, },
"node_modules/fast-equals": { "node_modules/fast-equals": {
"version": "5.2.2", "version": "5.2.2",
@@ -3105,6 +3118,15 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/klona": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/kolorist": { "node_modules/kolorist": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",

View File

@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@mantine/form": "^8.0.2",
"@mantine/hooks": "^8.0.2", "@mantine/hooks": "^8.0.2",
"@mantine/notifications": "^8.0.2", "@mantine/notifications": "^8.0.2",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.80.6",

View File

@@ -71,36 +71,29 @@ export function useMutationAPI() {
const response = await fetch(`${url}/${endpoint}`, options); const response = await fetch(`${url}/${endpoint}`, options);
if (!response.ok) { if (!response.ok) {
const responseData = await response.json(); const responseData = await response.json();
if (typeof responseData.detail !== 'string') { if (typeof responseData === 'object') {
const errorResponse = JSON.stringify({ if (responseData.error.code === 'ER_DUP_ENTRY') {
status: response.status, setTimeout(() => {
statusText: response.statusText, notifications.update({
method: options.method, id: id,
endpoint: endpoint, loading: false,
msg: responseData.detail[0]?.msg, title: (
loc: responseData.detail[0]?.loc, <Text fw={600} c={'var(--mantine-color-accent-0)'}>
type: responseData.detail[0]?.type, {method} Failure!
</Text>
),
message: (
<Text c={'var(--mantine-color-primary-0)'}>
Entry with that name already exists.
</Text>
),
autoClose: 2500,
icon: <FaCheck size={24} />,
}); });
throw new Error(errorResponse); }, 2500);
} }
if (typeof responseData.detail === 'object') {
const errorResponse = JSON.stringify({
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail.message,
});
throw new Error(errorResponse);
} }
const errorResponse = JSON.stringify({ return await responseData;
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail,
});
throw new Error(errorResponse);
} }
setTimeout(() => { setTimeout(() => {
notifications.update({ notifications.update({

View File

@@ -247,8 +247,15 @@ function SettingsButton() {
color: 'var(--mantine-color-primary-0)', color: 'var(--mantine-color-primary-0)',
}} }}
onClick={() => { onClick={() => {
setCurrentThemePlaylist('All'); const themeList = JSON.parse(
handleThemeChange(allThemes, true); themePlaylistData?.find(
(t: ThemePlaylist) => t?.name === currentThemePlaylist,
)?.themeList ?? '[]',
);
const filteredThemes = allThemes.filter((t: Theme) =>
themeList.includes(t.id),
);
handleThemeChange(filteredThemes, false);
}} }}
> >
<FaDice /> <FaDice />

View File

@@ -14,6 +14,7 @@ import {
Text, Text,
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -36,10 +37,19 @@ const ApplicationSettings = () => {
url: '', url: '',
icon: '', icon: '',
baseUrl: '', baseUrl: '',
filterName: '', filterName: 'Default',
index: 0, index: 0,
enabled: 1, enabled: 1,
}; };
const form = useForm({
initialValues: defaultApplication,
validate: {
name: isNotEmpty('Name is required'),
url: isNotEmpty('URL is required'),
icon: isNotEmpty('Icon is required'),
filterName: isNotEmpty('Filter name is required'),
},
});
const appsEndpoint = 'api/applications'; const appsEndpoint = 'api/applications';
const appsOrderEndpoint = 'api/applicationOrder'; const appsOrderEndpoint = 'api/applicationOrder';
const { data: applicationData, isFetching: isAppsFetching } = useQueryAPI({ const { data: applicationData, isFetching: isAppsFetching } = useQueryAPI({
@@ -48,7 +58,6 @@ const ApplicationSettings = () => {
const [applications, setApplications] = useState<Application[]>([]); const [applications, setApplications] = useState<Application[]>([]);
const [applicationList, setApplicationList] = useState<Application[]>([]); const [applicationList, setApplicationList] = useState<Application[]>([]);
const { isMobile, reorder } = useCommonHooks(); const { isMobile, reorder } = useCommonHooks();
const [newApplication, setNewApplication] = useState<Application>(defaultApplication);
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
@@ -80,7 +89,7 @@ const ApplicationSettings = () => {
const handleClose = () => { const handleClose = () => {
closeModal(); closeModal();
setNewApplication(defaultApplication); form.reset();
}; };
useEffect(() => { useEffect(() => {
if (applicationData && !isAppsFetching) { if (applicationData && !isAppsFetching) {
@@ -116,52 +125,57 @@ const ApplicationSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new application'} title={'Add a new application'}
body={ body={
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: appsEndpoint,
body: {
...values,
index: applications.length,
},
});
handleClose();
})}
>
<Stack gap={0} align='center'> <Stack gap={0} align='center'>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<TextInput <TextInput
key={form.key('name')}
{...form.getInputProps('name')}
label={'Name'} label={'Name'}
placeholder='Nextcloud'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
w={'45%'} w={'45%'}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newApplication.name} withAsterisk
onChange={(event) => {
setNewApplication({
...newApplication,
name: event.target.value,
});
}}
/> />
<TextInput <TextInput
key={form.key('baseUrl')}
{...form.getInputProps('baseUrl')}
label={'Base URL'} label={'Base URL'}
placeholder='/nextcloud'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
w={'45%'} w={'45%'}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newApplication.baseUrl}
onChange={(event) => {
setNewApplication({
...newApplication,
baseUrl: event.target.value,
});
}}
/> />
</Group> </Group>
<Group w={isMobile ? '94%' : '92%'} justify='center'> <Group w={isMobile ? '94%' : '92%'} justify='center'>
<TextInput <TextInput
key={form.key('url')}
{...form.getInputProps('url')}
label={'URL'} label={'URL'}
placeholder='https://domain.example.com/nextcloud'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
w={'100%'} w={'100%'}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newApplication.url} withAsterisk
onChange={(event) => {
setNewApplication({
...newApplication,
url: event.target.value,
});
}}
/> />
</Group> </Group>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<Select <Select
key={form.key('icon')}
{...form.getInputProps('icon')}
label={'Icon'} label={'Icon'}
className={selectClasses.settingsItem} className={selectClasses.settingsItem}
classNames={{ classNames={{
@@ -178,16 +192,9 @@ const ApplicationSettings = () => {
data={Object.keys(allIcons).map((icon) => { data={Object.keys(allIcons).map((icon) => {
return { value: icon, label: icon }; return { value: icon, label: icon };
})} })}
defaultValue={newApplication.icon}
comboboxProps={{ comboboxProps={{
zIndex: 3000, zIndex: 3000,
}} }}
onChange={(value: string | null) => {
setNewApplication({
...newApplication,
icon: value ?? '',
});
}}
renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => { renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
const Icon = allIcons[item.option.value]; const Icon = allIcons[item.option.value];
return ( return (
@@ -201,40 +208,28 @@ const ApplicationSettings = () => {
maxDropdownHeight={175} maxDropdownHeight={175}
onDropdownOpen={() => setIsOpen(true)} onDropdownOpen={() => setIsOpen(true)}
onDropdownClose={() => setIsOpen(false)} onDropdownClose={() => setIsOpen(false)}
withAsterisk
/> />
<TextInput <TextInput
key={form.key('filter')}
{...form.getInputProps('filter')}
label={'Filter'} label={'Filter'}
placeholder='Storage'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
pt={'1rem'} pt={'1rem'}
w={'45%'} w={'45%'}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newApplication.filterName} defaultValue={defaultApplication.filterName}
onChange={(event) => { withAsterisk
setNewApplication({
...newApplication,
filterName: event.target.value,
});
}}
/> />
</Group> </Group>
<Divider color='black' my='lg' /> <Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}> <Group pos={'absolute'} bottom={8}>
<Button <Button
type='submit'
color={'green'} color={'green'}
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => {
if (newApplication) {
mutateAsync({
method: 'POST',
endpoint: appsEndpoint,
body: {
...newApplication,
index: applications.length,
},
}).then(handleClose);
}
}}
> >
Save Save
</Button> </Button>
@@ -247,6 +242,7 @@ const ApplicationSettings = () => {
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form>
} }
/> />
<SettingsHeader <SettingsHeader

View File

@@ -129,6 +129,7 @@ function DebouncedApplication({
name: event.target.value, name: event.target.value,
}); });
}} }}
disabled
/> />
<TextInput <TextInput
label={'Base URL'} label={'Base URL'}

View File

@@ -1,5 +1,6 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { Box, Button, Card, Divider, Group, Stack, Text, TextInput } from '@mantine/core'; import { Box, Button, Card, Divider, Group, Stack, Text, TextInput } from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -16,6 +17,13 @@ const BookmarkSettings = () => {
categoryIndex: 0, categoryIndex: 0,
filterName: '', filterName: '',
}; };
const form = useForm({
initialValues: defaultGroup,
validate: {
categoryName: isNotEmpty('Category name is required'),
filterName: isNotEmpty('Filter name is required'),
},
});
const bookmarksEndpoint = 'api/bookmarks'; const bookmarksEndpoint = 'api/bookmarks';
const { data: bookmarkData, isFetched: bookmarksIsFetched } = useQueryAPI({ const { data: bookmarkData, isFetched: bookmarksIsFetched } = useQueryAPI({
endpoint: bookmarksEndpoint, endpoint: bookmarksEndpoint,
@@ -27,7 +35,6 @@ const BookmarkSettings = () => {
}); });
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const [groups, setGroups] = useState<BookmarkGroup[]>([]); const [groups, setGroups] = useState<BookmarkGroup[]>([]);
const [newGroup, setNewGroup] = useState<BookmarkGroup>(defaultGroup);
const [groupList, setGroupList] = useState<BookmarkGroup[]>([]); const [groupList, setGroupList] = useState<BookmarkGroup[]>([]);
const { isMobile, reorder } = useCommonHooks(); const { isMobile, reorder } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
@@ -37,8 +44,8 @@ const BookmarkSettings = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleClose = () => { const handleClose = () => {
setNewGroup(defaultGroup);
closeModal(); closeModal();
form.reset();
}; };
function onDragEnd(result: any) { function onDragEnd(result: any) {
@@ -118,49 +125,47 @@ const BookmarkSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new Bookmark Group'} title={'Add a new Bookmark Group'}
body={ body={
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: categoriesEndpoint,
body: {
...values,
categoryIndex: groups.length,
},
});
handleClose();
})}
>
<Stack gap={0} w={'100%'} align='center'> <Stack gap={0} w={'100%'} align='center'>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<TextInput <TextInput
key={form.key('categoryName')}
{...form.getInputProps('categoryName')}
label={'Category Name'} label={'Category Name'}
placeholder='Shopping'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newGroup.categoryName} withAsterisk
onChange={(event) => {
setNewGroup({
...newGroup,
categoryName: event.target.value,
});
}}
/> />
<TextInput <TextInput
key={form.key('filterName')}
{...form.getInputProps('filterName')}
label={'Filter Name'} label={'Filter Name'}
placeholder='Personal'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newGroup.filterName} withAsterisk
onChange={(event) => {
setNewGroup({
...newGroup,
filterName: event.target.value,
});
}}
/> />
</Group> </Group>
<Divider color='black' my='lg' /> <Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}> <Group pos={'absolute'} bottom={8}>
<Button <Button
type='submit'
color={'green'} color={'green'}
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => {
if (newGroup) {
mutateAsync({
method: 'POST',
endpoint: categoriesEndpoint,
body: { ...newGroup, categoryIndex: groups.length },
});
handleClose();
}
}}
> >
Save Save
</Button> </Button>
@@ -173,6 +178,7 @@ const BookmarkSettings = () => {
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form>
} }
/> />
<SettingsHeader <SettingsHeader

View File

@@ -17,6 +17,7 @@ import {
TextInput, TextInput,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI } from 'hooks'; import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -47,6 +48,13 @@ function DebouncedGroupSettingsItem({
category: group.categoryName, category: group.categoryName,
index: 0, index: 0,
}); });
const form = useForm({
initialValues: newBookmark,
validate: {
name: isNotEmpty('Name is required'),
url: isNotEmpty('URL is required'),
},
});
const [bookmarkList, setBookmarkList] = useState<Bookmark[]>(bookmarks); const [bookmarkList, setBookmarkList] = useState<Bookmark[]>(bookmarks);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -193,7 +201,7 @@ function DebouncedGroupSettingsItem({
borderRadius: '100%', borderRadius: '100%',
backgroundColor: 'var(--mantine-color-accent-0)', backgroundColor: 'var(--mantine-color-accent-0)',
position: 'absolute', position: 'absolute',
top: isMobile ? 140 : 78, top: isMobile ? 140 : 92,
right: 18, right: 18,
zIndex: 10, zIndex: 10,
fontSize: '1.25rem', fontSize: '1.25rem',
@@ -307,10 +315,24 @@ function DebouncedGroupSettingsItem({
</Popover.Target> </Popover.Target>
<Popover.Dropdown w={isMobile ? '90%' : undefined}> <Popover.Dropdown w={isMobile ? '90%' : undefined}>
<Card className={cardClasses.settingsItem} classNames={cardClasses}> <Card className={cardClasses.settingsItem} classNames={cardClasses}>
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: 'api/bookmarks',
body: { ...values, index: bookmarks.length },
});
setPopoverOpened(false);
form.reset();
})}
>
<Stack w={'100%'} display={'flex'} pos={'relative'}> <Stack w={'100%'} display={'flex'} pos={'relative'}>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<TextInput <TextInput
key={form.key('name')}
{...form.getInputProps('name')}
label={'Name'} label={'Name'}
placeholder='Google'
labelProps={{ labelProps={{
style: { style: {
color: 'var(--mantine-color-primary-0)', color: 'var(--mantine-color-primary-0)',
@@ -319,14 +341,11 @@ function DebouncedGroupSettingsItem({
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
pt={'1rem'} pt={'1rem'}
classNames={textInputClasses} classNames={textInputClasses}
onChange={(event) => { withAsterisk
setNewBookmark({
...newBookmark,
name: event.target.value,
});
}}
/> />
<TextInput <TextInput
key={form.key('category')}
{...form.getInputProps('category')}
label={'Category'} label={'Category'}
labelProps={{ labelProps={{
style: { style: {
@@ -338,17 +357,14 @@ function DebouncedGroupSettingsItem({
classNames={textInputClasses} classNames={textInputClasses}
disabled disabled
defaultValue={group.categoryName} defaultValue={group.categoryName}
onChange={(event) => {
setNewBookmark({
...newBookmark,
category: event.target.value,
});
}}
/> />
</Group> </Group>
<Group w={'100%'} justify='center' mb={60}> <Group w={'100%'} justify='center' mb={60}>
<TextInput <TextInput
key={form.key('url')}
{...form.getInputProps('url')}
label={'URL'} label={'URL'}
placeholder='https://google.com'
labelProps={{ labelProps={{
style: { style: {
color: 'var(--mantine-color-primary-0)', color: 'var(--mantine-color-primary-0)',
@@ -358,12 +374,7 @@ function DebouncedGroupSettingsItem({
w={'100%'} w={'100%'}
pt={'1rem'} pt={'1rem'}
classNames={textInputClasses} classNames={textInputClasses}
onChange={(event) => { withAsterisk
setNewBookmark({
...newBookmark,
url: event.target.value,
});
}}
/> />
</Group> </Group>
<Group <Group
@@ -374,18 +385,9 @@ function DebouncedGroupSettingsItem({
justify='center' justify='center'
> >
<Button <Button
type='submit'
color={'green'} color={'green'}
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => {
if (newBookmark) {
mutateAsync({
method: 'POST',
endpoint: 'api/bookmarks',
body: { ...newBookmark, index: bookmarks.length },
});
setPopoverOpened(false);
}
}}
> >
Save Save
</Button> </Button>
@@ -394,12 +396,14 @@ function DebouncedGroupSettingsItem({
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => { onClick={() => {
setPopoverOpened(false); setPopoverOpened(false);
form.reset();
}} }}
> >
Cancel Cancel
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form>
</Card> </Card>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>

View File

@@ -9,6 +9,7 @@ import {
Stack, Stack,
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -27,6 +28,14 @@ const SearchProviderSettings = () => {
url: '', url: '',
prefix: '', prefix: '',
}; };
const form = useForm({
initialValues: defaultProvider,
validate: {
name: isNotEmpty('Name is required'),
url: isNotEmpty('URL is required'),
prefix: isNotEmpty('Prefix is required'),
},
});
const endpoint = 'api/providers'; const endpoint = 'api/providers';
const { data: providerData, isFetched: providerDataIsFetched } = useQueryAPI({ const { data: providerData, isFetched: providerDataIsFetched } = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
@@ -34,7 +43,6 @@ const SearchProviderSettings = () => {
const [providers, setProviders] = useState<Provider[]>([]); const [providers, setProviders] = useState<Provider[]>([]);
const { isMobile } = useCommonHooks(); const { isMobile } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [newProvider, setNewProvider] = useState<Provider>(defaultProvider);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
@@ -42,8 +50,8 @@ const SearchProviderSettings = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleClose = () => { const handleClose = () => {
setNewProvider(defaultProvider);
closeModal(); closeModal();
form.reset();
}; };
useEffect(() => { useEffect(() => {
@@ -70,62 +78,55 @@ const SearchProviderSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new Search Provider'} title={'Add a new Search Provider'}
body={ body={
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: values,
});
handleClose();
})}
>
<Stack gap={0} w={'100%'} align='center'> <Stack gap={0} w={'100%'} align='center'>
<Stack w={'100%'} align='center'> <Stack w={'100%'} align='center'>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<TextInput <TextInput
key={form.key('name')}
{...form.getInputProps('name')}
label={'Name'} label={'Name'}
placeholder='Google'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newProvider.name} withAsterisk
onChange={(event) => {
setNewProvider({
...newProvider,
name: event.target.value,
});
}}
/> />
<TextInput <TextInput
key={form.key('prefix')}
{...form.getInputProps('prefix')}
label={'Prefix'} label={'Prefix'}
placeholder='/g'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newProvider.prefix} withAsterisk
onChange={(event) => {
setNewProvider({
...newProvider,
prefix: event.target.value,
});
}}
/> />
</Group> </Group>
<TextInput <TextInput
key={form.key('url')}
{...form.getInputProps('url')}
label={'Url'} label={'Url'}
placeholder='https://www.google.com/search?q='
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
w={isMobile ? '64%' : '100%'} w={'64%'}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newProvider.url} withAsterisk
onChange={(event) => {
setNewProvider({
...newProvider,
url: event.target.value,
});
}}
/> />
</Stack> </Stack>
<Divider color='black' my='lg' /> <Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}> <Group pos={'absolute'} bottom={8}>
<Button <Button
type='submit'
color={'green'} color={'green'}
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => {
if (newProvider) {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: newProvider,
}).then(handleClose);
}
}}
> >
Save Save
</Button> </Button>
@@ -138,6 +139,7 @@ const SearchProviderSettings = () => {
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form>
} }
/> />
<SettingsHeader <SettingsHeader

View File

@@ -8,6 +8,7 @@ import {
Stack, Stack,
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
import { isNotEmpty, matches, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -25,6 +26,16 @@ const WolClientSettings = () => {
hostname: '', hostname: '',
macAddress: '', macAddress: '',
}; };
const form = useForm({
initialValues: defaultWolClient,
validate: {
hostname: isNotEmpty('Hostname is required'),
macAddress: matches(
/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/,
'Invalid MAC Address',
),
},
});
const endpoint = 'api/wolClients'; const endpoint = 'api/wolClients';
const { data: wolClientsData, isFetched: wolClientsIsFetched } = useQueryAPI({ const { data: wolClientsData, isFetched: wolClientsIsFetched } = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
@@ -32,7 +43,6 @@ const WolClientSettings = () => {
const [wolClients, setWolClients] = useState<WolClient[]>([]); const [wolClients, setWolClients] = useState<WolClient[]>([]);
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const { isMobile } = useCommonHooks(); const { isMobile } = useCommonHooks();
const [newWolClient, setNewWolClient] = useState<WolClient>(defaultWolClient);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
@@ -40,8 +50,8 @@ const WolClientSettings = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleClose = () => { const handleClose = () => {
setNewWolClient(defaultWolClient);
closeModal(); closeModal();
form.reset();
}; };
useEffect(() => { useEffect(() => {
@@ -64,62 +74,56 @@ const WolClientSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new WoL Client'} title={'Add a new WoL Client'}
body={ body={
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: values,
});
handleClose();
})}
>
<Stack gap={0} w={'100%'} align='center'> <Stack gap={0} w={'100%'} align='center'>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<TextInput <TextInput
key={form.key('hostname')}
{...form.getInputProps('hostname')}
label={'Hostname'} label={'Hostname'}
placeholder='Workstation'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newWolClient.hostname} withAsterisk
onChange={(event) => {
setNewWolClient({
...newWolClient,
hostname: event.target.value,
});
}}
/> />
<TextInput <TextInput
key={form.key('macAddress')}
{...form.getInputProps('macAddress')}
label={'MAC Address'} label={'MAC Address'}
placeholder='00:00:00:00:00:00'
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newWolClient.macAddress} withAsterisk
onChange={(event) => {
setNewWolClient({
...newWolClient,
macAddress: event.target.value,
});
}}
/> />
</Group> </Group>
<Divider color='black' my='lg' /> <Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}> <Group pos={'absolute'} bottom={8}>
<Button <Button
type='submit'
color={'green'} color={'green'}
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => {
if (newWolClient) {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: newWolClient,
});
closeModal();
}
}}
> >
Save Save
</Button> </Button>
<Button <Button
color={'red'} color={'red'}
styles={{ label: { color: 'black' } }} styles={{ label: { color: 'black' } }}
onClick={() => { onClick={handleClose}
closeModal();
}}
> >
Cancel Cancel
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form>
} }
/> />
<SettingsHeader <SettingsHeader