1
Fork 0
mirror of https://github.com/diced/zipline.git synced 2025-04-04 23:21:17 -05:00
This commit is contained in:
diced 2023-04-30 15:21:25 -07:00
parent 3be9f1521e
commit adb984b2db
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
5 changed files with 425 additions and 15 deletions

View file

@ -47,6 +47,7 @@ import { FileMeta } from '.';
import Type from '../Type';
import Tag from 'components/File/tag/Tag';
import Item from 'components/File/tag/Item';
import { useDeleteFileTags, useFileTags, useTags, useUpdateFileTags } from 'lib/queries/tags';
export default function FileModal({
open,
@ -70,15 +71,14 @@ export default function FileModal({
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const folders = useFolders();
const [overrideRender, setOverrideRender] = useState(false);
const tags = useFileTags(file.id);
const updateTags = useUpdateFileTags(file.id);
const removeTags = useDeleteFileTags(file.id);
const clipboard = useClipboard();
const [tags, setTags] = useState<{ label: string; value: string; color: string }[]>([
{ label: 'Tag 1', value: 'tag-1', color: '#ff0000' },
{ label: 'Tag 2', value: 'tag-2', color: '#00ff00' },
{ label: 'Tag 3', value: 'tag-3', color: '#0000ff' },
]);
const allTags = useTags();
const [overrideRender, setOverrideRender] = useState(false);
const handleDelete = async () => {
deleteFile.mutate(file.id, {
@ -227,6 +227,40 @@ export default function FileModal({
console.log('should save');
};
const handleAddTags = (t: string[]) => {
// filter out existing tags from t
t = t.filter((tag) => !tags.data.find((t) => t.id === tag));
const fullTag = allTags.data.find((tag) => tag.id === t[0]);
if (!fullTag) return;
updateTags.mutate([...tags.data, fullTag], {
onSuccess: () => {
showNotification({
title: 'Added tag',
message: fullTag.name,
color: 'green',
icon: <IconTags size='1rem' />,
});
},
});
};
const handleRemoveTags = (t: string[]) => {
const fullTag = allTags.data.find((tag) => tag.id === t[0]);
removeTags.mutate(t, {
onSuccess: () =>
showNotification({
title: 'Removed tag',
message: fullTag.name,
color: 'green',
icon: <IconTags size='1rem' />,
}),
});
};
return (
<Modal
opened={open}
@ -298,8 +332,9 @@ export default function FileModal({
<Accordion.Control icon={<IconTags size='1rem' />}>Tags</Accordion.Control>
<Accordion.Panel>
<MultiSelect
data={tags}
placeholder={tags.length ? 'Add tags' : 'Add tags (optional)'}
value={tags.data?.map((t) => t.id) ?? []}
data={allTags.data?.map((t) => ({ value: t.id, label: t.name, color: t.color })) ?? []}
placeholder={allTags.data?.length ? 'Add tags' : 'Add tags (optional)'}
icon={<IconTags size='1rem' />}
valueComponent={Tag}
itemComponent={Item}
@ -316,9 +351,11 @@ export default function FileModal({
</Text>
</Group>
)}
// onChange={(t) => (t.length === 1 ? handleRemoveTags(t) : handleAddTags(t))}
onChange={(t) => console.log(t)}
onCreate={(t) => {
const item = { value: t, label: t, color: colorHash(t) };
setTags([...tags, item]);
// setLabelTags([...labelTags, item]);
return item;
}}
onBlur={handleTagsSave}

View file

@ -0,0 +1,197 @@
import {
ActionIcon,
Button,
ColorInput,
Group,
Modal,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useDeleteTags, useTags } from 'lib/queries/tags';
import { showNotification } from '@mantine/notifications';
import { IconRefresh, IconTag, IconTags, IconTagsOff } from '@tabler/icons-react';
import { useState } from 'react';
import { colorHash } from 'utils/client';
import useFetch from 'hooks/useFetch';
import { useModals } from '@mantine/modals';
import MutedText from 'components/MutedText';
export function TagCard({ tags, tag }) {
const deleteTags = useDeleteTags();
const modals = useModals();
const deleteTag = () => {
modals.openConfirmModal({
zIndex: 1000,
size: 'auto',
title: (
<Title>
Delete tag <b style={{ color: tag.color }}>{tag.name}</b>?
</Title>
),
children: `This will remove the tag from ${tag.files.length} file${tag.files.length === 1 ? '' : 's'}`,
labels: {
confirm: 'Delete',
cancel: 'Cancel',
},
onCancel() {
modals.closeAll();
},
onConfirm() {
deleteTags.mutate([tag.id], {
onSuccess: () => {
showNotification({
title: 'Tag deleted',
message: `Tag ${tag.name} was deleted`,
color: 'green',
icon: <IconTags size='1rem' />,
});
modals.closeAll();
tags.refetch();
},
});
},
});
};
return (
<Paper
radius='sm'
sx={(t) => ({
backgroundColor: tag.color,
'&:hover': {
backgroundColor: t.fn.darken(tag.color, 0.1),
},
cursor: 'pointer',
})}
px='xs'
onClick={deleteTag}
>
<Group position='apart'>
<Text>
{tag.name} ({tag.files.length})
</Text>
</Group>
</Paper>
);
}
export function CreateTagModal({ tags, open, onClose }) {
const [color, setColor] = useState('');
const [name, setName] = useState('');
const [colorError, setColorError] = useState('');
const [nameError, setNameError] = useState('');
const onSubmit = async (e) => {
e.preventDefault();
setNameError('');
setColorError('');
const n = name.trim();
const c = color.trim();
if (n.length === 0 && c.length === 0) {
setNameError('Name is required');
setColorError('Color is required');
return;
} else if (n.length === 0) {
setNameError('Name is required');
setColorError('');
return;
} else if (c.length === 0) {
setNameError('');
setColorError('Color is required');
return;
}
const data = await useFetch('/api/user/tags', 'POST', {
tags: [
{
name: n,
color: c,
},
],
});
if (!data.error) {
showNotification({
title: 'Tag created',
message: (
<>
Tag <b style={{ color: color }}>{name}</b> was created
</>
),
color: 'green',
icon: <IconTags size='1rem' />,
});
tags.refetch();
onClose();
} else {
showNotification({
title: 'Error creating tag',
message: data.error,
color: 'red',
icon: <IconTagsOff size='1rem' />,
});
}
};
return (
<Modal title={<Title>Create Tag</Title>} size='xs' opened={open} onClose={onClose} zIndex={300}>
<form onSubmit={onSubmit}>
<TextInput
icon={<IconTag size='1rem' />}
label='Name'
value={name}
onChange={(e) => setName(e.currentTarget.value)}
error={nameError}
/>
<ColorInput
dropdownZIndex={301}
label='Color'
value={color}
onChange={setColor}
error={colorError}
rightSection={
<Tooltip label='Generate color from name'>
<ActionIcon variant='subtle' onClick={() => setColor(colorHash(name))} color='primary'>
<IconRefresh size='1rem' />
</ActionIcon>
</Tooltip>
}
/>
<Button type='submit' fullWidth variant='outline' my='sm'>
Create Tag
</Button>
</form>
</Modal>
);
}
export default function TagsModal({ open, onClose }) {
const tags = useTags();
const [createOpen, setCreateOpen] = useState(false);
return (
<>
<CreateTagModal tags={tags} open={createOpen} onClose={() => setCreateOpen(false)} />
<Modal title={<Title>Tags</Title>} size='auto' opened={open} onClose={onClose}>
<MutedText size='sm'>Click on a tag to delete it.</MutedText>
<Stack>
{tags.isSuccess && tags.data.map((tag) => <TagCard key={tag.id} tags={tags} tag={tag} />)}
</Stack>
<Button mt='xl' variant='outline' onClick={() => setCreateOpen(true)} fullWidth compact>
Create Tag
</Button>
</Modal>
</>
);
}

View file

@ -1,5 +1,5 @@
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title, Tooltip } from '@mantine/core';
import { IconFileUpload, IconPhotoUp } from '@tabler/icons-react';
import { IconFileUpload, IconPhotoUp, IconTags } from '@tabler/icons-react';
import File from 'components/File';
import useFetch from 'hooks/useFetch';
import { usePaginatedFiles } from 'lib/queries/files';
@ -7,13 +7,15 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';
import FilePagation from './FilePagation';
import PendingFilesModal from './PendingFilesModal';
import TagsModal from 'components/pages/Files/TagsModal';
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
const [open, setOpen] = useState(false);
const [pendingOpen, setPendingOpen] = useState(false);
const [tagsOpen, setTagsOpen] = useState(false);
useEffect(() => {
(async () => {
@ -24,7 +26,8 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
return (
<>
<PendingFilesModal open={open} onClose={() => setOpen(false)} />
<PendingFilesModal open={pendingOpen} onClose={() => setPendingOpen(false)} />
<TagsModal open={tagsOpen} onClose={() => setTagsOpen(false)} />
<Group mb='md'>
<Title>Files</Title>
@ -33,10 +36,15 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
</ActionIcon>
<Tooltip label='View pending uploads'>
<ActionIcon onClick={() => setOpen(true)} variant='filled' color='primary'>
<ActionIcon onClick={() => setPendingOpen(true)} variant='filled' color='primary'>
<IconPhotoUp size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='View tags'>
<ActionIcon onClick={() => setTagsOpen(true)} variant='filled' color='primary'>
<IconTags size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
{favoritePages.isSuccess && favoritePages.data.length ? (
<Accordion

168
src/lib/queries/tags.ts Normal file
View file

@ -0,0 +1,168 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import queryClient from 'lib/queries/client';
export type UserTagsResponse = {
id: string;
name: string;
color: string;
files: {
id: string;
}[];
};
export type TagsRequest = {
id?: string;
name?: string;
color?: string;
};
export const useTags = () => {
return useQuery<UserTagsResponse[]>(['tags'], async () => {
return fetch('/api/user/tags')
.then((res) => res.json() as Promise<UserTagsResponse[]>)
.then((data) => data);
});
};
export const useFileTags = (id: string) => {
return useQuery<UserTagsResponse[]>(['tags', id], async () => {
return fetch(`/api/user/file/${id}/tags`)
.then((res) => res.json() as Promise<UserTagsResponse[]>)
.then((data) => data);
});
};
export const useUpdateFileTags = (id: string) => {
return useMutation(
(tags: TagsRequest[]) =>
fetch(`/api/user/file/${id}/tags`, {
method: 'POST',
body: JSON.stringify({ tags }),
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json()),
{
onSuccess: () => {
queryClient.refetchQueries(['tags', id]);
queryClient.refetchQueries(['files']);
},
}
);
};
export const useDeleteFileTags = (id: string) => {
return useMutation(
(tags: string[]) =>
fetch(`/api/user/file/${id}/tags`, {
method: 'DELETE',
body: JSON.stringify({ tags }),
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json()),
{
onSuccess: () => {
queryClient.refetchQueries(['tags', id]);
},
}
);
};
export const useDeleteTags = () => {
return useMutation(
(tags: string[]) =>
fetch('/api/user/tags', {
method: 'DELETE',
body: JSON.stringify({ tags }),
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json()),
{
onSuccess: () => {
queryClient.refetchQueries(['tags']);
queryClient.refetchQueries(['files']);
},
}
);
};
// export const usePaginatedFiles = (page?: number, filter = 'media', favorite = null) => {
// const queryBuilder = new URLSearchParams({
// page: Number(page || '1').toString(),
// filter,
// ...(favorite !== null && { favorite: favorite.toString() }),
// });
// const queryString = queryBuilder.toString();
//
// return useQuery<UserFilesResponse[]>(['files', queryString], async () => {
// return fetch('/api/user/paged?' + queryString)
// .then((res) => res.json() as Promise<UserFilesResponse[]>)
// .then((data) =>
// data.map((x) => ({
// ...x,
// createdAt: new Date(x.createdAt),
// expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
// }))
// );
// });
// };
//
// export const useRecent = (filter?: string) => {
// return useQuery<UserFilesResponse[]>(['recent', filter], async () => {
// return fetch(`/api/user/recent?filter=${encodeURIComponent(filter)}`)
// .then((res) => res.json())
// .then((data) =>
// data.map((x) => ({
// ...x,
// createdAt: new Date(x.createdAt),
// expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
// }))
// );
// });
// };
//
// export function useFileDelete() {
// // '/api/user/files', 'DELETE', { id: image.id }
// return useMutation(
// async (id: string) => {
// return fetch('/api/user/files', {
// method: 'DELETE',
// body: JSON.stringify({ id }),
// headers: {
// 'content-type': 'application/json',
// },
// }).then((res) => res.json());
// },
// {
// onSuccess: () => {
// queryClient.refetchQueries(['files']);
// },
// }
// );
// }
//
// export function useFileFavorite() {
// // /api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite }
// return useMutation(
// async (data: { id: string; favorite: boolean }) => {
// return fetch('/api/user/files', {
// method: 'PATCH',
// body: JSON.stringify(data),
// headers: {
// 'content-type': 'application/json',
// },
// }).then((res) => res.json());
// },
// {
// onSuccess: () => {
// queryClient.refetchQueries(['files']);
// },
// }
// );
// }
//
// export function invalidateFiles() {
// return queryClient.invalidateQueries(['files', 'recent', 'stats']);
// }

View file

@ -72,7 +72,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
if (!tags) return res.badRequest('no tags');
if (!tags.length) return res.badRequest('no tags');
// if the tag has an id, it means it already exists so we just connect it
// if the tag has an id, it means it already exists, so we just connect it
// if it doesn't have an id, we create it and then connect it
const nFile = await prisma.file.update({
where: { id },