#623 - Fix update metadata on move

This commit is contained in:
Elio Struyf
2023-09-14 17:31:57 +02:00
parent c952cf4057
commit 180ea7880b
24 changed files with 279 additions and 126 deletions

View File

@@ -15,6 +15,7 @@
### 🐞 Fixes
- [#623](https://github.com/estruyf/vscode-front-matter/issues/623): Fix issue where metadata is not maintained on file move
- [#629](https://github.com/estruyf/vscode-front-matter/issues/629): Fix array indent to the new property
- [#660](https://github.com/estruyf/vscode-front-matter/issues/660): Allow only to select unique content relationship values
- [#661](https://github.com/estruyf/vscode-front-matter/issues/661): Fixing the dropdowns when used at the bottom of a collapsible group

View File

@@ -337,5 +337,6 @@
"dashboard.steps.stepsToGetStarted.template.description": "Wählen Sie eine Vorlage aus, um die Datei frontmatter.json mit den empfohlenen Einstellungen vorzufüllen.",
"listeners.dashboard.settingsListener.triggerTemplate.notification": "Vorlagendateien kopiert.",
"common.openOnWebsite": "Auf der Website öffnen",
"common.filter.value": "Filtern nach {0}"
"common.filter.value": "Filtern nach {0}",
"dashboard.media.detailsSlideOver.unmapped.description": "Möchten Sie die Metadaten von nicht zugeordneten Dateien neu zuordnen?"
}

View File

@@ -337,5 +337,6 @@
"dashboard.steps.stepsToGetStarted.template.description": "Sélectionnez un modèle pour préremplir le fichier frontmatter.json avec les paramètres recommandés.",
"listeners.dashboard.settingsListener.triggerTemplate.notification": "Fichiers de modèle copiés.",
"common.openOnWebsite": "Ouvrir sur le site web",
"common.filter.value": "Filtrer par {0}"
"common.filter.value": "Filtrer par {0}",
"dashboard.media.detailsSlideOver.unmapped.description": "Voulez-vous remapper les métadonnées des fichiers non mappés?"
}

View File

@@ -337,5 +337,6 @@
"dashboard.steps.stepsToGetStarted.template.description": "Seleziona un modello per riempire in anticipo il file frontmatter.json con le impostazioni consigliate.",
"listeners.dashboard.settingsListener.triggerTemplate.notification": "File del modello copiati.",
"common.openOnWebsite": "Apri sul sito web",
"common.filter.value": "Filtra per {0}"
"common.filter.value": "Filtra per {0}",
"dashboard.media.detailsSlideOver.unmapped.description": "Vuoi riassegnare i metadati dei file non mappati?"
}

View File

@@ -337,5 +337,6 @@
"dashboard.steps.stepsToGetStarted.template.description": "おすすめの設定でfrontmatter.jsonファイルを事前に埋めるテンプレートを選択します。",
"listeners.dashboard.settingsListener.triggerTemplate.notification": "テンプレートファイルがコピーされました。",
"common.openOnWebsite": "ウェブサイトで開く",
"common.filter.value": "{0} でフィルタリング"
"common.filter.value": "{0} でフィルタリング",
"dashboard.media.detailsSlideOver.unmapped.description": "未割り当てのファイルのメタデータを再マップしますか"
}

View File

@@ -284,6 +284,8 @@
"dashboard.welcomeScreen.actions.description": "You can also use the extension from the Front Matter side panel. There you will find the actions you can perform specifically for your pages.",
"dashboard.welcomeScreen.actions.thanks": "We hope you enjoy Front Matter!",
"dashboard.media.detailsSlideOver.unmapped.description": "Do you want to remap the metadata of unmapped files?",
"panel.contentType.contentTypeValidator.title": "Content-type",
"panel.contentType.contentTypeValidator.hint": "We noticed field differences between the content-type and the front matter data. \n Would you like to create, update, or set the content-type for this content?",
"panel.contentType.contentTypeValidator.button.create": "Create content-type",

View File

@@ -37,6 +37,8 @@ export enum DashboardMessage {
createMediaFolder = 'createMediaFolder',
insertFile = 'insertFile',
createHexoAssetFolder = 'createHexoAssetFolder',
getUnmappedMedia = 'getUnmappedMedia',
remapMediaMetadata = 'remapMediaMetadata',
// Data dashboard
getDataEntries = 'getDataEntries',

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { useRecoilState, useRecoilValue } from 'recoil';
import { NavigationType, Page } from '../../models';
import { DashboardViewAtom, SettingsSelector } from '../../state';
import { Overview } from './Overview';
import { Spinner } from '../Common/Spinner';
import { SponsorMsg } from '../Layout/SponsorMsg';
@@ -23,10 +23,13 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
}: React.PropsWithChildren<IContentsProps>) => {
const settings = useRecoilValue(SettingsSelector);
const { pageItems } = usePages(pages);
const [, setView] = useRecoilState(DashboardViewAtom);
const pageFolders = [...new Set(pageItems.map((page) => page.fmFolder))];
useEffect(() => {
setView(NavigationType.Contents);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewContentsView
});

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { Header } from '../Header';
import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { useRecoilState, useRecoilValue } from 'recoil';
import { DashboardViewAtom, SettingsSelector } from '../../state';
import { DataForm } from './DataForm';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { DataFile } from '../../../models/DataFile';
@@ -24,6 +24,7 @@ import { NavigationItem } from '../Layout';
import useThemeColors from '../../hooks/useThemeColors';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { NavigationType } from '../../models';
export interface IDataViewProps { }
@@ -35,6 +36,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
const [dataEntries, setDataEntries] = useState<any | any[] | null>(null);
const settings = useRecoilValue(SettingsSelector);
const { getColors } = useThemeColors();
const [, setView] = useRecoilState(DashboardViewAtom);
const setSchema = (dataFile: DataFile) => {
setSelectedData(dataFile);
@@ -135,6 +137,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
}, [selectedData, , dataEntries, selectedIndex]);
useEffect(() => {
setView(NavigationType.Data);
Messenger.listen(messageListener);
Messenger.send(DashboardMessage.sendTelemetry, {

View File

@@ -5,12 +5,11 @@ import { basename } from 'path';
import * as React from 'react';
import { Fragment, useCallback, useMemo } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { MediaInfo } from '../../../models';
import { Messenger } from '@estruyf/vscode/dist/client';
import { MediaInfo, UnmappedMedia } from '../../../models';
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { useRecoilValue } from 'recoil';
import { PageSelector, SelectedMediaFolderSelector } from '../../state';
import useThemeColors from '../../hooks/useThemeColors';
import { DetailsItem } from './DetailsItem';
import { DetailsInput } from './DetailsInput';
import * as l10n from '@vscode/l10n';
@@ -44,10 +43,10 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
const [filename, setFilename] = React.useState<string>(media.filename);
const [caption, setCaption] = React.useState<string | undefined>(media.caption);
const [title, setTitle] = React.useState<string | undefined>(media.title);
const [unmapped, setUnmapped] = React.useState<UnmappedMedia[]>([]);
const [alt, setAlt] = React.useState(media.alt);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const page = useRecoilValue(PageSelector);
const { getColors } = useThemeColors();
const createdDate = useMemo(() => DateHelper.tryParse(media.ctime), [media]);
const modifiedDate = useMemo(() => DateHelper.tryParse(media.mtime), [media]);
@@ -70,6 +69,17 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
onEditClose();
}, [media, filename, caption, alt, title, selectedFolder, page]);
const remapMetadata = useCallback((item: UnmappedMedia) => {
Messenger.send(DashboardMessage.remapMediaMetadata, {
file: media.fsPath,
unmappedItem: item,
folder: selectedFolder,
page
});
onEditClose();
}, [media, filename, caption, alt, title, selectedFolder, page]);
React.useEffect(() => {
setTitle(media.title);
setAlt(media.alt);
@@ -77,15 +87,21 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
setFilename(media.filename);
}, [media]);
React.useEffect(() => {
if (showForm) {
messageHandler.request<UnmappedMedia[]>(DashboardMessage.getUnmappedMedia, filename).then((result) => {
setUnmapped(result);
});
} else {
setUnmapped([]);
}
}, [showForm, filename]);
return (
<Transition.Root show={true} as={Fragment}>
<Dialog onClose={onDismiss} as={'div' as any} className="fixed inset-0 overflow-hidden z-50">
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className={`absolute inset-0 transition-opacity ${getColors(
'bg-vulcan-500 bg-opacity-75',
'bg-[var(--vscode-editor-background)] opacity-75'
)
}`} />
<Dialog.Overlay className={`absolute inset-0 transition-opacity bg-[var(--vscode-editor-background)] opacity-75`} />
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<Transition.Child
@@ -98,28 +114,16 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
leaveTo="translate-x-full"
>
<div className="pointer-events-auto w-screen max-w-md">
<div className={`flex h-full flex-col overflow-y-scroll border-l py-6 shadow-xl ${getColors(
`bg-white dark:bg-vulcan-400 border-whisper-900 dark:border-vulcan-50`,
`bg-[var(--vscode-sideBar-background)] border-[var(--frontmatter-border)]`
)
}`}>
<div className={`flex h-full flex-col overflow-y-scroll border-l py-6 shadow-xl bg-[var(--vscode-sideBar-background)] border-[var(--frontmatter-border)]`}>
<div className="px-4 sm:px-6">
<div className="flex items-start justify-between">
<Dialog.Title className={`text-lg font-medium ${getColors(
'text-vulcan-300 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<Dialog.Title className={`text-lg font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaDialogTitle)}
</Dialog.Title>
<div className="ml-3 flex h-7 items-center">
<button
type="button"
className={`focus:outline-none ${getColors(
'text-vulcan-300 dark:text-whisper-900 hover:text-vulcan-500 dark:hover:text-whisper-500',
'text-[var(--vscode-titleBar-inactiveForeground)] hover:text-[var(--vscode-titleBar-activeForeground)]'
)
}`}
className={`focus:outline-none text-[var(--vscode-titleBar-inactiveForeground)] hover:text-[var(--vscode-titleBar-activeForeground)]`}
onClick={onDismiss}
>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardMediaPanelClose)}</span>
@@ -133,28 +137,16 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
<div>
{isImageFile && (
<div className={`block w-full aspect-w-10 aspect-h-7 overflow-hidden border rounded ${getColors(
'border-gray-200 dark:border-vulcan-200',
'border-[var(--frontmatter-border)] bg-[var(--vscode-editor-background)]'
)
}`}>
<div className={`block w-full aspect-w-10 aspect-h-7 overflow-hidden border rounded border-[var(--frontmatter-border)] bg-[var(--vscode-editor-background)]`}>
<img src={imgSrc} alt={media.filename} className="object-cover max-h-[300px] mx-auto" />
</div>
)}
<div className="mt-4 flex items-start justify-between">
<div>
<h2 className={`text-lg font-medium ${getColors(
'text-vulcan-300 dark:text-whisper-500',
'text-[var(--vscode-foreground)]'
)
}`}>
<h2 className={`text-lg font-medium text-[var(--vscode-foreground)]`}>
{media.filename}
</h2>
<p className={`text-sm font-medium ${getColors(
'text-vulcan-100 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<p className={`text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{size}
</p>
</div>
@@ -165,44 +157,53 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{/* EDIT METADATA FORM */}
{showForm && (
<>
<h3 className={`text-base ${getColors(
'text-vulcan-300 dark:text-whisper-500',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<h3 className={`text-base text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelTitle)}
</h3>
<p className={`text-sm font-medium ${getColors(
'text-vulcan-100 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
{
unmapped && unmapped.length > 0 && (
<div className="flex flex-col py-3 space-y-3">
<p className={`text-sm my-3 font-medium text-[var(--vscode-editor-foreground)] opacity-90`}>
{l10n.t(LocalizationKey.dashboardMediaDetailsSlideOverUnmappedDescription)}
</p>
<ul className='pl-4'>
{
unmapped.map((item) => (
<li className='list-disc'>
<button
key={item.file}
className='hover:text-[var(--frontmatter-link-hover)]'
onClick={() => remapMetadata(item)}>
{item.file}{item.metadata.title ? ` (${item.metadata.title})` : ''}
</button>
</li>
))
}
</ul>
</div>
)
}
<p className={`text-sm my-3 font-medium text-[var(--vscode-editor-foreground)] opacity-90`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelDescription)}
</p>
<div className="flex flex-col py-3 space-y-3">
<div>
<label className={`block text-sm font-medium ${getColors(
'text-gray-700 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)}
</label>
<div className="relative mt-1">
<DetailsInput value={name || ""} onChange={(e) => setFilename(`${e.target.value}.${extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className={`sm:text-sm ${getColors('text-gray-500', 'placeholder-[var(--vscode-input-placeholderForeground)]')}`}>.{extension}</span>
<span className={`sm:text-sm placeholder-[var(--vscode-input-placeholderForeground)]`}>.{extension}</span>
</div>
</div>
</div>
<div>
<label className={`block text-sm font-medium ${getColors(
'text-gray-700 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}
</label>
<div className="mt-1">
@@ -213,11 +214,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{isImageFile && (
<>
<div>
<label className={`block text-sm font-medium ${getColors(
'text-gray-700 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonCaption)}
</label>
<div className="mt-1">
@@ -225,11 +222,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
</div>
</div>
<div>
<label className={`block text-sm font-medium ${getColors(
'text-gray-700 dark:text-whisper-900',
'text-[var(--vscode-editor-foreground)]'
)
}`}>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonAlt)}
</label>
<div className="mt-1">
@@ -243,11 +236,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className={`w-full inline-flex justify-center rounded border-transparent shadow-sm px-4 py-2 text-base font-medium sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30 ${getColors(
'border focus:outline-none focus:ring-2 focus:ring-offset-2 text-white bg-teal-600 hover:bg-teal-700 dark:hover:bg-teal-900 focus:ring-teal-500',
'bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] outline-[var(--vscode-focusBorder)] outline-1'
)
}`}
className={`w-full inline-flex justify-center rounded border-transparent shadow-sm px-4 py-2 text-base font-medium sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30 bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] outline-[var(--vscode-focusBorder)] outline-1`}
onClick={onSubmitMetadata}
disabled={!filename}
>
@@ -255,11 +244,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
</button>
<button
type="button"
className={`mt-3 w-full inline-flex justify-center rounded shadow-sm px-4 py-2 text-base font-medium focus:outline-none sm:mt-0 sm:w-auto sm:text-sm ${getColors(
'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-200',
'bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] text-[var(--vscode-button-secondaryForeground)]'
)
}`}
className={`mt-3 w-full inline-flex justify-center rounded shadow-sm px-4 py-2 text-base font-medium focus:outline-none sm:mt-0 sm:w-auto sm:text-sm bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] text-[var(--vscode-button-secondaryForeground)]`}
onClick={onEditClose}
>
{l10n.t(LocalizationKey.commonCancel)}
@@ -270,22 +255,14 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{!showForm && (
<>
<h3 className={`text-base flex items-center ${getColors(
'text-vulcan-300 dark:text-whisper-500',
'text-[var(--vscode-foreground)]'
)
}`}>
<h3 className={`text-base flex items-center text-[var(--vscode-foreground)]`}>
<span>{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFormMetadataTitle)}</span>
<button onClick={onEdit}>
<PencilAltIcon className="w-4 h-4 ml-2" aria-hidden="true" />
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
</button>
</h3>
<dl className={`mt-2 border-t border-b divide-y ${getColors(
'border-gray-200 dark:border-vulcan-200 divide-gray-200 dark:divide-vulcan-200',
'border-[var(--frontmatter-border)] divide-[var(--frontmatter-border)]'
)
}`}>
<dl className={`mt-2 border-t border-b divide-y border-[var(--frontmatter-border)] divide-[var(--frontmatter-border)]`}>
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)} details={media.filename} />
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaCommonTitle)} details={media.title || ""} />
@@ -302,18 +279,10 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{!showForm && (
<div>
<h3 className={`text-base ${getColors(
'text-vulcan-300 dark:text-whisper-500',
'text-[var(--vscode-foreground)]'
)
}`}>
<h3 className={`text-base text-[var(--vscode-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFormInformationTitle)}
</h3>
<dl className={`mt-2 border-t border-b divide-y ${getColors(
'border-gray-200 dark:border-vulcan-200 divide-gray-200 dark:divide-vulcan-200',
'border-[var(--frontmatter-border)] divide-[var(--frontmatter-border)]'
)
}`}>
<dl className={`mt-2 border-t border-b divide-y border-[var(--frontmatter-border)] divide-[var(--frontmatter-border)]`}>
{createdDate && (
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaMetadataPanelFormInformationCreatedDate)} details={format(createdDate, 'MMM dd, yyyy')} />
)}

View File

@@ -1,8 +1,9 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { UploadIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
DashboardViewAtom,
LoadingAtom,
MediaFoldersAtom,
SelectedMediaFolderAtom,
@@ -28,6 +29,7 @@ import { MediaInfo } from '../../../models';
import useThemeColors from '../../hooks/useThemeColors';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { NavigationType } from '../../models';
export interface IMediaProps { }
@@ -41,6 +43,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (
const folders = useRecoilValue(MediaFoldersAtom);
const loading = useRecoilValue(LoadingAtom);
const { getColors } = useThemeColors();
const [, setView] = useRecoilState(DashboardViewAtom);
const currentStaticFolder = useMemo(() => {
if (settings?.staticFolder) {
@@ -150,6 +153,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (
);
useEffect(() => {
setView(NavigationType.Media);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewMediaView
});

View File

@@ -2,14 +2,14 @@ import { Messenger } from '@estruyf/vscode/dist/client';
import { CodeIcon, PlusSmIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { TelemetryEvent } from '../../../constants/TelemetryEvent';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { DashboardMessage } from '../../DashboardMessage';
import useThemeColors from '../../hooks/useThemeColors';
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { DashboardViewAtom, ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { FilterInput } from '../Header/FilterInput';
import { PageLayout } from '../Layout/PageLayout';
import { FormDialog } from '../Modals/FormDialog';
@@ -18,6 +18,7 @@ import { Item } from './Item';
import { NewForm } from './NewForm';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { NavigationType } from '../../models';
export interface ISnippetsProps { }
@@ -34,6 +35,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (
const [mediaSnippet, setMediaSnippet] = useState(false);
const [snippetFilter, setSnippetFilter] = useState<string>('');
const { getColors } = useThemeColors();
const [, setView] = useRecoilState(DashboardViewAtom);
const snippets = settings?.snippets || {};
const snippetKeys = useMemo(() => {
@@ -82,6 +84,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (
};
useEffect(() => {
setView(NavigationType.Snippets);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewSnippetsView
});

View File

@@ -2,12 +2,12 @@ import { Messenger } from '@estruyf/vscode/dist/client';
import { ChevronRightIcon, DownloadIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { TelemetryEvent } from '../../../constants';
import { TaxonomyData } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { NavigationType, Page } from '../../models';
import { DashboardViewAtom, SettingsSelector } from '../../state';
import { NavigationBar, NavigationItem } from '../Layout';
import { PageLayout } from '../Layout/PageLayout';
import { SponsorMsg } from '../Layout/SponsorMsg';
@@ -25,6 +25,7 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
const settings = useRecoilValue(SettingsSelector);
const [taxonomySettings, setTaxonomySettings] = useState<TaxonomyData>();
const [selectedTaxonomy, setSelectedTaxonomy] = useState<string | null>(`tags`);
const [, setView] = useRecoilState(DashboardViewAtom);
const onImport = () => {
Messenger.send(DashboardMessage.importTaxonomy);
@@ -39,6 +40,7 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
}, [settings?.tags, settings?.categories, settings?.customTaxonomy]);
useEffect(() => {
setView(NavigationType.Taxonomy);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewTaxonomyDashboard
});

View File

@@ -11,6 +11,9 @@ import useThemeColors from '../../hooks/useThemeColors';
import { WelcomeLink } from './WelcomeLink';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DashboardViewAtom } from '../../state';
import { useRecoilState } from 'recoil';
import { NavigationType } from '../../models';
export interface IWelcomeScreenProps {
settings: Settings;
@@ -20,8 +23,10 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({
settings
}: React.PropsWithChildren<IWelcomeScreenProps>) => {
const { getColors } = useThemeColors();
const [, setView] = useRecoilState(DashboardViewAtom);
React.useEffect(() => {
setView(NavigationType.Welcome);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewWelcomeScreen
});

View File

@@ -1,4 +1,5 @@
export enum NavigationType {
Welcome = 'welcome',
Contents = 'contents',
Media = 'media',
Data = 'data',

View File

@@ -1,7 +1,9 @@
import { Notifications } from './Notifications';
import { Uri, workspace } from 'vscode';
import { Uri } from 'vscode';
import { Folders } from '../commands/Folders';
import { isValidFile } from './isValidFile';
import { parseWinPath } from './parseWinPath';
import { join } from 'path';
export class FilesHelper {
/**
@@ -28,4 +30,15 @@ export class FilesHelper {
return pages;
}
/**
* Relative path to absolute path
* @param filePath
* @returns
*/
public static relToAbsPath(filePath: string): string {
const wsFolder = Folders.getWorkspaceFolder();
let absPath = join(parseWinPath(wsFolder?.fsPath || ''), filePath);
return parseWinPath(absPath);
}
}

View File

@@ -26,6 +26,7 @@ import imageSize from 'image-size';
import { EditorHelper } from '@estruyf/vscode';
import { SortOption } from '../dashboardWebView/constants/SortOption';
import { DataListener, MediaListener } from '../listeners/panel';
import { MediaListener as DashboardMediaListener } from '../listeners/dashboard';
import { ArticleHelper } from './ArticleHelper';
import { lookup } from 'mime-types';
import { existsAsync, readdirAsync, unlinkAsync, writeFileAsync } from '../utils';
@@ -273,6 +274,10 @@ export class MediaHelpers {
*/
public static resetMedia() {
MediaHelpers.media = [];
if (Dashboard.isOpen) {
DashboardMediaListener.sendMediaFiles();
}
}
/**

View File

@@ -1,5 +1,5 @@
import { MediaHelpers } from './MediaHelpers';
import { workspace } from 'vscode';
import { Disposable, workspace } from 'vscode';
import { Config, JsonDB } from 'node-json-db';
import { basename, dirname, join, parse } from 'path';
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders';
@@ -16,6 +16,7 @@ interface MediaRecord {
export class MediaLibrary {
private db: JsonDB | undefined;
private renameFilesListener: Disposable | undefined;
private static instance: MediaLibrary;
private constructor() {
@@ -65,17 +66,23 @@ export class MediaLibrary {
)
);
workspace.onDidRenameFiles((e) => {
e.files.forEach((f) => {
if (this.renameFilesListener) {
this.renameFilesListener.dispose();
}
this.renameFilesListener = workspace.onDidRenameFiles((e) => {
e.files.forEach(async (f) => {
const path = f.oldUri.path.toLowerCase();
// Check if file is an image
if (
path.endsWith('.jpeg') ||
path.endsWith('.jpg') ||
path.endsWith('.png') ||
path.endsWith('.gif')
path.endsWith('.gif') ||
path.endsWith('.mp4') ||
path.endsWith('.pdf')
) {
this.rename(f.oldUri.fsPath, f.newUri.fsPath);
await this.rename(f.oldUri.fsPath, f.newUri.fsPath);
MediaHelpers.resetMedia();
}
});
@@ -90,6 +97,10 @@ export class MediaLibrary {
return MediaLibrary.instance;
}
public static reset() {
MediaLibrary.instance = new MediaLibrary();
}
public async get(id: string): Promise<MediaRecord | undefined> {
try {
const fileId = this.parsePath(id);
@@ -102,15 +113,24 @@ export class MediaLibrary {
}
}
public async getAll() {
try {
const data = await this.db?.getData('/');
return data;
} catch {
return undefined;
}
}
public set(id: string, metadata: any): void {
const fileId = this.parsePath(id);
this.db?.push(fileId, metadata, true);
}
public rename(oldId: string, newId: string): void {
public async rename(oldId: string, newId: string): Promise<void> {
const fileId = this.parsePath(oldId);
const newFileId = this.parsePath(newId);
const data = this.db?.getData(fileId);
const data = await this.get(fileId);
if (data) {
this.db?.delete(fileId);
this.db?.push(newFileId, data, true);
@@ -130,7 +150,7 @@ export class MediaLibrary {
Notifications.warning(`The name "${filename}" already exists at the file location.`);
} else {
await renameAsync(filePath, newPath);
this.rename(filePath, newPath);
await this.rename(filePath, newPath);
MediaHelpers.resetMedia();
}
} catch (err) {
@@ -139,7 +159,7 @@ export class MediaLibrary {
}
}
private parsePath(path: string) {
public parsePath(path: string) {
const wsFolder = Folders.getWorkspaceFolder();
const isWindows = process.platform === 'win32';
let absPath = path.replace(parseWinPath(wsFolder?.fsPath || ''), WORKSPACE_PLACEHOLDER);

View File

@@ -8,7 +8,9 @@ import { commands, env, Uri } from 'vscode';
import { COMMAND_NAME, TelemetryEvent } from '../../constants';
import * as os from 'os';
import { Folders } from '../../commands';
import { PostMessageData } from '../../models';
import { PostMessageData, UnmappedMedia } from '../../models';
import { FilesHelper, MediaLibrary } from '../../helpers';
import { existsAsync, flattenObjectKeys } from '../../utils';
export class MediaListener extends BaseListener {
private static timers: { [folder: string]: any } = {};
@@ -49,6 +51,12 @@ export class MediaListener extends BaseListener {
Telemetry.send(TelemetryEvent.updateMediaMetadata);
this.update(msg.payload);
break;
case DashboardMessage.getUnmappedMedia:
this.getUnmappedMedia(msg);
break;
case DashboardMessage.remapMediaMetadata:
this.remapMediaMetadata(msg);
break;
case DashboardMessage.createMediaFolder:
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.payload);
break;
@@ -71,10 +79,15 @@ export class MediaListener extends BaseListener {
folder: string = '',
sorting: SortingOption | null = null
) {
MediaLibrary.reset();
const files = await MediaHelpers.getMedia(page, folder, sorting);
this.sendMsg(DashboardCommand.media, files);
}
/**
* Open file in finder or explorer
* @param file
*/
private static openFileInFinder(file: string) {
if (file) {
if (os.type() === 'Linux' && env.remoteName?.toLowerCase() === 'wsl') {
@@ -85,6 +98,62 @@ export class MediaListener extends BaseListener {
}
}
private static async remapMediaMetadata({ command, payload }: PostMessageData) {
if (!payload || !command) {
return;
}
const { unmappedItem, file, folder, page } = payload;
if (!unmappedItem || !(unmappedItem as UnmappedMedia).absPath || !file) {
return;
}
const mediaLib = MediaLibrary.getInstance();
await mediaLib.rename((unmappedItem as UnmappedMedia).absPath, file);
this.sendMediaFiles(page || 0, folder || '');
}
/**
* Find all the unmapped media file with the given name
* @param msg
*/
private static async getUnmappedMedia({ command, payload, requestId }: PostMessageData) {
if (!payload || !command || !requestId) {
return;
}
const mediaLib = MediaLibrary.getInstance();
const allMetadata = await mediaLib.getAll();
const allFilePaths = flattenObjectKeys(allMetadata);
const filesEndingWith = allFilePaths.filter((f) => f.endsWith(payload));
// Check if the files exist
const unmappedFiles: UnmappedMedia[] = [];
for (const file of filesEndingWith) {
const absPath = FilesHelper.relToAbsPath(file);
if (!(await existsAsync(absPath))) {
const parsedPath = mediaLib.parsePath(absPath);
const metadata = await mediaLib.get(parsedPath);
if (metadata) {
unmappedFiles.push({
file,
absPath,
metadata: {
...(metadata as { [key: string]: any })
}
} as UnmappedMedia);
}
}
}
if (unmappedFiles && unmappedFiles.length > 0) {
this.sendRequest(command as any, requestId, unmappedFiles);
}
}
/**
* Store the file and send a message after multiple uploads
* @param data

View File

@@ -931,6 +931,10 @@ export enum LocalizationKey {
* We hope you enjoy Front Matter!
*/
dashboardWelcomeScreenActionsThanks = 'dashboard.welcomeScreen.actions.thanks',
/**
* Found the following unmapped files. Do you want to remap the metadata?
*/
dashboardMediaDetailsSlideOverUnmappedDescription = 'dashboard.media.detailsSlideOver.unmapped.description',
/**
* Content-type
*/

View File

@@ -0,0 +1,8 @@
export interface UnmappedMedia {
file: string;
absPath: string;
metadata: {
title: string;
[prop: string]: string;
};
}

View File

@@ -25,4 +25,5 @@ export * from './StaticFolder';
export * from './TaxonomyData';
export * from './TaxonomyType';
export * from './Template';
export * from './UnmappedMedia';
export * from './VersionInfo';

View File

@@ -0,0 +1,32 @@
import { join } from 'path';
export const flattenObjectKeys = (obj: any, crntKey: string = '') => {
let toReturn: string[] = [];
const keys = Object.keys(obj);
for (const key of keys) {
const crntObj = obj[key];
if (typeof crntObj === 'object' && crntObj !== null && Object.keys(crntObj).length > 0) {
const hasTextKeys = Object.keys(crntObj).some((subKey) => {
if (typeof crntObj[subKey] === 'string') {
return true;
}
return false;
});
if (hasTextKeys) {
toReturn.push(join(crntKey, key));
continue;
}
const flatKeyNames = flattenObjectKeys(crntObj, join(crntKey, key));
toReturn = [...toReturn, ...flatKeyNames];
} else if (typeof crntObj !== 'string' || Object.keys(crntObj).length === 0) {
toReturn.push(join(crntKey, key));
}
}
return [...new Set(toReturn)];
};

View File

@@ -3,6 +3,8 @@ export * from './encodeEmoji';
export * from './existsAsync';
export * from './fetchWithTimeout';
export * from './fieldWhenClause';
export * from './flattenObjectKeys';
export * from './getLocalizationFile';
export * from './mkdirAsync';
export * from './readFileAsync';
export * from './readdirAsync';