Files
vscode-front-matter/src/dashboardWebView/components/Header/ActionsBar.tsx
2024-10-24 12:05:07 +02:00

300 lines
12 KiB
TypeScript

import * as React from 'react';
import { NavigationType, Page } from '../../models';
import { CommandLineIcon, PencilIcon, TrashIcon, ChevronDownIcon, XMarkIcon, EyeIcon, LanguageIcon, CheckIcon } from '@heroicons/react/24/outline';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MultiSelectedItemsAtom, PagedItems, SelectedItemActionAtom, SelectedMediaFolderSelector, SettingsSelector } from '../../state';
import { ActionsBarItem } from './ActionsBarItem';
import { LocalizationKey, localize } from '../../../localization';
import { Alert } from '../Modals/Alert';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { CustomScript, ScriptType } from '../../../models';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { useFilesContext } from '../../providers/FilesProvider';
import { COMMAND_NAME, GeneralCommands } from '../../../constants';
import { RenameIcon } from '../../../components/icons/RenameIcon';
import { openFile } from '../../utils';
export interface IActionsBarProps {
view: NavigationType;
}
export const ActionsBar: React.FunctionComponent<IActionsBarProps> = ({
view
}: React.PropsWithChildren<IActionsBarProps>) => {
const [selectedFiles, setSelectedFiles] = useRecoilState(MultiSelectedItemsAtom);
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const [showAlert, setShowAlert] = React.useState(false);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const settings = useRecoilValue(SettingsSelector);
const { files } = useFilesContext();
const pagedItems = useRecoilValue(PagedItems);
const viewFile = React.useCallback(() => {
if (selectedFiles.length === 1) {
if (view === NavigationType.Contents) {
openFile(selectedFiles[0]);
} else if (view === NavigationType.Media) {
setSelectedItemAction({ path: selectedFiles[0], action: 'view' })
}
}
}, [selectedFiles]);
const onDeleteConfirm = React.useCallback(() => {
for (const file of selectedFiles) {
if (file) {
if (view === NavigationType.Contents) {
messageHandler.send(DashboardMessage.deleteFile, file);
} else if (view === NavigationType.Media) {
messageHandler.send(DashboardMessage.deleteMedia, {
file: file,
folder: selectedFolder
});
}
}
}
setSelectedFiles([]);
setShowAlert(false);
}, [selectedFiles]);
const runCustomScript = React.useCallback((script: CustomScript) => {
for (const file of selectedFiles) {
messageHandler.send(DashboardMessage.runCustomScript, {
script,
path: file
});
}
}, [selectedFiles]);
const selectAllItems = React.useCallback(() => {
const allSelected = [...selectedFiles, ...pagedItems];
setSelectedFiles(Array.from(new Set(allSelected)));
}, [selectedFiles, pagedItems]);
const hasAllItemsSelectedOnPage = React.useMemo(() => {
const selectedItemsOnPage = selectedFiles.filter((file) => pagedItems.includes(file));
return selectedItemsOnPage.length >= pagedItems.length;
}, [selectedFiles, pagedItems]);
const languageActions = React.useMemo(() => {
const actions: React.ReactNode[] = [];
if (view === NavigationType.Contents && files.length > 0 && selectedFiles.length === 1) {
const selectedItem = selectedFiles[0];
const page = ((files || []) as Page[]).find((f: Page) => f.fmFilePath === selectedItem);
if (page?.fmLocale) {
const locale = page.fmLocale;
const translations = page.fmTranslations;
actions.push(
<ActionsBarItem
key="translate"
onClick={() => {
messageHandler.send(GeneralCommands.toVSCode.runCommand, {
command: COMMAND_NAME.i18n.create,
args: selectedItem
})
}}>
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{localize(LocalizationKey.commonTranslate)}</span>
</ActionsBarItem>
)
if (translations && Object.keys(translations).length > 0) {
const crntLocale = translations[locale.locale];
const otherLocales = Object.entries(translations).filter(([key]) => key !== locale.locale);
if (otherLocales.length > 0) {
actions.push(
<DropdownMenu>
<DropdownMenuTrigger
className='flex items-center text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'
>
<LanguageIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{localize(LocalizationKey.commonLanguages)}</span>
<ChevronDownIcon className="ml-2 h-4 w-4" aria-hidden={true} />
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
<DropdownMenuItem onClick={() => openFile(crntLocale.path)}>
<span>{crntLocale.locale.title || crntLocale.locale.locale}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{
otherLocales.map(([key, value]) => (
<DropdownMenuItem
key={key}
onClick={() => openFile(value.path)}
>
<span>{value.locale.title || value.locale.locale}</span>
</DropdownMenuItem>
))
}
</DropdownMenuContent>
</DropdownMenu>
)
}
}
}
}
return actions;
}, [files, selectedFiles]);
const customScriptActions = React.useMemo(() => {
if (!settings?.scripts) {
return null;
}
const { scripts } = settings;
let crntScripts: CustomScript[] = [];
if (view === NavigationType.Contents) {
crntScripts = (scripts || [])
.filter((script) => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk && !script.hidden);
} else if (view === NavigationType.Media) {
crntScripts = (scripts || [])
.filter((script) => script.type === ScriptType.MediaFile && !script.hidden);
}
if (crntScripts.length > 0) {
return (
<DropdownMenu>
<DropdownMenuTrigger
className='flex items-center text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] disabled:opacity-50 disabled:hover:text-[var(--vscode-tab-inactiveForeground)]'
disabled={selectedFiles.length === 0}
>
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{localize(LocalizationKey.commonScripts)}</span>
<ChevronDownIcon className="ml-2 h-4 w-4" aria-hidden={true} />
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
{
crntScripts.map((script) => (
<DropdownMenuItem
key={script.id || script.title}
onClick={() => runCustomScript(script)}
>
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{script.title}</span>
</DropdownMenuItem>
))
}
</DropdownMenuContent>
</DropdownMenu>
);
}
return null;
}, [view, settings?.scripts, selectedFiles]);
return (
<>
<div
className={`w-full flex items-center justify-between py-2 px-4 border-b bg-[var(--vscode-sideBar-background)] text-[var(--vscode-sideBar-foreground)] border-[var(--frontmatter-border)]`}
aria-label="Item actions"
>
<div className='flex items-center space-x-6'>
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={viewFile}
title={localize(LocalizationKey.commonView)}
>
<EyeIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{localize(LocalizationKey.commonView)}</span>
</ActionsBarItem>
{
view === NavigationType.Contents && (
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={() => {
messageHandler.send(DashboardMessage.rename, selectedFiles[0]);
setSelectedFiles([]);
}}
title={localize(LocalizationKey.commonRename)}
>
<RenameIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{localize(LocalizationKey.commonRename)}</span>
</ActionsBarItem>
)
}
{
view === NavigationType.Media && (
<>
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={() => setSelectedItemAction({
path: selectedFiles[0],
action: 'edit'
})}
title={localize(LocalizationKey.commonEdit)}
>
<PencilIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{localize(LocalizationKey.commonEdit)}</span>
</ActionsBarItem>
</>
)
}
{languageActions}
{customScriptActions}
<ActionsBarItem
className='hover:text-[var(--vscode-statusBarItem-errorBackground)]'
disabled={selectedFiles.length === 0}
onClick={() => setShowAlert(true)}
title={localize(LocalizationKey.commonDelete)}
>
<TrashIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{localize(LocalizationKey.commonDelete)}</span>
</ActionsBarItem>
</div>
<div className='flex gap-4'>
{
selectedFiles.length > 0 && (
<ActionsBarItem
className='flex items-center hover:text-[var(--vscode-statusBarItem-warningBackground)]'
onClick={() => setSelectedFiles([])}
title={localize(LocalizationKey.dashboardHeaderActionsBarItemsSelected, selectedFiles.length)}
>
<XMarkIcon className="w-4 h-4 mr-1" aria-hidden="true" />
<span>{localize(LocalizationKey.dashboardHeaderActionsBarItemsSelected, selectedFiles.length)}</span>
</ActionsBarItem>
)
}
<ActionsBarItem
disabled={hasAllItemsSelectedOnPage}
onClick={selectAllItems}
title={localize(LocalizationKey.dashboardHeaderActionsBarSelectAll)}
>
<div className='w-4 h-4 inline-flex items-center justify-center border border-[var(--vscode-sideBar-foreground)] group-hover:border-[var(--vscode-statusBarItem-warningBackground)] rounded mr-1'>
<CheckIcon className="w-3 h-3" aria-hidden="true" />
</div>
<span>{localize(LocalizationKey.dashboardHeaderActionsBarSelectAll)}</span>
</ActionsBarItem>
</div>
</div >
{showAlert && (
<Alert
title={`${localize(LocalizationKey.dashboardHeaderActionsBarAlertDeleteTitle)}`}
description={localize(LocalizationKey.dashboardHeaderActionsBarAlertDeleteDescription)}
okBtnText={localize(LocalizationKey.commonDelete)}
cancelBtnText={localize(LocalizationKey.commonCancel)}
dismiss={() => setShowAlert(false)}
trigger={onDeleteConfirm}
/>
)
}
</>
);
};