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>
throw new Error(errorResponse); ),
message: (
<Text c={'var(--mantine-color-primary-0)'}>
Entry with that name already exists.
</Text>
),
autoClose: 2500,
icon: <FaCheck size={24} />,
});
}, 2500);
}
} }
if (typeof responseData.detail === 'object') { return await responseData;
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({
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,137 +125,124 @@ const ApplicationSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new application'} title={'Add a new application'}
body={ body={
<Stack gap={0} align='center'> <form
<Group w={'100%'} justify='center'> onSubmit={form.onSubmit((values) => {
<TextInput mutateAsync({
label={'Name'} method: 'POST',
className={textInputClasses.settingsItem} endpoint: appsEndpoint,
w={'45%'} body: {
classNames={textInputClasses} ...values,
defaultValue={newApplication.name} index: applications.length,
onChange={(event) => { },
setNewApplication({ });
...newApplication, handleClose();
name: event.target.value, })}
}); >
}} <Stack gap={0} align='center'>
/> <Group w={'100%'} justify='center'>
<TextInput <TextInput
label={'Base URL'} key={form.key('name')}
className={textInputClasses.settingsItem} {...form.getInputProps('name')}
w={'45%'} label={'Name'}
classNames={textInputClasses} placeholder='Nextcloud'
defaultValue={newApplication.baseUrl} className={textInputClasses.settingsItem}
onChange={(event) => { w={'45%'}
setNewApplication({ classNames={textInputClasses}
...newApplication, withAsterisk
baseUrl: event.target.value, />
}); <TextInput
}} key={form.key('baseUrl')}
/> {...form.getInputProps('baseUrl')}
</Group> label={'Base URL'}
<Group w={isMobile ? '94%' : '92%'} justify='center'> placeholder='/nextcloud'
<TextInput className={textInputClasses.settingsItem}
label={'URL'} w={'45%'}
className={textInputClasses.settingsItem} classNames={textInputClasses}
w={'100%'} />
classNames={textInputClasses} </Group>
defaultValue={newApplication.url} <Group w={isMobile ? '94%' : '92%'} justify='center'>
onChange={(event) => { <TextInput
setNewApplication({ key={form.key('url')}
...newApplication, {...form.getInputProps('url')}
url: event.target.value, label={'URL'}
}); placeholder='https://domain.example.com/nextcloud'
}} className={textInputClasses.settingsItem}
/> w={'100%'}
</Group> classNames={textInputClasses}
<Group w={'100%'} justify='center'> withAsterisk
<Select />
label={'Icon'} </Group>
className={selectClasses.settingsItem} <Group w={'100%'} justify='center'>
classNames={{ <Select
input: selectClasses.menuSelectInput, key={form.key('icon')}
label: selectClasses.menuSelectLabel, {...form.getInputProps('icon')}
options: selectClasses.menuSelectOptions, label={'Icon'}
option: selectClasses.menuSelectOption, className={selectClasses.settingsItem}
dropdown: selectClasses.dropdown, classNames={{
}} input: selectClasses.menuSelectInput,
w={'45%'} label: selectClasses.menuSelectLabel,
mt={'.75rem'} options: selectClasses.menuSelectOptions,
limit={100} option: selectClasses.menuSelectOption,
placeholder='Search for an icon...' dropdown: selectClasses.dropdown,
data={Object.keys(allIcons).map((icon) => { }}
return { value: icon, label: icon }; w={'45%'}
})} mt={'.75rem'}
defaultValue={newApplication.icon} limit={100}
comboboxProps={{ placeholder='Search for an icon...'
zIndex: 3000, data={Object.keys(allIcons).map((icon) => {
}} return { value: icon, label: icon };
onChange={(value: string | null) => { })}
setNewApplication({ comboboxProps={{
...newApplication, zIndex: 3000,
icon: value ?? '', }}
}); renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
}} const Icon = allIcons[item.option.value];
renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => { return (
const Icon = allIcons[item.option.value]; <Group>
return ( {item.option.value}
<Group> <Icon />
{item.option.value} </Group>
<Icon /> );
</Group> }}
); searchable
}} maxDropdownHeight={175}
searchable onDropdownOpen={() => setIsOpen(true)}
maxDropdownHeight={175} onDropdownClose={() => setIsOpen(false)}
onDropdownOpen={() => setIsOpen(true)} withAsterisk
onDropdownClose={() => setIsOpen(false)} />
/> <TextInput
<TextInput key={form.key('filter')}
label={'Filter'} {...form.getInputProps('filter')}
className={textInputClasses.settingsItem} label={'Filter'}
pt={'1rem'} placeholder='Storage'
w={'45%'} className={textInputClasses.settingsItem}
classNames={textInputClasses} pt={'1rem'}
defaultValue={newApplication.filterName} w={'45%'}
onChange={(event) => { classNames={textInputClasses}
setNewApplication({ defaultValue={defaultApplication.filterName}
...newApplication, withAsterisk
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
color={'green'} type='submit'
styles={{ label: { color: 'black' } }} color={'green'}
onClick={() => { styles={{ label: { color: 'black' } }}
if (newApplication) { >
mutateAsync({ Save
method: 'POST', </Button>
endpoint: appsEndpoint, <Button
body: { color={'red'}
...newApplication, styles={{ label: { color: 'black' } }}
index: applications.length, onClick={handleClose}
}, >
}).then(handleClose); Cancel
} </Button>
}} </Group>
> </Stack>
Save </form>
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Stack>
} }
/> />
<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,61 +125,60 @@ const BookmarkSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new Bookmark Group'} title={'Add a new Bookmark Group'}
body={ body={
<Stack gap={0} w={'100%'} align='center'> <form
<Group w={'100%'} justify='center'> onSubmit={form.onSubmit((values) => {
<TextInput mutateAsync({
label={'Category Name'} method: 'POST',
className={textInputClasses.settingsItem} endpoint: categoriesEndpoint,
classNames={textInputClasses} body: {
defaultValue={newGroup.categoryName} ...values,
onChange={(event) => { categoryIndex: groups.length,
setNewGroup({ },
...newGroup, });
categoryName: event.target.value, handleClose();
}); })}
}} >
/> <Stack gap={0} w={'100%'} align='center'>
<TextInput <Group w={'100%'} justify='center'>
label={'Filter Name'} <TextInput
className={textInputClasses.settingsItem} key={form.key('categoryName')}
classNames={textInputClasses} {...form.getInputProps('categoryName')}
defaultValue={newGroup.filterName} label={'Category Name'}
onChange={(event) => { placeholder='Shopping'
setNewGroup({ className={textInputClasses.settingsItem}
...newGroup, classNames={textInputClasses}
filterName: event.target.value, withAsterisk
}); />
}} <TextInput
/> key={form.key('filterName')}
</Group> {...form.getInputProps('filterName')}
label={'Filter Name'}
placeholder='Personal'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
/>
</Group>
<Divider color='black' my='lg' /> <Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}> <Group pos={'absolute'} bottom={8}>
<Button <Button
color={'green'} type='submit'
styles={{ label: { color: 'black' } }} color={'green'}
onClick={() => { styles={{ label: { color: 'black' } }}
if (newGroup) { >
mutateAsync({ Save
method: 'POST', </Button>
endpoint: categoriesEndpoint, <Button
body: { ...newGroup, categoryIndex: groups.length }, color={'red'}
}); styles={{ label: { color: 'black' } }}
handleClose(); onClick={handleClose}
} >
}} Cancel
> </Button>
Save </Group>
</Button> </Stack>
<Button </form>
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Stack>
} }
/> />
<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,99 +315,95 @@ 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}>
<Stack w={'100%'} display={'flex'} pos={'relative'}> <form
<Group w={'100%'} justify='center'> onSubmit={form.onSubmit((values) => {
<TextInput mutateAsync({
label={'Name'} method: 'POST',
labelProps={{ endpoint: 'api/bookmarks',
style: { body: { ...values, index: bookmarks.length },
color: 'var(--mantine-color-primary-0)', });
}, setPopoverOpened(false);
}} form.reset();
className={textInputClasses.settingsItem} })}
pt={'1rem'} >
classNames={textInputClasses} <Stack w={'100%'} display={'flex'} pos={'relative'}>
onChange={(event) => { <Group w={'100%'} justify='center'>
setNewBookmark({ <TextInput
...newBookmark, key={form.key('name')}
name: event.target.value, {...form.getInputProps('name')}
}); label={'Name'}
}} placeholder='Google'
/> labelProps={{
<TextInput style: {
label={'Category'} color: 'var(--mantine-color-primary-0)',
labelProps={{ },
style: { }}
color: 'var(--mantine-color-primary-0)', className={textInputClasses.settingsItem}
}, pt={'1rem'}
}} classNames={textInputClasses}
className={textInputClasses.settingsItem} withAsterisk
pt={'1rem'} />
classNames={textInputClasses} <TextInput
disabled key={form.key('category')}
defaultValue={group.categoryName} {...form.getInputProps('category')}
onChange={(event) => { label={'Category'}
setNewBookmark({ labelProps={{
...newBookmark, style: {
category: event.target.value, color: 'var(--mantine-color-primary-0)',
}); },
}} }}
/> className={textInputClasses.settingsItem}
</Group> pt={'1rem'}
<Group w={'100%'} justify='center' mb={60}> classNames={textInputClasses}
<TextInput disabled
label={'URL'} defaultValue={group.categoryName}
labelProps={{ />
style: { </Group>
color: 'var(--mantine-color-primary-0)', <Group w={'100%'} justify='center' mb={60}>
}, <TextInput
}} key={form.key('url')}
className={textInputClasses.settingsItem} {...form.getInputProps('url')}
label={'URL'}
placeholder='https://google.com'
labelProps={{
style: {
color: 'var(--mantine-color-primary-0)',
},
}}
className={textInputClasses.settingsItem}
w={'100%'}
pt={'1rem'}
classNames={textInputClasses}
withAsterisk
/>
</Group>
<Group
w={'100%'} w={'100%'}
pt={'1rem'} pos={'absolute'}
classNames={textInputClasses} bottom={8}
onChange={(event) => { align='center'
setNewBookmark({ justify='center'
...newBookmark, >
url: event.target.value, <Button
}); type='submit'
}} color={'green'}
/> styles={{ label: { color: 'black' } }}
</Group> >
<Group Save
w={'100%'} </Button>
pos={'absolute'} <Button
bottom={8} color={'red'}
align='center' styles={{ label: { color: 'black' } }}
justify='center' onClick={() => {
>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
if (newBookmark) {
mutateAsync({
method: 'POST',
endpoint: 'api/bookmarks',
body: { ...newBookmark, index: bookmarks.length },
});
setPopoverOpened(false); setPopoverOpened(false);
} form.reset();
}} }}
> >
Save Cancel
</Button> </Button>
<Button </Group>
color={'red'} </Stack>
styles={{ label: { color: 'black' } }} </form>
onClick={() => {
setPopoverOpened(false);
}}
>
Cancel
</Button>
</Group>
</Stack>
</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,74 +78,68 @@ const SearchProviderSettings = () => {
handleClose={handleClose} handleClose={handleClose}
title={'Add a new Search Provider'} title={'Add a new Search Provider'}
body={ body={
<Stack gap={0} w={'100%'} align='center'> <form
<Stack w={'100%'} align='center'> onSubmit={form.onSubmit((values) => {
<Group w={'100%'} justify='center'> mutateAsync({
method: 'POST',
endpoint: endpoint,
body: values,
});
handleClose();
})}
>
<Stack gap={0} w={'100%'} align='center'>
<Stack w={'100%'} align='center'>
<Group w={'100%'} justify='center'>
<TextInput
key={form.key('name')}
{...form.getInputProps('name')}
label={'Name'}
placeholder='Google'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
/>
<TextInput
key={form.key('prefix')}
{...form.getInputProps('prefix')}
label={'Prefix'}
placeholder='/g'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
/>
</Group>
<TextInput <TextInput
label={'Name'} key={form.key('url')}
{...form.getInputProps('url')}
label={'Url'}
placeholder='https://www.google.com/search?q='
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
w={'64%'}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={newProvider.name} withAsterisk
onChange={(event) => {
setNewProvider({
...newProvider,
name: event.target.value,
});
}}
/>
<TextInput
label={'Prefix'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={newProvider.prefix}
onChange={(event) => {
setNewProvider({
...newProvider,
prefix: event.target.value,
});
}}
/> />
</Stack>
<Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}>
<Button
type='submit'
color={'green'}
styles={{ label: { color: 'black' } }}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group> </Group>
<TextInput
label={'Url'}
className={textInputClasses.settingsItem}
w={isMobile ? '64%' : '100%'}
classNames={textInputClasses}
defaultValue={newProvider.url}
onChange={(event) => {
setNewProvider({
...newProvider,
url: event.target.value,
});
}}
/>
</Stack> </Stack>
<Divider color='black' my='lg' /> </form>
<Group pos={'absolute'} bottom={8}>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
if (newProvider) {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: newProvider,
}).then(handleClose);
}
}}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Stack>
} }
/> />
<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={
<Stack gap={0} w={'100%'} align='center'> <form
<Group w={'100%'} justify='center'> onSubmit={form.onSubmit((values) => {
<TextInput mutateAsync({
label={'Hostname'} method: 'POST',
className={textInputClasses.settingsItem} endpoint: endpoint,
classNames={textInputClasses} body: values,
defaultValue={newWolClient.hostname} });
onChange={(event) => { handleClose();
setNewWolClient({ })}
...newWolClient, >
hostname: event.target.value, <Stack gap={0} w={'100%'} align='center'>
}); <Group w={'100%'} justify='center'>
}} <TextInput
/> key={form.key('hostname')}
<TextInput {...form.getInputProps('hostname')}
label={'MAC Address'} label={'Hostname'}
className={textInputClasses.settingsItem} placeholder='Workstation'
classNames={textInputClasses} className={textInputClasses.settingsItem}
defaultValue={newWolClient.macAddress} classNames={textInputClasses}
onChange={(event) => { withAsterisk
setNewWolClient({ />
...newWolClient, <TextInput
macAddress: event.target.value, key={form.key('macAddress')}
}); {...form.getInputProps('macAddress')}
}} label={'MAC Address'}
/> placeholder='00:00:00:00:00:00'
</Group> className={textInputClasses.settingsItem}
<Divider color='black' my='lg' /> classNames={textInputClasses}
<Group pos={'absolute'} bottom={8}> withAsterisk
<Button />
color={'green'} </Group>
styles={{ label: { color: 'black' } }} <Divider color='black' my='lg' />
onClick={() => { <Group pos={'absolute'} bottom={8}>
if (newWolClient) { <Button
mutateAsync({ type='submit'
method: 'POST', color={'green'}
endpoint: endpoint, styles={{ label: { color: 'black' } }}
body: newWolClient, >
}); Save
closeModal(); </Button>
} <Button
}} color={'red'}
> styles={{ label: { color: 'black' } }}
Save onClick={handleClose}
</Button> >
<Button Cancel
color={'red'} </Button>
styles={{ label: { color: 'black' } }} </Group>
onClick={() => { </Stack>
closeModal(); </form>
}}
>
Cancel
</Button>
</Group>
</Stack>
} }
/> />
<SettingsHeader <SettingsHeader