mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-19 05:45:01 -04:00
[ENG-443] Allow folders to be tagged (#1712)
* allow folders to be tagged * remove bruh * custom useAssignItems hook * fix types
This commit is contained in:
@@ -1,149 +0,0 @@
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import clsx from 'clsx';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
Object,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
usePlausibleEvent,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { dialogManager, ModifierKeys } from '@sd/ui';
|
||||
import CreateDialog from '~/app/$libraryId/settings/library/tags/CreateDialog';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { useScrolled } from '~/hooks/useScrolled';
|
||||
import { keybindForOs } from '~/util/keybinds';
|
||||
|
||||
import { Menu } from './Menu';
|
||||
|
||||
export default (props: { objects: Object[] }) => {
|
||||
const os = useOperatingSystem();
|
||||
const keybind = keybindForOs(os);
|
||||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
const rspc = useRspcLibraryContext();
|
||||
const tags = useLibraryQuery(['tags.list'], { suspense: true });
|
||||
// Map<tag::id, Vec<object::id>>
|
||||
const tagsWithObjects = useLibraryQuery([
|
||||
'tags.getWithObjects',
|
||||
props.objects.map(({ id }) => id)
|
||||
]);
|
||||
|
||||
const assignTag = useLibraryMutation('tags.assign', {
|
||||
onSuccess: () => {
|
||||
submitPlausibleEvent({ event: { type: 'tagAssign' } });
|
||||
}
|
||||
});
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: tags.data?.length || 0,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 30,
|
||||
paddingStart: 2
|
||||
});
|
||||
|
||||
const { isScrolled } = useScrolled(parentRef, 10);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Item
|
||||
className="tag-menu"
|
||||
label="New tag"
|
||||
icon={Plus}
|
||||
iconProps={{ size: 15 }}
|
||||
keybind={keybind([ModifierKeys.Control], ['N'])}
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => <CreateDialog {...dp} objects={props.objects} />);
|
||||
}}
|
||||
/>
|
||||
<Menu.Separator className={clsx('mx-0 mb-0 transition', isScrolled && 'shadow')} />
|
||||
{tags.data && tags.data.length > 0 ? (
|
||||
<div
|
||||
ref={parentRef}
|
||||
style={{
|
||||
maxHeight: `400px`,
|
||||
height: `100%`,
|
||||
width: `100%`,
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const tag = tags.data[virtualRow.index];
|
||||
if (!tag) return null;
|
||||
|
||||
const objectsWithTag = tagsWithObjects.data?.[tag?.id];
|
||||
|
||||
// only unassign if all objects have tag
|
||||
// this is the same functionality as finder
|
||||
const unassign = objectsWithTag?.length === props.objects.length;
|
||||
|
||||
// TODO: UI to differentiate tag assigning when some objects have tag when no objects have tag - ENG-965
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
await assignTag.mutateAsync({
|
||||
unassign,
|
||||
tag_id: tag.id,
|
||||
object_ids: unassign
|
||||
? // use objects that already have tag
|
||||
objectsWithTag
|
||||
: // use objects that don't have tag
|
||||
props.objects
|
||||
.filter(
|
||||
(o) =>
|
||||
!objectsWithTag?.some(
|
||||
(ot) => ot === o.id
|
||||
)
|
||||
)
|
||||
.map((o) => o.id)
|
||||
});
|
||||
if (unassign)
|
||||
rspc.queryClient.invalidateQueries(['search.objects']);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mr-0.5 h-[15px] w-[15px] shrink-0 rounded-full border"
|
||||
style={{
|
||||
backgroundColor:
|
||||
objectsWithTag &&
|
||||
objectsWithTag.length > 0 &&
|
||||
tag.color
|
||||
? tag.color
|
||||
: 'transparent',
|
||||
borderColor: tag.color || '#efefef'
|
||||
}}
|
||||
/>
|
||||
<span className="truncate">{tag.name}</span>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1 text-center text-xs text-ink-faint">
|
||||
{tags.data ? 'No tags' : 'Failed to load tags'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user