forked from iarv/vscode-front-matter
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a12cf70a80 | |||
| 4b1d80f04b | |||
| a84fecaf96 | |||
| 5b9c279fa2 | |||
| f67be9efb9 | |||
| 4c50100230 | |||
| 82ace03692 | |||
| 576d07fdef | |||
| f6bc4fb630 | |||
| c35f4ab070 | |||
| 59cbc03b0c | |||
| 0ea06a841e | |||
| b54eb5a360 | |||
| 16b6fff6dc | |||
| 7a46729a46 | |||
| 32182c3df0 | |||
| 78587509b3 | |||
| e3bd7eebbe | |||
| f1a8e0d425 | |||
| 33e294d702 | |||
| e098442eaa | |||
| 1de14122c5 | |||
| c0838fffd4 | |||
| 082c25144f | |||
| d701651a05 | |||
| 5205b2d079 | |||
| e864d56081 | |||
| c6a4c239a0 | |||
| 42fbdf9708 | |||
| 8d53990aea | |||
| b9a0c656d3 | |||
| 8a8db67e82 | |||
| 0ac4571859 | |||
| a072957793 | |||
| fad5ad7243 | |||
| b248ee7184 | |||
| 4e850e5cb9 | |||
| f89d4fce3f | |||
| 1ecf75ae9c | |||
| 888e5c5229 | |||
| 45eb542619 | |||
| 5a565f1154 | |||
| 78002563be | |||
| be3071dc18 | |||
| 5c9d7eda17 | |||
| 9f3cfd9d3a | |||
| 0c6ae47a7b | |||
| 726a26850d | |||
| 5fbb05f083 | |||
| afca99b53a | |||
| a8d2c428bc | |||
| 5254f2b7f9 | |||
| 13a71cfd82 | |||
| 07d67bf881 | |||
| 27887bedef | |||
| 2b8f08c03c | |||
| cb2194bc48 | |||
| 46872f81ac |
@@ -1,5 +1,39 @@
|
||||
# Change Log
|
||||
|
||||
## [8.2.0] - 2022-12-08 - [Release notes](https://beta.frontmatter.codes/updates/v8.2.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#362](https://github.com/estruyf/vscode-front-matter/issues/362): Support for conditional metadata
|
||||
- [#412](https://github.com/estruyf/vscode-front-matter/issues/412): Allow `frontmatter.json` to be split in multiple files
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#360](https://github.com/estruyf/vscode-front-matter/issues/360): Define which content types can be used on your page folders
|
||||
- [#406](https://github.com/estruyf/vscode-front-matter/issues/406): Added support for single data entries in the data dashboard
|
||||
- [#428](https://github.com/estruyf/vscode-front-matter/issues/428): Improved UX for inserting images to your content
|
||||
- [#430](https://github.com/estruyf/vscode-front-matter/issues/430): Support for HEXO its `post_asset_folder` setting (image location)
|
||||
- [#434](https://github.com/estruyf/vscode-front-matter/issues/434): Webview errors are logged in the extension output
|
||||
- [#440](https://github.com/estruyf/vscode-front-matter/issues/440): Type to search/filter in the snippets dashboard
|
||||
- [#447](https://github.com/estruyf/vscode-front-matter/issues/447): Allow to use placeholders on git commit messages
|
||||
- [#449](https://github.com/estruyf/vscode-front-matter/issues/449): Show `filename` if the `title` is not set
|
||||
- [#450](https://github.com/estruyf/vscode-front-matter/issues/450): Additional time placeholders added `{{hour12}}`, `{{hour24}}`, `{{ampm}}`, and `{{minute}}`
|
||||
- [#458](https://github.com/estruyf/vscode-front-matter/issues/458): Ability to configure the file prefix on folder level
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- [#431](https://github.com/estruyf/vscode-front-matter/issues/431): Performance improvements for the content dashboard
|
||||
- [#448](https://github.com/estruyf/vscode-front-matter/issues/448): Retrieving files fails when content folder name and workspace folder name are the same
|
||||
- [#455](https://github.com/estruyf/vscode-front-matter/issues/455): Show a description for the SEO section when title nor description is set
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Fix field error message color
|
||||
- [#433](https://github.com/estruyf/vscode-front-matter/issues/433): Fix issue with rendering an incorrect title value on the content dashboard
|
||||
- [#462](https://github.com/estruyf/vscode-front-matter/issues/462): Fix issue in script error notification
|
||||
- [#465](https://github.com/estruyf/vscode-front-matter/issues/465): Deleted content does not get added in git when syncing
|
||||
- [#471](https://github.com/estruyf/vscode-front-matter/issues/471): Fix typo on data dashboard
|
||||
|
||||
## [8.1.2] - 2022-10-06
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
+2
-2
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "8.1.2",
|
||||
"version": "8.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "8.1.2",
|
||||
"version": "8.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.7"
|
||||
|
||||
+79
-5
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"displayName": "Front Matter",
|
||||
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"displayName": "Front Matter CMS",
|
||||
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "8.1.2",
|
||||
"version": "8.2.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -206,6 +206,17 @@
|
||||
],
|
||||
"default": null,
|
||||
"description": "Defines a custom preview path for the folder."
|
||||
},
|
||||
"filePrefix": {
|
||||
"type": [ "null", "string" ],
|
||||
"description": "Defines a prefix for the file name."
|
||||
},
|
||||
"contentTypes": {
|
||||
"type": "array",
|
||||
"description": "Defines which content types can be used for the current location. If not defined, all content types will be available.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -271,6 +282,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"description": "The snippet title.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "The snippet description.",
|
||||
"type": "string"
|
||||
@@ -447,9 +462,9 @@
|
||||
"scope": "Custom scripts"
|
||||
},
|
||||
"frontMatter.dashboard.content.pagination": {
|
||||
"type": "boolean",
|
||||
"type": ["boolean", "number"],
|
||||
"default": true,
|
||||
"markdownDescription": "Specify if you want to enable/disable pagination for your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)",
|
||||
"markdownDescription": "Specify if you want to enable/disable pagination for your content. You can define your page number up to 52. Default items per page is `16`. Disabling the pagination can be done by setting it to `false`. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.dashboard.content.cardTags": {
|
||||
@@ -550,6 +565,11 @@
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"description": "If you are using data types, you can specify your type ID."
|
||||
},
|
||||
"singleEntry": {
|
||||
"type": "boolean",
|
||||
"description": "If you want to use a single entry for your data file.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -601,6 +621,11 @@
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"description": "If you are using data types, you can specify your type ID."
|
||||
},
|
||||
"singleEntry": {
|
||||
"type": "boolean",
|
||||
"description": "If you want to use a single entry for your data files in the folder.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1039,6 +1064,50 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the field is required"
|
||||
},
|
||||
"when": {
|
||||
"type": "object",
|
||||
"description": "Specify the conditions to show the field",
|
||||
"properties": {
|
||||
"fieldRef": {
|
||||
"type": "string",
|
||||
"description": "The field ID to use"
|
||||
},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"description": "The operator to use",
|
||||
"enum": [
|
||||
"eq",
|
||||
"neq",
|
||||
"contains",
|
||||
"notContains",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"exlusiveMinimum",
|
||||
"exclusiveMaximum"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"array"
|
||||
],
|
||||
"description": "The value to compare"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Specify if the comparison is case sensitive. Default: true"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1732,6 +1801,11 @@
|
||||
"command": "frontMatter.git.sync",
|
||||
"title": "Sync",
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.cache.clear",
|
||||
"title": "Clear cache",
|
||||
"category": "Front Matter"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -72,4 +72,7 @@ export const COMMAND_NAME = {
|
||||
|
||||
// Config
|
||||
reloadConfig: getCommandName("config.reload"),
|
||||
|
||||
// Cache
|
||||
clearCache: getCommandName("cache.clear"),
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const STATIC_FOLDER_PLACEHOLDER = {
|
||||
hexo: {
|
||||
postsFolder: "source/_posts",
|
||||
placeholder: "hexo:post_asset_folder",
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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`}
|
||||
|
||||
@@ -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();
|
||||
@@ -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} />
|
||||
|
||||
{
|
||||
|
||||
@@ -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 & drop new files.</p>
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & 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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './AllContentFoldersAtom';
|
||||
export * from './AllStaticFoldersAtom';
|
||||
export * from './CategoryAtom';
|
||||
export * from './DashboardViewAtom';
|
||||
export * from './FolderAtom';
|
||||
|
||||
@@ -100,7 +100,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
}
|
||||
}, this);
|
||||
|
||||
Settings.onConfigChange((global?: any) => {
|
||||
Settings.onConfigChange(() => {
|
||||
SettingsListener.getSettings();
|
||||
});
|
||||
}
|
||||
|
||||
+15
-19
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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`)) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || "");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './SettingsListener';
|
||||
export * from './SnippetListener';
|
||||
export * from './TelemetryListener';
|
||||
export * from './TaxonomyListener';
|
||||
export * from './LogListener';
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ export interface ContentFolder {
|
||||
|
||||
excludeSubdir?: boolean;
|
||||
previewPath?: string;
|
||||
filePrefix?: string;
|
||||
contentTypes?: string[];
|
||||
}
|
||||
@@ -6,4 +6,5 @@ export interface DataFile {
|
||||
labelField: string;
|
||||
schema?: any;
|
||||
type?: string;
|
||||
singleEntry?: boolean;
|
||||
}
|
||||
@@ -6,4 +6,5 @@ export interface DataFolder {
|
||||
labelField: string;
|
||||
schema?: any;
|
||||
type?: string;
|
||||
singleEntry?: boolean;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ export interface MediaPaths {
|
||||
media: MediaInfo[];
|
||||
total: number;
|
||||
folders: string[];
|
||||
allContentFolders: string[];
|
||||
allStaticfolders: string[];
|
||||
selectedFolder: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { copyFile as copyFileCb } from "fs";
|
||||
|
||||
export const copyFileAsync = promisify(copyFileCb);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { mkdir as mkdirCb } from "fs";
|
||||
|
||||
export const mkdirAsync = promisify(mkdirCb);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { readFile as readFileCb } from "fs";
|
||||
|
||||
export const readFileAsync = promisify(readFileCb);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { readdir as readdirCb } from "fs";
|
||||
|
||||
export const readdirAsync = promisify(readdirCb);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { rename as renameCb } from "fs";
|
||||
|
||||
export const renameAsync = promisify(renameCb);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { unlink as unlinkCb } from "fs";
|
||||
|
||||
export const unlinkAsync = promisify(unlinkCb);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { promisify } from "util";
|
||||
import { writeFile as writeFileCb } from "fs";
|
||||
|
||||
export const writeFileAsync = promisify(writeFileCb);
|
||||
Reference in New Issue
Block a user