diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b21fbe4..482c901d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Change Log +## [8.2.0] - 2022-12-08 - [Release notes](https://beta.frontmatter.codes/updates/v8.2.0) + +### ✨ New features + +- [#362](https://github.com/estruyf/vscode-front-matter/issues/362): Support for conditional metadata +- [#412](https://github.com/estruyf/vscode-front-matter/issues/412): Allow `frontmatter.json` to be split in multiple files + +### 🎨 Enhancements + +- [#360](https://github.com/estruyf/vscode-front-matter/issues/360): Define which content types can be used on your page folders +- [#406](https://github.com/estruyf/vscode-front-matter/issues/406): Added support for single data entries in the data dashboard +- [#428](https://github.com/estruyf/vscode-front-matter/issues/428): Improved UX for inserting images to your content +- [#430](https://github.com/estruyf/vscode-front-matter/issues/430): Support for HEXO its `post_asset_folder` setting (image location) +- [#434](https://github.com/estruyf/vscode-front-matter/issues/434): Webview errors are logged in the extension output +- [#440](https://github.com/estruyf/vscode-front-matter/issues/440): Type to search/filter in the snippets dashboard +- [#447](https://github.com/estruyf/vscode-front-matter/issues/447): Allow to use placeholders on git commit messages +- [#449](https://github.com/estruyf/vscode-front-matter/issues/449): Show `filename` if the `title` is not set +- [#450](https://github.com/estruyf/vscode-front-matter/issues/450): Additional time placeholders added `{{hour12}}`, `{{hour24}}`, `{{ampm}}`, and `{{minute}}` +- [#458](https://github.com/estruyf/vscode-front-matter/issues/458): Ability to configure the file prefix on folder level + +### ⚡️ Optimizations + +- [#431](https://github.com/estruyf/vscode-front-matter/issues/431): Performance improvements for the content dashboard +- [#448](https://github.com/estruyf/vscode-front-matter/issues/448): Retrieving files fails when content folder name and workspace folder name are the same +- [#455](https://github.com/estruyf/vscode-front-matter/issues/455): Show a description for the SEO section when title nor description is set + +### 🐞 Fixes + +- Fix field error message color +- [#433](https://github.com/estruyf/vscode-front-matter/issues/433): Fix issue with rendering an incorrect title value on the content dashboard +- [#462](https://github.com/estruyf/vscode-front-matter/issues/462): Fix issue in script error notification +- [#465](https://github.com/estruyf/vscode-front-matter/issues/465): Deleted content does not get added in git when syncing +- [#471](https://github.com/estruyf/vscode-front-matter/issues/471): Fix typo on data dashboard + ## [8.1.2] - 2022-10-06 ### 🐞 Fixes diff --git a/README.beta.md b/README.beta.md index 1a461828..42b5c509 100644 --- a/README.beta.md +++ b/README.beta.md @@ -191,6 +191,6 @@ You can open showcase issues for the following things:

- - + +

\ No newline at end of file diff --git a/README.md b/README.md index 1da49a36..08ce0bf5 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,6 @@ You can open showcase issues for the following things:

- +

\ No newline at end of file diff --git a/e2e/src/command.test.ts b/e2e/src/command.test.ts index 482c37b2..103afe18 100644 --- a/e2e/src/command.test.ts +++ b/e2e/src/command.test.ts @@ -68,11 +68,9 @@ describe("Initialization testing", function() { async function notificationExists(workbench: Workbench, text: string): Promise { const notifications = await (await (new StatusBar()).openNotificationsCenter()).getNotifications(NotificationType.Info); - console.log(`Notifications:`, notifications.length); for (const notification of notifications) { const message = await notification.getMessage(); - console.log(message) if (message.indexOf(text) >= 0) { return notification; } diff --git a/package-lock.json b/package-lock.json index 60faf700..b0f494ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-front-matter-beta", - "version": "8.1.2", + "version": "8.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-front-matter-beta", - "version": "8.1.2", + "version": "8.2.0", "license": "MIT", "dependencies": { "node-fetch": "^2.6.7" diff --git a/package.json b/package.json index 37045c54..45ea8eb7 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "vscode-front-matter-beta", - "displayName": "Front Matter", - "description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...", + "displayName": "Front Matter CMS", + "description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...", "icon": "assets/frontmatter-teal-128x128.png", - "version": "8.1.2", + "version": "8.2.0", "preview": false, "publisher": "eliostruyf", "galleryBanner": { @@ -206,6 +206,17 @@ ], "default": null, "description": "Defines a custom preview path for the folder." + }, + "filePrefix": { + "type": [ "null", "string" ], + "description": "Defines a prefix for the file name." + }, + "contentTypes": { + "type": "array", + "description": "Defines which content types can be used for the current location. If not defined, all content types will be available.", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -271,6 +282,10 @@ "type": "string" } }, + "title": { + "description": "The snippet title.", + "type": "string" + }, "description": { "description": "The snippet description.", "type": "string" @@ -447,9 +462,9 @@ "scope": "Custom scripts" }, "frontMatter.dashboard.content.pagination": { - "type": "boolean", + "type": ["boolean", "number"], "default": true, - "markdownDescription": "Specify if you want to enable/disable pagination for your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)", + "markdownDescription": "Specify if you want to enable/disable pagination for your content. You can define your page number up to 52. Default items per page is `16`. Disabling the pagination can be done by setting it to `false`. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)", "scope": "Dashboard" }, "frontMatter.dashboard.content.cardTags": { @@ -550,6 +565,11 @@ "type": "string", "default": "content", "description": "If you are using data types, you can specify your type ID." + }, + "singleEntry": { + "type": "boolean", + "description": "If you want to use a single entry for your data file.", + "default": false } }, "additionalProperties": false, @@ -601,6 +621,11 @@ "type": "string", "default": "content", "description": "If you are using data types, you can specify your type ID." + }, + "singleEntry": { + "type": "boolean", + "description": "If you want to use a single entry for your data files in the folder.", + "default": false } }, "additionalProperties": false, @@ -1039,6 +1064,50 @@ "type": "boolean", "default": false, "description": "Specify if the field is required" + }, + "when": { + "type": "object", + "description": "Specify the conditions to show the field", + "properties": { + "fieldRef": { + "type": "string", + "description": "The field ID to use" + }, + "operator": { + "type": "string", + "description": "The operator to use", + "enum": [ + "eq", + "neq", + "contains", + "notContains", + "startsWith", + "endsWith", + "gt", + "gte", + "lt", + "lte", + "minimum", + "maximum", + "exlusiveMinimum", + "exclusiveMaximum" + ] + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "array" + ], + "description": "The value to compare" + }, + "caseSensitive": { + "type": "boolean", + "default": true, + "description": "Specify if the comparison is case sensitive. Default: true" + } + } } }, "additionalProperties": false, @@ -1732,6 +1801,11 @@ "command": "frontMatter.git.sync", "title": "Sync", "category": "Front Matter" + }, + { + "command": "frontMatter.cache.clear", + "title": "Clear cache", + "category": "Front Matter" } ], "menus": { diff --git a/scripts/beta-release.js b/scripts/beta-release.js index 262738f8..782ee92c 100644 --- a/scripts/beta-release.js +++ b/scripts/beta-release.js @@ -8,7 +8,7 @@ const version = packageJson.version.split('.'); packageJson.version = `${version[0]}.${version[1]}.${process.argv[process.argv.length-1].substr(0, 7)}`; packageJson.preview = true; packageJson.name = "vscode-front-matter-beta"; -packageJson.displayName = `${packageJson.displayName} BETA`; +packageJson.displayName = `${packageJson.displayName} (BETA)`; packageJson.description = `BETA Version of Front Matter. ${packageJson.description}`; packageJson.icon = "assets/frontmatter-beta.png"; packageJson.homepage = "https://beta.frontmatter.codes"; diff --git a/src/commands/Article.ts b/src/commands/Article.ts index fc82963d..a16624f0 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -198,7 +198,7 @@ export class Article { Telemetry.send(TelemetryEvent.generateSlug); const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string; - const filePrefix = Settings.get(SETTING_TEMPLATES_PREFIX); + let filePrefix = Settings.get(SETTING_TEMPLATES_PREFIX); const editor = vscode.window.activeTextEditor; if (!editor) { @@ -210,6 +210,12 @@ export class Article { return; } + // Retrieve the file prefix from the folder + const filePrefixOnFolder = Folders.getFilePrefixBeFilePath(editor.document.uri.fsPath); + if (typeof filePrefixOnFolder !== "undefined") { + filePrefix = filePrefixOnFolder; + } + const contentType = ArticleHelper.getContentType(article.data); const titleField = "title"; const articleTitle: string = article.data[titleField]; diff --git a/src/commands/Cache.ts b/src/commands/Cache.ts new file mode 100644 index 00000000..5175f5b8 --- /dev/null +++ b/src/commands/Cache.ts @@ -0,0 +1,24 @@ +import { commands } from "vscode"; +import { COMMAND_NAME, ExtensionState } from "../constants"; +import { Extension, Notifications } from "../helpers"; + +export class Cache { + + public static async registerCommands() { + const ext = Extension.getInstance(); + const subscriptions = ext.subscriptions; + + subscriptions.push( + commands.registerCommand(COMMAND_NAME.clearCache, Cache.clear) + ); + } + + private static async clear() { + const ext = Extension.getInstance(); + + await ext.setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace"); + await ext.setState(ExtensionState.Dashboard.Pages.Index, undefined, "workspace"); + + Notifications.info("Cache cleared"); + } +} \ No newline at end of file diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index e0d888b9..2bcc13e4 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -7,7 +7,7 @@ import { Extension } from '../helpers/Extension'; import { WebviewHelper } from '@estruyf/vscode'; import { DashboardData } from '../models/DashboardData'; import { MediaLibrary } from '../helpers/MediaLibrary'; -import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener } from '../listeners/dashboard'; +import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener, LogListener } from '../listeners/dashboard'; import { MediaListener as PanelMediaListener } from '../listeners/panel' import { GitListener, ModeListener } from '../listeners/general'; @@ -130,8 +130,8 @@ export class Dashboard { await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, false); }); - SettingsHelper.onConfigChange((global?: any) => { - SettingsListener.getSettings(); + SettingsHelper.onConfigChange(() => { + SettingsListener.getSettings(true); }); Dashboard.webview.webview.onDidReceiveMessage(async (msg) => { @@ -148,6 +148,7 @@ export class Dashboard { ModeListener.process(msg); GitListener.process(msg); TaxonomyListener.process(msg); + LogListener.process(msg); }); } diff --git a/src/commands/Diagnostics.ts b/src/commands/Diagnostics.ts index 23974cfb..fc24be1d 100644 --- a/src/commands/Diagnostics.ts +++ b/src/commands/Diagnostics.ts @@ -3,6 +3,7 @@ import { ViewColumn, workspace } from "vscode"; import ContentProvider from "../providers/ContentProvider"; import { join } from "path"; import { ContentFolder } from "../models"; +import { Settings } from "../helpers/SettingsHelper"; export class Diagnostics { @@ -38,6 +39,12 @@ ${all} # Folders to search files ${folderData.join("\n")} + +# Complete frontmatter.json config + +\`\`\`json +${JSON.stringify(Settings.globalConfig, null, 2)} +\`\`\` `; ContentProvider.show(logging, `${projectName} diagnostics`, "markdown", ViewColumn.One); diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index c12977fd..ed280a5f 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -1,3 +1,4 @@ +import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder'; import { Questions } from './../helpers/Questions'; import { SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_STATIC_FOLDER, SETTING_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants'; import { commands, Uri, workspace, window } from "vscode"; @@ -7,7 +8,7 @@ import uniqBy = require("lodash.uniqby"); import { Template } from "./Template"; import { Notifications } from "../helpers/Notifications"; import { Logger, Settings } from "../helpers"; -import { existsSync, mkdirSync } from 'fs'; +import { existsSync } from 'fs'; import { format } from 'date-fns'; import { Dashboard } from './Dashboard'; import { parseWinPath } from '../helpers/parseWinPath'; @@ -16,6 +17,8 @@ import { MediaListener, PagesListener, SettingsListener } from '../listeners/das import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes'; import { Telemetry } from '../helpers/Telemetry'; import { glob } from 'glob'; +import { mkdirAsync } from '../utils/mkdirAsync'; +import { existsAsync } from '../utils'; export const WORKSPACE_PLACEHOLDER = `[[workspace]]`; @@ -41,6 +44,10 @@ export class Folders { startPath += "/"; } + if (startPath.includes(STATIC_FOLDER_PLACEHOLDER.hexo.placeholder)) { + startPath = startPath.replace(STATIC_FOLDER_PLACEHOLDER.hexo.placeholder, STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder); + } + const folderName = await window.showInputBox({ title: `Add media folder`, prompt: `Which name would you like to give to your folder (use "/" to create multi-level folders)?`, @@ -54,22 +61,17 @@ export class Folders { return; } - const folders = folderName.split("/").filter(f => f); - let parentFolders: string[] = []; + await Folders.createFolder(join(parseWinPath(wsFolder?.fsPath || ""), folderName)); + } - for (const folder of folders) { - const folderPath = join(parseWinPath(wsFolder?.fsPath || ""), parentFolders.join("/"), folder); - - parentFolders.push(folder); - - if (!existsSync(folderPath)) { - mkdirSync(folderPath); - } + public static async createFolder(folderPath: string) { + if (!(await existsAsync(folderPath))) { + await mkdirAsync(folderPath, { recursive: true }); } if (Dashboard.isOpen) { MediaHelpers.resetMedia(); - MediaListener.sendMediaFiles(0, folderName); + MediaListener.sendMediaFiles(0, folderPath); } Telemetry.send(TelemetryEvent.addMediaFolder); @@ -79,7 +81,7 @@ export class Folders { * Create content in a registered folder * @returns */ - public static async create() { + public static async create() { const selectedFolder = await Questions.SelectContentFolder(); if (!selectedFolder) { return; @@ -137,7 +139,7 @@ export class Folders { Telemetry.send(TelemetryEvent.registerFolder); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } } @@ -210,9 +212,9 @@ export class Folders { if (!projectFolder) { window.showWorkspaceFolderPick({ placeHolder: `Please select the main workspace folder for Front Matter to use.` - }).then(selectedFolder => { + }).then(async (selectedFolder) => { if (selectedFolder) { - Settings.createGlobalFile(selectedFolder.uri); + await Settings.createGlobalFile(selectedFolder.uri); // Full reload to make sure the whole extension is reloaded correctly commands.executeCommand(`workbench.action.reloadWindow`); } @@ -242,13 +244,14 @@ export class Folders { public static async getInfo(limit?: number): Promise { const supportedFiles = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES); const folders = Folders.get(); + const wsFolder = parseWinPath(Folders.getWorkspaceFolder()?.fsPath || ""); + if (folders && folders.length > 0) { let folderInfo: FolderInfo[] = []; for (const folder of folders) { try { - const projectName = Folders.getProjectFolderName(); - let projectStart = folder.path.split(projectName).pop(); + let projectStart = parseWinPath(folder.path).replace(wsFolder, ""); if (projectStart) { projectStart = projectStart.replace(/\\/g, '/'); @@ -424,6 +427,51 @@ export class Folders { return uniqueFolders.map(folder => relative(wsFolder?.path || "", folder)); } + /** + * Returns the file prefix for the given folder path + * @param folderPath + * @returns + */ + public static getFilePrefixByFolderPath(folderPath: string) { + const folders = Folders.get(); + const pageFolder = folders.find(f => parseWinPath(f.path) === parseWinPath(folderPath)); + + if (pageFolder && typeof pageFolder.filePrefix !== "undefined") { + return pageFolder.filePrefix; + } + + return; + } + + + /** + * Returns the file prefix for the given file path + * @param filePath + * @returns + */ + public static getFilePrefixBeFilePath(filePath: string) { + const folders = Folders.get(); + if (folders.length > 0) { + filePath = parseWinPath(filePath); + + let selectedFolder: ContentFolder | null = null; + for (const folder of folders) { + const folderPath = parseWinPath(folder.path); + if (filePath.startsWith(folderPath)) { + if (!selectedFolder || selectedFolder.path.length < folderPath.length) { + selectedFolder = folder; + } + } + } + + if (selectedFolder && typeof selectedFolder.filePrefix !== "undefined") { + return selectedFolder.filePrefix; + } + } + + return; + } + /** * Retrieve all content folders * @param pattern diff --git a/src/commands/Project.ts b/src/commands/Project.ts index 8e708f4c..748bd0bf 100644 --- a/src/commands/Project.ts +++ b/src/commands/Project.ts @@ -2,13 +2,13 @@ import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType'; import { Telemetry } from './../helpers/Telemetry'; import { workspace, Uri } from "vscode"; import { join } from "path"; -import * as fs from "fs"; import { Notifications } from "../helpers/Notifications"; import { Template } from "./Template"; import { Folders } from "./Folders"; import { FrameworkDetector, Logger, Settings } from "../helpers"; import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants"; import { SettingsListener } from '../listeners/dashboard'; +import { existsAsync, writeFileAsync } from '../utils'; export class Project { @@ -34,10 +34,10 @@ categories: [] */ public static async init(sampleTemplate?: boolean) { try { - Settings.createTeamSettings(); + await Settings.createTeamSettings(); // Add the default content type - Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true); + await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true); if (sampleTemplate !== undefined) { await Project.createSampleTemplate(); @@ -49,13 +49,13 @@ categories: [] // Check if you can find the framework const wsFolder = Folders.getWorkspaceFolder(); - const framework = FrameworkDetector.get(wsFolder?.fsPath || ""); + const framework = await FrameworkDetector.get(wsFolder?.fsPath || ""); if (framework) { - SettingsListener.setFramework(framework.name); + await SettingsListener.setFramework(framework.name); } - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } catch (err: any) { Logger.error(`Project::init: ${err?.message || err}`); Notifications.error(`Sorry, something went wrong - ${err?.message || err}`); @@ -79,12 +79,12 @@ categories: [] const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`)); - if (!fs.existsSync(templatePath.fsPath)) { + if (!(await existsAsync(templatePath.fsPath))) { await workspace.fs.createDirectory(templatePath); } if (sampleTemplate) { - fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" }); + await writeFileAsync(article.fsPath, Project.content, { encoding: "utf-8" }); Notifications.info("Sample template created."); } } diff --git a/src/commands/Template.ts b/src/commands/Template.ts index 03b6119c..6201c2c3 100644 --- a/src/commands/Template.ts +++ b/src/commands/Template.ts @@ -1,7 +1,6 @@ import { Questions } from './../helpers/Questions'; import * as vscode from 'vscode'; import * as path from 'path'; -import * as fs from 'fs'; import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEvent } from '../constants'; import { ArticleHelper, Settings } from '../helpers'; import { Article } from '.'; @@ -12,6 +11,7 @@ import { ContentType as IContentType } from '../models'; import { PagesListener } from '../listeners/dashboard'; import { extname } from 'path'; import { Telemetry } from '../helpers/Telemetry'; +import { writeFileAsync, copyFileAsync } from '../utils'; export class Template { @@ -60,7 +60,7 @@ export class Template { let fileContents = ArticleHelper.stringifyFrontMatter(keepContents === "no" ? "" : clonedArticle.content, clonedArticle.data); const templateFile = path.join(templatePath.fsPath, `${titleValue}.${fileType}`); - fs.writeFileSync(templateFile, fileContents, { encoding: "utf-8" }); + await writeFileAsync(templateFile, fileContents, { encoding: "utf-8" }); Notifications.info(`Template created and is now available in your ${folder} folder.`); } @@ -120,23 +120,23 @@ export class Template { return; } - const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath); + const templateData = await ArticleHelper.getFrontMatterByPath(template.fsPath); let contentType: IContentType | undefined; if (templateData && templateData.data && templateData.data.type) { contentType = contentTypes?.find(t => t.name === templateData.data.type); } const fileExtension = extname(template.fsPath).replace(".", ""); - let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension); + let newFilePath: string | undefined = await ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension); if (!newFilePath) { return; } // Start the new file creation - fs.copyFileSync(template.fsPath, newFilePath); + await copyFileAsync(template.fsPath, newFilePath); // Update the properties inside the template - let frontMatter = ArticleHelper.getFrontMatterByPath(newFilePath); + let frontMatter = await ArticleHelper.getFrontMatterByPath(newFilePath); if (!frontMatter) { Notifications.warning(`Something failed when retrieving the newly created file.`); return; @@ -147,7 +147,7 @@ export class Template { frontMatter = Article.updateDate(frontMatter); - fs.writeFileSync(newFilePath, ArticleHelper.stringifyFrontMatter(frontMatter.content, frontMatter.data), { encoding: "utf8" }); + await writeFileAsync(newFilePath, ArticleHelper.stringifyFrontMatter(frontMatter.content, frontMatter.data), { encoding: "utf8" }); await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(newFilePath)); } diff --git a/src/commands/index.ts b/src/commands/index.ts index d5aa6bdc..7c391ede 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,3 +1,13 @@ export * from './Article'; +export * from './Backers'; +export * from './Cache'; +export * from './Content'; +export * from './Dashboard'; +export * from './Diagnostics'; +export * from './Folders'; +export * from './Preview'; +export * from './Project'; export * from './Settings'; export * from './StatusListener'; +export * from './Template'; +export * from './Wysiwyg'; diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index 6e0eb252..4dc68df7 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -72,4 +72,7 @@ export const COMMAND_NAME = { // Config reloadConfig: getCommandName("config.reload"), + + // Cache + clearCache: getCommandName("cache.clear"), }; \ No newline at end of file diff --git a/src/constants/FrameworkDetectors.ts b/src/constants/FrameworkDetectors.ts index 76e9f488..7f11381d 100644 --- a/src/constants/FrameworkDetectors.ts +++ b/src/constants/FrameworkDetectors.ts @@ -85,5 +85,17 @@ export const FrameworkDetectors = [{ commands: { start: "npx @11ty/eleventy --serve" } + }, + { + framework: { + name: "hexo", + dist: "public", + build: "npx hexo-cli generate" + }, + requiredFiles: ["_config.js"], + requiredDependencies: ["hexo"], + commands: { + start: "npx hexo-cli server" + } } ]; \ No newline at end of file diff --git a/src/constants/StaticFolderPlaceholder.ts b/src/constants/StaticFolderPlaceholder.ts new file mode 100644 index 00000000..259c9ae0 --- /dev/null +++ b/src/constants/StaticFolderPlaceholder.ts @@ -0,0 +1,8 @@ + + +export const STATIC_FOLDER_PLACEHOLDER = { + hexo: { + postsFolder: "source/_posts", + placeholder: "hexo:post_asset_folder", + } +} \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index e268e063..a0cb97be 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -12,6 +12,7 @@ export * from './LocalStore'; export * from './Navigation'; export * from './NotificationType'; export * from './PreviewCommands'; +export * from './StaticFolderPlaceholder'; export * from './TelemetryEvent'; export * from './charCode'; export * from './charMap'; diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index a261c287..de42e892 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -31,6 +31,7 @@ export enum DashboardMessage { updateMediaMetadata = 'updateMediaMetadata', createMediaFolder = 'createMediaFolder', insertFile = 'insertFile', + createHexoAssetFolder = 'createHexoAssetFolder', // Data dashboard getDataEntries = 'getDataEntries', @@ -57,4 +58,5 @@ export enum DashboardMessage { setState = 'setState', runCustomScript = 'runCustomScript', sendTelemetry = 'sendTelemetry', + logError = 'logError', } \ No newline at end of file diff --git a/src/dashboardWebView/components/App.tsx b/src/dashboardWebView/components/App.tsx index 36085faf..9c56f034 100644 --- a/src/dashboardWebView/components/App.tsx +++ b/src/dashboardWebView/components/App.tsx @@ -16,6 +16,9 @@ import { Route, Routes, useNavigate } from 'react-router-dom'; import { routePaths } from '..'; import { useEffect, useMemo } from 'react'; import { UnknownView } from './UnknownView'; +import { ErrorBoundary } from '@sentry/react'; +import { ErrorView } from './ErrorView'; +import { DashboardMessage } from '../DashboardMessage'; export interface IAppProps { showWelcome: boolean; @@ -68,23 +71,32 @@ export const App: React.FunctionComponent = ({showWelcome}: React.Pro } return ( -
- - } /> - } /> - } /> - } /> - - { - allowDataView && } /> - } + )} + onError={(error: Error, componentStack: string, eventId: string) => { + Messenger.send(DashboardMessage.logError, `Event ID: ${eventId} +Message: ${error.message} - { - allowTaxonomyView && } /> - } +Stack: ${componentStack}`); + }}> +
+ + } /> + } /> + } /> + } /> + + { + allowDataView && } /> + } - } /> - -
+ { + allowTaxonomyView && } /> + } + + } /> +
+
+ ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index bfd4becc..2b7f3d4a 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -19,6 +19,22 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti const view = useRecoilValue(ViewSelector); const settings = useRecoilValue(SettingsSelector); const draftField = useMemo(() => settings?.draftField, [settings]); + + const escapedTitle = useMemo(() => { + if (title && typeof title !== 'string') { + return ''; + } + + return title; + }, [title]); + + const escapedDescription = useMemo(() => { + if (description && typeof description !== 'string') { + return ''; + } + + return description; + }, [description]); const openFile = () => { Messenger.send(DashboardMessage.openFile, fmFilePath); @@ -57,7 +73,7 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti + - + { tags && tags.length > 0 && ( @@ -110,13 +126,13 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti
= ({pages, settings}: React.PropsWithChildren) => { const grouping = useRecoilValue(GroupingSelector); const page = useRecoilValue(PageAtom); + const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination); const pagedPages = useMemo(() => { - if (settings?.dashboardState.contents.pagination) { - return pages.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT)); + if (pageSetNr) { + return pages.slice(page * pageSetNr, ((page + 1) * pageSetNr)); } return pages; - }, [pages, page, settings]); + }, [pages, page, pageSetNr]); const groupName = useCallback((groupId, groupedPages) => { if (grouping === GroupOption.Draft) { diff --git a/src/dashboardWebView/components/DataView/DataView.tsx b/src/dashboardWebView/components/DataView/DataView.tsx index 0f4d9a41..ce342c66 100644 --- a/src/dashboardWebView/components/DataView/DataView.tsx +++ b/src/dashboardWebView/components/DataView/DataView.tsx @@ -3,7 +3,7 @@ import { Header } from '../Header'; import { useRecoilValue } from 'recoil'; import { SettingsSelector } from '../../state'; import { DataForm } from './DataForm'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { DataFile } from '../../../models/DataFile'; import { Messenger } from '@estruyf/vscode/dist/client'; import { DashboardMessage } from '../../DashboardMessage'; @@ -27,7 +27,7 @@ export interface IDataViewProps {} export const DataView: React.FunctionComponent = (props: React.PropsWithChildren) => { const [ selectedData, setSelectedData ] = useState(null); const [ selectedIndex, setSelectedIndex ] = useState(null); - const [ dataEntries, setDataEntries ] = useState(null); + const [ dataEntries, setDataEntries ] = useState(null); const settings = useRecoilValue(SettingsSelector); const setSchema = (dataFile: DataFile) => { @@ -57,6 +57,12 @@ export const DataView: React.FunctionComponent = (props: React.P const onSubmit = useCallback((data: any) => { + if (selectedData?.singleEntry) { + // Needs to add a single entry + updateData(data); + return; + } + const dataClone: any[] = Object.assign([], dataEntries); if (selectedIndex !== null && selectedIndex !== undefined) { dataClone[selectedIndex] = data; @@ -102,6 +108,14 @@ export const DataView: React.FunctionComponent = (props: React.P }); }, [selectedData]); + const dataEntry = useMemo(() => { + if (selectedData?.singleEntry) { + return dataEntries || {}; + } + + return (dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null; + }, [selectedData, , dataEntries, selectedIndex]); + useEffect(() => { Messenger.listen(messageListener); @@ -171,49 +185,53 @@ export const DataView: React.FunctionComponent = (props: React.P { selectedData ? ( <> -
-

Your {selectedData?.title?.toLowerCase() || ""} data items

+ { + !selectedData.singleEntry && ( +
+

Your {selectedData?.title?.toLowerCase() || ""} data items

-
- { - (dataEntries && dataEntries.length > 0) ? ( - <> - - { - (dataEntries || []).map((dataEntry, idx) => ( - setSelectedIndex(index)} - onDeleteItem={deleteItem} - /> - )) - } - - - - ) : ( -
-

No {selectedData.title.toLowerCase()} data entries found

-
- ) - } -
-
-
+
+ { + (dataEntries && dataEntries.length > 0) ? ( + <> + + { + (dataEntries as any[] || []).map((dataEntry, idx) => ( + setSelectedIndex(index)} + onDeleteItem={deleteItem} + /> + )) + } + + + + ) : ( +
+

No {selectedData.title.toLowerCase()} data entries found

+
+ ) + } +
+
+ ) + } +

Create or modify your {selectedData.title.toLowerCase()} data

{ selectedData ? ( setSelectedIndex(null)} /> ) : ( @@ -234,7 +252,7 @@ export const DataView: React.FunctionComponent = (props: React.P

No data files found

- Read read more to get started using data files

+ Read more to get started using data files

) diff --git a/src/dashboardWebView/components/ErrorView/index.tsx b/src/dashboardWebView/components/ErrorView/index.tsx new file mode 100644 index 00000000..17efb741 --- /dev/null +++ b/src/dashboardWebView/components/ErrorView/index.tsx @@ -0,0 +1,14 @@ +import { ExclamationIcon } from '@heroicons/react/solid'; +import * as React from 'react'; + +export interface IErrorViewProps {} + +export const ErrorView: React.FunctionComponent = (props: React.PropsWithChildren) => { + return ( +
+ +

Sorry, something went wrong.

+

Please close the dashboard and try again.

+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/FilterInput.tsx b/src/dashboardWebView/components/Header/FilterInput.tsx new file mode 100644 index 00000000..5f8e8b75 --- /dev/null +++ b/src/dashboardWebView/components/Header/FilterInput.tsx @@ -0,0 +1,45 @@ +import { SearchIcon, XCircleIcon } from '@heroicons/react/outline'; +import * as React from 'react'; + +export interface IFilterInputProps { + placeholder: string; + value: string; + isReady: boolean; + autoFocus: boolean; + onReset?: () => void; + onChange: (value: string) => void; +} + +export const FilterInput: React.FunctionComponent = ({ placeholder, value, isReady, autoFocus, onReset, onChange}: React.PropsWithChildren) => { + return ( +
+
+ +
+
+
+ + ) => onChange(event.target.value)} + disabled={!isReady} + /> + + { + value && onReset && ( + + ) + } +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index 5be485bb..230ddeeb 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -23,8 +23,10 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { routePaths } from '../..'; import { useEffect, useMemo } from 'react'; import { SyncButton } from './SyncButton'; -import { PAGE_LIMIT, Pagination } from './Pagination'; +import { Pagination } from './Pagination'; import { GroupOption } from '../../constants/GroupOption'; +import usePagination from '../../hooks/usePagination'; +import { PaginationStatus } from './PaginationStatus'; export interface IHeaderProps { header?: React.ReactNode; @@ -37,13 +39,14 @@ export interface IHeaderProps { folders?: string[]; } -export const Header: React.FunctionComponent = ({header, totalPages, folders, settings }: React.PropsWithChildren) => { +export const Header: React.FunctionComponent = ({header, totalPages, settings }: React.PropsWithChildren) => { const [ crntTag, setCrntTag ] = useRecoilState(TagAtom); const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom); const grouping = useRecoilValue(GroupingSelector); const resetSorting = useResetRecoilState(SortingAtom); const location = useLocation(); const navigate = useNavigate(); + const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination); const createContent = () => { Messenger.send(DashboardMessage.createContent); @@ -180,8 +183,10 @@ export const Header: React.FunctionComponent = ({header, totalPage
{ - (settings?.dashboardState.contents.pagination) && (totalPages || 0) > PAGE_LIMIT && (!grouping || grouping === GroupOption.none) && ( -
+ (pageSetNr > 0) && (totalPages || 0) > pageSetNr && (!grouping || grouping === GroupOption.none) && ( +
+ +
) diff --git a/src/dashboardWebView/components/Header/Pagination.tsx b/src/dashboardWebView/components/Header/Pagination.tsx index 3b52baf3..967046a8 100644 --- a/src/dashboardWebView/components/Header/Pagination.tsx +++ b/src/dashboardWebView/components/Header/Pagination.tsx @@ -1,43 +1,37 @@ import * as React from 'react'; -import { useEffect, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useCallback, useEffect, useMemo } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { routePaths } from '../..'; -import { MediaTotalSelector, PageAtom } from '../../state'; +import usePagination from '../../hooks/usePagination'; +import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state'; import { PaginationButton } from './PaginationButton'; export interface IPaginationProps { totalPages?: number; } -export const PAGE_LIMIT = 16; - export const Pagination: React.FunctionComponent = ({ totalPages }: React.PropsWithChildren) => { const [ page, setPage ] = useRecoilState(PageAtom); const totalMedia = useRecoilValue(MediaTotalSelector); - const location = useLocation(); + const settings = useRecoilValue(SettingsAtom); + const { pageSetNr, totalPagesNr } = usePagination(settings?.dashboardState.contents.pagination, totalPages, totalMedia); - const totalItems: number = useMemo(() => { - if (location.pathname === routePaths.contents) { - return Math.ceil((totalPages || 0) / PAGE_LIMIT) - 1 - } else { - return Math.ceil(totalMedia / PAGE_LIMIT) - 1; - } - }, [location.pathname, totalPages, totalMedia]); - - const getButtons = (): number[] => { + const getButtons = useCallback((): number[] => { const maxButtons = 5; const buttons: number[] = []; const start = page - maxButtons; const end = page + maxButtons; for (let i = start; i <= end; i++) { - if (i >= 0 && i <= totalItems) { + if (i >= 0 && i <= totalPagesNr) { buttons.push(i); } } return buttons; - }; + }, [page, totalPagesNr]); + + useEffect(() => { + setPage(0); + }, [pageSetNr]); useEffect(() => { setPage(0); @@ -77,13 +71,13 @@ export const Pagination: React.FunctionComponent = ({ totalPag = totalItems} + disabled={page >= totalPagesNr} onClick={() => setPage(page + 1)} /> = totalItems} - onClick={() => setPage(totalItems)} /> + disabled={page >= totalPagesNr} + onClick={() => setPage(totalPagesNr)} />
); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/PaginationStatus.tsx b/src/dashboardWebView/components/Header/PaginationStatus.tsx index b145e18e..ce3caea6 100644 --- a/src/dashboardWebView/components/Header/PaginationStatus.tsx +++ b/src/dashboardWebView/components/Header/PaginationStatus.tsx @@ -1,27 +1,32 @@ import * as React from 'react'; +import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; -import { MediaTotalSelector, PageAtom } from '../../state'; -import { PAGE_LIMIT } from './Pagination'; +import usePagination from '../../hooks/usePagination'; +import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state'; -export interface IPaginationStatusProps {} +export interface IPaginationStatusProps { + totalPages?: number; +} -export const PaginationStatus: React.FunctionComponent = (props: React.PropsWithChildren) => { +export const PaginationStatus: React.FunctionComponent = ({ totalPages }: React.PropsWithChildren) => { const totalMedia = useRecoilValue(MediaTotalSelector); const page = useRecoilValue(PageAtom); + const settings = useRecoilValue(SettingsAtom); + const { pageSetNr, totalItems } = usePagination(settings?.dashboardState.contents.pagination, totalPages || 0, totalMedia); - const getTotalPage = () => { - const mediaItems = ((page + 1) * PAGE_LIMIT); - if (totalMedia < mediaItems) { - return totalMedia; + const totelItemsOnPage = useMemo(() => { + const items = ((page + 1) * pageSetNr); + if (totalItems < items) { + return totalItems; } - return mediaItems; - }; + return totalItems; + }, [page, totalMedia, pageSetNr]); return (

- Showing {(page * PAGE_LIMIT) + 1} to {getTotalPage()} of{' '} - {totalMedia} results + Showing {(page * pageSetNr) + 1} to {totelItemsOnPage} of{' '} + {totalItems} results

); diff --git a/src/dashboardWebView/components/Media/FolderCreation.tsx b/src/dashboardWebView/components/Media/FolderCreation.tsx index d6591620..48b0801c 100644 --- a/src/dashboardWebView/components/Media/FolderCreation.tsx +++ b/src/dashboardWebView/components/Media/FolderCreation.tsx @@ -2,16 +2,34 @@ import * as React from 'react'; import {FolderAddIcon, LightningBoltIcon} from '@heroicons/react/outline'; import { useRecoilValue } from 'recoil'; import { DashboardMessage } from '../../DashboardMessage'; -import { SelectedMediaFolderAtom, SettingsSelector } from '../../state'; +import { AllContentFoldersAtom, AllStaticFoldersAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state'; import { Messenger } from '@estruyf/vscode/dist/client'; import { ChoiceButton } from '../ChoiceButton'; import { CustomScript, ScriptType } from '../../../models'; +import { STATIC_FOLDER_PLACEHOLDER } from '../../../constants'; +import { useCallback, useMemo } from 'react'; +import { extname } from 'path'; +import { parseWinPath } from '../../../helpers/parseWinPath'; export interface IFolderCreationProps {} export const FolderCreation: React.FunctionComponent = (props: React.PropsWithChildren) => { const selectedFolder = useRecoilValue(SelectedMediaFolderAtom); const settings = useRecoilValue(SettingsSelector); + const allStaticFolders = useRecoilValue(AllStaticFoldersAtom); + const allContentFolders = useRecoilValue(AllContentFoldersAtom); + const viewData = useRecoilValue(ViewDataSelector); + + const hexoAssetFolderPath = useMemo(() => { + const path = viewData?.data?.filePath?.replace(extname(viewData.data.filePath), ''); + return parseWinPath(path); + }, [viewData?.data?.filePath]); + + const onAssetFolderCreation = useCallback(() => { + Messenger.send(DashboardMessage.createHexoAssetFolder, { + hexoAssetFolderPath + }); + }, [hexoAssetFolderPath]); const onFolderCreation = () => { Messenger.send(DashboardMessage.createMediaFolder, { @@ -23,11 +41,34 @@ export const FolderCreation: React.FunctionComponent = (pr Messenger.send(DashboardMessage.runCustomScript, {script, path: selectedFolder}); }; + const isHexoPostAssetsEnabled = useMemo(() => { + if (allStaticFolders && allContentFolders && settings?.staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder && hexoAssetFolderPath) { + return ![...allStaticFolders, ...allContentFolders].some(f => f.startsWith(hexoAssetFolderPath)); + } + return false; + }, [settings?.staticFolder, allStaticFolders, allContentFolders, hexoAssetFolderPath]); + const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder && !script.hidden); + const renderPostAssetsButton = useMemo(() => { + if (isHexoPostAssetsEnabled) { + return ( + + ); + } + return null; + }, [isHexoPostAssetsEnabled]); + if (scripts.length > 0) { return (
+ { renderPostAssetsButton } ({ @@ -43,6 +84,7 @@ export const FolderCreation: React.FunctionComponent = (pr return (
+ { renderPostAssetsButton } +
+ { + (viewData?.data?.position && mediaSnippets.length > 0) && ( +
+ +
+ ) + } +
+ ) + }
@@ -443,7 +477,7 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi viewData?.data?.filePath ? ( <> Insert image markdown
} + title={
Insert image
} onClick={insertToArticle} /> { diff --git a/src/dashboardWebView/components/Media/Media.tsx b/src/dashboardWebView/components/Media/Media.tsx index 141fa1ac..673d5ac1 100644 --- a/src/dashboardWebView/components/Media/Media.tsx +++ b/src/dashboardWebView/components/Media/Media.tsx @@ -9,15 +9,16 @@ import { Item } from './Item'; import { Lightbox } from './Lightbox'; import { List } from './List'; import { useDropzone } from 'react-dropzone' -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { DashboardMessage } from '../../DashboardMessage'; import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon'; import { FolderItem } from './FolderItem'; import useMedia from '../../hooks/useMedia'; -import { TelemetryEvent } from '../../../constants'; +import { STATIC_FOLDER_PLACEHOLDER, TelemetryEvent } from '../../../constants'; import { PageLayout } from '../Layout/PageLayout'; import { parseWinPath } from '../../../helpers/parseWinPath'; import { basename, extname, join } from 'path'; +import { MediaInfo } from '../../../models'; export interface IMediaProps {} @@ -29,9 +30,20 @@ export const Media: React.FunctionComponent = (props: React.PropsWi const folders = useRecoilValue(MediaFoldersAtom); const loading = useRecoilValue(LoadingAtom); - const contentFolders = React.useMemo(() => { - // Check if content allows page bundle - if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) { + const currentStaticFolder = useMemo(() => { + if (settings?.staticFolder) { + let staticFolderPath = join('/', settings?.staticFolder || '', '/'); + if (settings?.staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) { + staticFolderPath = join('/', STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder, '/'); + } + return staticFolderPath; + } + return; + }, [settings?.staticFolder]) + + const contentFolders = useMemo(() => { + // Check if content allows page bundle or if Hexo post assets are enabled + if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle && settings?.staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) { return []; } @@ -46,17 +58,26 @@ export const Media: React.FunctionComponent = (props: React.PropsWi } return groupedFolders; - }, [folders, viewData, settings?.contentFolders]); + }, [folders, viewData, settings?.contentFolders, settings?.staticFolder]); - const publicFolders = React.useMemo(() => { - return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/'))); - }, [folders, viewData, settings?.staticFolder]); + const publicFolders = useMemo(() => { + if (currentStaticFolder && settings?.staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) { + return folders.filter(f => parseWinPath(f).includes(currentStaticFolder)); + } - const allMedia = React.useMemo(() => { - let mediaFiles = media; + return undefined; + }, [folders, viewData, currentStaticFolder, settings?.staticFolder]); + + const allMedia = useMemo(() => { + let mediaFiles: MediaInfo[] = Object.assign([], media); // Check if content allows page bundle - if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) { - mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/'))); + if (currentStaticFolder && viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) { + mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(currentStaticFolder)); + } + + // Filter if Hexo post folder + if (currentStaticFolder && settings?.staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) { + mediaFiles = mediaFiles.filter(m => parseWinPath(m.fsPath).includes(currentStaticFolder)); } if (viewData && viewData.data && viewData.data.type === "file" && viewData.data.fileExtensions && viewData.data.fileExtensions.length > 0) { @@ -70,7 +91,7 @@ export const Media: React.FunctionComponent = (props: React.PropsWi } return mediaFiles; - }, [media, viewData, settings?.staticFolder]); + }, [media, viewData, currentStaticFolder, settings?.staticFolder]); const onDrop = useCallback((acceptedFiles: File[]) => { acceptedFiles.forEach((file) => { @@ -120,7 +141,7 @@ export const Media: React.FunctionComponent = (props: React.PropsWi

- {selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`} + {selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${currentStaticFolder || "public"} folder.`}

) @@ -132,7 +153,7 @@ export const Media: React.FunctionComponent = (props: React.PropsWi
-

No media files to show. You can drag & drop new files.

+

No media files to show. You can drag & drop new files by holding your [shift] key.

) @@ -147,7 +168,7 @@ export const Media: React.FunctionComponent = (props: React.PropsWi { group.folders.map((folder) => ( - + )) } @@ -160,13 +181,13 @@ export const Media: React.FunctionComponent = (props: React.PropsWi publicFolders && publicFolders.length > 0 && (
{ - contentFolders && contentFolders.length > 0 && (

Public folder{settings?.staticFolder && (: {settings?.staticFolder})}

) + contentFolders && contentFolders.length > 0 && (

Public folder{currentStaticFolder && (: {currentStaticFolder})}

) } { publicFolders.map((folder) => ( - + )) } diff --git a/src/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx index 498c1651..18832547 100644 --- a/src/dashboardWebView/components/SnippetsView/Item.tsx +++ b/src/dashboardWebView/components/SnippetsView/Item.tsx @@ -1,5 +1,5 @@ import { Messenger } from '@estruyf/vscode/dist/client'; -import { CodeIcon, DocumentTextIcon, DotsHorizontalIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline'; +import { CodeIcon, DocumentTextIcon, DotsHorizontalIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline'; import * as React from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; @@ -16,11 +16,11 @@ import { NewForm } from './NewForm'; import SnippetForm, { SnippetFormHandle } from './SnippetForm'; export interface IItemProps { - title: string; + snippetKey: string; snippet: Snippet; } -export const Item: React.FunctionComponent = ({ title, snippet }: React.PropsWithChildren) => { +export const Item: React.FunctionComponent = ({ snippetKey, snippet }: React.PropsWithChildren) => { const viewData = useRecoilValue(ViewDataSelector); const settings = useRecoilValue(SettingsSelector); const mode = useRecoilValue(ModeAtom); @@ -50,13 +50,17 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re setMediaSnippet(false); }; + const showFile = useCallback(() => { + Messenger.send(DashboardMessage.openFile, snippet.sourcePath); + }, [ snippet ]); + const onOpenEdit = useCallback(() => { - setSnippetTitle(title); + setSnippetTitle(snippet.title || snippetKey); setSnippetDescription(snippet.description); setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`)); setShowEditDialog(true); setMediaSnippet(!!snippet.isMediaSnippet); - }, [snippet]); + }, [snippet, snippetKey]); const onSnippetUpdate = useCallback(() => { if (!snippetTitle || !snippetOriginalBody) { @@ -64,10 +68,10 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re return; } - const snippets: Snippets = Object.assign({}, settings?.snippets || {}); + let snippets: Snippets = Object.assign({}, settings?.snippets || {}); const snippetLines = snippetOriginalBody.split("\n"); - const crntSnippet = Object.assign({}, snippets[title]); + const crntSnippet = Object.assign({}, snippets[snippetKey]); const fields = SnippetParser.getFields(snippetLines, crntSnippet.fields || [], crntSnippet?.openingTags, crntSnippet?.closingTags); @@ -83,27 +87,33 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re snippetContents.isMediaSnippet = true; } - // Check if new or update - if (title === snippetTitle) { - snippets[title] = snippetContents; + // Check if there is a title set in the snippet + if (snippet.title) { + snippetContents.title = snippetTitle; + snippets[snippetKey] = snippetContents; } else { - delete snippets[title]; - snippets[snippetTitle] = snippetContents; + // Check if new or update + if (snippetKey === snippetTitle) { + snippets[snippetKey] = snippetContents; + } else { + delete snippets[snippetKey]; + snippets[snippetTitle] = snippetContents; + } } Messenger.send(DashboardMessage.updateSnippet, { snippets }); reset(); - }, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]); + }, [settings?.snippets, snippetKey, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]); const onDelete = useCallback(() => { const snippets = Object.assign({}, settings?.snippets || {}); - delete snippets[title]; + delete snippets[snippetKey]; Messenger.send(DashboardMessage.updateSnippet, { snippets }); setShowAlert(false); - }, [settings?.snippets, title]); + }, [settings?.snippets, snippetKey]); return ( <> @@ -115,7 +125,7 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re

{ snippet.isMediaSnippet ? : } - {title} + {snippet.title || snippetKey}

= ({ title, snippet }: Re ) } - - + { + !snippet.sourcePath ? ( + <> + + - setShowAlert(true)}> - + setShowAlert(true)}> + + + ) : ( + + + ) + }
@@ -182,8 +204,8 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re { showInsertDialog && ( setShowInsertDialog(false)} @@ -202,8 +224,8 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re { showEditDialog && ( = ({ title, snippet }: Re { showAlert && ( setShowAlert(false)} diff --git a/src/dashboardWebView/components/SnippetsView/Snippets.tsx b/src/dashboardWebView/components/SnippetsView/Snippets.tsx index da35f135..371b2235 100644 --- a/src/dashboardWebView/components/SnippetsView/Snippets.tsx +++ b/src/dashboardWebView/components/SnippetsView/Snippets.tsx @@ -9,6 +9,7 @@ import { TelemetryEvent } from '../../../constants/TelemetryEvent'; import { SnippetParser } from '../../../helpers/SnippetParser'; import { DashboardMessage } from '../../DashboardMessage'; import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state'; +import { FilterInput } from '../Header/FilterInput'; import { PageLayout } from '../Layout/PageLayout'; import { FormDialog } from '../Modals/FormDialog'; import { SponsorMsg } from '../SponsorMsg'; @@ -26,9 +27,20 @@ export const Snippets: React.FunctionComponent = (props: React.P const [ snippetBody, setSnippetBody ] = useState(''); const [ showCreateDialog, setShowCreateDialog ] = useState(false); const [ mediaSnippet, setMediaSnippet ] = useState(false); + const [ snippetFilter, setSnippetFilter ] = useState(''); const snippets = settings?.snippets || {}; - const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]); + const snippetKeys = useMemo(() => { + const allSnippetKeys = Object.keys(snippets).sort((a, b) => a.localeCompare(b)); + return allSnippetKeys.filter((key) => { + const value = snippetFilter.toLowerCase(); + const keyValue = key.toLowerCase(); + const descriptionValue = snippets[key].description?.toLowerCase() || ''; + + // Contains in key or description, values included in key are ranked higher (sort and fuzzy search) + return keyValue.includes(value) || descriptionValue.includes(value); + }); + }, [settings?.snippets, snippetFilter]); const onSnippetAdd = useCallback(() => { if (!snippetTitle || !snippetBody) { @@ -70,6 +82,15 @@ export const Snippets: React.FunctionComponent = (props: React.P className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100" aria-label="snippets header" > + setSnippetFilter(value)} + onReset={() => setSnippetFilter('')} + /> +