mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-07-05 17:31:22 +02:00
@@ -5,6 +5,7 @@
|
||||
### ✨ New features
|
||||
|
||||
- [#113](https://github.com/estruyf/vscode-front-matter/issues/113): Integrating a local DB for media metadata (caption, alt)
|
||||
- [#132](https://github.com/estruyf/vscode-front-matter/issues/132): Major changes to the media dashboard which allows you to navigate through all folders
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
@@ -17,6 +18,7 @@
|
||||
- [#126](https://github.com/estruyf/vscode-front-matter/issues/126): Create new content from the available content types
|
||||
- [#127](https://github.com/estruyf/vscode-front-matter/issues/127): Title bar action added to open the dashboard
|
||||
- [#128](https://github.com/estruyf/vscode-front-matter/issues/128): Support for multi-select on image fields added
|
||||
- [#131](https://github.com/estruyf/vscode-front-matter/issues/131): Folder creation support added on media dashboard
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
|
||||
+74
-40
@@ -1,7 +1,7 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES } from './../constants/settings';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES } from './../constants/settings';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { basename, dirname, extname, join } from "path";
|
||||
import { existsSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
|
||||
import { Settings as SettingsHelper } from '../helpers';
|
||||
import { TaxonomyType } from '../models';
|
||||
@@ -164,7 +164,7 @@ export class Dashboard {
|
||||
Extension.getInstance().setState(EXTENSION_STATE_PAGES_VIEW, msg.data);
|
||||
break;
|
||||
case DashboardMessage.getMedia:
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder)
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
|
||||
break;
|
||||
case DashboardMessage.copyToClipboard:
|
||||
env.clipboard.writeText(msg.data);
|
||||
@@ -185,13 +185,24 @@ export class Dashboard {
|
||||
case DashboardMessage.updateMediaMetadata:
|
||||
Dashboard.updateMediaMetadata(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset media array
|
||||
*/
|
||||
public static resetMedia() {
|
||||
Dashboard.media = [];
|
||||
}
|
||||
|
||||
public static switchFolder(folderPath: string) {
|
||||
Dashboard.resetMedia();
|
||||
Dashboard.getMedia(0, folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an image into the front matter or contents
|
||||
@@ -236,7 +247,7 @@ export class Dashboard {
|
||||
data: {
|
||||
beta: ext.isBetaVersion(),
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS),
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
|
||||
folders: Folders.get(),
|
||||
initialized: await Template.isInitialized(),
|
||||
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
|
||||
@@ -246,6 +257,7 @@ export class Dashboard {
|
||||
pageViewType: await ext.getState<ViewType | undefined>(EXTENSION_STATE_PAGES_VIEW),
|
||||
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
contentFolders: Folders.get().map(f => f.path),
|
||||
} as Settings
|
||||
});
|
||||
}
|
||||
@@ -261,49 +273,58 @@ export class Dashboard {
|
||||
/**
|
||||
* Retrieve all media files
|
||||
*/
|
||||
private static async getMedia(page: number = 0, folder: string = '') {
|
||||
private static async getMedia(page: number = 0, selectedFolder: string = '') {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const contentFolders = Folders.get();
|
||||
|
||||
if (Dashboard.media.length === 0) {
|
||||
const contentFolder = Folders.get();
|
||||
let allMedia: MediaInfo[] = [];
|
||||
const relSelectedFolderPath = selectedFolder ? selectedFolder.substring((wsFolder?.fsPath || "").length + 1) : '';
|
||||
|
||||
let allMedia: MediaInfo[] = [];
|
||||
|
||||
if (relSelectedFolderPath) {
|
||||
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
|
||||
const media = Dashboard.filterMedia(files);
|
||||
allMedia = [...media];
|
||||
} else {
|
||||
if (staticFolder) {
|
||||
const files = await workspace.findFiles(`${staticFolder || ""}/**/*`);
|
||||
const folderSearch = join(staticFolder || "", '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = Dashboard.filterMedia(files);
|
||||
|
||||
allMedia = [...media];
|
||||
}
|
||||
|
||||
if (contentFolder && wsFolder) {
|
||||
for (let i = 0; i < contentFolder.length; i++) {
|
||||
const folder = contentFolder[i];
|
||||
const relFolderPath = folder.path.substring(wsFolder.fsPath.length + 1);
|
||||
const files = await workspace.findFiles(`${relFolderPath}/**/*`);
|
||||
if (contentFolders && wsFolder) {
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const contentFolder = contentFolders[i];
|
||||
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
|
||||
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = Dashboard.filterMedia(files);
|
||||
|
||||
allMedia = [...allMedia, ...media];
|
||||
}
|
||||
}
|
||||
|
||||
allMedia = allMedia.sort((a, b) => {
|
||||
if (b.fsPath < a.fsPath) {
|
||||
return -1;
|
||||
}
|
||||
if (b.fsPath > a.fsPath) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
Dashboard.media = Object.assign([], allMedia);
|
||||
}
|
||||
|
||||
allMedia = allMedia.sort((a, b) => {
|
||||
if (b.fsPath < a.fsPath) {
|
||||
return -1;
|
||||
}
|
||||
if (b.fsPath > a.fsPath) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
Dashboard.media = Object.assign([], allMedia);
|
||||
|
||||
// Filter the media
|
||||
let files: MediaInfo[] = Dashboard.media;
|
||||
if (folder) {
|
||||
files = files.filter(f => f.fsPath.includes(folder));
|
||||
if (relSelectedFolderPath) {
|
||||
const folderPath = `${relSelectedFolderPath.startsWith("/") ? "" : "/"}${relSelectedFolderPath}${relSelectedFolderPath.endsWith("/") ? "" : "/"}`
|
||||
files = files.filter(f => f.fsPath.includes(folderPath));
|
||||
}
|
||||
|
||||
// Retrieve the total after filtering and before the slicing happens
|
||||
@@ -327,23 +348,36 @@ export class Dashboard {
|
||||
});
|
||||
files = files.filter(f => f.stats !== undefined);
|
||||
|
||||
const folders = [...new Set(Dashboard.media.map((file) => {
|
||||
let relFolderPath = wsFolder ? file.fsPath.substring(wsFolder.fsPath.length + 1) : file.fsPath;
|
||||
if (staticFolder && relFolderPath.startsWith(staticFolder)) {
|
||||
relFolderPath = relFolderPath.substring(staticFolder.length);
|
||||
// Retrieve all the folders
|
||||
let allContentFolders: string[] = [];
|
||||
let allFolders: string[] = [];
|
||||
|
||||
if (selectedFolder) {
|
||||
if (existsSync(selectedFolder)) {
|
||||
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => join(selectedFolder, dir.name));
|
||||
}
|
||||
if (relFolderPath?.startsWith('/')) {
|
||||
relFolderPath = relFolderPath.substring(1);
|
||||
} else {
|
||||
for (const contentFolder of contentFolders) {
|
||||
const contentPath = contentFolder.path;
|
||||
if (contentPath && existsSync(contentPath)) {
|
||||
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => join(contentPath, dir.name));
|
||||
allContentFolders = [...allContentFolders, ...subFolders];
|
||||
}
|
||||
}
|
||||
return dirname(relFolderPath);
|
||||
}))];
|
||||
|
||||
const staticPath = join(wsFolder?.fsPath || "", staticFolder || "");
|
||||
if (staticPath && existsSync(staticPath)) {
|
||||
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => join(staticPath, dir.name));
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.media,
|
||||
data: {
|
||||
media: files,
|
||||
total: total,
|
||||
folders
|
||||
folders: [...allContentFolders, ...allFolders],
|
||||
selectedFolder
|
||||
} as MediaPaths
|
||||
});
|
||||
}
|
||||
@@ -356,7 +390,7 @@ export class Dashboard {
|
||||
|
||||
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
|
||||
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Page[] = [];
|
||||
@@ -457,7 +491,7 @@ export class Dashboard {
|
||||
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
|
||||
if (fileName && contents) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const wsPath = wsFolder ? wsFolder.fsPath : "";
|
||||
let absFolderPath = join(wsPath, staticFolder || "", folder || "");
|
||||
|
||||
|
||||
+54
-2
@@ -1,5 +1,5 @@
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import { SETTINGS_CONTENT_PAGE_FOLDERS } from './../constants/settings';
|
||||
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER } from './../constants/settings';
|
||||
import { commands, Uri, workspace, window } from "vscode";
|
||||
import { basename, join } from "path";
|
||||
import { ContentFolder, FileInfo, FolderInfo } from "../models";
|
||||
@@ -7,12 +7,64 @@ import uniqBy = require("lodash.uniqby");
|
||||
import { Template } from "./Template";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Settings } from "../helpers";
|
||||
import { existsSync } from 'fs';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
import { Dashboard } from './Dashboard';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
export class Folders {
|
||||
|
||||
/**
|
||||
* Add a media folder
|
||||
* @returns
|
||||
*/
|
||||
public static async addMediaFolder(data?: {selectedFolder?: string}) {
|
||||
let wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
let startPath = "";
|
||||
|
||||
if (data?.selectedFolder) {
|
||||
startPath = data.selectedFolder.replace((wsFolder?.fsPath || ""), "");
|
||||
} else if (staticFolder) {
|
||||
startPath = `/${staticFolder}`;
|
||||
}
|
||||
|
||||
if (startPath && !startPath.endsWith("/")) {
|
||||
startPath += "/";
|
||||
}
|
||||
|
||||
const folderName = await window.showInputBox({
|
||||
prompt: `Which name would you like to give to your folder (use "/" to create multi-level folders)?`,
|
||||
value: startPath,
|
||||
ignoreFocusOut: true,
|
||||
placeHolder: `${format(new Date(), `yyyy/MM`)}`
|
||||
});
|
||||
|
||||
if (!folderName) {
|
||||
Notifications.warning(`No folder name was specified.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = folderName.split("/").filter(f => f);
|
||||
let parentFolders: string[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
const folderPath = join(wsFolder?.fsPath || "", parentFolders.join("/"), folder);
|
||||
|
||||
parentFolders.push(folder);
|
||||
|
||||
if (!existsSync(folderPath)) {
|
||||
mkdirSync(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.switchFolder(folderName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content in a registered folder
|
||||
* @returns
|
||||
|
||||
@@ -33,4 +33,5 @@ export const COMMAND_NAME = {
|
||||
dashboard: getCommandName("dashboard"),
|
||||
promote: getCommandName("promoteSettings"),
|
||||
insertImage: getCommandName("insertImage"),
|
||||
createFolder: getCommandName("createFolder"),
|
||||
};
|
||||
@@ -36,7 +36,7 @@ export const SETTING_CUSTOM_SCRIPTS = "custom.scripts";
|
||||
|
||||
export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
|
||||
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDERS = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
|
||||
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
|
||||
@@ -17,4 +17,5 @@ export enum DashboardMessage {
|
||||
deleteMedia = 'deleteMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { HomeIcon } from '@heroicons/react/solid';
|
||||
import { basename, join } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
|
||||
|
||||
export interface IBreadcrumbProps {}
|
||||
|
||||
export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: React.PropsWithChildren<IBreadcrumbProps>) => {
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const [ folders, setFolders ] = React.useState<string[]>([]);
|
||||
|
||||
if (!settings?.wsFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const { wsFolder, staticFolder, contentFolders } = settings;
|
||||
|
||||
const isValid = (folderPath: string) => {
|
||||
if (staticFolder) {
|
||||
const staticPath = join(wsFolder, staticFolder);
|
||||
const relPath = folderPath.replace(staticPath, '');
|
||||
if (relPath.length > 1 && folderPath.startsWith(staticPath)) {
|
||||
return true;
|
||||
} else if (relPath.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const contentFolder = contentFolders[i];
|
||||
const relContentPath = folderPath.replace(contentFolder, '');
|
||||
return relContentPath.length > 1 && folderPath.startsWith(contentFolder);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!selectedFolder) {
|
||||
setFolders([]);
|
||||
} else {
|
||||
const relPath = selectedFolder.replace(settings.wsFolder, '');
|
||||
const folderParts = relPath.split('/').filter(f => f);
|
||||
const allFolders: string[] = [];
|
||||
let previousFolder = settings.wsFolder;
|
||||
|
||||
for (const part of folderParts) {
|
||||
const folder = join(previousFolder, part);
|
||||
if (isValid(folder)) {
|
||||
allFolders.push(folder);
|
||||
}
|
||||
previousFolder = folder;
|
||||
}
|
||||
|
||||
setFolders(allFolders);
|
||||
}
|
||||
}, [selectedFolder]);
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex py-2" aria-label="Breadcrumb">
|
||||
<ol role="list" className="w-full mx-auto flex space-x-4 px-5">
|
||||
<li className="flex">
|
||||
<div className="flex items-center">
|
||||
<button onClick={() => setSelectedFolder(null)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
|
||||
<HomeIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Home</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{folders.map((folder) => (
|
||||
<li key={folder} className="flex">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => setSelectedFolder(folder)}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
|
||||
>
|
||||
{basename(folder)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { FolderAtom } from '../../state';
|
||||
|
||||
@@ -17,6 +17,7 @@ import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIco
|
||||
import { PhotographIcon } from '@heroicons/react/outline';
|
||||
import { Pagination } from '../Media/Pagination';
|
||||
import { ChoiceButton } from '../ChoiceButton';
|
||||
import { Breadcrumb } from './Breadcrumb';
|
||||
|
||||
export interface IHeaderProps {
|
||||
settings: Settings | null;
|
||||
@@ -113,7 +114,10 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
|
||||
{
|
||||
view === "media" && (
|
||||
<Pagination />
|
||||
<>
|
||||
<Pagination />
|
||||
<Breadcrumb />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { FolderAddIcon } from '@heroicons/react/outline';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SelectedMediaFolderAtom } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
|
||||
export interface IFolderCreationProps {}
|
||||
|
||||
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
|
||||
|
||||
const onFolderCreation = () => {
|
||||
Messenger.send(DashboardMessage.createMediaFolder, {
|
||||
selectedFolder
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
onClick={onFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create new folder</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FolderIcon } from '@heroicons/react/solid';
|
||||
import { basename } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { SelectedMediaFolderAtom } from '../../state';
|
||||
|
||||
export interface IFolderItemProps {
|
||||
folder: string;
|
||||
wsFolder?: string;
|
||||
staticFolder?: string;
|
||||
}
|
||||
|
||||
export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder, wsFolder, staticFolder }: React.PropsWithChildren<IFolderItemProps>) => {
|
||||
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
|
||||
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
|
||||
|
||||
return (
|
||||
<li className={`group relative bg-gray-200 dark:bg-vulcan-300 hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
|
||||
<button className={`w-full flex flex-col items-center`} onClick={() => setSelectedFolder(folder)}>
|
||||
<FolderIcon className={`h-auto w-1/2`} />
|
||||
|
||||
<p className="text-sm font-bold pointer-events-none flex items-center">
|
||||
{basename(relFolderPath)}
|
||||
</p>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { XIcon } from '@heroicons/react/outline';
|
||||
import Downshift from 'downshift';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaFoldersSelector, SelectedMediaFolderAtom } from '../../state';
|
||||
|
||||
export interface IFolderSelectionProps {}
|
||||
|
||||
export const FolderSelection: React.FunctionComponent<IFolderSelectionProps> = (props: React.PropsWithChildren<IFolderSelectionProps>) => {
|
||||
const folders = useRecoilValue(MediaFoldersSelector);
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ focus, setFocus ] = React.useState(false);
|
||||
|
||||
let allFolders: string[] = Object.assign([], folders);
|
||||
allFolders = allFolders.sort((a: string, b: string) => {
|
||||
if (a.toLowerCase() < b.toLowerCase()) return -1;
|
||||
if (a.toLowerCase() > b.toLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Downshift
|
||||
isOpen={focus}
|
||||
selectedItem={selectedFolder}
|
||||
onOuterClick={() => setFocus(false)}
|
||||
onSelect={(selFolder) => {
|
||||
setSelectedFolder(selFolder);
|
||||
setFocus(false);
|
||||
}}>
|
||||
{
|
||||
({
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
getMenuProps,
|
||||
isOpen,
|
||||
inputValue,
|
||||
getRootProps
|
||||
}) => (
|
||||
<div className={`relative flex items-center`}>
|
||||
<label className={`text-sm text-gray-500 dark:text-whisper-900`}>Filter by: </label>
|
||||
|
||||
<div
|
||||
className={`inline-flex items-center`}
|
||||
{...getRootProps({} as any, {suppressRefError: true})}
|
||||
>
|
||||
<input disabled={!!selectedFolder} onFocus={() => setFocus(true)} className={`ml-2 py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`} {...getInputProps()} />
|
||||
|
||||
{
|
||||
selectedFolder && (
|
||||
<button title={`Clear`} onClick={() => setSelectedFolder(null)}><XIcon className={`ml-2 h-6 w-6 text-red-500 hover:text-red-800`} /></button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={`${focus ? `block` : `hidden`} top-8 absolute right-0 z-10 mt-2 w-min 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`} {...getMenuProps()}>
|
||||
{isOpen
|
||||
? allFolders
|
||||
.filter((item: string) => !inputValue || item.includes(inputValue))
|
||||
.map((item, index) => (
|
||||
<div
|
||||
className="cursor-pointer text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100"
|
||||
{...getItemProps({ key: item, index, item })}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Downshift>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { Header } from '../Header';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
@@ -15,6 +15,10 @@ import { List } from './List';
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useCallback } from 'react';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { FolderIcon } from '@heroicons/react/solid';
|
||||
import { basename } from 'path';
|
||||
import { FolderItem } from './FolderItem';
|
||||
|
||||
export interface IMediaProps {}
|
||||
|
||||
@@ -22,10 +26,10 @@ export const LIMIT = 16;
|
||||
|
||||
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
|
||||
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
|
||||
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
|
||||
const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom);
|
||||
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
@@ -57,6 +61,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
setMedia(message.data.data.media);
|
||||
setTotal(message.data.data.total);
|
||||
setFolders(message.data.data.folders);
|
||||
setSelectedFolder(message.data.data.selectedFolder);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,6 +100,32 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(media.length === 0 && folders.length === 0 && !loading) && (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
|
||||
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & drop new files.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
folders && folders.length > 0 && (
|
||||
<div className={`mb-8`}>
|
||||
<List>
|
||||
{
|
||||
folders && folders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<List>
|
||||
{
|
||||
media.map((file) => (
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector } from '../../state';
|
||||
import { FolderSelection } from './FolderSelection';
|
||||
import { FolderCreation } from './FolderCreation';
|
||||
import { LIMIT } from './Media';
|
||||
import { PaginationButton } from './PaginationButton';
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FolderSelection />
|
||||
<FolderCreation />
|
||||
|
||||
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
|
||||
@@ -16,4 +16,5 @@ export interface Settings {
|
||||
pageViewType: ViewType | undefined;
|
||||
mediaSnippet: string[];
|
||||
contentTypes: ContentType[];
|
||||
contentFolders: string[];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { Template } from '../commands/Template';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
|
||||
import * as os from 'os';
|
||||
import { PanelSettings, CustomScript } from '../models/PanelSettings';
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
|
||||
@@ -216,7 +216,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const filePath = window.activeTextEditor?.document.uri.fsPath;
|
||||
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const contentTypes = Settings.get<string>(SETTING_TAXONOMY_CONTENT_TYPES);
|
||||
|
||||
const articleDetails = this.getArticleDetails();
|
||||
|
||||
+4
-1
@@ -106,6 +106,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
const unregisterFolder = vscode.commands.registerCommand(COMMAND_NAME.unregisterFolder, Folders.unregister);
|
||||
|
||||
const createFolder = vscode.commands.registerCommand(COMMAND_NAME.createFolder, Folders.addMediaFolder);
|
||||
|
||||
const createByContentType = vscode.commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent);
|
||||
const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create);
|
||||
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create);
|
||||
@@ -191,7 +193,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
createByContentType,
|
||||
createByTemplate,
|
||||
projectInit,
|
||||
collapseAll
|
||||
collapseAll,
|
||||
createFolder
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Field } from '../models';
|
||||
import { existsSync } from 'fs';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { Settings } from './SettingsHelper';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDERS } from '../constants';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER } from '../constants';
|
||||
|
||||
export class ImageHelper {
|
||||
|
||||
@@ -49,7 +49,7 @@ export class ImageHelper {
|
||||
*/
|
||||
public static relToAbs(filePath: string, value: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const staticPath = join(wsFolder?.fsPath || "", staticFolder || "", value);
|
||||
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
|
||||
@@ -68,7 +68,7 @@ export class ImageHelper {
|
||||
*/
|
||||
public static absToRel(imgValue: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
let relPath = imgValue || "";
|
||||
if (imgValue) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Notifications } from './Notifications';
|
||||
import { commands, Uri, workspace } from 'vscode';
|
||||
import * as vscode from 'vscode';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT } from '../constants';
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, SETTINGS_CONTENT_STATIC_FOLDER } from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename } from 'path';
|
||||
import { existsSync, readFileSync, watch, writeFileSync } from 'fs';
|
||||
@@ -223,7 +223,6 @@ export class Settings {
|
||||
return hasSetting;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if its the project config
|
||||
* @param filePath
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface MediaPaths {
|
||||
media: MediaInfo[];
|
||||
total: number;
|
||||
folders: string[];
|
||||
selectedFolder: string;
|
||||
}
|
||||
|
||||
export interface MediaInfo {
|
||||
|
||||
@@ -5,8 +5,6 @@ import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { GlobalSettings } from './GlobalSettings';
|
||||
import { OtherActions } from './OtherActions';
|
||||
import { FileList } from './FileList';
|
||||
import { VsLabel } from './VscodeComponents';
|
||||
import { FolderAndFiles } from './FolderAndFiles';
|
||||
import { SponsorMsg } from './SponsorMsg';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user