mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
#623 - Fix update metadata on move
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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": "未割り当てのファイルのメタデータを再マップしますか"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -37,6 +37,8 @@ export enum DashboardMessage {
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
insertFile = 'insertFile',
|
||||
createHexoAssetFolder = 'createHexoAssetFolder',
|
||||
getUnmappedMedia = 'getUnmappedMedia',
|
||||
remapMediaMetadata = 'remapMediaMetadata',
|
||||
|
||||
// Data dashboard
|
||||
getDataEntries = 'getDataEntries',
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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')} />
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NavigationType {
|
||||
Welcome = 'welcome',
|
||||
Contents = 'contents',
|
||||
Media = 'media',
|
||||
Data = 'data',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
8
src/models/UnmappedMedia.ts
Normal file
8
src/models/UnmappedMedia.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface UnmappedMedia {
|
||||
file: string;
|
||||
absPath: string;
|
||||
metadata: {
|
||||
title: string;
|
||||
[prop: string]: string;
|
||||
};
|
||||
}
|
||||
@@ -25,4 +25,5 @@ export * from './StaticFolder';
|
||||
export * from './TaxonomyData';
|
||||
export * from './TaxonomyType';
|
||||
export * from './Template';
|
||||
export * from './UnmappedMedia';
|
||||
export * from './VersionInfo';
|
||||
|
||||
32
src/utils/flattenObjectKeys.ts
Normal file
32
src/utils/flattenObjectKeys.ts
Normal 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)];
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user