#303 - Content card actions

This commit is contained in:
Elio Struyf
2022-03-30 11:47:25 +02:00
parent 9dbdd092f6
commit bd09a10fd8
15 changed files with 215 additions and 60 deletions
+2 -1
View File
@@ -10,9 +10,10 @@ export enum DashboardMessage {
createByTemplate = 'createByTemplate',
refreshPages = 'refreshPages',
searchPages = 'searchPages',
openFile = 'openFile',
deleteFile = 'deleteFile',
// Media Dashboard
openFile = 'openFile',
getMedia = 'getMedia',
copyToClipboard = 'copyToClipboard',
refreshMedia = 'refreshMedia',
@@ -0,0 +1,100 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { EyeIcon, TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { CustomScript, ScriptType } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
import { Alert } from '../Modals/Alert';
export interface IContentActionsProps {
title: string;
path: string;
scripts: CustomScript[] | undefined;
onOpen: () => void;
}
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen }: React.PropsWithChildren<IContentActionsProps>) => {
const [ showDeletionAlert, setShowDeletionAlert ] = React.useState(false);
const onView = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onOpen();
};
const onDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setShowDeletionAlert(true);
};
const onDeleteConfirm = () => {
if (path) {
Messenger.send(DashboardMessage.deleteFile, path);
}
}
const runCustomScript = React.useCallback((e: React.MouseEvent<HTMLButtonElement>, script: CustomScript) => {
e.stopPropagation();
Messenger.send(DashboardMessage.runCustomScript, {script, path});
}, [path]);
const customScriptActions = React.useMemo(() => {
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
<MenuItem
key={script.title}
title={script.title}
onClick={(value, e) => runCustomScript(e, script)} />
))
}, [scripts]);
return (
<>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='hidden group-hover:flex'>
<QuickAction
title={`View content`}
onClick={onView}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete content`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
<Menu as="div" className="relative z-10 flex text-left">
<ActionMenuButton title={`Menu`} />
<MenuItems widthClass='w-40'>
<MenuItem
title={`View`}
onClick={onView} />
{ customScriptActions }
<MenuItem
title={`Delete`}
onClick={(value, e) => onDelete(e)} />
</MenuItems>
</Menu>
</div>
</div>
{
showDeletionAlert && (
<Alert
title={`Delete: ${title}`}
description={`Are you sure you want to delete the "${title}" content?`}
okBtnText={`Delete`}
cancelBtnText={`Cancel`}
dismiss={() => setShowDeletionAlert(false)}
trigger={onDeleteConfirm} />
)
}
</>
);
};
@@ -8,6 +8,7 @@ import { DateField } from '../DateField';
import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardViewType } from '../../models';
import { ContentActions } from './ContentActions';
export interface IItemProps extends Page {}
@@ -35,6 +36,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
{
pageData[PREVIEW_IMAGE_FIELD] ? (
@@ -47,6 +49,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
}
</div>
<ContentActions
title={title}
path={fmFilePath}
scripts={settings?.scripts}
onOpen={openFile} />
<div className="p-4 w-full">
<div className={`flex justify-between items-center`}>
<Status draft={draft} />
+11 -11
View File
@@ -12,10 +12,10 @@ import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { ActionMenuButton } from '../Menu/ActionMenuButton';
import { QuickAction } from '../Menu/QuickAction';
import { Alert } from '../Modals/Alert';
import { DetailsSlideOver } from './DetailsSlideOver';
import { MenuButton } from './MenuButton'
import { QuickAction } from './QuickAction';
export interface IItemProps {
media: MediaInfo;
@@ -219,17 +219,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='hidden group-hover:inline-block h-5'>
<div className='hidden group-hover:flex'>
<QuickAction
title='View media details'
onClick={viewMediaDetails}>
<EyeIcon className={`h-5 w-5`} aria-hidden="true" />
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Edit metadata'
onClick={updateMetadata}>
<PencilIcon className={`h-5 w-5`} aria-hidden="true" />
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{
@@ -238,7 +238,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<QuickAction
title={(viewData.data.metadataInsert && viewData.data.fieldName) ? `Insert image for your "${viewData.data.fieldName}" field` : `Insert image with markdown markup`}
onClick={insertToArticle}>
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{
@@ -246,7 +246,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<QuickAction
title='Insert snippet'
onClick={insertSnippet}>
<CodeIcon className={`h-5 w-5`} aria-hidden="true" />
<CodeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
@@ -256,7 +256,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<QuickAction
title='Copy media path'
onClick={copyToClipboard}>
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</>
)
@@ -265,12 +265,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
<Menu as="div" className="relative z-10 inline-block text-left h-5">
<MenuButton title={`Menu`} />
<Menu as="div" className="relative z-10 flex text-left">
<ActionMenuButton title={`Menu`} />
<MenuItems widthClass='w-40'>
<MenuItem
@@ -1,20 +0,0 @@
import { Menu } from '@headlessui/react';
import {DotsVerticalIcon} from '@heroicons/react/outline';
import * as React from 'react';
export interface IMenuButtonProps {
title: string;
disabled?: boolean;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`inline-flex items-center ${disabled ? 'opacity-50' : ''}`}>
<Menu.Button disabled={disabled} className="group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600">
<span className="sr-only">{title}</span>
<DotsVerticalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
);
};
@@ -0,0 +1,18 @@
import { Menu } from '@headlessui/react';
import {DotsVerticalIcon} from '@heroicons/react/outline';
import * as React from 'react';
export interface IActionMenuButtonProps {
title: string;
disabled?: boolean;
}
export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IActionMenuButtonProps>) => {
return (
<Menu.Button disabled={disabled} className={`group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600 ${disabled ? 'opacity-50' : ''}`}>
<span className="sr-only">{title}</span>
<DotsVerticalIcon className="w-4 h-4" aria-hidden="true" />
</Menu.Button>
);
};
@@ -6,7 +6,7 @@ export interface IMenuItemProps {
value?: any;
isCurrent?: boolean;
disabled?: boolean;
onClick: (value: any) => void;
onClick: (value: any, e: React.MouseEvent<HTMLButtonElement>) => void;
}
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, disabled, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
@@ -14,7 +14,7 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value,
<Menu.Item>
<button
disabled={disabled}
onClick={() => onClick(value)}
onClick={(e) => onClick(value, e)}
className={`${!isCurrent ? `font-normal` : `font-bold`} text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
>
{title}
@@ -17,7 +17,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className={`${widthClass || ""} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<Menu.Items className={`${widthClass || ""} origin-top-right absolute right-0 z-10 mt-5 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<div className="py-1">
{children}
</div>
@@ -2,7 +2,7 @@ import * as React from 'react';
export interface IQuickActionProps {
title: string;
onClick: () => void;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({title, onClick, children}: React.PropsWithChildren<IQuickActionProps>) => {
@@ -1,3 +1,5 @@
export * from './ActionMenuButton';
export * from './MenuButton';
export * from './MenuItem';
export * from './MenuItems';
export * from './QuickAction';
@@ -4,9 +4,10 @@ import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { Snippet, SnippetField, Snippets } from '../../../models';
import { Snippet, Snippets } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsSelector, ViewDataSelector } from '../../state';
import { QuickAction } from '../Menu';
import { Alert } from '../Modals/Alert';
import { FormDialog } from '../Modals/FormDialog';
import { NewForm } from './NewForm';
@@ -106,27 +107,30 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
<DotsHorizontalIcon className="w-4 h-4" />
</div>
<div className='hidden group-hover:flex space-x-2'>
<div className='hidden group-hover:flex'>
{
viewData?.data?.filePath && (
<>
<button onClick={() => setShowInsertDialog(true)}>
<PlusIcon className='w-4 h-4' />
<span className='sr-only'>Insert snippet</span>
</button>
<QuickAction
title={`Insert snippet`}
onClick={() => setShowInsertDialog(true)}>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</>
)
}
<button onClick={onOpenEdit}>
<PencilIcon className='w-4 h-4' />
<span className='sr-only'>Edit snippet</span>
</button>
<QuickAction
title={`Edit snippet`}
onClick={onOpenEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<button onClick={() => setShowAlert(true)}>
<TrashIcon className='w-4 h-4' />
<span className='sr-only'>Delete snippet</span>
</button>
<QuickAction
title={`Delete snippet`}
onClick={() => setShowAlert(true)}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
</div>
</div>
+25 -9
View File
@@ -30,6 +30,9 @@ export class CustomScript {
if (script.bulk) {
// Run script on all files
CustomScript.bulkRun(wsPath, script);
} else if (path) {
// Run script for provided path
CustomScript.singleRun(wsPath, script, path);
} else {
// Run script on current file.
CustomScript.singleRun(wsPath, script);
@@ -38,18 +41,31 @@ export class CustomScript {
}
}
private static async singleRun(wsPath: string, script: ICustomScript): Promise<void> {
const editor = window.activeTextEditor;
if (!editor) return;
private static async singleRun(wsPath: string, script: ICustomScript, path: string | null = null): Promise<void> {
let articlePath: string | null = path;
let article: ParsedFrontMatter | null = null;
const article = ArticleHelper.getFrontMatter(editor);
if (!path) {
const editor = window.activeTextEditor;
if (!editor) return;
if (article) {
const output = await CustomScript.runScript(wsPath, article, editor.document.uri.fsPath, script);
CustomScript.showOutput(output, script);
articlePath = editor.document.uri.fsPath;
article = ArticleHelper.getFrontMatter(editor);
} else {
Notifications.warning(`${script.title}: Current article couldn't be retrieved.`);
article = ArticleHelper.getFrontMatterByPath(path);
}
if (articlePath && article) {
window.withProgress({
location: ProgressLocation.Notification,
title: `Executing: ${script.title}`,
cancellable: false
}, async () => {
const output = await CustomScript.runScript(wsPath, article, articlePath as string, script);
CustomScript.showOutput(output, script);
});
} else {
Notifications.warning(`${script.title}: Article couldn't be retrieved.`);
}
}
+2
View File
@@ -1,4 +1,5 @@
import { Uri, workspace, window } from "vscode";
import { Logger } from "./Logger";
import { Notifications } from "./Notifications";
export const openFileInEditor = async (filePath: string) => {
@@ -8,6 +9,7 @@ export const openFileInEditor = async (filePath: string) => {
await window.showTextDocument(doc, 1, false);
} catch (e) {
Notifications.error(`Couldn't open the file.`);
Logger.error(`${filePath}: ${(e as Error).message}`);
}
}
};
+24 -1
View File
@@ -1,5 +1,5 @@
import { isValidFile } from '../../helpers/isValidFile';
import { existsSync } from "fs";
import { existsSync, unlinkSync } from "fs";
import { basename, dirname, join } from "path";
import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode";
import { Dashboard } from "../../commands/Dashboard";
@@ -48,6 +48,9 @@ export class PagesListener extends BaseListener {
case DashboardMessage.searchPages:
this.searchPages(msg.data);
break;
case DashboardMessage.deleteFile:
this.deletePage(msg.data);
break;
}
}
@@ -96,6 +99,26 @@ export class PagesListener extends BaseListener {
}
}
/**
* Delete a page
* @param path
*/
private static async deletePage(path: string) {
if (!path) {
return;
}
Logger.info(`Deleting file: ${path}`)
unlinkSync(path);
this.lastPages = this.lastPages.filter(p => p.fmFilePath !== path);
this.sendPageData(this.lastPages);
const ext = Extension.getInstance();
await ext.setState(ExtensionState.Dashboard.Pages.Cache, this.lastPages, "workspace");
}
/**
* Watcher for processing page updates
* @param file