2023-02-20 22:11:30 +01:00
|
|
|
import {
|
|
|
|
|
Autocomplete,
|
|
|
|
|
Box,
|
|
|
|
|
CloseButton,
|
|
|
|
|
createStyles,
|
|
|
|
|
Group,
|
|
|
|
|
Image,
|
|
|
|
|
Loader,
|
|
|
|
|
ScrollArea,
|
|
|
|
|
SelectItemProps,
|
|
|
|
|
Stack,
|
|
|
|
|
Text,
|
|
|
|
|
Title,
|
|
|
|
|
} from '@mantine/core';
|
|
|
|
|
import { UseFormReturnType } from '@mantine/form';
|
|
|
|
|
import { IconSearch } from '@tabler/icons';
|
|
|
|
|
import { useTranslation } from 'next-i18next';
|
|
|
|
|
import { forwardRef } from 'react';
|
|
|
|
|
import { humanFileSize } from '../../../../../../tools/humanFileSize';
|
|
|
|
|
import { NormalizedIconRepositoryResult } from '../../../../../../tools/server/images/abstract-icons-repository';
|
|
|
|
|
import { AppType } from '../../../../../../types/app';
|
|
|
|
|
import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
|
|
|
|
|
|
|
|
|
|
interface IconSelectorProps {
|
|
|
|
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
|
|
|
|
data: NormalizedIconRepositoryResult[] | undefined;
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
disallowAppNameProgagation: () => void;
|
|
|
|
|
allowAppNamePropagation: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const IconSelector = ({
|
|
|
|
|
form,
|
|
|
|
|
data,
|
|
|
|
|
isLoading,
|
|
|
|
|
allowAppNamePropagation,
|
|
|
|
|
disallowAppNameProgagation,
|
|
|
|
|
}: IconSelectorProps) => {
|
|
|
|
|
const { t } = useTranslation('layout/modals/add-app');
|
|
|
|
|
const { classes } = useStyles();
|
|
|
|
|
|
|
|
|
|
const a =
|
|
|
|
|
data === undefined
|
|
|
|
|
? []
|
|
|
|
|
: data.flatMap((repository) =>
|
|
|
|
|
repository.entries.map((entry) => ({
|
|
|
|
|
url: entry.url,
|
|
|
|
|
label: entry.name,
|
|
|
|
|
size: entry.size,
|
|
|
|
|
value: entry.url,
|
|
|
|
|
group: repository.name,
|
|
|
|
|
copyright: repository.copyright,
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Stack w="100%">
|
|
|
|
|
<Autocomplete
|
|
|
|
|
nothingFound={
|
|
|
|
|
<Stack align="center" spacing="xs" my="lg">
|
|
|
|
|
<IconSearch />
|
|
|
|
|
<Title order={6} align="center">
|
|
|
|
|
{t('appearance.icon.autocomplete.title')}
|
|
|
|
|
</Title>
|
|
|
|
|
<Text align="center" maw={350}>
|
|
|
|
|
{t('appearance.icon.autocomplete.text')}
|
|
|
|
|
</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
}
|
|
|
|
|
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
|
|
|
|
|
rightSection={
|
|
|
|
|
form.values.appearance.iconUrl.length > 0 ? (
|
|
|
|
|
<CloseButton onClick={() => form.setFieldValue('appearance.iconUrl', '')} />
|
|
|
|
|
) : null
|
|
|
|
|
}
|
|
|
|
|
itemComponent={AutoCompleteItem}
|
|
|
|
|
className={classes.textInput}
|
|
|
|
|
data={a}
|
|
|
|
|
limit={25}
|
|
|
|
|
label={t('appearance.icon.label')}
|
|
|
|
|
description={t('appearance.icon.description', {
|
|
|
|
|
suggestionsCount: data?.reduce((a, b) => a + b.count, 0) ?? 0,
|
|
|
|
|
})}
|
|
|
|
|
filter={(search, item) =>
|
|
|
|
|
item.value
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replaceAll('_', '')
|
|
|
|
|
.replaceAll(' ', '-')
|
|
|
|
|
.includes(search.toLowerCase().replaceAll('_', '').replaceAll(' ', '-'))
|
|
|
|
|
}
|
|
|
|
|
variant="default"
|
|
|
|
|
withAsterisk
|
2023-03-03 12:38:56 +09:00
|
|
|
dropdownComponent={(props: any) => <ScrollArea {...props} mah={250} />}
|
|
|
|
|
dropdownPosition="bottom"
|
2023-02-20 22:11:30 +01:00
|
|
|
required
|
|
|
|
|
onChange={(event) => {
|
|
|
|
|
if (allowAppNamePropagation) {
|
|
|
|
|
disallowAppNameProgagation();
|
|
|
|
|
}
|
|
|
|
|
form.setFieldValue('appearance.iconUrl', event);
|
|
|
|
|
}}
|
|
|
|
|
value={form.values.appearance.iconUrl}
|
|
|
|
|
/>
|
|
|
|
|
{(!data || isLoading) && (
|
|
|
|
|
<Group>
|
|
|
|
|
<Loader variant="oval" size="sm" />
|
|
|
|
|
<Stack spacing={0}>
|
|
|
|
|
<Text size="xs" weight="bold">
|
|
|
|
|
{t('appearance.icon.noItems.title')}
|
|
|
|
|
</Text>
|
|
|
|
|
<Text color="dimmed" size="xs">
|
|
|
|
|
{t('appearance.icon.noItems.text')}
|
|
|
|
|
</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
</Group>
|
|
|
|
|
)}
|
|
|
|
|
</Stack>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const useStyles = createStyles(() => ({
|
|
|
|
|
textInput: {
|
|
|
|
|
flexGrow: 1,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const AutoCompleteItem = forwardRef<HTMLDivElement, ItemProps>(
|
|
|
|
|
({ label, size, copyright, url, ...others }: ItemProps, ref) => (
|
|
|
|
|
<div ref={ref} {...others}>
|
|
|
|
|
<Group noWrap>
|
|
|
|
|
<Box
|
|
|
|
|
sx={(theme) => ({
|
|
|
|
|
backgroundColor:
|
|
|
|
|
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2],
|
|
|
|
|
borderRadius: theme.radius.md,
|
|
|
|
|
})}
|
|
|
|
|
p={2}
|
|
|
|
|
>
|
|
|
|
|
<Image src={url} width={30} height={30} fit="contain" />
|
|
|
|
|
</Box>
|
|
|
|
|
<Stack spacing={0}>
|
|
|
|
|
<Text>{label}</Text>
|
|
|
|
|
<Group>
|
|
|
|
|
<Text color="dimmed" size="xs">
|
|
|
|
|
{humanFileSize(size, false)}
|
|
|
|
|
</Text>
|
|
|
|
|
{copyright && (
|
|
|
|
|
<Text color="dimmed" size="xs">
|
|
|
|
|
© {copyright}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Group>
|
|
|
|
|
</Stack>
|
|
|
|
|
</Group>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
interface ItemProps extends SelectItemProps {
|
|
|
|
|
url: string;
|
|
|
|
|
group: string;
|
|
|
|
|
size: number;
|
|
|
|
|
copyright: string | undefined;
|
|
|
|
|
}
|