mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 04:52:49 +00:00
new context menu
This commit is contained in:
parent
92ba328b18
commit
cedbe86381
|
@ -1,182 +1,122 @@
|
|||
import { explorerStore, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerData } from '@sd/core';
|
||||
import {
|
||||
ArrowBendUpRight,
|
||||
LockSimple,
|
||||
Package,
|
||||
Plus,
|
||||
Share,
|
||||
TagSimple,
|
||||
Trash,
|
||||
TrashSimple
|
||||
ArrowBendUpRight,
|
||||
FilePlus,
|
||||
FileX,
|
||||
LockSimple,
|
||||
Package,
|
||||
Plus,
|
||||
Share,
|
||||
TagSimple,
|
||||
Trash,
|
||||
TrashSimple,
|
||||
} from 'phosphor-react';
|
||||
import React from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { WithContextMenu } from '../layout/MenuOverlay';
|
||||
import { NewContextMenu as CM } from "@sd/ui"
|
||||
|
||||
const AssignTagMenuItems = (props: { objectId: number }) => {
|
||||
const tags = useLibraryQuery(['tags.getAll'], { suspense: true });
|
||||
const tagsForFile = useLibraryQuery(['tags.getForFile', props.objectId], { suspense: true });
|
||||
|
||||
const { mutate: assignTag } = useLibraryMutation('tags.assign');
|
||||
|
||||
return (
|
||||
<>
|
||||
{tags.data?.map(tag => {
|
||||
const active = !!tagsForFile.data?.find(t => t.id === tag.id)
|
||||
|
||||
return <CM.Item
|
||||
key={tag.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.objectId === null) return;
|
||||
|
||||
assignTag({
|
||||
tag_id: tag.id,
|
||||
file_id: props.objectId,
|
||||
unassign: active
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="block w-[15px] h-[15px] mr-0.5 border rounded-full"
|
||||
style={{
|
||||
backgroundColor: active
|
||||
? tag.color || '#efefef'
|
||||
: 'transparent' || '#efefef',
|
||||
borderColor: tag.color || '#efefef'
|
||||
}}
|
||||
/>
|
||||
<p>{tag.name}</p>
|
||||
</CM.Item>
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ExplorerContextMenu(props: Props) {
|
||||
const store = useSnapshot(explorerStore);
|
||||
const store = useSnapshot(explorerStore);
|
||||
|
||||
const { data: tags } = useLibraryQuery(['tags.getAll'], {});
|
||||
return (
|
||||
<div className="relative" >
|
||||
<CM.ContextMenu
|
||||
trigger={props.children}
|
||||
>
|
||||
<CM.Item label="Open" />
|
||||
<CM.Item label="Open with..." />
|
||||
|
||||
const { mutate: assignTag } = useLibraryMutation('tags.assign');
|
||||
<CM.Separator />
|
||||
|
||||
const { data: tagsForFile } = useLibraryQuery([
|
||||
'tags.getForFile',
|
||||
store.contextMenuObjectId || -1
|
||||
]);
|
||||
return (
|
||||
<div className="relative">
|
||||
<WithContextMenu
|
||||
menu={[
|
||||
[
|
||||
// `file-${props.identifier}`,
|
||||
{
|
||||
label: 'Open'
|
||||
},
|
||||
{
|
||||
label: 'Open with...'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Quick view'
|
||||
},
|
||||
{
|
||||
label: 'Open in Finder'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Rename'
|
||||
},
|
||||
{
|
||||
label: 'Duplicate'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Share',
|
||||
icon: Share,
|
||||
onClick(e) {
|
||||
e.preventDefault();
|
||||
navigator.share?.({
|
||||
title: 'Spacedrive',
|
||||
text: 'Check out this cool app',
|
||||
url: 'https://spacedrive.com'
|
||||
});
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Assign tag',
|
||||
icon: TagSimple,
|
||||
children: [
|
||||
tags?.map((tag) => {
|
||||
const active = !!tagsForFile?.find((t) => t.id === tag.id);
|
||||
return {
|
||||
label: tag.name || '',
|
||||
<CM.Item label="Quick view" />
|
||||
<CM.Item label="Open in Finder" />
|
||||
|
||||
// leftItem: <Checkbox checked={!!tagsForFile?.find((t) => t.id === tag.id)} />,
|
||||
leftItem: (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="block w-[15px] h-[15px] mr-0.5 border rounded-full"
|
||||
style={{
|
||||
backgroundColor: active
|
||||
? tag.color || '#efefef'
|
||||
: 'transparent' || '#efefef',
|
||||
borderColor: tag.color || '#efefef'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
onClick(e) {
|
||||
e.preventDefault();
|
||||
if (store.contextMenuObjectId != null)
|
||||
assignTag({
|
||||
tag_id: tag.id,
|
||||
file_id: store.contextMenuObjectId,
|
||||
unassign: active
|
||||
});
|
||||
}
|
||||
};
|
||||
}) || []
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'More actions...',
|
||||
icon: Plus,
|
||||
<CM.Separator />
|
||||
|
||||
children: [
|
||||
// [
|
||||
// {
|
||||
// label: 'Move to library',
|
||||
// icon: FilePlus,
|
||||
// children: [libraries?.map((library) => ({ label: library.config.name })) || []]
|
||||
// },
|
||||
// {
|
||||
// label: 'Remove from library',
|
||||
// icon: FileX
|
||||
// }
|
||||
// ],
|
||||
[
|
||||
{
|
||||
label: 'Encrypt',
|
||||
icon: LockSimple
|
||||
},
|
||||
{
|
||||
label: 'Compress',
|
||||
icon: Package
|
||||
},
|
||||
{
|
||||
label: 'Convert to',
|
||||
icon: ArrowBendUpRight,
|
||||
<CM.Item label="Rename" />
|
||||
<CM.Item label="Duplicate" />
|
||||
|
||||
children: [
|
||||
[
|
||||
{
|
||||
label: 'PNG'
|
||||
},
|
||||
{
|
||||
label: 'WebP'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
// {
|
||||
// label: 'Mint NFT',
|
||||
// icon: TrashIcon
|
||||
// }
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Secure delete',
|
||||
icon: TrashSimple
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: Trash,
|
||||
danger: true
|
||||
}
|
||||
]
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</WithContextMenu>
|
||||
</div>
|
||||
);
|
||||
<CM.Separator />
|
||||
|
||||
<CM.Item label="Share" icon={Share} onClick={e => {
|
||||
e.preventDefault();
|
||||
|
||||
navigator.share?.({
|
||||
title: 'Spacedrive',
|
||||
text: 'Check out this cool app',
|
||||
url: 'https://spacedrive.com'
|
||||
});
|
||||
}} />
|
||||
|
||||
<CM.Separator />
|
||||
|
||||
{store.contextMenuObjectId && <CM.SubMenu label="Assign tag" icon={TagSimple}>
|
||||
<AssignTagMenuItems objectId={store.contextMenuObjectId} />
|
||||
</CM.SubMenu>}
|
||||
<CM.SubMenu label="More actions..." icon={Plus}>
|
||||
<CM.SubMenu label="Move to library" icon={FilePlus}>
|
||||
{/* {libraries.map(library => <CM.Item key={library.id} label={library.config.name} />)} */}
|
||||
<CM.Item label="Remove from library" icon={FileX} />
|
||||
</CM.SubMenu>
|
||||
<CM.Separator />
|
||||
<CM.Item label="Encrypt" icon={LockSimple} />
|
||||
<CM.Item label="Compress" icon={Package} />
|
||||
<CM.SubMenu label="Convert to" icon={ArrowBendUpRight}>
|
||||
<CM.Item label="PNG" />
|
||||
<CM.Item label="WebP" />
|
||||
</CM.SubMenu>
|
||||
<CM.Item label="Secure delete" icon={TrashSimple} />
|
||||
</CM.SubMenu>
|
||||
|
||||
<CM.Separator />
|
||||
|
||||
<CM.Item icon={Trash} label="Delete" variant="danger" />
|
||||
</CM.ContextMenu>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,11 +21,13 @@
|
|||
"@heroicons/react": "^2.0.10",
|
||||
"@radix-ui/react-context-menu": "^1.0.0",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"class-variance-authority": "^0.2.3",
|
||||
"clsx": "^1.2.1",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"postcss": "^8.4.14",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook": "^6.5.10",
|
||||
"tailwindcss": "^3.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
107
packages/ui/src/ContextMenu/index.tsx
Normal file
107
packages/ui/src/ContextMenu/index.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import * as RadixCM from "@radix-ui/react-context-menu"
|
||||
import clsx from "clsx";
|
||||
import { CaretRight, Icon } from "phosphor-react";
|
||||
import { HTMLAttributes, PropsWithChildren, Suspense, } from "react"
|
||||
import React from 'react'
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
interface Props extends RadixCM.MenuContentProps {
|
||||
trigger: React.ReactNode,
|
||||
}
|
||||
|
||||
const MENU_CLASSES = `
|
||||
flex flex-col
|
||||
min-w-[11rem] p-2 space-y-1
|
||||
text-left text-sm dark:text-gray-100 text-gray-800
|
||||
bg-gray-50 border-gray-200 dark:bg-gray-950
|
||||
shadow-md shadow-gray-300 dark:shadow-gray-750
|
||||
select-none cursor-default rounded-lg
|
||||
`;
|
||||
|
||||
export const ContextMenu = ({ trigger, children, className, ...props }: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<RadixCM.Root>
|
||||
<RadixCM.Trigger>{trigger}</RadixCM.Trigger>
|
||||
<RadixCM.Portal>
|
||||
<RadixCM.Content {...props} className={clsx(MENU_CLASSES, className)}>
|
||||
{children}
|
||||
</RadixCM.Content>
|
||||
</RadixCM.Portal>
|
||||
</RadixCM.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export const Separator = () =>
|
||||
<RadixCM.Separator className="mx-2 border-0 border-b pointer-events-none border-b-gray-300 dark:border-b-gray-600" />
|
||||
|
||||
export const SubMenu = ({ label, icon, className, ...props }: RadixCM.MenuSubContentProps & ItemProps) => {
|
||||
return (
|
||||
<RadixCM.Sub>
|
||||
<RadixCM.SubTrigger className="[&[data-state='open']_div]:bg-primary focus:outline-none">
|
||||
<DivItem rightArrow {...{ label, icon }} />
|
||||
</RadixCM.SubTrigger>
|
||||
<RadixCM.Portal>
|
||||
<Suspense fallback={null}>
|
||||
<RadixCM.SubContent {...props} className={clsx(MENU_CLASSES, "-mt-2", className)} />
|
||||
</Suspense>
|
||||
</RadixCM.Portal>
|
||||
</RadixCM.Sub>
|
||||
)
|
||||
}
|
||||
|
||||
const ITEM_CLASSES = `
|
||||
flex flex-row items-center justify-start flex-1
|
||||
px-2 py-1 space-x-2
|
||||
cursor-default rounded
|
||||
focus:outline-none
|
||||
`;
|
||||
|
||||
const itemStyles = cva([ITEM_CLASSES], {
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-primary focus:bg-primary',
|
||||
danger: `
|
||||
text-red-600 dark:text-red-400
|
||||
hover:text-white focus:text-white
|
||||
hover:bg-red-500 focus:bg-red-500
|
||||
`
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default'
|
||||
}
|
||||
})
|
||||
|
||||
interface ItemProps extends VariantProps<typeof itemStyles> {
|
||||
icon?: Icon,
|
||||
rightArrow?: boolean,
|
||||
label?: string,
|
||||
}
|
||||
|
||||
export const Item = ({ icon, label, rightArrow, children, variant, ...props }: ItemProps & RadixCM.MenuItemProps) => (
|
||||
<RadixCM.Item {...props} className={itemStyles({ variant })}>
|
||||
{children ? children : <ItemInternals {...{icon, label, rightArrow}} />}
|
||||
</RadixCM.Item>
|
||||
)
|
||||
|
||||
const DivItem = ({ variant, ...props }: ItemProps) => (
|
||||
<div className={itemStyles({ variant })}>
|
||||
<ItemInternals {...props} />
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
const ItemInternals = ({ icon, label, rightArrow }: ItemProps) => {
|
||||
const ItemIcon = icon;
|
||||
return (
|
||||
<>
|
||||
{ItemIcon && <ItemIcon size={18} />}
|
||||
{label && <p>{label}</p>}
|
||||
|
||||
{rightArrow && <>
|
||||
<div className="flex-1"/>
|
||||
<CaretRight weight="fill" size={12} alt=""/>
|
||||
</>}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export * from './Button';
|
||||
export * from './Dropdown';
|
||||
export * from './ContextMenu';
|
||||
export * as NewContextMenu from "./ContextMenu/index"
|
||||
export * from './Input';
|
||||
|
|
1116
pnpm-lock.yaml
1116
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue