Compare commits

..

61 Commits

Author SHA1 Message Date
Elio Struyf a12cf70a80 Merge pull request #477 from estruyf/dev 2022-12-08 18:12:53 +01:00
Elio Struyf 4b1d80f04b Prepare v8.2.0 release 2022-12-08 18:08:58 +01:00
Elio Struyf a84fecaf96 Update json schema + changelog 2022-12-08 16:46:45 +01:00
Elio Struyf 5b9c279fa2 #471 - Fix typo 2022-12-02 11:49:56 +01:00
Elio Struyf f67be9efb9 update changelog 2022-11-24 17:26:22 +01:00
Elio Struyf 4c50100230 Merge pull request #466 from albbus-stack/git-deleted-files-fix
Thank you @albbus-stack!
2022-11-24 17:25:01 +01:00
albbus-stack 82ace03692 Added git deleted files in the push method 2022-11-24 14:24:09 +01:00
Elio Struyf 576d07fdef Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-11-14 19:40:42 +01:00
Elio Struyf f6bc4fb630 #365 - conditional support 2022-11-14 19:40:38 +01:00
Elio Struyf c35f4ab070 Merge branch 'issue/362-ui' into dev 2022-11-14 19:39:11 +01:00
Elio Struyf 59cbc03b0c #458 - Add prefix to page bundles 2022-11-14 10:48:15 +01:00
Elio Struyf 0ea06a841e #412 - script splitting fix 2022-11-14 10:40:52 +01:00
Elio Struyf b54eb5a360 #462 - Fix issue in script error notification 2022-11-14 10:40:30 +01:00
Elio Struyf 16b6fff6dc #362 - Case sensitive + insensitive option 2022-11-13 20:21:47 +01:00
Elio Struyf 7a46729a46 #362 - additional operators 2022-11-12 18:11:24 +01:00
Elio Struyf 32182c3df0 #362 - Start on conditional ui 2022-11-10 17:21:16 +01:00
Elio Struyf 78587509b3 Update readme 2022-11-09 13:59:19 +01:00
Elio Struyf e3bd7eebbe #360 - Define which content types can be used on your page folders 2022-11-09 13:59:14 +01:00
Elio Struyf f1a8e0d425 #458 - Ability to configure the file prefix on folder level 2022-11-09 10:31:49 +01:00
Elio Struyf 33e294d702 #455 - Show a description for the SEO section 2022-11-08 11:51:41 +01:00
Elio Struyf e098442eaa #412 - Allow title on snippets 2022-11-07 17:28:40 +01:00
Elio Struyf 1de14122c5 #440 - Filter on description 2022-11-07 15:59:20 +01:00
Elio Struyf c0838fffd4 #448 - Full workspace path replacement 2022-11-07 10:50:01 +01:00
Elio Struyf 082c25144f #448 - Fix file retrieval 2022-11-05 18:29:47 +01:00
Elio Struyf d701651a05 #440 - Type to search/filter in the snippets dashboard 2022-11-04 15:25:00 +01:00
Elio Struyf 5205b2d079 #450 - Additional date placeholders 2022-11-04 10:34:46 +01:00
Elio Struyf e864d56081 #447 - Allow to use placeholders on git commit messages 2022-11-04 10:33:23 +01:00
Elio Struyf c6a4c239a0 #449 - Show filename if the title is not set 2022-11-04 09:58:54 +01:00
Elio Struyf 42fbdf9708 #412 - Support for sub-folders 2022-10-31 09:59:33 +01:00
Elio Struyf 8d53990aea #430 - support for post_asset_folder folder 2022-10-08 17:33:42 +02:00
Elio Struyf b9a0c656d3 async updates for settings 2022-10-07 13:32:49 +02:00
Elio Struyf 8a8db67e82 #427 - Add hexo as SSG option 2022-10-07 13:32:42 +02:00
Elio Struyf 0ac4571859 Merge branch 'issue/431' into dev 2022-10-07 10:38:55 +02:00
Elio Struyf a072957793 Updated exists to async 2022-10-06 21:49:53 +02:00
Elio Struyf fad5ad7243 Moving away from fs sync methods 2022-10-06 21:36:51 +02:00
Elio Struyf b248ee7184 Merge branch 'release/v8.1.2' into dev 2022-10-06 14:49:17 +02:00
Elio Struyf cf2d170d6f Merge pull request #437 from estruyf/release/v8.1.2 2022-10-06 14:47:13 +02:00
Elio Struyf 8d577ceb79 #435 #436 - fixes 2022-10-06 14:42:43 +02:00
Elio Struyf 5748aa0540 8.1.2 2022-10-06 14:38:37 +02:00
Elio Struyf 4e850e5cb9 Fix field error message color 2022-10-06 14:23:49 +02:00
Elio Struyf f89d4fce3f #431 - Update changelog 2022-10-04 17:05:04 +02:00
Elio Struyf 1ecf75ae9c Merge branch 'issue/431' into dev 2022-10-04 17:00:48 +02:00
Elio Struyf 888e5c5229 Get webview URI for Windows 2022-10-04 13:47:41 +02:00
Elio Struyf 45eb542619 #431 - Allow pagination page nr 2022-10-03 21:31:23 +02:00
Elio Struyf 5a565f1154 #412 - Override duplicates in config split 2022-10-03 14:07:14 +02:00
Elio Struyf 78002563be Clear cache command 2022-10-03 13:22:40 +02:00
Elio Struyf be3071dc18 Merge branch 'dev' into issue/431 2022-10-02 20:18:46 +02:00
Elio Struyf 5c9d7eda17 #433 - fix title and description rendering if not string 2022-10-02 20:01:59 +02:00
Elio Struyf 9f3cfd9d3a #434 - Webview errors are logged in the extension output 2022-10-02 14:26:22 +02:00
Elio Struyf 0c6ae47a7b #434 - Webview errors are logged in the extension output 2022-10-02 14:25:11 +02:00
Elio Struyf 726a26850d #431 - Cache changes + Tab navigation 2022-10-02 14:23:51 +02:00
Elio Struyf 5fbb05f083 #431 - Performance improvements for first load 2022-10-01 20:30:18 +02:00
Elio Struyf afca99b53a Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-10-01 10:36:34 +02:00
Elio Struyf a8d2c428bc #428 - Image inserting UX enhancement 2022-10-01 10:36:29 +02:00
Elio Struyf 5254f2b7f9 #412 Support added for data files and custom scripts 2022-09-30 14:44:07 +02:00
Elio Struyf 13a71cfd82 #412 - Update setting casing 2022-09-30 10:44:14 +02:00
Elio Struyf 07d67bf881 #412 - allow config folders to use lowercase 2022-09-30 09:54:55 +02:00
Elio Struyf 27887bedef #412 - splitting configuration files 2022-09-29 20:36:02 +02:00
Elio Struyf 2b8f08c03c #406 - Single data entries 2022-09-27 13:16:44 +02:00
Elio Struyf cb2194bc48 8.2.0 2022-09-27 12:04:50 +02:00
Elio Struyf 46872f81ac Include CMS in the display name 2022-09-27 09:00:14 +02:00
87 changed files with 1929 additions and 616 deletions
+41
View File
@@ -1,5 +1,46 @@
# 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
- [#435](https://github.com/estruyf/vscode-front-matter/issues/435): Fix required fields text color
- [#436](https://github.com/estruyf/vscode-front-matter/issues/436): Fix inserting image/video snippets without defined fields
## [8.1.1] - 2022-09-23
### 🐞 Fixes
+2 -2
View File
@@ -191,6 +191,6 @@ You can open showcase issues for the following things:
<p align="center">
<a href="https://visitorbadge.io">
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
</a>
<img src="https://api.visitorbadge.io/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
</a>
</p>
+1 -1
View File
@@ -190,6 +190,6 @@ You can open showcase issues for the following things:
<p align="center">
<a href="https://visitorbadge.io">
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
<img src="https://api.visitorbadge.io/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
</a>
</p>
-2
View File
@@ -68,11 +68,9 @@ describe("Initialization testing", function() {
async function notificationExists(workbench: Workbench, text: string): Promise<Notification | undefined> {
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;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "vscode-front-matter-beta",
"version": "8.1.1",
"version": "8.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-front-matter-beta",
"version": "8.1.1",
"version": "8.2.0",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.7"
+79 -5
View File
@@ -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.1",
"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": {
+1 -1
View File
@@ -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";
+7 -1
View File
@@ -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<string>(SETTING_TEMPLATES_PREFIX);
let filePrefix = Settings.get<string>(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];
+24
View File
@@ -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");
}
}
+4 -3
View File
@@ -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);
});
}
+7
View File
@@ -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);
+66 -18
View File
@@ -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<FolderInfo[] | null> {
const supportedFiles = Settings.get<string[]>(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
+8 -8
View File
@@ -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.");
}
}
+7 -7
View File
@@ -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));
}
+10
View File
@@ -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';
+3
View File
@@ -72,4 +72,7 @@ export const COMMAND_NAME = {
// Config
reloadConfig: getCommandName("config.reload"),
// Cache
clearCache: getCommandName("cache.clear"),
};
+12
View File
@@ -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"
}
}
];
+8
View File
@@ -0,0 +1,8 @@
export const STATIC_FOLDER_PLACEHOLDER = {
hexo: {
postsFolder: "source/_posts",
placeholder: "hexo:post_asset_folder",
}
}
+1
View File
@@ -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';
+2
View File
@@ -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',
}
+28 -16
View File
@@ -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<IAppProps> = ({showWelcome}: React.Pro
}
return (
<main className={`h-full w-full`}>
<Routes>
<Route path={routePaths.welcome} element={<WelcomeScreen settings={settings} />} />
<Route path={routePaths.contents} element={<Contents pages={pages} loading={loading} />} />
<Route path={routePaths.media} element={<Media />} />
<Route path={routePaths.snippets} element={<Snippets />} />
{
allowDataView && <Route path={routePaths.data} element={<DataView />} />
}
<ErrorBoundary
fallback={(<ErrorView />)}
onError={(error: Error, componentStack: string, eventId: string) => {
Messenger.send(DashboardMessage.logError, `Event ID: ${eventId}
Message: ${error.message}
{
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
}
Stack: ${componentStack}`);
}}>
<main className={`h-full w-full`}>
<Routes>
<Route path={routePaths.welcome} element={<WelcomeScreen settings={settings} />} />
<Route path={routePaths.contents} element={<Contents pages={pages} loading={loading} />} />
<Route path={routePaths.media} element={<Media />} />
<Route path={routePaths.snippets} element={<Snippets />} />
{
allowDataView && <Route path={routePaths.data} element={<DataView />} />
}
<Route path={`*`} element={<UnknownView />} />
</Routes>
</main>
{
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
}
<Route path={`*`} element={<UnknownView />} />
</Routes>
</main>
</ErrorBoundary>
);
};
@@ -19,6 +19,22 @@ export const Item: React.FunctionComponent<IItemProps> = ({ 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 '<invalid title>';
}
return title;
}, [title]);
const escapedDescription = useMemo(() => {
if (description && typeof description !== 'string') {
return '<invalid description>';
}
return description;
}, [description]);
const openFile = () => {
Messenger.send(DashboardMessage.openFile, fmFilePath);
@@ -57,7 +73,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<button onClick={openFile} className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200 cursor-pointer">
{
pageData[PREVIEW_IMAGE_FIELD] ? (
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={escapedTitle} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
) : (
<div className={`flex items-center justify-center bg-whisper-500 dark:bg-vulcan-200 dark:group-hover:bg-vulcan-100`}>
<MarkdownIcon className={`h-32 text-vulcan-100 dark:text-whisper-100`} />
@@ -79,9 +95,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
onOpen={openFile} />
</div>
<button onClick={openFile} className={`text-left`}><h2 className="mt-2 mb-2 font-bold">{title}</h2></button>
<button onClick={openFile} className={`text-left block`}><h2 className="mt-2 mb-2 font-bold">{escapedTitle}</h2></button>
<button onClick={openFile} className={`text-left`}><p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p></button>
<button onClick={openFile} className={`text-left block`}><p className="text-xs text-vulcan-200 dark:text-whisper-800">{escapedDescription}</p></button>
{
tags && tags.length > 0 && (
@@ -110,13 +126,13 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<div className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`}>
<div className="col-span-8 font-bold truncate flex items-center space-x-4">
<button
title={`Open: ${title}`}
title={`Open: ${escapedTitle}`}
onClick={openFile}>
{title}
{escapedTitle}
</button>
<ContentActions
title={title}
title={escapedTitle}
path={fmFilePath}
scripts={settings?.scripts}
onOpen={openFile}
@@ -9,9 +9,9 @@ import { GroupOption } from '../../constants/GroupOption';
import { Page } from '../../models/Page';
import { Settings } from '../../models/Settings';
import { GroupingSelector, PageAtom } from '../../state';
import { PAGE_LIMIT } from '../Header/Pagination';
import { Item } from './Item';
import { List } from './List';
import usePagination from '../../hooks/usePagination';
export interface IOverviewProps {
pages: Page[];
@@ -21,14 +21,15 @@ export interface IOverviewProps {
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settings}: React.PropsWithChildren<IOverviewProps>) => {
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) {
@@ -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<IDataViewProps> = (props: React.PropsWithChildren<IDataViewProps>) => {
const [ selectedData, setSelectedData ] = useState<DataFile | null>(null);
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
const [ dataEntries, setDataEntries ] = useState<any[] | null>(null);
const [ dataEntries, setDataEntries ] = useState<any | any[] | null>(null);
const settings = useRecoilValue(SettingsSelector);
const setSchema = (dataFile: DataFile) => {
@@ -57,6 +57,12 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (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<IDataViewProps> = (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<IDataViewProps> = (props: React.P
{
selectedData ? (
<>
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData?.title?.toLowerCase() || ""} data items</h2>
{
!selectedData.singleEntry && (
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData?.title?.toLowerCase() || ""} data items</h2>
<div className='py-4'>
{
(dataEntries && dataEntries.length > 0) ? (
<>
<Container onSortEnd={onSortEnd} useDragHandle>
{
(dataEntries || []).map((dataEntry, idx) => (
<SortableItem
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
index={idx}
crntIndex={idx}
selectedIndex={selectedIndex}
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
onDeleteItem={deleteItem}
/>
))
}
</Container>
<Button
className='mt-4'
onClick={() => setSelectedIndex(null)}>
Add a new entry
</Button>
</>
) : (
<div className={`flex flex-col items-center justify-center`}>
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
</div>
)
}
</div>
</div>
<div className={`w-2/3 py-6 px-4 overflow-auto`}>
<div className='py-4'>
{
(dataEntries && dataEntries.length > 0) ? (
<>
<Container onSortEnd={onSortEnd} useDragHandle>
{
(dataEntries as any[] || []).map((dataEntry, idx) => (
<SortableItem
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
index={idx}
crntIndex={idx}
selectedIndex={selectedIndex}
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
onDeleteItem={deleteItem}
/>
))
}
</Container>
<Button
className='mt-4'
onClick={() => setSelectedIndex(null)}>
Add a new entry
</Button>
</>
) : (
<div className={`flex flex-col items-center justify-center`}>
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
</div>
)
}
</div>
</div>
)
}
<div className={`${selectedData.singleEntry ? "w-full" : "w-2/3"} py-6 px-4 overflow-auto`}>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Create or modify your {selectedData.title.toLowerCase()} data</h2>
{
selectedData ? (
<DataForm
schema={selectedData?.schema}
model={(dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null}
model={dataEntry}
onSubmit={onSubmit}
onClear={() => setSelectedIndex(null)} />
) : (
@@ -234,7 +252,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
<DatabaseIcon className='w-32 h-32' />
<p className='text-3xl mt-2'>No data files found</p>
<p className='text-xl mt-4'>
<a className={`text-teal-700 hover:text-teal-900`} href={`https://frontmatter.codes/docs/dashboard#data-files-view`} title={`Read read more to get started using data files`}>Read read more to get started using data files</a></p>
<a className={`text-teal-700 hover:text-teal-900`} href={`https://frontmatter.codes/docs/dashboard#data-files-view`} title={`Read more to get started using data files`}>Read more to get started using data files</a></p>
</div>
</div>
)
@@ -0,0 +1,14 @@
import { ExclamationIcon } from '@heroicons/react/solid';
import * as React from 'react';
export interface IErrorViewProps {}
export const ErrorView: React.FunctionComponent<IErrorViewProps> = (props: React.PropsWithChildren<IErrorViewProps>) => {
return (
<main className={`h-full w-full flex flex-col justify-center items-center space-y-2`}>
<ExclamationIcon className="w-24 h-24 text-red-500" />
<p className='text-xl'>Sorry, something went wrong.</p>
<p className='text-base'>Please close the dashboard and try again.</p>
</main>
);
};
@@ -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<IFilterInputProps> = ({ placeholder, value, isReady, autoFocus, onReset, onChange}: React.PropsWithChildren<IFilterInputProps>) => {
return (
<div className="flex space-x-4 flex-1">
<div className="min-w-0">
<label htmlFor="search" className="sr-only">Search</label>
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="search"
className={`block w-full py-2 pl-10 pr-3 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 appearance-none disabled:opacity-50`}
placeholder={placeholder || "Search"}
value={value}
autoFocus={autoFocus}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value)}
disabled={!isReady}
/>
{
value && onReset && (
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center">
<XCircleIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</button>
)
}
</div>
</div>
</div>
);
};
@@ -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<IHeaderProps> = ({header, totalPages, folders, settings }: React.PropsWithChildren<IHeaderProps>) => {
export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPages, settings }: React.PropsWithChildren<IHeaderProps>) => {
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<IHeaderProps> = ({header, totalPage
</div>
{
(settings?.dashboardState.contents.pagination) && (totalPages || 0) > PAGE_LIMIT && (!grouping || grouping === GroupOption.none) && (
<div className={`flex justify-center py-2 border-b border-gray-300 dark:border-vulcan-100`}>
(pageSetNr > 0) && (totalPages || 0) > pageSetNr && (!grouping || grouping === GroupOption.none) && (
<div className={`px-4 flex justify-between py-2 border-b border-gray-300 dark:border-vulcan-100`}>
<PaginationStatus totalPages={totalPages || 0} />
<Pagination totalPages={totalPages || 0} />
</div>
)
@@ -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<IPaginationProps> = ({ totalPages }: React.PropsWithChildren<IPaginationProps>) => {
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<IPaginationProps> = ({ totalPag
<PaginationButton
title="Next"
disabled={page >= totalItems}
disabled={page >= totalPagesNr}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalItems}
onClick={() => setPage(totalItems)} />
disabled={page >= totalPagesNr}
onClick={() => setPage(totalPagesNr)} />
</div>
);
};
@@ -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<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = ({ totalPages }: React.PropsWithChildren<IPaginationStatusProps>) => {
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 (
<div className="hidden sm:flex">
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * PAGE_LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
Showing <span className="font-medium">{(page * pageSetNr) + 1}</span> to <span className="font-medium">{totelItemsOnPage}</span> of{' '}
<span className="font-medium">{totalItems}</span> results
</p>
</div>
);
@@ -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<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
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<IFolderCreationProps> = (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 (
<button
className={`mr-2 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 post asset folder`}
onClick={onAssetFolderCreation}>
<FolderAddIcon className={`mr-2 h-6 w-6`} />
<span className={``}>Create post asset folder</span>
</button>
);
}
return null;
}, [isHexoPostAssetsEnabled]);
if (scripts.length > 0) {
return (
<div className="flex flex-1 justify-end">
{ renderPostAssetsButton }
<ChoiceButton
title={`Create new folder`}
choices={scripts.map(s => ({
@@ -43,6 +84,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
return (
<div className="flex flex-1 justify-end">
{ renderPostAssetsButton }
<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`}
+39 -5
View File
@@ -41,6 +41,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const hasViewData = useMemo(() => {
return viewData?.data?.filePath !== undefined;
}, [viewData]);
const [referenceElement, setReferenceElement] = useState<any>(null);
const [popperElement, setPopperElement] = useState<any>(null);
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
@@ -57,6 +61,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return keys.filter(key => (settings.snippets || {})[key].isMediaSnippet).map(key => ({ title: key, ...(settings.snippets || {})[key]}));
}, [settings]);
const showMediaSnippet = useMemo(() => {
return viewData?.data?.position && mediaSnippets.length > 0;
}, [viewData, mediaSnippets]);
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
let relPath = media.fsPath.split(settings.wsFolder).pop();
@@ -158,7 +166,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
mediaHeight: media?.dimensions?.height?.toString() || "",
};
if (snippet.fields.length === 0) {
if (!snippet.fields || snippet.fields.length === 0) {
setShowSnippetFormDialog(false);
setMediaData(undefined);
@@ -350,15 +358,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}, [media.fsPath]);
useEffect(() => {
if (!viewData?.data?.filePath) {
if (!hasViewData) {
clearFormData();
}
}, [viewData]);
}, [viewData, hasViewData]);
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50">
<button className={`relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImageFile ? "cursor-pointer" : "cursor-default"}`} onClick={openLightbox}>
<button className={`group-scope relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImageFile ? "cursor-pointer" : "cursor-default"}`} onClick={hasViewData ? undefined : openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
{
renderMediaIcon
@@ -367,6 +375,32 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
{ renderMedia }
</div>
{
hasViewData && (
<div className={`hidden group-scope-hover:flex absolute top-0 right-0 bottom-0 left-0 items-center bg-vulcan-500 bg-opacity-70 justify-center`}>
<div className={`h-full ${showMediaSnippet ? 'w-1/3' : 'w-full'} flex items-center justify-center`}>
<button
title='Insert image'
className={`text-gray-300 hover:text-teal-600 h-1/3`}
onClick={insertToArticle}>
<PlusIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
</button>
</div>
{
(viewData?.data?.position && mediaSnippets.length > 0) && (
<div className={`h-full w-1/3 flex items-center justify-center`}>
<button
title='Insert snippet'
className={`text-gray-300 hover:text-teal-600 h-1/3`}
onClick={insertSnippet}>
<CodeIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
</button>
</div>
)
}
</div>
)
}
</button>
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`group-scope absolute top-4 right-4 flex flex-col space-y-4`}>
@@ -443,7 +477,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
viewData?.data?.filePath ? (
<>
<MenuItem
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image</span></div>}
onClick={insertToArticle} />
{
+40 -19
View File
@@ -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<IMediaProps> = (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<IMediaProps> = (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<IMediaProps> = (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<IMediaProps> = (props: React.PropsWi
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
<UploadIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{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.`}
</p>
</div>
)
@@ -132,7 +153,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
<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 &amp; drop new files.</p>
<p className={`text-xl font-medium`}>No media files to show. You can drag &amp; drop new files by holding your [shift] key.</p>
</div>
</div>
)
@@ -147,7 +168,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
<List gap={0}>
{
group.folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
<FolderItem key={folder} folder={folder} staticFolder={currentStaticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
@@ -160,13 +181,13 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
publicFolders && publicFolders.length > 0 && (
<div className={`mb-8`}>
{
contentFolders && contentFolders.length > 0 && (<h2 className='text-lg mb-8'>Public folder{settings?.staticFolder && (<span>: <b>{settings?.staticFolder}</b></span>)}</h2>)
contentFolders && contentFolders.length > 0 && (<h2 className='text-lg mb-8'>Public folder{currentStaticFolder && (<span>: <b>{currentStaticFolder}</b></span>)}</h2>)
}
<List gap={0}>
{
publicFolders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
<FolderItem key={folder} folder={folder} staticFolder={currentStaticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
@@ -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<IItemProps> = ({ title, snippet }: React.PropsWithChildren<IItemProps>) => {
export const Item: React.FunctionComponent<IItemProps> = ({ snippetKey, snippet }: React.PropsWithChildren<IItemProps>) => {
const viewData = useRecoilValue(ViewDataSelector);
const settings = useRecoilValue(SettingsSelector);
const mode = useRecoilValue(ModeAtom);
@@ -50,13 +50,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({ 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<IItemProps> = ({ 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<IItemProps> = ({ 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<IItemProps> = ({ title, snippet }: Re
<h2 className="mt-2 mb-2 font-bold flex items-center" title={snippet.isMediaSnippet ? "Media snippet" : "Content snippet"}>
{ snippet.isMediaSnippet ? <PhotographIcon className='w-5 h-5 mr-1' aria-hidden={true} /> : <DocumentTextIcon className='w-5 h-5 mr-1' aria-hidden={true} /> }
{title}
{snippet.title || snippetKey}
</h2>
<FeatureFlag
@@ -160,17 +170,29 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
)
}
<QuickAction
title={`Edit snippet`}
onClick={onOpenEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{
!snippet.sourcePath ? (
<>
<QuickAction
title={`Edit snippet`}
onClick={onOpenEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete snippet`}
onClick={() => setShowAlert(true)}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete snippet`}
onClick={() => setShowAlert(true)}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</>
) : (
<QuickAction
title={`View snippet file`}
onClick={showFile}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
</div>
</div>
</div>
@@ -182,8 +204,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
{
showInsertDialog && (
<FormDialog
title={`Insert snippet: ${title}`}
description={`Insert the ${title.toLowerCase()} snippet into the current article`}
title={`Insert snippet: ${(snippet.title || snippetKey)}`}
description={`Insert the ${(snippet.title || snippetKey).toLowerCase()} snippet into the current article`}
isSaveDisabled={!insertToContent}
trigger={insertToArticle}
dismiss={() => setShowInsertDialog(false)}
@@ -202,8 +224,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
{
showEditDialog && (
<FormDialog
title={`Edit snippet: ${title}`}
description={`Edit the ${title.toLowerCase()} snippet`}
title={`Edit snippet: ${(snippet.title || snippetKey)}`}
description={`Edit the ${(snippet.title || snippetKey).toLowerCase()} snippet`}
isSaveDisabled={!snippetTitle || !snippetOriginalBody}
trigger={onSnippetUpdate}
dismiss={reset}
@@ -227,8 +249,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
{
showAlert && (
<Alert
title={`Delete snippet: ${title}`}
description={`Are you sure you want to delete the ${title.toLowerCase()} snippet?`}
title={`Delete snippet: ${(snippet.title || snippetKey)}`}
description={`Are you sure you want to delete the ${(snippet.title || snippetKey).toLowerCase()} snippet?`}
okBtnText={`Delete`}
cancelBtnText={`Cancel`}
dismiss={() => setShowAlert(false)}
@@ -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<ISnippetsProps> = (props: React.P
const [ snippetBody, setSnippetBody ] = useState<string>('');
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
const [ mediaSnippet, setMediaSnippet ] = useState(false);
const [ snippetFilter, setSnippetFilter ] = useState<string>('');
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<ISnippetsProps> = (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"
>
<FilterInput
placeholder='Search'
isReady={true}
autoFocus={true}
value={snippetFilter}
onChange={(value: string) => setSnippetFilter(value)}
onReset={() => setSnippetFilter('')}
/>
<div className="flex flex-1 justify-end">
<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`}
@@ -99,7 +120,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
snippetKeys.map((snippetKey: any, index: number) => (
<Item
key={index}
title={snippetKey}
snippetKey={snippetKey}
snippet={snippets[snippetKey]} />
))
}
@@ -163,10 +163,10 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
];
React.useEffect(() => {
if (settings.crntFramework) {
setFramework(settings.crntFramework);
if (settings.crntFramework || settings.framework?.name) {
setFramework(settings.crntFramework || settings.framework?.name || null);
}
}, [settings.crntFramework]);
}, [settings.crntFramework, settings.framework]);
return (
<nav aria-label="Progress">
+12 -5
View File
@@ -4,9 +4,9 @@ import { useState, useEffect, useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../models';
import { DashboardCommand } from '../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SelectedMediaFolderAtom } from '../state';
import { AllContentFoldersAtom, AllStaticFoldersAtom, LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../state';
import Fuse from 'fuse.js';
import { PAGE_LIMIT } from '../components/Header/Pagination';
import usePagination from './usePagination';
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
keys: [
@@ -26,12 +26,16 @@ export default function useMedia() {
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
const [ , setAllContentFolders ] = useRecoilState(AllContentFoldersAtom);
const [ , setAllStaticFolders ] = useRecoilState(AllStaticFoldersAtom);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const search = useRecoilValue(SearchAtom);
const settings = useRecoilValue(SettingsAtom);
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
const getMedia = useCallback(() => {
return searchedMedia.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT));
}, [searchedMedia, page]);
return searchedMedia.slice(page * pageSetNr, ((page + 1) * pageSetNr));
}, [searchedMedia, page, pageSetNr]);
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.media) {
@@ -42,6 +46,8 @@ export default function useMedia() {
setFolders(data.folders);
setSelectedFolder(data.selectedFolder);
setSearchedMedia(data.media);
setAllContentFolders(data.allContentFolders);
setAllStaticFolders(data.allStaticfolders);
}
};
@@ -57,8 +63,9 @@ export default function useMedia() {
return;
}
setTotal(media.length);
setSearchedMedia(media);
}, [search]);
}, [search, media]);
useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
+43 -19
View File
@@ -13,6 +13,7 @@ import { parseWinPath } from '../../helpers/parseWinPath';
export default function usePages(pages: Page[]) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
const [ sortedPages, setSortedPages ] = useState<Page[]>([]);
const [ sorting, setSorting ] = useRecoilState(SortingAtom);
const [ tabInfo , setTabInfo ] = useRecoilState(TabInfoAtom);
const settings = useRecoilValue(SettingsSelector);
@@ -22,8 +23,10 @@ export default function usePages(pages: Page[]) {
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
const processPages = useCallback((searchedPages: Page[]) => {
const draftField = settings?.draftField;
/**
* Process all the pages by applying the sorting, filtering and searching.
*/
const processPages = useCallback((searchedPages: Page[], fullProcess: boolean = true) => {
const framework = settings?.crntFramework;
// Filter the pages
@@ -93,40 +96,52 @@ export default function usePages(pages: Page[]) {
pagesSorted = pagesSorted.filter(page => page.fmCategories && page.fmCategories.includes(category));
}
setSortedPages(pagesSorted);
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
/**
* Process the pages when the tab changes
*/
const processByTab = useCallback((pages: Page[]) => {
const draftField = settings?.draftField;
let crntPages: Page[] = Object.assign([], pages);
// Process the tab data
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesSorted.length;
draftTypes[Tab.All] = crntPages.length;
// Filter by draft status
if (draftField && draftField.type === 'choice') {
const draftChoices = settings?.draftField?.choices;
for (const choice of (draftChoices || [])) {
if (choice) {
draftTypes[choice] = pagesSorted.filter(page => page.fmDraft === choice).length;
draftTypes[choice] = crntPages.filter(page => page.fmDraft === choice).length;
}
}
if (tab !== Tab.All) {
pagesSorted = pagesSorted.filter(page => page.fmDraft === tab);
crntPages = crntPages.filter(page => page.fmDraft === tab);
} else {
pagesSorted = pagesSorted;
crntPages = crntPages;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesSorted.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesSorted.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
const drafts = crntPages.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = crntPages.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesSorted = draftField?.invert ? drafts : published;
crntPages = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesSorted = draftField?.invert ? published : drafts;
crntPages = draftField?.invert ? published : drafts;
} else {
pagesSorted = pagesSorted;
crntPages = crntPages;
}
}
@@ -134,10 +149,14 @@ export default function usePages(pages: Page[]) {
setTabInfo(draftTypes);
// Set the pages
setPageItems(pagesSorted);
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
setPageItems(crntPages);
}, [ tab, tabInfo, settings ]);
/**
* Search listener for filtered pages
* @param message
*/
const searchListener = (message: MessageEvent<EventData<any>>) => {
switch (message.data.command) {
case DashboardMessage.searchPages:
@@ -146,6 +165,7 @@ export default function usePages(pages: Page[]) {
}
};
useEffect(() => {
let usedSorting = sorting;
@@ -160,15 +180,19 @@ export default function usePages(pages: Page[]) {
// Check if search needs to be performed
let searchedPages = pages;
if (search) {
// const fuse = new Fuse(pages, fuseOptions);
// const results = fuse.search(search);
// searchedPages = results.map(page => page.item);
Messenger.send(DashboardMessage.searchPages, { query: search });
} else {
processPages(searchedPages);
}
}, [ settings?.draftField, pages, sorting, search, tab, tag, category, folder ]);
}, [ settings?.draftField, pages, sorting, search, tag, category, folder ]);
useEffect(() => {
if (sortedPages.length > 0) {
processByTab(sortedPages);
}
}, [sortedPages, tab])
useEffect(() => {
Messenger.listen(searchListener);
@@ -0,0 +1,62 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { routePaths } from '..';
export const PAGE_LIMIT = 16;
export default function usePagination(value: number | boolean | null | undefined, totalPages?: number, totalMedia?: number) {
const location = useLocation();
const pagination = useMemo(() => {
if (location.pathname === routePaths.contents) {
if (typeof value === 'number') {
const pageNr = value > 0 ? value : 0;
if (pageNr > 52) {
return 52;
}
return pageNr;
} else if (typeof value === 'boolean') {
return value ? PAGE_LIMIT : 0;
}
}
return PAGE_LIMIT;
}, [value, location.pathname]);
const totalPagesNr: number = useMemo(() => {
if (location.pathname === routePaths.contents) {
if (totalPages) {
return Math.ceil((totalPages || 0) / pagination) - 1
}
} else {
if (totalMedia) {
return Math.ceil(totalMedia / pagination) - 1;
}
}
return 0;
}, [location.pathname, totalPages, totalMedia, pagination]);
/**
* The total items (pages or media)
*/
const totalItems: number = useMemo(() => {
if (location.pathname === routePaths.contents) {
if (totalPages) {
return totalPages;
}
} else {
if (totalMedia) {
return totalMedia;
}
}
return 0;
}, [location.pathname, totalPages, totalMedia, pagination]);
return {
pageSetNr: pagination,
totalPagesNr,
totalItems
};
}
+5 -1
View File
@@ -1,6 +1,10 @@
import { Uri } from "vscode";
export interface Page {
// Properties for caching
fmCachePath: string;
fmCacheModifiedTime: number;
// Front matter fields
fmFolder: string;
fmFilePath: string;
fmFileName: string;
+1 -1
View File
@@ -44,7 +44,7 @@ export interface ContentsViewState {
defaultSorting: string | null | undefined;
tags: string | null | undefined;
templatesEnabled: boolean | null | undefined;
pagination: boolean | null | undefined;
pagination: boolean | number | null | undefined;
}
export interface MediaViewState extends ContentsViewState {
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const AllContentFoldersAtom = atom<string[] | undefined>({
key: 'AllContentFoldersAtom',
default: undefined
});
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const AllStaticFoldersAtom = atom<string[] | undefined>({
key: 'AllStaticFoldersAtom',
default: undefined
});
+2
View File
@@ -1,3 +1,5 @@
export * from './AllContentFoldersAtom';
export * from './AllStaticFoldersAtom';
export * from './CategoryAtom';
export * from './DashboardViewAtom';
export * from './FolderAtom';
+1 -1
View File
@@ -100,7 +100,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
}
}, this);
Settings.onConfigChange((global?: any) => {
Settings.onConfigChange(() => {
SettingsListener.getSettings();
});
}
+15 -19
View File
@@ -1,30 +1,19 @@
import { GitListener } from './listeners/general/GitListener';
import * as vscode from 'vscode';
import { Telemetry } from './helpers/Telemetry';
import { ContentType } from './helpers/ContentType';
import { Dashboard } from './commands/Dashboard';
import { Article, Settings, StatusListener } from './commands';
import { Folders } from './commands/Folders';
import { Preview } from './commands/Preview';
import { Project } from './commands/Project';
import { Template } from './commands/Template';
import { COMMAND_NAME, TelemetryEvent } from './constants';
import { TaxonomyType } from './models';
import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider';
import { TagType } from './panelWebView/TagType';
import { ExplorerView } from './explorerView/ExplorerView';
import { Extension } from './helpers/Extension';
import { DashboardData } from './models/DashboardData';
import { debounceCallback, Logger, Settings as SettingsHelper } from './helpers';
import { Content } from './commands/Content';
import { DashboardSettings, debounceCallback, Logger, Settings as SettingsHelper } from './helpers';
import ContentProvider from './providers/ContentProvider';
import { Wysiwyg } from './commands/Wysiwyg';
import { Diagnostics } from './commands/Diagnostics';
import { PagesListener } from './listeners/dashboard';
import { Backers } from './commands/Backers';
import { DataListener, SettingsListener } from './listeners/panel';
import { NavigationType } from './dashboardWebView/models';
import { ModeSwitch } from './services/ModeSwitch';
import { PagesParser } from './services/PagesParser';
import { ContentType, Telemetry, Extension } from './helpers';
import { TaxonomyType, DashboardData } from './models';
import { Backers, Diagnostics, Wysiwyg, Content, Cache, Template, Project, Preview, Folders, Dashboard, Article, Settings, StatusListener } from './commands';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
@@ -35,13 +24,13 @@ export async function activate(context: vscode.ExtensionContext) {
const { subscriptions, extensionUri, extensionPath } = context;
const extension = Extension.getInstance(context);
Backers.init(context);
Backers.init(context).then(() => {});
if (!extension.checkIfExtensionCanRun()) {
return undefined;
}
SettingsHelper.init();
await SettingsHelper.init();
extension.migrateSettings();
SettingsHelper.checkToPromote();
@@ -201,7 +190,7 @@ export async function activate(context: vscode.ExtensionContext) {
});
// Things to do when configuration changes
SettingsHelper.onConfigChange((global?: any) => {
SettingsHelper.onConfigChange(() => {
Preview.init();
GitListener.init();
@@ -266,6 +255,13 @@ export async function activate(context: vscode.ExtensionContext) {
// Git
GitListener.init();
// Once everything is registered, the page parsing can start in the background
DashboardSettings.get();
PagesParser.start();
// Cache commands
Cache.registerCommands();
// Subscribe all commands
subscriptions.push(
insertTags,
+25 -12
View File
@@ -4,7 +4,6 @@ import { Uri, workspace } from 'vscode';
import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
import * as vscode from 'vscode';
import * as fs from "fs";
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD, DefaultFieldValues } from '../constants';
import { DumpOptions } from 'js-yaml';
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
@@ -15,7 +14,6 @@ import { Article } from '../commands';
import { join } from 'path';
import { EditorHelper } from '@estruyf/vscode';
import sanitize from '../helpers/Sanitize';
import { existsSync, mkdirSync } from 'fs';
import { ContentType } from '../models';
import { DateHelper } from './DateHelper';
import { DiagnosticSeverity, Position, window, Range } from 'vscode';
@@ -26,6 +24,8 @@ import { Content } from 'mdast';
import { processKnownPlaceholders } from './PlaceholderHelper';
import { CustomScript } from './CustomScript';
import { Folders } from '../commands/Folders';
import { existsAsync, readFileAsync } from '../utils';
import { mkdirAsync } from '../utils/mkdirAsync';
export class ArticleHelper {
private static notifiedFiles: string[] = [];
@@ -70,8 +70,8 @@ export class ArticleHelper {
* Retrieve the file's front matter by its path
* @param filePath
*/
public static getFrontMatterByPath(filePath: string) {
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
public static async getFrontMatterByPath(filePath: string) {
const file = await readFileAsync(filePath, { encoding: "utf-8" });
return ArticleHelper.parseFile(file, filePath);
}
@@ -328,36 +328,49 @@ export class ArticleHelper {
* @param titleValue
* @returns The new file path
*/
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined {
public static async createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): Promise<string | undefined> {
FrontMatterParser.currentContent = null;
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
let prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
const filePrefixOnFolder = Folders.getFilePrefixByFolderPath(folderPath);
if (typeof filePrefixOnFolder !== "undefined") {
prefix = filePrefixOnFolder;
}
if (prefix && typeof prefix === "string") {
prefix = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}`;
}
// Name of the file or folder to create
const sanitizedName = ArticleHelper.sanitize(titleValue);
let sanitizedName = ArticleHelper.sanitize(titleValue);
let newFilePath: string | undefined;
// Create a folder with the `index.md` file
if (contentType?.pageBundle) {
if (prefix && typeof prefix === "string") {
sanitizedName = `${prefix}-${sanitizedName}`;
}
const newFolder = join(folderPath, sanitizedName);
if (existsSync(newFolder)) {
if (await existsAsync(newFolder)) {
Notifications.error(`A page bundle with the name ${sanitizedName} already exists in ${folderPath}`);
return;
} else {
mkdirSync(newFolder);
await mkdirAsync(newFolder);
newFilePath = join(newFolder, `index.${fileExtension || contentType.fileType || fileType}`);
}
} else {
let newFileName = `${sanitizedName}.${fileExtension || contentType?.fileType || fileType}`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;
newFileName = `${prefix}-${newFileName}`;
}
newFilePath = join(folderPath, newFileName);
if (existsSync(newFilePath)) {
if (await existsAsync(newFilePath)) {
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
return;
}
@@ -400,7 +413,7 @@ export class ArticleHelper {
* @param title
* @returns
*/
public static async processCustomPlaceholders(value: string, title: string, filePath: string) {
public static async processCustomPlaceholders(value: string, title: string | undefined, filePath: string | undefined) {
if (value && typeof value === "string") {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
const placeholders = Settings.get<CustomPlaceholder[]>(SETTING_CONTENT_PLACEHOLDERS);
+22 -18
View File
@@ -6,13 +6,13 @@ import { ContentType as IContentType, DraftField, Field, FieldGroup, FieldType,
import { Uri, commands, window, ProgressLocation, workspace } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { existsSync, writeFileSync } from "fs";
import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { Telemetry } from './Telemetry';
import { processKnownPlaceholders } from './PlaceholderHelper';
import { basename } from 'path';
import { ParsedFrontMatter } from '../parsers';
import { existsAsync, writeFileAsync } from '../utils';
export class ContentType {
@@ -64,11 +64,6 @@ export class ContentType {
* @returns
*/
public static async createContent() {
const selectedContentType = await Questions.SelectContentType();
if (!selectedContentType) {
return;
}
const selectedFolder = await Questions.SelectContentFolder();
if (!selectedFolder) {
return;
@@ -76,10 +71,19 @@ export class ContentType {
const contentTypes = ContentType.getAll();
const folders = Folders.get();
const folder = folders.find(f => f.title === selectedFolder);
const location = folders.find(f => f.title === selectedFolder);
if (contentTypes && location) {
const folderPath = Folders.getFolderPath(Uri.file(location.path));
if (!folder) {
return;
}
const selectedContentType = await Questions.SelectContentType(folder.contentTypes || []);
if (!selectedContentType) {
return;
}
if (contentTypes && folder) {
const folderPath = Folders.getFolderPath(Uri.file(folder.path));
const contentType = contentTypes.find(ct => ct.name === selectedContentType);
if (folderPath && contentType) {
ContentType.create(contentType, folderPath);
@@ -194,12 +198,12 @@ export class ContentType {
contentTypes.push(newContentType);
}
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = Settings.projectConfigPath;
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && await existsAsync(configPath) ? `Open settings` : undefined);
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
if (notificationAction === "Open settings" && configPath && await existsAsync(configPath)) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
@@ -228,12 +232,12 @@ export class ContentType {
const index = contentTypes.findIndex(ct => ct.name === contentType.name);
contentTypes[index].fields = updatedFields;
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = Settings.projectConfigPath;
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && await existsAsync(configPath) ? `Open settings` : undefined);
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
if (notificationAction === "Open settings" && configPath && await existsAsync(configPath)) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
@@ -558,10 +562,10 @@ export class ContentType {
let templateData: ParsedFrontMatter | null = null;
if (templatePath) {
templatePath = Folders.getAbsFilePath(templatePath);
templateData = ArticleHelper.getFrontMatterByPath(templatePath);
templateData = await ArticleHelper.getFrontMatterByPath(templatePath);
}
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
let newFilePath: string | undefined = await ArticleHelper.createContent(contentType, folderPath, titleValue);
if (!newFilePath) {
return;
}
@@ -586,7 +590,7 @@ export class ContentType {
const content = ArticleHelper.stringifyFrontMatter(templateData?.content || ``, data);
writeFileSync(newFilePath, content, { encoding: "utf8" });
await writeFileAsync(newFilePath, content, { encoding: "utf8" });
// Check if the content type has a post script to execute
if (contentType.postScript) {
+12 -7
View File
@@ -14,7 +14,7 @@ import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { ParsedFrontMatter } from '../parsers';
import { TelemetryEvent } from '../constants/TelemetryEvent';
import { SETTING_CUSTOM_SCRIPTS } from '../constants';
import { existsSync } from 'fs';
import { existsAsync } from '../utils';
export class CustomScript {
@@ -77,7 +77,7 @@ export class CustomScript {
articlePath = editor.document.uri.fsPath;
article = ArticleHelper.getFrontMatter(editor);
} else {
article = ArticleHelper.getFrontMatterByPath(path);
article = await ArticleHelper.getFrontMatterByPath(path);
}
if (articlePath && article) {
@@ -119,7 +119,7 @@ export class CustomScript {
if (folder.lastModified.length > 0) {
for await (const file of folder.lastModified) {
try {
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
const article = await ArticleHelper.getFrontMatterByPath(file.filePath);
if (article) {
const crntOutput = await CustomScript.runScript(wsPath, article, file.filePath, script);
if (crntOutput) {
@@ -195,7 +195,11 @@ export class CustomScript {
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${contentPath}" ${articleData}`);
return output;
} catch (e) {
Notifications.error(`${script.title}: ${(e as Error).message}`);
if (typeof e === "string") {
Notifications.error(`${script.title}: ${e}`);
} else {
Notifications.error(`${script.title}: ${(e as Error).message}`);
}
return null;
}
}
@@ -220,7 +224,7 @@ export class CustomScript {
articlePath = editor.document.uri.fsPath;
article = ArticleHelper.getFrontMatter(editor);
} else {
article = ArticleHelper.getFrontMatterByPath(articlePath);
article = await ArticleHelper.getFrontMatterByPath(articlePath);
}
if (article && article.data) {
@@ -264,7 +268,7 @@ export class CustomScript {
* @returns
*/
public static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
// Check the command to use
let command = script.nodeBin || "node";
@@ -274,7 +278,7 @@ export class CustomScript {
const scriptPath = join(wsPath, script.script);
if (!existsSync(scriptPath)) {
if (!await existsAsync(scriptPath)) {
reject(new Error(`Script not found: ${scriptPath}`));
return;
}
@@ -285,6 +289,7 @@ export class CustomScript {
exec(fullScript, (error, stdout) => {
if (error) {
reject(error.message);
return;
}
if (stdout && stdout.endsWith(`\n`)) {
+18 -6
View File
@@ -16,15 +16,24 @@ import { parseWinPath } from './parseWinPath';
export class DashboardSettings {
private static cachedSettings: ISettings | undefined = undefined;
public static async get() {
public static async get(clear: boolean = false) {
if (!this.cachedSettings || clear) {
this.cachedSettings = await this.getSettings();
}
return this.cachedSettings;
}
public static async getSettings() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = Project.isInitialized();
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
const pagination = Settings.get<boolean>(SETTING_DASHBOARD_CONTENT_PAGINATION)
const pagination = Settings.get<boolean | number>(SETTING_DASHBOARD_CONTENT_PAGINATION)
return {
const settings = {
git: {
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
actions: gitActions || false
@@ -44,7 +53,7 @@ export class DashboardSettings {
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
contentFolders: Folders.get(),
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
framework: (!isInitialized && wsFolder) ? await FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []),
date: {
format: Settings.get<string>(SETTING_DATE_FORMAT) || ""
@@ -71,7 +80,9 @@ export class DashboardSettings {
dataTypes: Settings.get<DataType[]>(SETTING_DATA_TYPES),
snippets: Settings.get<Snippets>(SETTING_CONTENT_SNIPPETS),
isBacker: await ext.getState<boolean | undefined>(CONTEXT.backer, 'global')
} as ISettings
} as ISettings;
return settings;
}
/**
@@ -113,7 +124,8 @@ export class DashboardSettings {
fileType: dataFile.fsPath.endsWith('.json') ? 'json' : 'yaml',
labelField: folder.labelField,
schema: folder.schema,
type: folder.type
type: folder.type,
singleEntry: typeof folder.singleEntry === 'boolean' ? folder.singleEntry : false,
} as DataFile)
}
}
+5 -5
View File
@@ -1,4 +1,3 @@
import { existsSync, readFileSync } from "fs";
import { Folders } from "../commands/Folders";
import { DataFile } from "../models";
import * as yaml from 'js-yaml';
@@ -7,6 +6,7 @@ import { Notifications } from "./Notifications";
import { commands } from "vscode";
import { COMMAND_NAME, SETTING_DATA_FILES } from "../constants";
import { Settings } from "./SettingsHelper";
import { existsAsync, readFileAsync } from "../utils";
export class DataFileHelper {
@@ -16,10 +16,10 @@ export class DataFileHelper {
* @param filePath
* @returns
*/
public static get(filePath: string) {
public static async get(filePath: string) {
const absPath = Folders.getAbsFilePath(filePath);
if (existsSync(absPath)) {
return readFileSync(absPath, 'utf8');
if (await existsAsync(absPath)) {
return await readFileAsync(absPath, 'utf8');
}
return null;
@@ -53,7 +53,7 @@ export class DataFileHelper {
public static async process(data: DataFile) {
try {
const { file, fileType } = data;
const dataFile = DataFileHelper.get(file);
const dataFile = await DataFileHelper.get(file);
if (fileType === "yaml") {
return yaml.safeLoad(dataFile || "");
+2 -2
View File
@@ -154,7 +154,7 @@ export class Extension {
// Create team settings
if (Settings.hasSettings()) {
Settings.createTeamSettings();
await Settings.createTeamSettings();
}
const hideDateDeprecation = await Extension.getInstance().getState<boolean>(ExtensionState.Updates.v7_0_0.dateFields, "workspace");
@@ -232,7 +232,7 @@ export class Extension {
ignoreFocusOut: true
});
Settings.update(SETTING_TEMPLATES_ENABLED, answer?.toLocaleLowerCase() === "yes", true);
await Settings.update(SETTING_TEMPLATES_ENABLED, answer?.toLocaleLowerCase() === "yes", true);
}
}
}
+84 -15
View File
@@ -1,13 +1,16 @@
import { parseWinPath } from './parseWinPath';
import * as jsoncParser from 'jsonc-parser';
import { existsSync, readFileSync } from "fs";
import jsyaml = require("js-yaml");
import { join, resolve } from "path";
import { commands, Uri } from "vscode";
import { Folders } from "../commands/Folders";
import { COMMAND_NAME } from "../constants";
import { COMMAND_NAME, SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID, STATIC_FOLDER_PLACEHOLDER } from "../constants";
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
import { Framework } from "../models";
import { Logger } from "./Logger";
import { existsAsync, readFileAsync } from '../utils';
import { Settings } from '.';
import { parse } from 'path';
export class FrameworkDetector {
@@ -19,7 +22,7 @@ export class FrameworkDetector {
return FrameworkDetectors.map((detector: any) => detector.framework);
}
private static check(folder: string) {
private static async check(folder: string) {
let dependencies = null;
let devDependencies = null;
let gemContent = null;
@@ -27,8 +30,8 @@ export class FrameworkDetector {
// Try fetching the package JSON file
try {
const pkgFile = join(folder, 'package.json');
if (existsSync(pkgFile)) {
let packageJson: any = readFileSync(pkgFile, "utf8");
if (await existsAsync(pkgFile)) {
let packageJson: any = await readFileAsync(pkgFile, "utf8");
if (packageJson) {
packageJson = typeof packageJson === "string" ? jsoncParser.parse(packageJson) : packageJson;
@@ -43,8 +46,8 @@ export class FrameworkDetector {
// Try fetching the Gemfile
try {
const gemFile = join(folder, 'Gemfile');
if (existsSync(gemFile)) {
gemContent = readFileSync(gemFile, "utf8");
if (await existsAsync(gemFile)) {
gemContent = await readFileAsync(gemFile, "utf8");
}
} catch (e) {
// do nothing
@@ -70,7 +73,7 @@ export class FrameworkDetector {
// Verify by files
for (const filename of detector.requiredFiles ?? []) {
const fileExists = existsSync(resolve(folder, filename));
const fileExists = await existsAsync(resolve(folder, filename));
if (fileExists) {
return detector.framework;
}
@@ -81,21 +84,87 @@ export class FrameworkDetector {
return undefined;
}
public static checkDefaultSettings(framework: Framework) {
public static async checkDefaultSettings(framework: Framework) {
if (framework.name.toLowerCase() === "jekyll") {
FrameworkDetector.jekyll();
await FrameworkDetector.jekyll();
} else if (framework.name.toLowerCase() === "hexo") {
await FrameworkDetector.hexo();
}
}
/**
* Check if there are any changes for the current framework that need to be applied
* @param relAssetPath
* @param filePath
*/
public static relAssetPathUpdate(relAssetPath: string, filePath: string): string {
const staticFolder = Folders.getStaticFolderRelativePath();
const frameworkId = Settings.get(SETTING_FRAMEWORK_ID);
private static jekyll() {
// Support for HEXO post asset folders
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
relAssetPath = relAssetPath.replace(STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder, "");
// Filename without the extension
const fileParsing = parse(filePath);
const name = fileParsing.name;
relAssetPath = relAssetPath.replace(name, "");
relAssetPath = join(relAssetPath);
// Remove remove the slash at the beginning
relAssetPath = parseWinPath(relAssetPath);
if (relAssetPath.startsWith("/")) {
relAssetPath = relAssetPath.substring(1);
}
}
// Support for HEXO image folder
else if (frameworkId === "hexo") {
relAssetPath = parseWinPath(relAssetPath);
if (relAssetPath.startsWith("/")) {
relAssetPath = relAssetPath.substring(1);
}
}
return parseWinPath(relAssetPath);
}
/**
* Define the default settings for Hexo
*/
private static async hexo() {
try {
const wsFolder = Folders.getWorkspaceFolder();
const hexoConfig = join(wsFolder?.fsPath || "", '_config.yml');
let assetFoler = "source/images";
if (await existsAsync(hexoConfig)) {
const content = await readFileAsync(hexoConfig, "utf8");
// Convert YAML to JSON
const config = jsyaml.safeLoad(content);
// Check if post assets are used: https://hexo.io/docs/asset-folders.html#Post-Asset-Folder
if (config.post_asset_folder) {
assetFoler = STATIC_FOLDER_PLACEHOLDER.hexo.placeholder;
}
}
await Settings.update(SETTING_CONTENT_STATIC_FOLDER, assetFoler, true);
} catch (e) {
Logger.error(`Something failed while processing your Hexo configuration. ${(e as Error).message}`);
}
}
/**
* Define the default settings for Jekyll
*/
private static async jekyll() {
try {
const wsFolder = Folders.getWorkspaceFolder();
const jekyllConfig = join(wsFolder?.fsPath || "", '_config.yml');
let collectionDir = "";
if (existsSync(jekyllConfig)) {
const content = readFileSync(jekyllConfig, "utf8");
if (await existsAsync(jekyllConfig)) {
const content = await readFileAsync(jekyllConfig, "utf8");
// Convert YAML to JSON
const config = jsyaml.safeLoad(content);
@@ -107,7 +176,7 @@ export class FrameworkDetector {
const draftsPath = join(wsFolder?.fsPath || "", collectionDir, "_drafts");
const postsPath = join(wsFolder?.fsPath || "", collectionDir, "_posts");
if (existsSync(draftsPath)) {
if (await existsAsync(draftsPath)) {
const folderUri = Uri.file(draftsPath);
commands.executeCommand(COMMAND_NAME.registerFolder, {
title: "drafts",
@@ -115,7 +184,7 @@ export class FrameworkDetector {
});
}
if (existsSync(postsPath)) {
if (await existsAsync(postsPath)) {
const folderUri = Uri.file(postsPath);
commands.executeCommand(COMMAND_NAME.registerFolder, {
title: "posts",
+17 -2
View File
@@ -1,6 +1,7 @@
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
import { ExplorerView } from './../explorerView/ExplorerView';
import { Uri, window } from 'vscode';
import { dirname, join } from "path";
import { dirname, extname, join } from "path";
import { Field } from '../models';
import { existsSync } from 'fs';
import { Folders } from '../commands/Folders';
@@ -49,7 +50,21 @@ export class ImageHelper {
*/
public static relToAbs(filePath: string, value: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Folders.getStaticFolderRelativePath();
let staticFolder = Folders.getStaticFolderRelativePath();
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const editor = window.activeTextEditor;
if (editor) {
const document = editor.document;
const filePath = parseWinPath(document.fileName);
const pathWithoutExtension = filePath.replace(extname(filePath), '');
const assetFilePath = join(pathWithoutExtension, value);
if (existsSync(assetFilePath)) {
return Uri.file(assetFilePath);
}
}
}
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
+55 -32
View File
@@ -1,18 +1,20 @@
import { decodeBase64, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
import { decodeBase64, Extension, FrameworkDetector, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { SortingOption } from "../dashboardWebView/models";
import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models";
import { basename, extname, join, parse, dirname, relative } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, workspace, window, Position } from "vscode";
import { basename, join, parse, dirname, relative } from "path";
import { statSync } from "fs";
import { Uri, workspace, window, Position } from "vscode";
import imageSize from "image-size";
import { EditorHelper } from "@estruyf/vscode";
import { SortOption } from "../dashboardWebView/constants/SortOption";
import { DataListener, MediaListener } from "../listeners/panel";
import { ArticleHelper } from "./ArticleHelper";
import { lookup } from "mime-types";
import { existsAsync, readdirAsync, unlinkAsync, writeFileAsync } from "../utils";
export class MediaHelpers {
@@ -48,7 +50,7 @@ export class MediaHelpers {
if (viewData?.data?.filePath && (viewData?.data?.filePath.endsWith('index.md') || viewData?.data?.filePath.endsWith('index.mdx'))) {
const folderPath = parse(viewData.data.filePath).dir;
selectedFolder = folderPath;
} else if (stateValue && existsSync(stateValue)) {
} else if (stateValue && await existsAsync(stateValue)) {
selectedFolder = stateValue;
}
}
@@ -77,11 +79,17 @@ export class MediaHelpers {
allMedia = [...media];
} else {
if (staticFolder) {
if (staticFolder && staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const folderSearch = join(staticFolder || "", '/*');
const files = await workspace.findFiles(folderSearch);
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
allMedia = [...media];
} else if (staticFolder && staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const folderSearch = join(STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder, '/*');
const files = await workspace.findFiles(folderSearch);
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
allMedia = [...media];
}
@@ -149,31 +157,40 @@ export class MediaHelpers {
let allContentFolders: string[] = [];
let allFolders: string[] = [];
let foldersFromSelection: string[] = [];
if (selectedFolder) {
if (existsSync(selectedFolder)) {
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
if (await existsAsync(selectedFolder)) {
foldersFromSelection = (await readdirAsync(selectedFolder, { withFileTypes: true })).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
}
} else {
if (pageBundleContentTypes.length > 0) {
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 => parseWinPath(join(contentPath, dir.name)));
allContentFolders = [...allContentFolders, ...subFolders];
}
}
// Retrieve all the content folders
if (pageBundleContentTypes.length > 0) {
for (const contentFolder of contentFolders) {
const contentPath = contentFolder.path;
if (contentPath && await existsAsync(contentPath)) {
const subFolders = (await readdirAsync(contentPath, { withFileTypes: true })).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
allContentFolders = [...allContentFolders, ...subFolders];
}
}
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
if (staticPath && existsSync(staticPath)) {
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
}
}
// Retrieve all the static folders
let staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
staticPath = join(parseWinPath(wsFolder?.fsPath || ""), STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder);
}
if (staticPath && await existsAsync(staticPath)) {
allFolders = (await readdirAsync(staticPath, { withFileTypes: true })).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
}
// Store the last opened folder
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
let sortedFolders = [...allContentFolders, ...allFolders];
let sortedFolders = selectedFolder ? foldersFromSelection : [...allContentFolders, ...allFolders];
sortedFolders = sortedFolders.sort((a, b) => {
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
@@ -192,7 +209,9 @@ export class MediaHelpers {
media: files,
total: total,
folders: sortedFolders,
selectedFolder
selectedFolder,
allContentFolders,
allStaticfolders: allFolders,
} as MediaPaths
}
@@ -218,11 +237,11 @@ export class MediaHelpers {
absFolderPath = folder;
}
if (!existsSync(absFolderPath)) {
if (!(await existsAsync(absFolderPath))) {
absFolderPath = join(wsPath, folder || "");
}
if (!existsSync(absFolderPath)) {
if (!(await existsAsync(absFolderPath))) {
Notifications.error(`We couldn't find your selected folder.`);
return;
}
@@ -231,7 +250,7 @@ export class MediaHelpers {
const imgData = decodeBase64(contents);
if (imgData) {
writeFileSync(staticPath, imgData.data);
await writeFileAsync(staticPath, imgData.data);
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
return true;
@@ -255,7 +274,7 @@ export class MediaHelpers {
}
try {
unlinkSync(file);
await unlinkAsync(file);
MediaHelpers.media = [];
return true;
@@ -275,6 +294,10 @@ export class MediaHelpers {
Dashboard.resetViewData();
const editor = window.activeTextEditor;
if (!editor) {
return;
}
const wsFolder = Folders.getWorkspaceFolder();
const filePath = data.file;
let relPath = data.relPath;
@@ -303,7 +326,7 @@ export class MediaHelpers {
// Snippets are already parsed, so update the URL of the image
if (data.snippet) {
data.snippet = data.snippet.replace(data.relPath, relPath);
data.snippet = data.snippet.replace(data.relPath, FrameworkDetector.relAssetPathUpdate(relPath, editor.document.fileName));
}
}
}
@@ -324,7 +347,7 @@ export class MediaHelpers {
const caption = isFile ? `${data.title || ""}` : `${data.alt || data.caption || ""}`;
const snippet = data.snippet || `${isFile ? "" : "!"}[${caption}](${relPath.replace(/ /g, "%20")})`;
const snippet = data.snippet || `${isFile ? "" : "!"}[${caption}](${FrameworkDetector.relAssetPathUpdate(relPath, editor.document.fileName).replace(/ /g, "%20")})`;
if (selection !== undefined) {
builder.replace(selection, snippet);
} else {
@@ -338,7 +361,7 @@ export class MediaHelpers {
DataListener.updateMetadata({
field: data.fieldName,
value: relPath,
value: FrameworkDetector.relAssetPathUpdate(relPath, editor.document.fileName),
parents: data.parents,
blockData: data.blockData
});
@@ -350,14 +373,14 @@ export class MediaHelpers {
* Update the metadata of a media file
* @param data
*/
public static updateMetadata(data: any) {
public static async updateMetadata(data: any) {
const { file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; } = data;
const mediaLib = MediaLibrary.getInstance();
mediaLib.set(file, metadata);
// Check if filename needs to be updated
mediaLib.updateFilename(file, filename);
await mediaLib.updateFilename(file, filename);
}
/**
+4 -4
View File
@@ -3,10 +3,10 @@ import { workspace } from 'vscode';
import { JsonDB } from 'node-json-db/dist/JsonDB';
import { basename, dirname, join, parse } from 'path';
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders';
import { existsSync, renameSync } from 'fs';
import { Notifications } from './Notifications';
import { parseWinPath } from './parseWinPath';
import { LocalStore } from '../constants';
import { existsAsync, renameAsync } from '../utils';
interface MediaRecord {
description: string;
@@ -75,7 +75,7 @@ export class MediaLibrary {
}
}
public updateFilename(filePath: string, filename: string) {
public async updateFilename(filePath: string, filename: string) {
const name = basename(filePath);
if (name !== filename && filename) {
@@ -84,10 +84,10 @@ export class MediaLibrary {
const newFileInfo = parse(filename);
const newPath = join(dirname(filePath), `${newFileInfo.name}${oldFileInfo.ext}`);
if (existsSync(newPath)) {
if (await existsAsync(newPath)) {
Notifications.warning(`The name "${filename}" already exists at the file location.`);
} else {
renameSync(filePath, newPath);
await renameAsync(filePath, newPath);
this.rename(filePath, newPath);
MediaHelpers.resetMedia();
}
+24 -4
View File
@@ -8,16 +8,16 @@ import { SlugHelper } from "./SlugHelper";
* @param title
* @returns
*/
export const processKnownPlaceholders = (value: string, title: string, dateFormat: string) => {
export const processKnownPlaceholders = (value: string, title: string | undefined, dateFormat: string) => {
if (value && typeof value === "string") {
if (value.includes("{{title}}")) {
const regex = new RegExp("{{title}}", "g");
value = value.replace(regex, title);
value = value.replace(regex, title || "");
}
if (value.includes("{{slug}}")) {
const regex = new RegExp("{{slug}}", "g");
value = value.replace(regex, SlugHelper.createSlug(title) || "");
value = value.replace(regex, SlugHelper.createSlug(title || "") || "");
}
if (value.includes("{{now}}")) {
@@ -26,7 +26,7 @@ export const processKnownPlaceholders = (value: string, title: string, dateForma
if (dateFormat && typeof dateFormat === "string") {
value = value.replace(regex, format(new Date(), DateHelper.formatUpdate(dateFormat) as string));
} else {
return (new Date()).toISOString();
value = value.replace(regex, (new Date()).toISOString());
}
}
@@ -44,6 +44,26 @@ export const processKnownPlaceholders = (value: string, title: string, dateForma
const regex = new RegExp("{{day}}", "g");
value = value.replace(regex, format(new Date(), "dd"));
}
if (value.includes("{{hour12}}")) {
const regex = new RegExp("{{hour12}}", "g");
value = value.replace(regex, format(new Date(), "hh"));
}
if (value.includes("{{hour24}}")) {
const regex = new RegExp("{{hour24}}", "g");
value = value.replace(regex, format(new Date(), "HH"));
}
if (value.includes("{{ampm}}")) {
const regex = new RegExp("{{ampm}}", "g");
value = value.replace(regex, format(new Date(), "aaa"));
}
if (value.includes("{{minute}}")) {
const regex = new RegExp("{{minute}}", "g");
value = value.replace(regex, format(new Date(), "mm"));
}
}
return value;
+9 -3
View File
@@ -46,7 +46,7 @@ export class Questions {
* @returns
*/
public static async SelectContentFolder(showWarning: boolean = true): Promise<string | undefined> {
const folders = Folders.get();
let folders = Folders.get();
let selectedFolder: string | undefined;
if (folders.length > 1) {
@@ -72,16 +72,22 @@ export class Questions {
/**
* Select the content type to create new content
* @param allowedCts Allowed content types for the folder
* @param showWarning
* @returns
*/
public static async SelectContentType(showWarning: boolean = true): Promise<string | undefined> {
const contentTypes = ContentType.getAll();
public static async SelectContentType(allowedCts: string[], showWarning: boolean = true): Promise<string | undefined> {
let contentTypes = ContentType.getAll();
if (!contentTypes || contentTypes.length === 0) {
Notifications.warning("No content types found. Please create a content type first.");
return;
}
// Only allow content types that are allowed for the folder
if (allowedCts && allowedCts.length > 0) {
contentTypes = contentTypes.filter(ct => allowedCts.find(allowedCt => allowedCt === ct.name));
}
if (contentTypes.length === 1) {
return contentTypes[0].name;
}
+184 -28
View File
@@ -3,26 +3,29 @@ import { Telemetry } from './Telemetry';
import { Notifications } from './Notifications';
import { commands, Uri, workspace, window } from 'vscode';
import * as vscode from 'vscode';
import { ContentType, CustomTaxonomy, TaxonomyType } from '../models';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent, COMMAND_NAME } from '../constants';
import { ContentFolder, ContentType, CustomPlaceholder, CustomTaxonomy, DataFile, DataFolder, DataType, TaxonomyType } from '../models';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent, COMMAND_NAME, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_SNIPPETS, SETTING_CONTENT_PLACEHOLDERS, SETTING_CUSTOM_SCRIPTS, SETTING_DATA_FILES, SETTING_DATA_TYPES, SETTING_DATA_FOLDERS } from '../constants';
import { Folders } from '../commands/Folders';
import { join, basename } from 'path';
import { existsSync, readFileSync, watch, writeFileSync } from 'fs';
import { join, basename, dirname, parse } from 'path';
import { existsSync } from 'fs';
import { Extension } from './Extension';
import { debounceCallback } from './DebounceCallback';
import { Logger } from './Logger';
import * as jsoncParser from 'jsonc-parser';
import { existsAsync, readFileAsync, writeFileAsync } from '../utils';
export class Settings {
public static globalFile = "frontmatter.json";
public static globalConfigFolder = ".frontmatter/config";
public static globalConfig: any;
private static config: vscode.WorkspaceConfiguration;
private static globalConfig: any;
private static isInitialized: boolean = false;
private static listeners: any[] = [];
private static fileCreationWatcher: vscode.FileSystemWatcher | undefined;
private static readConfigPromise: Promise<void> | undefined = undefined;
public static init() {
Settings.readConfig();
public static async init() {
await Settings.readConfig();
Settings.listeners = [];
@@ -34,8 +37,7 @@ export class Settings {
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
Settings.onConfigChange((global?: any) => {
Settings.readConfig();
Settings.onConfigChange(async () => {
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
});
}
@@ -97,19 +99,26 @@ export class Settings {
if (Settings.checkProjectConfig(filename)) {
Logger.info(`Config change detected - ${projectConfig} saved`);
const file = await workspace.openTextDocument(e.uri);
if (file) {
const fileContents = file.getText();
const json = jsoncParser.parse(fileContents);
configDebouncer(() => callback(json), 200);
// callback(json)
Logger.info(`Reloading config...`);
if (Settings.readConfigPromise === undefined) {
Settings.readConfigPromise = Settings.readConfig();
}
await Settings.readConfigPromise;
Logger.info(`Reloaded config...`);
configDebouncer(() => callback(), 200);
}
});
workspace.onDidDeleteFiles((e) => {
workspace.onDidDeleteFiles(async (e) => {
const needCallback = e?.files.find(f => Settings.checkProjectConfig(f.fsPath));
if (needCallback) {
Logger.info(`Reloading config...`);
if (Settings.readConfigPromise === undefined) {
Settings.readConfigPromise = Settings.readConfig();
}
await Settings.readConfigPromise;
callback();
}
});
@@ -173,17 +182,22 @@ export class Settings {
const fmConfig = Settings.projectConfigPath;
if (updateGlobal) {
if (fmConfig && existsSync(fmConfig)) {
const localConfig = readFileSync(fmConfig, 'utf8');
if (fmConfig && await existsAsync(fmConfig)) {
const localConfig = await readFileAsync(fmConfig, 'utf8');
Settings.globalConfig = jsoncParser.parse(localConfig);
Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value;
writeFileSync(fmConfig, JSON.stringify(Settings.globalConfig, null, 2), 'utf8');
const content = JSON.stringify(Settings.globalConfig, null, 2);
await writeFileAsync(fmConfig, content, 'utf8');
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
if (workspaceSettingValue) {
await Settings.update(name, undefined);
}
// Make sure to reload the whole config + all the data files
await Settings.readConfig();
return;
}
} else {
@@ -207,24 +221,24 @@ export class Settings {
/**
* Create team settings
*/
public static createTeamSettings() {
public static async createTeamSettings() {
const wsFolder = Folders.getWorkspaceFolder();
this.createGlobalFile(wsFolder);
await this.createGlobalFile(wsFolder);
}
/**
* Create the frontmatter.json file
* @param wsFolder
*/
public static createGlobalFile(wsFolder: Uri | undefined | null) {
public static async createGlobalFile(wsFolder: Uri | undefined | null) {
const initialConfig = {
"$schema": `https://${Extension.getInstance().isBetaVersion() ? `beta.` : ``}frontmatter.codes/frontmatter.schema.json`
};
if (wsFolder) {
const configPath = join(wsFolder.fsPath, Settings.globalFile);
if (!existsSync(configPath)) {
writeFileSync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
if (!(await existsAsync(configPath))) {
await writeFileAsync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
}
}
}
@@ -391,7 +405,11 @@ export class Settings {
*/
private static checkProjectConfig(filePath: string) {
const fmConfig = Settings.projectConfigPath;
if (fmConfig && existsSync(fmConfig)) {
filePath = parseWinPath(filePath);
if (filePath.includes(Settings.globalConfigFolder)) {
return true;
} else if (fmConfig && existsSync(fmConfig)) {
return filePath &&
basename(filePath).toLowerCase() === Settings.globalFile.toLowerCase() &&
fmConfig.toLowerCase() === filePath.toLowerCase();
@@ -403,21 +421,159 @@ export class Settings {
/**
* Read the global config file
*/
private static readConfig() {
private static async readConfig() {
try {
const fmConfig = Settings.projectConfigPath;
if (fmConfig && existsSync(fmConfig)) {
const localConfig = readFileSync(fmConfig, 'utf8');
if (fmConfig && await existsAsync(fmConfig)) {
const localConfig = await readFileAsync(fmConfig, 'utf8');
Settings.globalConfig = jsoncParser.parse(localConfig);
commands.executeCommand('setContext', CONTEXT.isEnabled, true);
} else {
Settings.globalConfig = undefined;
}
// Read the files from the config folder
let configFiles = await workspace.findFiles(`**/${Settings.globalConfigFolder}/**/*.json`);
if (configFiles.length === 0) {
Logger.info(`No ".frontmatter/config" config files found.`);
}
// Sort the files by fsPath
configFiles = configFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath));
for await (const configFile of configFiles) {
await Settings.processConfigFile(configFile);
}
} catch (e) {
Settings.globalConfig = undefined;
Notifications.error(`Error reading "frontmatter.json" config file. Check [output window](command:${COMMAND_NAME.showOutputChannel}) for more details.`);
Logger.error((e as Error).message);
}
Settings.readConfigPromise = undefined;
}
/**
* Process the config file
* @param configFile
* @returns
*/
private static async processConfigFile(configFile: Uri) {
try {
const config = await workspace.fs.readFile(configFile);
const configJson = jsoncParser.parse(config.toString());
const filePath = parseWinPath(configFile.fsPath);
const configFilePath = filePath.split(Settings.globalConfigFolder).pop();
if (!configFilePath) {
return;
}
Logger.info(`Processing "${configFilePath}" config file.`);
// Get the path without the filename
const configFolder = parseWinPath(dirname(configFilePath));
let relSettingName = configFolder.split('/').join('.');
if (relSettingName.startsWith('.')) {
relSettingName = relSettingName.substring(1);
}
relSettingName = relSettingName.toLowerCase();
if (!Settings.globalConfig) {
Settings.globalConfig = {};
}
// Array settings
if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CUSTOM_SCRIPTS)) {
const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] || [];
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] = [...crntValue, configJson];
}
// Content types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CONTENT_TYPES, "name", configJson);
}
// Data files
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FILES)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FILES, "id", configJson);
}
// Data folders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FOLDERS, "id", configJson);
}
// Data types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_TYPES)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_TYPES, "id", configJson);
}
// Page folders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PAGE_FOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PAGE_FOLDERS, "path", configJson);
}
// Placeholders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PLACEHOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PLACEHOLDERS, "id", configJson);
}
// Object settings
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SNIPPETS)) {
Settings.updateGlobalConfigObjectByNameSetting(SETTING_CONTENT_SNIPPETS, configFilePath, configJson, filePath);
}
} catch (e) {
Logger.error(`Error reading config file: ${configFile.fsPath}`);
Logger.error((e as Error).message);
}
}
/**
* Check if the setting name is equal or starts with the reference setting name
* @param value
* @param startsWith
* @returns
*/
private static isEqualOrStartsWith(value: string, startsWith: string) {
value = value.toLowerCase();
startsWith = startsWith.toLowerCase();
return value === startsWith || value.startsWith(`${startsWith}.`);
}
/**
* Update an array setting in the global config
* @param settingName
* @param fieldName
* @param configJson
*/
private static updateGlobalConfigArraySetting<T>(settingName: string, fieldName: string, configJson: any): void {
const crntValue: T[] = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || [];
// Check if folder is already added
const itemIdx = crntValue.findIndex((item: any) => item[fieldName] === configJson[fieldName]);
if (itemIdx === -1) {
crntValue.push(configJson);
}
Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] = [...crntValue];
}
/**
* Update an object by the file name in the global config
* @param settingName
* @param fileNamepath
* @param configJson
*/
private static updateGlobalConfigObjectByNameSetting<T>(settingName: string, fileNamepath: string, configJson: any, absPath: string): void {
const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || {};
// Filename is the key
const fileName = parse(fileNamepath).name;
configJson = {
...configJson,
sourcePath: absPath
};
if (!crntValue[fileName]) {
crntValue[fileName] = configJson;
Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] = { ...crntValue, ...{ [fileName]: configJson } };
}
}
/**
+5 -5
View File
@@ -4,13 +4,13 @@ import { CustomTaxonomy, TaxonomyType, ContentType as IContentType } from "../mo
import { FilesHelper } from "./FilesHelper";
import { ProgressLocation, window } from "vscode";
import { parseWinPath } from "./parseWinPath";
import { readFileSync, writeFileSync } from "fs";
import { FrontMatterParser } from "../parsers";
import { DumpOptions } from "js-yaml";
import { Settings } from "./SettingsHelper";
import { Notifications } from "./Notifications";
import { ArticleHelper } from './ArticleHelper';
import { ContentType } from './ContentType';
import { readFileAsync, writeFileAsync } from '../utils';
export class TaxonomyHelper {
@@ -186,7 +186,7 @@ export class TaxonomyHelper {
for (const file of allFiles) {
progress.report({ increment: (++i/progressNr) });
const mdFile = readFileSync(parseWinPath(file.fsPath), { encoding: "utf8" });
const mdFile = await readFileAsync(parseWinPath(file.fsPath), { encoding: "utf8" });
if (mdFile) {
try {
@@ -217,7 +217,7 @@ export class TaxonomyHelper {
const spaces = window.activeTextEditor?.options?.tabSize;
// Update the file
writeFileSync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
await writeFileAsync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any), { encoding: "utf8" });
}
@@ -291,7 +291,7 @@ export class TaxonomyHelper {
for (const file of allFiles) {
progress.report({ increment: (++i/progressNr) });
const mdFile = readFileSync(parseWinPath(file.fsPath), { encoding: "utf8" });
const mdFile = await readFileAsync(parseWinPath(file.fsPath), { encoding: "utf8" });
if (mdFile) {
try {
@@ -324,7 +324,7 @@ export class TaxonomyHelper {
const spaces = window.activeTextEditor?.options?.tabSize;
// Update the file
writeFileSync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
await writeFileAsync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any), { encoding: "utf8" });
}
+10 -9
View File
@@ -4,10 +4,11 @@ import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { BaseListener } from "./BaseListener";
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
import { Folders } from '../../commands/Folders';
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';
import { dirname } from 'path';
import * as yaml from 'js-yaml';
import { DataFileHelper } from '../../helpers';
import { existsAsync, readFileAsync, writeFileAsync } from '../../utils';
import { mkdirAsync } from '../../utils/mkdirAsync';
export class DataListener extends BaseListener {
@@ -35,28 +36,28 @@ export class DataListener extends BaseListener {
* Process the data update
* @param msgData
*/
private static processDataUpdate(msgData: any) {
const { file, fileType, entries } = msgData as { file: string, fileType: string, entries: any[] };
private static async processDataUpdate(msgData: any) {
const { file, fileType, entries } = msgData as { file: string, fileType: string, entries: unknown | unknown[] };
const absPath = Folders.getAbsFilePath(file);
if (!existsSync(absPath)) {
if (!await existsAsync(absPath)) {
const dirPath = dirname(absPath);
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
if (!await existsAsync(dirPath)) {
await mkdirAsync(dirPath, { recursive: true });
}
}
const fileContent = readFileSync(absPath, 'utf8');
const fileContent = await readFileAsync(absPath, 'utf8');
// check if file content ends with newline
const newFileContent = fileContent.endsWith('\n');
const insertFinalNewLine = newFileContent || workspace.getConfiguration().get('files.insertFinalNewline');
if (fileType === 'yaml') {
const yamlData = yaml.safeDump(entries);
writeFileSync(absPath, insertFinalNewLine ? `${yamlData}\n` : yamlData, 'utf8');
await writeFileAsync(absPath, insertFinalNewLine ? `${yamlData}\n` : yamlData, 'utf8');
} else {
const jsonData = JSON.stringify(entries, null, 2);
writeFileSync(absPath, insertFinalNewLine ? `${jsonData}\n` : jsonData, 'utf8');
await writeFileAsync(absPath, insertFinalNewLine ? `${jsonData}\n` : jsonData, 'utf8');
}
this.processDataFile(msgData);
+21
View File
@@ -0,0 +1,21 @@
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Logger } from "../../helpers";
import { BaseListener } from "./BaseListener";
export class LogListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.logError:
Logger.error(msg.data);
break;
}
}
}
+8 -2
View File
@@ -7,6 +7,7 @@ import { SortingOption } from '../../dashboardWebView/models';
import { commands, env, Uri } from 'vscode';
import { COMMAND_NAME, TelemetryEvent } from '../../constants';
import * as os from 'os';
import { Folders } from '../../commands';
export class MediaListener extends BaseListener {
@@ -51,6 +52,11 @@ export class MediaListener extends BaseListener {
case DashboardMessage.createMediaFolder:
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
break;
case DashboardMessage.createHexoAssetFolder:
if (msg?.data.hexoAssetFolderPath) {
Folders.createFolder(msg?.data.hexoAssetFolderPath);
}
break;
}
}
@@ -113,11 +119,11 @@ export class MediaListener extends BaseListener {
* Update media metadata
* @param data
*/
private static update(data: any) {
private static async update(data: any) {
try {
const { page, folder } = data;
MediaHelpers.updateMetadata(data);
await MediaHelpers.updateMetadata(data);
this.sendMediaFiles(page || 0, folder || "");
} catch {}
+16 -177
View File
@@ -1,21 +1,17 @@
import { DEFAULT_CONTENT_TYPE_NAME } from './../../constants/ContentType';
import { isValidFile } from '../../helpers/isValidFile';
import { existsSync, unlinkSync } from "fs";
import { basename, dirname, join } from "path";
import { basename } from "path";
import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode";
import { Dashboard } from "../../commands/Dashboard";
import { Folders } from "../../commands/Folders";
import { COMMAND_NAME, DefaultFields, ExtensionState, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants";
import { COMMAND_NAME, ExtensionState } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Page } from "../../dashboardWebView/models";
import { ArticleHelper, Extension, Logger, Settings } from "../../helpers";
import { ContentType } from "../../helpers/ContentType";
import { DateHelper } from "../../helpers/DateHelper";
import { Notifications } from "../../helpers/Notifications";
import { ArticleHelper, Extension, Logger } from "../../helpers";
import { BaseListener } from "./BaseListener";
import { DataListener } from '../panel';
import Fuse from 'fuse.js';
import { PagesParser } from '../../services/PagesParser';
import { unlinkAsync } from "../../utils";
export class PagesListener extends BaseListener {
@@ -110,7 +106,7 @@ export class PagesListener extends BaseListener {
Logger.info(`Deleting file: ${path}`)
unlinkSync(path);
await unlinkAsync(path);
this.lastPages = this.lastPages.filter(p => p.fmFilePath !== path);
this.sendPageData(this.lastPages);
@@ -132,7 +128,7 @@ export class PagesListener extends BaseListener {
if (pageIdx !== -1) {
const stats = await workspace.fs.stat(file);
const crntPage = this.lastPages[pageIdx];
const updatedPage = this.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder);
const updatedPage = await PagesParser.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder);
if (updatedPage) {
this.lastPages[pageIdx] = updatedPage;
this.sendPageData(this.lastPages);
@@ -156,43 +152,18 @@ export class PagesListener extends BaseListener {
if (cachedPages) {
this.sendPageData(cachedPages);
}
} else {
PagesParser.reset();
}
// Update the dashboard with the fresh data
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
PagesParser.getPages(async (pages: Page[]) => {
this.lastPages = pages;
this.sendPageData(pages);
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (isValidFile(file.fileName)) {
try {
const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title);
if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) {
pages.push(page);
}
} catch (error: any) {
if ((error as Error)?.message.toLowerCase() === "webview is disposed") {
continue;
}
Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`);
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
}
}
this.lastPages = pages;
this.sendPageData(pages);
this.sendMsg(DashboardCommand.searchReady, true);
await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace");
await this.createSearchIndex(pages);
this.sendMsg(DashboardCommand.searchReady, true);
await this.createSearchIndex(pages);
});
}
/**
@@ -245,136 +216,4 @@ export class PagesListener extends BaseListener {
public static refresh() {
this.getPagesData(true);
}
/**
* Process the page content
* @param filePath
* @param fileMtime
* @param fileName
* @param folderTitle
* @returns
*/
private static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined {
const article = ArticleHelper.getFrontMatterByPath(filePath);
if (article?.data.title) {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined;
const modifiedField = ArticleHelper.getModifiedDateField(article) || null;
const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined;
const staticFolder = Folders.getStaticFolderRelativePath();
const page: Page = {
...article.data,
// FrontMatter properties
fmFolder: folderTitle,
fmFilePath: filePath,
fmFileName: fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime,
fmPublished: dateFieldValue ? dateFieldValue.getTime() : null,
fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null,
fmPreviewImage: "",
fmTags: [],
fmCategories: [],
fmContentType: DEFAULT_CONTENT_TYPE_NAME,
fmBody: article?.content || "",
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
date: article?.data[dateField] || "",
draft: article?.data.draft,
description: article?.data[descriptionField] || "",
};
const contentType = ArticleHelper.getContentType(article.data);
if (contentType) {
page.fmContentType = contentType.name;
}
let previewFieldParents = ContentType.findPreviewField(contentType.fields);
if (previewFieldParents.length === 0) {
const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview");
if (previewField) {
previewFieldParents = ["preview"];
}
}
let tagParents = ContentType.findFieldByType(contentType.fields, "tags");
const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]);
page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : tagsValue;
let categoryParents = ContentType.findFieldByType(contentType.fields, "categories");
const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]);
page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : categoriesValue;
// Check if parent fields were retrieved, if not there was no image present
if (previewFieldParents.length > 0) {
let fieldValue = null;
let crntPageData = article?.data;
for (let i = 0; i < previewFieldParents.length; i++) {
const previewField = previewFieldParents[i];
if (i === previewFieldParents.length - 1) {
fieldValue = crntPageData[previewField];
} else {
if (!crntPageData[previewField]) {
continue;
}
crntPageData = crntPageData[previewField];
// Check for preview image in block data
if (crntPageData instanceof Array && crntPageData.length > 0) {
// Get the first field block that contains the next field data
const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]);
if (fieldData) {
crntPageData = fieldData;
} else {
continue;
}
}
}
}
if (fieldValue && wsFolder) {
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
// Revalidate as the array could have been empty
if (fieldValue) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
const contentFolderPath = join(dirname(filePath), fieldValue);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
const preview = Dashboard.getWebview()?.asWebviewUri(previewUri);
page["fmPreviewImage"] = preview?.toString() || "";
}
}
}
}
return page;
}
return;
}
}
+11 -9
View File
@@ -42,15 +42,15 @@ export class SettingsListener extends BaseListener {
private static async update(data: { name: string, value: any }) {
if (data.name) {
await Settings.update(data.name, data.value);
this.getSettings();
this.getSettings(true);
}
}
/**
* Retrieve the settings for the dashboard
*/
public static async getSettings() {
const settings = await DashboardSettings.get();
public static async getSettings(clear: boolean = false) {
const settings = await DashboardSettings.get(clear);
this.sendMsg(DashboardCommand.settings, settings);
}
@@ -59,22 +59,24 @@ export class SettingsListener extends BaseListener {
* Set the current site-generator or framework + related settings
* @param frameworkId
*/
public static setFramework(frameworkId: string | null) {
Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
public static async setFramework(frameworkId: string | null) {
await Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
if (frameworkId) {
const allFrameworks = FrameworkDetector.getAll();
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
if (framework) {
Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
if (framework.static) {
await Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
}
FrameworkDetector.checkDefaultSettings(framework);
await FrameworkDetector.checkDefaultSettings(framework);
} else {
Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
await Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
}
}
SettingsListener.getSettings();
SettingsListener.getSettings(true);
}
private static addFolder(folder: string) {
+12 -3
View File
@@ -4,6 +4,7 @@ import { Dashboard } from "../../commands/Dashboard";
import { SETTING_CONTENT_SNIPPETS, TelemetryEvent } from "../../constants";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Notifications, Settings, Telemetry } from "../../helpers";
import { Snippets } from "../../models";
import { BaseListener } from "./BaseListener";
import { SettingsListener } from "./SettingsListener";
@@ -57,7 +58,7 @@ export class SnippetListener extends BaseListener {
snippets[title] = snippetContent;
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
SettingsListener.getSettings();
SettingsListener.getSettings(true);
}
private static async updateSnippet(data: any) {
@@ -68,8 +69,16 @@ export class SnippetListener extends BaseListener {
return;
}
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
SettingsListener.getSettings();
// Filter out external data snippets
const snippetsToStore = Object.keys(snippets).reduce((acc, key) => {
if (!snippets[key].sourcePath) {
acc[key] = snippets[key];
}
return acc;
}, {} as Snippets);
await Settings.update(SETTING_CONTENT_SNIPPETS, snippetsToStore, true);
SettingsListener.getSettings(true);
}
private static async insertSnippet(data: any) {
+1
View File
@@ -8,3 +8,4 @@ export * from './SettingsListener';
export * from './SnippetListener';
export * from './TelemetryListener';
export * from './TaxonomyListener';
export * from './LogListener';
+13 -4
View File
@@ -1,10 +1,10 @@
import { Settings } from './../../helpers/SettingsHelper';
import { Dashboard } from '../../commands/Dashboard';
import { ExplorerView } from '../../explorerView/ExplorerView';
import { Extension, Logger, Telemetry } from '../../helpers';
import { ArticleHelper, Extension, Logger, processKnownPlaceholders, Telemetry } from '../../helpers';
import { GeneralCommands } from './../../constants/GeneralCommands';
import simpleGit, { SimpleGit } from 'simple-git';
import { COMMAND_NAME, CONTEXT, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, TelemetryEvent } from '../../constants';
import { COMMAND_NAME, CONTEXT, SETTING_DATE_FORMAT, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, TelemetryEvent } from '../../constants';
import { Folders } from '../../commands/Folders';
import { commands } from 'vscode';
@@ -88,7 +88,13 @@ export class GitListener {
}
private static async push() {
const commitMsg = Settings.get<string>(SETTING_GIT_COMMIT_MSG);
let commitMsg = Settings.get<string>(SETTING_GIT_COMMIT_MSG);
if (commitMsg) {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
commitMsg = processKnownPlaceholders(commitMsg, undefined, dateFormat);
commitMsg = await ArticleHelper.processCustomPlaceholders(commitMsg, undefined, undefined);
}
const git = this.getClient();
if (!git) {
@@ -105,6 +111,9 @@ export class GitListener {
for (const file of status.modified) {
await git.add(file);
}
for (const file of status.deleted) {
await git.add(file);
}
await git.commit(commitMsg || "Synced by Front Matter")
@@ -137,4 +146,4 @@ export class GitListener {
Dashboard.postWebviewMessage({ command: command as any, data });
}
}
}
+2
View File
@@ -4,4 +4,6 @@ export interface ContentFolder {
excludeSubdir?: boolean;
previewPath?: string;
filePrefix?: string;
contentTypes?: string[];
}
+1
View File
@@ -6,4 +6,5 @@ export interface DataFile {
labelField: string;
schema?: any;
type?: string;
singleEntry?: boolean;
}
+1
View File
@@ -6,4 +6,5 @@ export interface DataFolder {
labelField: string;
schema?: any;
type?: string;
singleEntry?: boolean;
}
+2
View File
@@ -4,6 +4,8 @@ export interface MediaPaths {
media: MediaInfo[];
total: number;
folders: string[];
allContentFolders: string[];
allStaticfolders: string[];
selectedFolder: string;
}
+23
View File
@@ -82,6 +82,29 @@ export interface Field {
dataFileId?: string;
dataFileKey?: string;
dataFileValue?: string;
// When clause
when?: WhenClause;
}
export enum WhenOperator {
equals = "eq",
notEquals = "neq",
contains = "contains",
notContains = "notContains",
startsWith = "startsWith",
endsWith = "endsWith",
greaterThan = "gt",
greaterThanOrEqual = "gte",
lessThan = "lt",
lessThanOrEqual = "lte",
}
export interface WhenClause {
fieldRef: string;
operator: WhenOperator;
value: any;
caseSensitive?: boolean;
}
export interface DateInfo {
+2
View File
@@ -5,12 +5,14 @@ export interface Snippets {
}
export interface Snippet {
title?: string;
description: string;
body: string[] | string;
fields: SnippetField[];
openingTags?: string;
closingTags?: string;
isMediaSnippet?: boolean;
sourcePath?: string;
}
export type SnippetSpecialPlaceholders = "FM_SELECTED_TEXT" | string;
@@ -1,10 +1,8 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { BlockFieldData, Field, PanelSettings } from '../../../models';
import { BlockFieldData, Field, PanelSettings, WhenOperator } from '../../../models';
import { Command } from '../../Command';
import { CommandToCode } from '../../CommandToCode';
import { TagType } from '../../TagType';
@@ -16,6 +14,7 @@ import { JsonField } from '../JsonField';
import { IMetadata } from '../Metadata';
import { TagPicker } from '../TagPicker';
import { ChoiceField, DataFileField, DateTimeField, DraftField, FieldTitle, FileField, ListField, Toggle, TextField, SlugField, PreviewImageField, PreviewImageValue, NumberField } from '.';
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
export interface IWrapperFieldProps {
field: Field;
@@ -111,6 +110,16 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
return null;
}
// Conditional fields
if (typeof field.when !== "undefined") {
const shouldRender = fieldWhenClause(field, parent);
if (!shouldRender) {
return null;
}
}
if (field.type === 'divider') {
return (
<div key={field.name} className="metadata_field__divider" />
+9 -5
View File
@@ -44,10 +44,6 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({ data, seo, focusE
}, 10);
}, [title, data[descriptionField], data?.articleDetails?.wordCount]);
if (!title && !data[descriptionField]) {
return null;
}
const renderContent = () => {
if (!isOpen) {
return null;
@@ -120,7 +116,15 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({ data, seo, focusE
return (
<Collapsible id={`seo`} title="SEO Status" sendUpdate={pushUpdate}>
{ renderContent() }
{
!title && !data[descriptionField] ? (
<div className={`seo__status__empty`}>
<p><b>Title</b> or <b>{descriptionField}</b> is needed.</p>
</div>
) : (
renderContent()
)
}
</Collapsible>
);
};
+1
View File
@@ -348,6 +348,7 @@
}
.metadata_field__required__message {
color: var(--vscode-inputValidation-errorBorder);
padding-top: .5rem;
font-size: .9rem;
margin-left: .5rem;
+296
View File
@@ -0,0 +1,296 @@
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
import { parseWinPath } from './../helpers/parseWinPath';
import { dirname, extname, join } from "path";
import { StatusBarAlignment, Uri, window } from "vscode";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { DefaultFields, DEFAULT_CONTENT_TYPE_NAME, ExtensionState, SETTING_SEO_DESCRIPTION_FIELD } from "../constants";
import { Page } from "../dashboardWebView/models";
import { ArticleHelper, ContentType, DateHelper, Extension, isValidFile, Logger, Notifications, Settings } from "../helpers";
import { existsAsync } from '../utils';
export class PagesParser {
public static allPages: Page[] = [];
public static cachedPages: Page[] | undefined = undefined;
private static parser: Promise<void> | undefined;
private static initialized: boolean = false;
/**
* Start the page parser
*/
public static start() {
if (!this.parser) {
this.parser = this.parsePages();
}
}
/**
* Retrieve the pages
* @param cb
*/
public static getPages(cb: (pages: Page[]) => void) {
if (this.parser) {
this.parser.then(() => cb(PagesParser.allPages));
} else if (!PagesParser.initialized) {
this.parser = this.parsePages();
this.parser.then(() => cb(PagesParser.allPages));
} else if (PagesParser.allPages === undefined || PagesParser.allPages.length === 0) {
this.parser = this.parsePages();
this.parser.then(() => cb(PagesParser.allPages));
} else {
cb(PagesParser.allPages);
}
}
/**
* Reset the cache
*/
public static async reset() {
this.parser = undefined;
PagesParser.allPages = [];
}
/**
* Parse all pages in the workspace
*/
public static async parsePages() {
const ext = Extension.getInstance();
// Update the dashboard with the fresh data
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
const statusBar = window.createStatusBarItem(StatusBarAlignment.Left);
if (folderInfo) {
statusBar.text = '$(sync~spin) Processing pages...';
statusBar.show();
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (isValidFile(file.fileName)) {
try {
let page = await PagesParser.getCachedPage(file.filePath, file.mtime);
if (!page) {
page = await this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title);
}
if (page && !pages.find(p => p.fmFilePath === page?.fmFilePath)) {
pages.push(page);
}
} catch (error: any) {
if ((error as Error)?.message.toLowerCase() === "webview is disposed") {
continue;
}
Logger.error(`PagesParser::parsePages: ${file.filePath} - ${error.message}`);
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
}
}
await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace");
PagesParser.cachedPages = undefined;
this.parser = undefined;
this.initialized = true;
PagesParser.allPages = [...pages];
statusBar.hide();
}
/**
* Find the page in the cached data
* @param filePath
* @param modifiedTime
* @returns
*/
public static async getCachedPage(filePath: string, modifiedTime: number): Promise<Page | undefined> {
if (!PagesParser.cachedPages) {
const ext = Extension.getInstance();
PagesParser.cachedPages = await ext.getState<Page[]>(ExtensionState.Dashboard.Pages.Cache, "workspace") || [];
}
return PagesParser.cachedPages.find(p => p.fmCachePath === parseWinPath(filePath) && p.fmCacheModifiedTime === modifiedTime);
}
/**
* Process the page content
* @param filePath
* @param fileMtime
* @param fileName
* @param folderTitle
* @returns
*/
public static async processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Promise<Page | undefined> {
const article = await ArticleHelper.getFrontMatterByPath(filePath);
const articleTitle = article?.data.title || fileName;
if (article?.data) {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined;
const modifiedField = ArticleHelper.getModifiedDateField(article) || null;
const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined;
const staticFolder = Folders.getStaticFolderRelativePath();
let escapedTitle = articleTitle;
if (escapedTitle && typeof escapedTitle !== "string") {
escapedTitle = "<invalid title>";
}
let escapedDescription = article?.data[descriptionField] || "";
if (escapedDescription && typeof escapedDescription !== "string") {
escapedDescription = "<invalid title>";
}
const page: Page = {
...article.data,
// Cache properties
fmCachePath: parseWinPath(filePath),
fmCacheModifiedTime: fileMtime,
// FrontMatter properties
fmFolder: folderTitle,
fmFilePath: filePath,
fmFileName: fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime,
fmPublished: dateFieldValue ? dateFieldValue.getTime() : null,
fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null,
fmPreviewImage: "",
fmTags: [],
fmCategories: [],
fmContentType: DEFAULT_CONTENT_TYPE_NAME,
fmBody: article?.content || "",
// Make sure these are always set
title: escapedTitle,
slug: article?.data.slug,
date: article?.data[dateField] || "",
draft: article?.data.draft,
description: escapedDescription,
};
const contentType = ArticleHelper.getContentType(article.data);
if (contentType) {
page.fmContentType = contentType.name;
}
let previewFieldParents = ContentType.findPreviewField(contentType.fields);
if (previewFieldParents.length === 0) {
const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview");
if (previewField) {
previewFieldParents = ["preview"];
}
}
let tagParents = ContentType.findFieldByType(contentType.fields, "tags");
const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]);
page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : tagsValue;
let categoryParents = ContentType.findFieldByType(contentType.fields, "categories");
const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]);
page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : categoriesValue;
// Check if parent fields were retrieved, if not there was no image present
if (previewFieldParents.length > 0) {
let fieldValue = null;
let crntPageData = article?.data;
for (let i = 0; i < previewFieldParents.length; i++) {
const previewField = previewFieldParents[i];
if (i === previewFieldParents.length - 1) {
fieldValue = crntPageData[previewField];
} else {
if (!crntPageData[previewField]) {
continue;
}
crntPageData = crntPageData[previewField];
// Check for preview image in block data
if (crntPageData instanceof Array && crntPageData.length > 0) {
// Get the first field block that contains the next field data
const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]);
if (fieldData) {
crntPageData = fieldData;
} else {
continue;
}
}
}
}
if (fieldValue && wsFolder) {
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
// Revalidate as the array could have been empty
if (fieldValue) {
let staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const crntFilePath = parseWinPath(filePath)
const pathWithoutExtension = crntFilePath.replace(extname(crntFilePath), '');
staticPath = join(pathWithoutExtension, fieldValue);
}
const contentFolderPath = join(dirname(filePath), fieldValue);
let previewUri = null;
if (await existsAsync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (await existsAsync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
let previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri);
if (!previewPath) {
previewPath = PagesParser.getWebviewUri(previewUri);
}
page["fmPreviewImage"] = previewPath?.toString() || "";
}
}
}
}
return page;
}
return;
}
/**
* Get the webview URI
* @param resource
* @returns
*/
private static getWebviewUri(resource: Uri) {
// Logic from: https://github.com/microsoft/vscode/blob/main/src/vs/workbench/common/webview.ts
const webviewResourceBaseHost = 'vscode-cdn.net';
const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`;
const authority = `${resource.scheme}+${encodeURI(resource.authority)}.${webviewRootResourceAuthority}`;
return Uri.from({
scheme: "https",
authority,
path: resource.path,
query: resource.query,
fragment: resource.fragment
});
}
}
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { copyFile as copyFileCb } from "fs";
export const copyFileAsync = promisify(copyFileCb);
+6
View File
@@ -0,0 +1,6 @@
import { stat } from "fs";
import { promisify } from "util";
export const existsAsync = async (path: string) => {
return promisify(stat)(path).then(() => true).catch(() => false);
};
+126
View File
@@ -0,0 +1,126 @@
import { WhenClause } from './../models/PanelSettings';
import { Field, WhenOperator } from "../models";
import { IMetadata } from "../panelWebView/components/Metadata";
/**
* Validate the field its "when" clause
* @param field
* @param parent
* @returns
*/
export const fieldWhenClause = (field: Field, parent: IMetadata): boolean => {
const when = field.when;
if (!when) {
return true;
}
let whenValue = parent[when.fieldRef];
if (when.caseSensitive || typeof when.caseSensitive === "undefined") {
return caseSensitive(when, field, whenValue);
} else {
return caseInsensitive(when, field, whenValue);
}
}
/**
* Case sensitive checks
* @param when
* @param field
* @param whenValue
* @returns
*/
const caseInsensitive = (when: WhenClause, field: Field, whenValue: string | IMetadata | string[] | null) => {
whenValue = lowerValue(whenValue);
const whenClone = Object.assign({}, when);
whenClone.value = lowerValue(whenClone.value);
return caseSensitive(whenClone, field, whenValue);
}
/**
* Case insensitive checks
* @param when
* @param field
* @param whenValue
* @returns
*/
const caseSensitive = (when: WhenClause, field: Field, whenValue: string | IMetadata | string[] | null) => {
switch (when.operator) {
case WhenOperator.equals:
if (whenValue !== when.value) {
return false;
}
break;
case WhenOperator.notEquals:
if (whenValue === when.value) {
return false;
}
break;
case WhenOperator.contains:
if ((typeof whenValue === "string" || whenValue instanceof Array) && !whenValue.includes(when.value)) {
return false;
}
break;
case WhenOperator.notContains:
if ((typeof whenValue === "string" || whenValue instanceof Array) && whenValue.includes(when.value)) {
return false;
}
break;
case WhenOperator.startsWith:
if (typeof whenValue === "string" && !whenValue.startsWith(when.value)) {
return false;
}
break;
case WhenOperator.endsWith:
if (typeof whenValue === "string" && !whenValue.endsWith(when.value)) {
return false;
}
break;
case WhenOperator.greaterThan:
if (typeof whenValue === "number" && whenValue <= when.value) {
return false;
}
break;
case WhenOperator.greaterThanOrEqual:
if (typeof whenValue === "number" && whenValue < when.value) {
return false;
}
break;
case WhenOperator.lessThan:
if (typeof whenValue === "number" && whenValue >= when.value) {
return false;
}
break;
case WhenOperator.lessThanOrEqual:
if (typeof whenValue === "number" && whenValue > when.value) {
return false;
}
break;
default:
break;
}
return true;
}
/**
* Lower the value(s)
* @param value
* @returns
*/
const lowerValue = (value: string | string[] | any) => {
if (typeof value === "string") {
value = value.toLowerCase();
} else if (value instanceof Array) {
value = value.map(crntValue => {
if (typeof crntValue === "string") {
return crntValue.toLowerCase();
}
return crntValue;
});
}
return value;
}
+9
View File
@@ -0,0 +1,9 @@
export * from './copyFileAsync';
export * from './existsAsync';
export * from './fieldWhenClause';
export * from './mkdirAsync';
export * from './readFileAsync';
export * from './readdirAsync';
export * from './renameAsync';
export * from './unlinkAsync';
export * from './writeFileAsync';
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { mkdir as mkdirCb } from "fs";
export const mkdirAsync = promisify(mkdirCb);
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { readFile as readFileCb } from "fs";
export const readFileAsync = promisify(readFileCb);
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { readdir as readdirCb } from "fs";
export const readdirAsync = promisify(readdirCb);
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { rename as renameCb } from "fs";
export const renameAsync = promisify(renameCb);
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { unlink as unlinkCb } from "fs";
export const unlinkAsync = promisify(unlinkCb);
+4
View File
@@ -0,0 +1,4 @@
import { promisify } from "util";
import { writeFile as writeFileCb } from "fs";
export const writeFileAsync = promisify(writeFileCb);