Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(webui): add context menu in to copy absolute and relative paths #69

Merged
merged 4 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified webui/bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@expo/styleguide-native": "^7.0.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
Expand Down
49 changes: 33 additions & 16 deletions webui/src/components/BreadcrumbLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Link } from 'expo-router';
import { ComponentProps, Fragment, useMemo } from 'react';
import { type ComponentProps, Fragment, useMemo } from 'react';

import { FilePathMenu } from '~/components/FilePathMenu';
import {
Breadcrumb,
BreadcrumbItem,
Expand Down Expand Up @@ -33,20 +34,24 @@ export function BreadcrumbLinks(props: BreadcrumbLinksProps) {
</Link>
</BreadcrumbLink>
{links.map((link) => (
<Fragment key={link.key}>
<Fragment key={link.filePath}>
<BreadcrumbSeparator className="text-secondary" />
<BreadcrumbItem>
{!link.href ? (
<BreadcrumbPage className="text-lg">{link.label}</BreadcrumbPage>
<FilePathMenu {...link}>
<BreadcrumbPage className="text-lg">{link.label}</BreadcrumbPage>
</FilePathMenu>
) : (
<BreadcrumbLink asChild>
<Link
className="text-lg text-default font-bold underline-offset-4 hover:underline"
href={link.href}
>
{link.label}
</Link>
</BreadcrumbLink>
<FilePathMenu {...link}>
<BreadcrumbLink asChild>
<Link
className="text-lg text-default font-bold underline-offset-4 hover:underline"
href={link.href}
>
{link.label}
</Link>
</BreadcrumbLink>
</FilePathMenu>
)}
</BreadcrumbItem>
</Fragment>
Expand All @@ -56,24 +61,36 @@ export function BreadcrumbLinks(props: BreadcrumbLinksProps) {
);
}

type BreadcrumbLinkItem = {
type BreadcrumbLinkItem = ComponentProps<typeof FilePathMenu> & {
key: string;
label: string;
filePath: string;
href?: ComponentProps<typeof Link>['href'];
};

function getBreadcrumbLinks(props: BreadcrumbLinksProps): BreadcrumbLinkItem[] {
return props.path.split('/').map((label, index, breadcrumbs) => {
const isLastSegment = index === breadcrumbs.length - 1;
const breadcrumb: BreadcrumbLinkItem = { key: `${index}-${label}`, label };
const filePath = breadcrumbs.slice(0, index + 1).join('/');
const breadcrumb: BreadcrumbLinkItem = {
key: `${index}-${label}`,
label,
filePath,
relative: {
path: filePath,
label: isLastSegment ? 'Copy relative file path' : 'Copy relative folder path',
},
absolute: {
path: `${props.bundle.sharedRoot}/${filePath}`,
label: isLastSegment ? 'Copy absolute file path' : 'Copy absolute folder path',
},
};

// NOTE(cedric): a bit of a workaround to avoid linking to the current page, might need to change this
if (!isLastSegment || !label.includes('.')) {
const path = breadcrumbs.slice(0, index + 1).join('/');
breadcrumb.key = path;
breadcrumb.href = {
pathname: '/(atlas)/[bundle]/folders/[path]',
params: { bundle: props.bundle.id, path },
params: { bundle: props.bundle.id, path: breadcrumb.filePath },
};
}

Expand Down
60 changes: 60 additions & 0 deletions webui/src/components/FilePathMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @ts-expect-error
import CheckIcon from 'lucide-react/dist/esm/icons/check';
import { PropsWithChildren, useState, useCallback } from 'react';

import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '~/ui/Menu';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/ui/Tooltip';

type FilePath = {
path: string;
label?: string;
copy?: string;
};

type FilePathMenuProps = PropsWithChildren<{
absolute: FilePath;
relative: FilePath;
}>;

export function FilePathMenu(props: FilePathMenuProps) {
const [tooltipContent, setTooltipContent] = useState<string | null>(null);

const showCopyTooltip = useCallback((content: string) => {
setTooltipContent(content);
setTimeout(() => setTooltipContent(null), 2000);
}, []);

const onCopyRelativePath = useCallback(() => {
navigator.clipboard.writeText(props.relative.path);
showCopyTooltip(props.relative.copy ?? 'Relative path copied');
}, [props.relative.path, props.relative.copy, showCopyTooltip]);

const onCopyAbsolutePath = useCallback(() => {
navigator.clipboard.writeText(props.absolute.path);
showCopyTooltip(props.absolute.copy ?? 'Absolute path copied');
}, [props.absolute.path, props.absolute.copy, showCopyTooltip]);

return (
<TooltipProvider>
<Tooltip open={!!tooltipContent}>
<ContextMenu>
<TooltipTrigger asChild>
<ContextMenuTrigger>{props.children}</ContextMenuTrigger>
</TooltipTrigger>
<TooltipContent className="inline-flex flex-row items-center">
{tooltipContent}
<CheckIcon size={14} className="ml-2" />
</TooltipContent>
<ContextMenuContent>
<ContextMenuItem onClick={onCopyRelativePath}>
{props.relative.label ?? 'Copy relative path'}
</ContextMenuItem>
<ContextMenuItem onClick={onCopyAbsolutePath}>
{props.absolute.label ?? 'Copy absolute path'}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</Tooltip>
</TooltipProvider>
);
}
2 changes: 1 addition & 1 deletion webui/src/components/FileSize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function FileSize(props: BundleFileSizeProps) {
size={12}
/>
</TooltipTrigger>
<TooltipContent className="py-2 px-3 max-w-80 bg-screen border-2 border-info rounded-md text-quaternary leading-6 shadow-md">
<TooltipContent className="border-info text-quaternary shadow-md">
<p className="mb-2">
All file sizes are calculated based on the transpiled JavaScript byte size.
</p>
Expand Down
46 changes: 26 additions & 20 deletions webui/src/components/ModuleReferenceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PackageIcon from 'lucide-react/dist/esm/icons/box';
import FileIcon from 'lucide-react/dist/esm/icons/file';
import { PropsWithChildren } from 'react';

import { FilePathMenu } from '~/components/FilePathMenu';
import type { AtlasModuleRef, PartialAtlasBundle } from '~core/data/types';

type ModuleReferenceListProps = PropsWithChildren<{
Expand Down Expand Up @@ -37,27 +38,32 @@ function ModuleImportLink(props: ModuleImportLinkProps) {
const Icon = props.reference.package ? PackageIcon : FileIcon;

return (
<Link
asChild
href={{
pathname: '/(atlas)/[bundle]/modules/[path]',
params: {
bundle: props.bundle.id,
path: props.reference.relativePath,
},
}}
<FilePathMenu
absolute={{ path: props.reference.absolutePath, label: 'Copy absolute import path' }}
relative={{ path: props.reference.relativePath, label: 'Copy relative import path' }}
>
<a
className="px-3 py-2 text-2xs border border-secondary rounded-md bg-default text-default inline-flex flex-row items-center group hover:bg-subtle transition-colors"
aria-label={props.reference.absolutePath}
title={props.reference.absolutePath}
<Link
asChild
href={{
pathname: '/(atlas)/[bundle]/modules/[path]',
params: {
bundle: props.bundle.id,
path: props.reference.relativePath,
},
}}
>
<Icon size={14} />
<span className="mx-2 whitespace-nowrap overflow-hidden text-ellipsis group-hover:underline underline-offset-2">
{props.reference.relativePath}
</span>
<span className="ml-2">→</span>
</a>
</Link>
<a
className="px-3 py-2 text-2xs border border-secondary rounded-md bg-default text-default inline-flex flex-row items-center group hover:bg-subtle transition-colors"
aria-label={props.reference.absolutePath}
title={props.reference.absolutePath}
>
<Icon size={14} />
<span className="mx-2 whitespace-nowrap overflow-hidden text-ellipsis group-hover:underline underline-offset-2">
{props.reference.relativePath}
</span>
<span className="ml-2">→</span>
</a>
</Link>
</FilePathMenu>
);
}
Loading