Compare commits

...

70 Commits

Author SHA1 Message Date
Elio Struyf
71072d9520 #275 - Invalid markdown syntax tree fix 2022-03-02 18:15:41 +01:00
Elio Struyf
b64dd8f88a Update changelog 2022-03-02 18:13:35 +01:00
Elio Struyf
173c89d86f 6.1.1 2022-03-02 18:12:24 +01:00
Elio Struyf
f5f558d5bc Merge pull request #274 from estruyf/dev 2022-02-28 17:40:50 +01:00
Elio Struyf
c9c38ef10b Replace fix 2022-02-28 17:34:53 +01:00
Elio Struyf
c30f401c4f Updated changelog for v6.1.0 release 2022-02-28 17:27:36 +01:00
Elio Struyf
9b92050af8 Undo version 2022-02-28 16:31:12 +01:00
Elio Struyf
31a41e2a66 Added version 2022-02-28 15:56:13 +01:00
Elio Struyf
baa56bc246 Updated activation events 2022-02-28 15:29:10 +01:00
Elio Struyf
f53e81e0cb Added release notes 2022-02-28 13:45:52 +01:00
Elio Struyf
f454266846 Updated changelog 2022-02-25 18:21:07 +01:00
Elio Struyf
0ba3c22795 #271 - Added image size placeholders for media snippets 2022-02-25 10:39:52 +01:00
Elio Struyf
ff38cf361c #241 - Enhanced the taxonomy field render 2022-02-25 08:43:36 +01:00
Elio Struyf
57e93b91c5 #268 - preserve casing of filename on creation 2022-02-24 18:04:09 +01:00
Elio Struyf
c1161b95ed #268 - Fix for panel not showing up after renaming 2022-02-24 17:12:42 +01:00
Elio Struyf
32dc63b62a Fix for keywords 2022-02-24 11:45:02 +00:00
Elio Struyf
0c1198c802 #264 - Fix for windows paths on content folder registration 2022-02-24 10:27:37 +00:00
Elio Struyf
ed4b78cfdc #262 - Fix related to YAML comments 2022-02-21 18:06:41 +01:00
Elio Struyf
65f77baf2b #261 - Update tags and categories 2022-02-19 17:32:32 +01:00
Elio Struyf
eabdf00d3d #257 - Allow preview images to be used in multi-dimensional fields 2022-02-18 11:27:31 -08:00
Elio Struyf
c084a15e08 updated changelog 2022-02-17 19:22:19 -08:00
Elio Struyf
e577ba591e #176 - Fix for tax fields 2022-02-17 19:21:29 -08:00
Elio Struyf
b17c7f888a #241 - Add taxonomy limit to limit the number of selections 2022-02-17 19:21:16 -08:00
Elio Struyf
0ed41b7d7e #176 - Extra setting updates 2022-02-17 19:12:05 -08:00
Elio Struyf
2e1faaa34f Sort all settings 2022-02-17 19:01:58 -08:00
Elio Struyf
63f02f4f0e Updated link 2022-02-17 18:59:34 -08:00
Elio Struyf
489fc5ec9e #176 - fieldGroups setting added to schema 2022-02-17 18:57:59 -08:00
Elio Struyf
4c8ecdb344 #255 - Implemented placeholder logic for WV to backend communication 2022-02-17 18:43:49 -08:00
Elio Struyf
8d705ff6c5 #176 #255 - Default block fields value 2022-02-17 11:32:34 -08:00
Elio Struyf
cfe68e65e8 Fix indent 2022-02-17 10:59:02 -08:00
Elio Struyf
0e179f5fd7 update changelog 2022-02-16 17:36:21 -08:00
Elio Struyf
6cabd6283b #242 - Keep comments at front matter root 2022-02-16 17:35:49 -08:00
Elio Struyf
6135e38fce Added now placeholder 2022-02-16 09:39:55 -08:00
Elio Struyf
935b2230af Merge pull request #249 from estruyf/dependabot/npm_and_yarn/follow-redirects-1.14.8 2022-02-15 15:50:57 -08:00
Elio Struyf
6dcd89e9cd #176 - label field support added to block fields 2022-02-15 11:13:46 -08:00
Elio Struyf
2775b2051f #176 - Save + cancel button and fixes in data storage 2022-02-15 07:41:52 -08:00
dependabot[bot]
5ebb2d7370 Bump follow-redirects from 1.14.6 to 1.14.8
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-15 03:35:22 +00:00
Elio Struyf
c9488e6661 #176 - Fix image webview paths 2022-02-14 16:54:54 -08:00
Elio Struyf
442261e655 #176 - Taxonomy field support added to block field 2022-02-14 16:07:59 -08:00
Elio Struyf
1aa2d41c95 #176 - Image field support added for block field 2022-02-14 15:45:16 -08:00
Elio Struyf
e5a2194c23 #176 - Fields field support 2022-02-14 12:42:45 -08:00
Elio Struyf
cf6f051ee8 Fix background color + labels 2022-02-14 11:38:13 -08:00
Elio Struyf
bebde4de68 #176 - Block field updated to support default fields 2022-02-14 09:18:44 -08:00
Elio Struyf
174c4b7734 #176 - Block to JSON field type 2022-02-13 18:11:51 -08:00
Elio Struyf
1f7519ee60 Dependency updates 2022-02-13 09:38:53 -08:00
Elio Struyf
b6482546a5 #248 - Highlighting support for other files added 2022-02-13 09:38:46 -08:00
Elio Struyf
0decd84f7f #247 - Fix front matter highlighting 2022-02-13 09:38:06 -08:00
Elio Struyf
a1dbda0b23 #176 - update dropdown style 2022-02-13 08:55:08 -08:00
Elio Struyf
427245f211 #176 - Select the first block type if single 2022-02-13 08:37:17 -08:00
Elio Struyf
4678189eab Update changelog 2022-02-13 08:26:01 -08:00
Elio Struyf
15d89e34cf Fix checkbox 2022-02-12 18:00:26 +01:00
Elio Struyf
cbb0d8f72b Refactoring explorer view to listeners 2022-02-12 17:58:59 +01:00
Elio Struyf
131150f5a6 #176 - Sorting + multi-block type support 2022-02-12 16:39:33 +01:00
Elio Struyf
a31bca73e7 Keep collapsible state 2022-02-12 10:03:40 +01:00
Elio Struyf
1d5f940c94 Remove logging 2022-02-11 11:30:30 +01:00
Elio Struyf
70ea6a5a16 Updated changelog 2022-02-11 11:25:45 +01:00
Elio Struyf
849af69ce2 #176 - updated styles for the autoform fields 2022-02-11 11:24:27 +01:00
Elio Struyf
754570a9ec #176 - Optimize styling of fields 2022-02-10 22:04:47 +01:00
Elio Struyf
f7f6f26997 #176 - New collection field + uniform custom components 2022-02-10 20:18:13 +01:00
Elio Struyf
946d84a7a9 Data click optimization 2022-02-10 12:00:23 +01:00
Elio Struyf
781ab6ac40 #243 - Refactoring front matter parsing 2022-02-08 13:17:56 +01:00
Elio Struyf
df86d02e8b Update activity bar icon 2022-02-02 12:03:41 +01:00
Elio Struyf
19e468c908 Fix reference 2022-02-01 14:31:57 +01:00
Elio Struyf
5a81ea19b8 Add telemetry property 2022-01-31 22:15:18 +01:00
Elio Struyf
64a38e56b9 Update activation events 2022-01-31 20:33:05 +01:00
Elio Struyf
fca0528a7e application insight fixes 2022-01-30 19:56:13 +01:00
Elio Struyf
936916acf8 6.1.0 2022-01-30 17:33:30 +01:00
Elio Struyf
61e9fc0308 start preping 6.1.0 2022-01-30 17:33:25 +01:00
Elio Struyf
2356623d7a Merge branch 'dev' 2022-01-25 17:52:20 +01:00
Elio Struyf
ee70acebb6 update changelog 2022-01-25 17:52:03 +01:00
134 changed files with 5329 additions and 1783 deletions

View File

@@ -1,6 +1,43 @@
# Change Log
## [6.0.0] - 2022-01-xx - [Release Notes](https://beta.frontmatter.codes/updates/v6.0.0)
## [6.1.1] - 2022-03-02
### 🐞 Fixes
- [#275](https://github.com/estruyf/vscode-front-matter/issues/275): Fix for rendering the panel when content contains an invalid markdown syntax tree
## [6.1.0] - 2022-02-28 - [Release notes](https://beta.frontmatter.codes/updates/v6.1.0)
### ✨ New features
- [#176](https://github.com/estruyf/vscode-front-matter/issues/176): New `block` field type that allows you to you to define a group of fields which can be used to create a list of data
### 🎨 Enhancements
- Updated the activity bar icon for better visibility
- Storing the panel collapse section states
- [#241](https://github.com/estruyf/vscode-front-matter/issues/241): Added taxonomy limit field property which allows you to limit the number of selections
- [#242](https://github.com/estruyf/vscode-front-matter/issues/242): Keep comments at the root of the front matter
- [#248](https://github.com/estruyf/vscode-front-matter/issues/248): Added support for front matter highlighting to all file types specified in `frontMatter.content.supportedFileTypes`
- [#255](https://github.com/estruyf/vscode-front-matter/issues/255): Added support for default values on block fields / data creation
- [#257](https://github.com/estruyf/vscode-front-matter/issues/257): Allow preview images to be used in multi-dimensional fields
- [#271](https://github.com/estruyf/vscode-front-matter/issues/271): Added image size placeholders for media snippets
### ⚡️ Optimizations
- Show the data item its details when clicking on the record
- Refactoring of the explorer view panel listeners
- Added `{{now}}` placeholder to the publishing date for content creation
- [#243](https://github.com/estruyf/vscode-front-matter/issues/243): Refactoring front matter parsing
### 🐞 Fixes
- [#247](https://github.com/estruyf/vscode-front-matter/issues/247): Fix the front matter highlighting in markdown documents
- [#261](https://github.com/estruyf/vscode-front-matter/issues/261): Fix to allow that tag and category fields can be renamed
- [#264](https://github.com/estruyf/vscode-front-matter/issues/264): Fix for Windows paths on content folder registration
- [#268](https://github.com/estruyf/vscode-front-matter/issues/268): Fix for panel which only shows loading indicator
## [6.0.0] - 2022-01-25 - [Release Notes](https://beta.frontmatter.codes/updates/v6.0.0)
### ✨ New features

View File

@@ -30,7 +30,7 @@
}
.inherit {
position: inherit !important;
position: relative !important;
}
.z-10 { z-index: 10 !important; }
@@ -143,6 +143,7 @@
}
.article__tags {
position: relative;
margin-bottom: 1rem;
}
@@ -165,6 +166,10 @@
border: 1px solid var(--vscode-inputValidation-infoBorder);
}
.article__tags__input input:disabled {
border-color: transparent;
}
.article__tags__input.freeform {
position: relative;
}
@@ -448,17 +453,17 @@ input:checked + .field__toggle__slider:before {
.vscode-dark .metadata_field__box {
background: rgba(255, 255, 255, 0.1);
border: 2px dashed rgba(255, 255, 255, 0.2);
border: 1px dashed rgba(255, 255, 255, 0.2);
}
.vscode-light .metadata_field__box {
background: rgba(0, 0, 0, 0.1);
border: 2px dashed rgba(0, 0, 0, 0.2);
border: 1px dashed rgba(0, 0, 0, 0.2);
}
.metadata_field__box {
background: rgba(255, 255, 255, 0.1);
border: 2px dashed rgba(255, 255, 255, 0.2);
border: 1px dashed rgba(255, 255, 255, 0.2);
margin-bottom: .5rem;
padding: .5rem 1rem;
}
@@ -634,7 +639,7 @@ input:checked + .field__toggle__slider:before {
.metadata_field__preview_image__button {
background-color: transparent;
border: 2px dashed var(--vscode-button-background);
border: 1px dashed var(--vscode-button-background);
padding: 1.5rem;
filter: brightness(85%);
}

1352
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"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...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "6.0.0",
"version": "6.1.1",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -45,26 +45,15 @@
"url": "https://github.com/estruyf/vscode-front-matter"
},
"activationEvents": [
"*",
"onCommand:frontMatter.insertTags",
"onCommand:frontMatter.insertCategories",
"onCommand:frontMatter.createTag",
"onCommand:frontMatter.createCategory",
"onCommand:frontMatter.exportTaxonomy",
"onCommand:frontMatter.remap",
"onCommand:frontMatter.setLastModifiedDate",
"onCommand:frontMatter.generateSlug",
"onCommand:frontMatter.createFromTemplate",
"onCommand:frontMatter.registerFolder",
"onCommand:frontMatter.unregisterFolder",
"onCommand:frontMatter.createContent",
"workspaceContains:**/.frontmatter",
"workspaceContains:**/frontmatter.json",
"onCommand:frontMatter.init",
"onCommand:frontMatter.collapseSections",
"onCommand:frontMatter.preview",
"onCommand:frontMatter.dashboard",
"onCommand:frontMatter.promoteSettings",
"onCommand:frontMatter.insertImage",
"onView:frontMatter.explorer"
"onCommand:frontMatter.dashboard.data",
"onCommand:frontMatter.dashboard.media",
"onCommand:workbench.view.extension.frontmatter-explorer",
"onView:frontMatter.explorer",
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
@@ -73,7 +62,7 @@
{
"id": "frontmatter-explorer",
"title": "FrontMatter",
"icon": "assets/frontmatter.svg"
"icon": "assets/frontmatter-short-min.svg"
}
]
},
@@ -82,7 +71,7 @@
{
"id": "frontMatter.explorer",
"name": "FrontMatter",
"icon": "assets/frontmatter.svg",
"icon": "assets/frontmatter-short-min.svg",
"contextualTitle": "FrontMatter",
"type": "webview"
}
@@ -360,7 +349,7 @@
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
"items": {
"type": "string",
"description": "The parts of your snippet. Use `{mediaUrl}` as placeholder where the path of the image needs to be inserted."
"description": "Use the `{mediaUrl}`, `{caption}`, `{alt}`, `{filename}`, `{mediaHeight}`, and `{mediaWidth}` placeholders in your snippet to automatically insert the media information."
},
"scope": "dashboard"
},
@@ -517,6 +506,12 @@
},
"scope": "Data"
},
"frontMatter.file.preserveCasing": {
"type": "boolean",
"default": false,
"markdownDescription": "Specify if you want to preserve the casing of your file names from the title. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.file.preservecasing)",
"scope": "File"
},
"frontMatter.framework.id": {
"type": "string",
"default": "",
@@ -651,7 +646,9 @@
"tags",
"categories",
"draft",
"fields"
"fields",
"json",
"block"
],
"description": "Define the type of field"
},
@@ -717,6 +714,33 @@
},
"fields": {
"$ref": "#contenttypefield"
},
"fieldGroup": {
"type": [
"string",
"array"
],
"default": [],
"description": "The ID(s) of your field group(s) defined in the `frontMatter.taxonomy.fieldGroups` setting",
"items": {
"type": "string"
}
},
"dataType": {
"type": [
"string",
"array"
],
"default": [],
"description": "The ID(s) of your data type(s) defined in the `frontMatter.data.types` setting",
"items": {
"type": "string"
}
},
"taxonomyLimit": {
"type": "number",
"default": 0,
"description": "Limit the number of taxonomies to select. Set to 0 to allow unlimited."
}
},
"additionalProperties": false,
@@ -766,6 +790,34 @@
"fields"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "block"
}
}
},
"then": {
"required": [
"fieldGroup"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "json"
}
}
},
"then": {
"required": [
"dataType"
]
}
}
]
}
@@ -808,7 +860,8 @@
{
"title": "Publishing date",
"name": "date",
"type": "datetime"
"type": "datetime",
"default": "{{now}}"
},
{
"title": "Content preview",
@@ -871,6 +924,28 @@
"markdownDescription": "Specify the date format for your articles. Check [date-fns formating](https://date-fns.org/v2.0.1/docs/format) for more information. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.dateformat)",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.fieldGroups": {
"type": "array",
"markdownDescription": "Define the field groups you want to use for your block fields. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.fieldgroups)",
"default": [],
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The name of the field group"
},
"fields": {
"$ref": "#contenttypefield"
}
},
"additionalProperties": false,
"required": [
"name",
"fields"
]
}
},
"frontMatter.taxonomy.frontMatterType": {
"type": "string",
"default": "YAML",
@@ -956,6 +1031,11 @@
},
"scope": "Taxonomy"
},
"frontMatter.telemetry.disable": {
"type": "boolean",
"default": false,
"markdownDescription": "Specify if you want to disable the telemetry. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.telemetry.disable)"
},
"frontMatter.templates.folder": {
"type": "string",
"default": ".frontmatter/templates",
@@ -1421,8 +1501,11 @@
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/glob": "7.1.3",
"@types/invariant": "^2.2.35",
"@types/js-yaml": "3.12.1",
"@types/lodash.omit": "^4.5.6",
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mocha": "^5.2.6",
"@types/node": "10.17.48",
"@types/node-fetch": "^2.5.12",
@@ -1431,7 +1514,8 @@
"@types/react-dom": "17.0.0",
"@types/vscode": "^1.63.0",
"@vscode/codicons": "0.0.20",
"@vscode/webview-ui-toolkit": "^0.8.1",
"@vscode/extension-telemetry": "^0.4.7",
"@vscode/webview-ui-toolkit": "^0.9.1",
"@webpack-cli/serve": "^1.6.0",
"ajv": "^8.8.2",
"array-move": "^4.0.0",
@@ -1445,8 +1529,11 @@
"html-loader": "1.3.2",
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
"invariant": "^2.2.4",
"lodash-es": "^4.17.21",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
@@ -1476,9 +1563,11 @@
"webpack": "^5.65.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0"
"webpack-dev-server": "^4.6.0",
"yaml": "^1.10.2",
"yawn-yaml": "^1.5.0"
},
"dependencies": {
"node-fetch": "^2.6.7"
}
}
}

View File

@@ -1,17 +1,18 @@
import { isValidFile } from './../helpers/isValidFile';
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTINGS_CONTENT_PLACEHOLDERS } from './../constants';
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTINGS_CONTENT_PLACEHOLDERS, TelemetryEvent } from './../constants';
import * as vscode from 'vscode';
import { Field, TaxonomyType } from "../models";
import { format } from "date-fns";
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
import matter = require('gray-matter');
import { Notifications } from '../helpers/Notifications';
import { extname, basename, parse, dirname } from 'path';
import { COMMAND_NAME, DefaultFields } from '../constants';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DateHelper } from '../helpers/DateHelper';
import { parseWinPath } from '../helpers/parseWinPath';
import { Telemetry } from '../helpers/Telemetry';
import { ParsedFrontMatter } from '../parsers';
import { MediaListener } from '../listeners/panel';
export class Article {
@@ -102,7 +103,7 @@ export class Article {
* Update the date in the front matter
* @param article
*/
public static updateDate(article: matter.GrayMatterFile<string>, forceCreate: boolean = false) {
public static updateDate(article: ParsedFrontMatter, forceCreate: boolean = false) {
article.data = ArticleHelper.updateDates(article.data);
return article;
}
@@ -124,7 +125,7 @@ export class Article {
ArticleHelper.update(
editor,
updatedArticle as matter.GrayMatterFile<string>
updatedArticle as ParsedFrontMatter
);
}
@@ -144,7 +145,7 @@ export class Article {
private static setLastModifiedDateInner(
document: vscode.TextDocument
): matter.GrayMatterFile<string> | undefined {
): ParsedFrontMatter | undefined {
const article = ArticleHelper.getFrontMatterFromDocument(document);
if (!article) {
@@ -165,6 +166,8 @@ export class Article {
* Generate the slug based on the article title
*/
public static async generateSlug() {
Telemetry.send(TelemetryEvent.generateSlug);
const prefix = Settings.get(SETTING_SLUG_PREFIX) as string;
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
@@ -334,13 +337,13 @@ export class Article {
} as DashboardData);
// Let the editor panel know you are selecting an image
ExplorerView.getInstance().getMediaSelection();
MediaListener.getMediaSelection();
}
/**
* Get the current article
*/
private static getCurrent(): matter.GrayMatterFile<string> | undefined {
private static getCurrent(): ParsedFrontMatter | undefined {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
@@ -361,7 +364,7 @@ export class Article {
* @param field
* @param forceCreate
*/
private static articleDate(article: matter.GrayMatterFile<string>, field: string, forceCreate: boolean) {
private static articleDate(article: ParsedFrontMatter, field: string, forceCreate: boolean) {
if (typeof article.data[field] !== "undefined" || forceCreate) {
article.data[field] = Article.formatDate(new Date());
}

View File

@@ -5,6 +5,7 @@ import { Credentials } from "../services/Credentials";
import fetch from "node-fetch";
import { ExplorerView } from '../explorerView/ExplorerView';
import { Dashboard } from './Dashboard';
import { SettingsListener } from '../listeners/panel';
export class Backers {
private static creds: Credentials | null = null;
@@ -60,7 +61,7 @@ export class Backers {
if (!prevData) {
const explorerView = ExplorerView.getInstance();
if (explorerView.visible) {
explorerView.getSettings();
SettingsListener.getSettings();
}
if (Dashboard.isOpen) {

View File

@@ -1,5 +1,3 @@
import { PagesListener } from './../listeners/PagesListener';
import { ExtensionListener } from './../listeners/ExtensionListener';
import { SETTINGS_DASHBOARD_OPENONSTART, CONTEXT } from '../constants';
import { join } from "path";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode";
@@ -10,8 +8,8 @@ import { WebviewHelper } from '@estruyf/vscode';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
import { MediaLibrary } from '../helpers/MediaLibrary';
import { DashboardListener, MediaListener, SettingsListener } from '../listeners';
import { DataListener } from '../listeners/DataListener';
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener } from '../listeners/dashboard';
import { MediaListener as PanelMediaListener } from '../listeners/panel'
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -116,8 +114,7 @@ export class Dashboard {
Dashboard.webview.onDidChangeViewState(async () => {
if (!this.webview?.visible) {
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
PanelMediaListener.getMediaSelection();
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: null });
}
@@ -128,8 +125,7 @@ export class Dashboard {
Dashboard.webview.onDidDispose(async () => {
Dashboard.isDisposed = true;
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
PanelMediaListener.getMediaSelection();
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, false);
});
@@ -146,6 +142,7 @@ export class Dashboard {
PagesListener.process(msg);
SettingsListener.process(msg);
DataListener.process(msg);
TelemetryListener.process(msg);
});
}

View File

@@ -1,5 +1,5 @@
import { Questions } from './../helpers/Questions';
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_CONTENT_SUPPORTED_FILETYPES } from './../constants';
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants';
import { commands, Uri, workspace, window } from "vscode";
import { basename, join } from "path";
import { ContentFolder, FileInfo, FolderInfo } from "../models";
@@ -12,8 +12,9 @@ import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
import { parseWinPath } from '../helpers/parseWinPath';
import { MediaHelpers } from '../helpers/MediaHelpers';
import { MediaListener, PagesListener } from '../listeners';
import { MediaListener, PagesListener } from '../listeners/dashboard';
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
import { Telemetry } from '../helpers/Telemetry';
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
@@ -68,6 +69,8 @@ export class Folders {
MediaHelpers.resetMedia();
MediaListener.sendMediaFiles(0, folderName);
}
Telemetry.send(TelemetryEvent.addMediaFolder);
}
/**
@@ -122,6 +125,8 @@ export class Folders {
await Folders.update(folders);
Notifications.info(`Folder registered`);
Telemetry.send(TelemetryEvent.registerFolder);
}
}
@@ -134,6 +139,8 @@ export class Folders {
let folders = Folders.get();
folders = folders.filter(f => f.path !== folder.fsPath);
await Folders.update(folders);
Telemetry.send(TelemetryEvent.unregisterFolder);
}
}
@@ -281,19 +288,6 @@ export class Folders {
path: Folders.absWsFolder(folder, wsFolder)
}));
}
/**
* Retrieve the absolute file path
* @param filePath
* @returns
*/
public static getAbsFilePath(filePath: string): string {
const wsFolder = Folders.getWorkspaceFolder();
const isWindows = process.platform === 'win32';
let absPath = filePath.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
}
/**
* Update the folder settings
@@ -313,6 +307,19 @@ export class Folders {
PagesListener.startWatchers();
}
/**
* Retrieve the absolute file path
* @param filePath
* @returns
*/
public static getAbsFilePath(filePath: string): string {
const wsFolder = Folders.getWorkspaceFolder();
const isWindows = process.platform === 'win32';
let absPath = filePath.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
}
/**
* Generate the absolute URL for the workspace
* @param folder
@@ -321,7 +328,7 @@ export class Folders {
*/
private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
}
@@ -334,12 +341,8 @@ export class Folders {
*/
private static relWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
let absPath = parseWinPath(folder.path).replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
return absPath;
}
}
function SETTINGS_CONTENT_SUPPORTED_FILES<T>(SETTINGS_CONTENT_SUPPORTED_FILES: any) {
throw new Error('Function not implemented.');
}

View File

@@ -1,4 +1,5 @@
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT } from './../constants';
import { Telemetry } from './../helpers/Telemetry';
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent } from './../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
import { commands, env, Uri, ViewColumn, window } from "vscode";
@@ -133,6 +134,8 @@ export class Preview {
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
</body>
</html>`;
Telemetry.send(TelemetryEvent.openPreview);
}
/**

View File

@@ -1,3 +1,4 @@
import { Telemetry } from './../helpers/Telemetry';
import { workspace, Uri } from "vscode";
import { join } from "path";
import * as fs from "fs";
@@ -5,7 +6,7 @@ import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Settings } from "../helpers";
import { SETTINGS_CONTENT_DEFAULT_FILETYPE } from "../constants";
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
export class Project {
@@ -47,6 +48,8 @@ categories: []
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
Notifications.info("Project initialized successfully.");
}
Telemetry.send(TelemetryEvent.initialization)
} catch (err: any) {
Notifications.error(`Sorry, something went wrong - ${err?.message || err}`);
}

View File

@@ -1,10 +1,9 @@
import * as vscode from 'vscode';
import * as matter from 'gray-matter';
import * as fs from 'fs';
import { TaxonomyType } from "../models";
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, EXTENSION_NAME } from '../constants';
import { ArticleHelper, Settings as SettingsHelper, FilesHelper } from '../helpers';
import { TomlEngine, getFmLanguage, getFormatOpts } from '../helpers/TomlEngine';
import { FrontMatterParser } from '../parsers';
import { DumpOptions } from 'js-yaml';
import { Notifications } from '../helpers/Notifications';
@@ -90,10 +89,6 @@ export class Settings {
const progressNr = allMdFiles.length/100;
progress.report({ increment: 0});
// Get language options
const language = getFmLanguage();
const langOpts = getFormatOpts(language);
let i = 0;
for (const file of allMdFiles) {
progress.report({ increment: (++i/progressNr) });
@@ -102,10 +97,7 @@ export class Settings {
const txtData = mdFile.getText();
if (txtData) {
try {
const article = matter(txtData, {
...TomlEngine,
...langOpts
});
const article = FrontMatterParser.fromFile(txtData);
if (article && article.data) {
const { data } = article;
const mdTags = data["tags"];
@@ -218,13 +210,8 @@ export class Settings {
progress.report({ increment: (++i/progressNr) });
const mdFile = fs.readFileSync(file.path, { encoding: "utf8" });
if (mdFile) {
const language = getFmLanguage();
const langOpts = getFormatOpts(language);
try {
const article = matter(mdFile, {
...TomlEngine,
...langOpts
});
const article = FrontMatterParser.fromFile(mdFile);
if (article && article.data) {
const { data } = article;
let taxonomies: string[] = data[matterProp];
@@ -239,9 +226,7 @@ export class Settings {
data[matterProp] = [...new Set(taxonomies)].sort();
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
// Update the file
fs.writeFileSync(file.path, matter.stringify(article.content, article.data, {
...TomlEngine,
...langOpts,
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, {
indent: spaces || 2
} as DumpOptions as any), { encoding: "utf8" });
}

View File

@@ -4,6 +4,7 @@ import { ArticleHelper, SeoHelper, Settings } from '../helpers';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DefaultFields } from '../constants';
import { ContentType } from '../helpers/ContentType';
import { DataListener } from '../listeners/panel';
export class StatusListener {
@@ -58,7 +59,7 @@ export class StatusListener {
const panel = ExplorerView.getInstance();
if (panel && panel.visible) {
panel.pushMetadata(article!.data);
DataListener.pushMetadata(article!.data);
}
return;
@@ -68,7 +69,7 @@ export class StatusListener {
} else {
const panel = ExplorerView.getInstance();
if (panel && panel.visible) {
panel.pushMetadata(null);
DataListener.pushMetadata(null);
}
}

View File

@@ -2,7 +2,7 @@ import { Questions } from './../helpers/Questions';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEvent } from '../constants';
import { ArticleHelper, Settings } from '../helpers';
import { Article } from '.';
import { Notifications } from '../helpers/Notifications';
@@ -11,8 +11,9 @@ import { Project } from './Project';
import { Folders } from './Folders';
import { ContentType } from '../helpers/ContentType';
import { ContentType as IContentType } from '../models';
import { PagesListener } from '../listeners';
import { PagesListener } from '../listeners/dashboard';
import { extname } from 'path';
import { Telemetry } from '../helpers/Telemetry';
export class Template {
@@ -175,6 +176,8 @@ export class Template {
Notifications.info(`Your new content has been created.`);
Telemetry.send(TelemetryEvent.createContentFromTemplate);
// Trigger a refresh for the dashboard
PagesListener.refresh();
}

View File

@@ -0,0 +1,41 @@
import * as invariant from 'invariant';
import { createAutoField } from 'uniforms';
import { PreviewImageField } from '../../panelWebView/components/Fields/PreviewImageField';
export { AutoFieldProps } from 'uniforms';
import BoolField from './BoolField';
import DateField from './DateField';
import ListField from './ListField';
import NestField from './NestField';
import NumField from './NumField';
import RadioField from './RadioField';
import SelectField from './SelectField';
import TextField from './TextField';
const AutoField = createAutoField(props => {
if (props.allowedValues) {
return props.checkboxes && props.fieldType !== Array
? RadioField
: SelectField;
}
switch (props.fieldType) {
case Array:
return ListField;
case Boolean:
return BoolField;
case Date:
return DateField;
case Number:
return NumField;
case Object:
return NestField;
case String:
return TextField;
}
return invariant(false, 'Unsupported field type: %s', props.fieldType);
});
export default AutoField;

View File

@@ -0,0 +1,29 @@
import { ComponentType, createElement, Fragment } from 'react';
import { useForm } from 'uniforms';
import AutoField from './AutoField';
export type AutoFieldsProps = {
autoField?: ComponentType<{ name: string }>;
element?: ComponentType | string;
fields?: string[];
omitFields?: string[];
};
export default function AutoFields({
autoField = AutoField,
element = Fragment,
fields,
omitFields = [],
...props
}: AutoFieldsProps) {
const { schema } = useForm();
return createElement(
element,
props,
(fields ?? schema.getSubfields())
.filter(field => !omitFields.includes(field))
.map(field => createElement(autoField, { key: field, name: field })),
);
}

View File

@@ -0,0 +1,13 @@
import { AutoForm } from 'uniforms';
import ValidatedQuickForm from './ValidatedQuickForm';
function Auto(parent: any) {
class _ extends AutoForm.Auto(parent) {
static Auto = Auto;
}
return _ as unknown as AutoForm;
}
export default Auto(ValidatedQuickForm);

View File

@@ -0,0 +1,13 @@
import { BaseForm } from 'uniforms';
function Unstyled(parent: any) {
class _ extends parent {
static Unstyled = Unstyled;
static displayName = `Unstyled${parent.displayName}`;
}
return _ as unknown as typeof BaseForm;
}
export default Unstyled(BaseForm);

View File

@@ -0,0 +1,52 @@
.field__toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.field__toggle input {
opacity: 0;
width: 0;
height: 0;
}
.field__toggle__slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--frontmatter-toggle-secondaryBackground, var(--vscode-button-secondaryBackground));
-webkit-transition: .4s;
transition: .4s;
border-radius: 34px;
}
.field__toggle__slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .field__toggle__slider {
background-color: var(--frontmatter-toggle-background, var(--vscode-button-background));
}
input:focus + .field__toggle__slider {
box-shadow: 0 0 1px var(--frontmatter-toggle-background, var(--vscode-button-background));
}
input:checked + .field__toggle__slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import './BoolField.css';
import { LabelField } from './LabelField';
export type BoolFieldProps = HTMLFieldProps<
boolean,
HTMLDivElement,
{ inputRef?: Ref<HTMLInputElement> }
>;
function Bool({
disabled,
id,
inputRef,
label,
name,
onChange,
readOnly,
value,
...props
}: BoolFieldProps) {
return (
<div {...filterDOMProps(props)}>
<LabelField label={label} id={id} required={props.required} />
<label className="field__toggle">
<input
checked={value || false}
disabled={disabled}
id={id}
name={name}
onChange={() => !disabled && !readOnly && onChange(!value)}
ref={inputRef}
type="checkbox"
/>
<span className="field__toggle__slider"></span>
</label>
</div>
);
}
export default connectField<BoolFieldProps>(Bool, { kind: 'leaf' });

View File

@@ -0,0 +1,57 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
/* istanbul ignore next */
const DateConstructor = (typeof global === 'object' ? global : window).Date;
const dateFormat = (value?: Date) => value?.toISOString().slice(0, -8);
export type DateFieldProps = HTMLFieldProps<
Date,
HTMLDivElement,
{ inputRef?: Ref<HTMLInputElement>; max?: Date; min?: Date }
>;
function Date({
disabled,
id,
inputRef,
label,
max,
min,
name,
onChange,
placeholder,
readOnly,
value,
...props
}: DateFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id}>{label}</label>}
<input
disabled={disabled}
id={id}
max={dateFormat(max)}
min={dateFormat(min)}
name={name}
onChange={event => {
const date = new DateConstructor(event.target.valueAsNumber);
if (date.getFullYear() < 10000) {
onChange(date);
} else if (isNaN(event.target.valueAsNumber)) {
onChange(undefined);
}
}}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
type="datetime-local"
value={dateFormat(value) ?? ''}
/>
</div>
);
}
export default connectField<DateFieldProps>(Date, { kind: 'leaf' });

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import { HTMLProps } from 'react';
import { Override, connectField, filterDOMProps } from 'uniforms';
export type ErrorFieldProps = Override<
Omit<HTMLProps<HTMLDivElement>, 'onChange'>,
{ error?: any; errorMessage?: string }
>;
function Error({ children, error, errorMessage, ...props }: ErrorFieldProps) {
return !error ? null : (
<div {...filterDOMProps(props)}>{children || errorMessage}</div>
);
}
export default connectField<ErrorFieldProps>(Error, {
initialValue: false,
kind: 'leaf',
});

View File

@@ -0,0 +1,18 @@
.autoform-error {
background-color: var(--frontmatter-error-background, var(--vscode-inputValidation-errorBackground));
border: 1px solid var(--frontmatter-error-border, var(--vscode-inputValidation-errorBorder));
border-radius: 2px;
margin: 20px 0px;
padding: 10px;
color: var(--frontmatter-error-foreground, var(--vscode-editor-foreground));
ul {
margin-bottom: 0;
}
li {
text-transform: capitalize;
}
}

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import { HTMLProps } from 'react';
import { filterDOMProps, useForm } from 'uniforms';
import './ErrorsField.css';
export type ErrorsFieldProps = HTMLProps<HTMLDivElement>;
export default function ErrorsField(props: ErrorsFieldProps) {
const { error, schema } = useForm();
return !error && !props.children ? null : (
<div className='autoform-error'>
<div {...filterDOMProps(props)}>
{props.children}
<ul>
{schema.getErrorMessages(error).map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import { HTMLProps, Ref, useEffect } from 'react';
import { Override, filterDOMProps, useField } from 'uniforms';
export type HiddenFieldProps = Override<
HTMLProps<HTMLInputElement>,
{
inputRef?: Ref<HTMLInputElement>;
name: string;
noDOM?: boolean;
value?: any;
}
>;
export default function HiddenField({ value, ...rawProps }: HiddenFieldProps) {
const props = useField(rawProps.name, rawProps, { initialValue: false })[0];
useEffect(() => {
if (value !== undefined && value !== props.value) {
props.onChange(value);
}
});
return props.noDOM ? null : (
<input
disabled={props.disabled}
name={props.name}
readOnly={props.readOnly}
ref={props.inputRef}
type="hidden"
value={value ?? props.value ?? ''}
{...filterDOMProps(props)}
/>
);
}

View File

@@ -0,0 +1,14 @@
.autoform__label {
display: block;
margin-bottom: 0.5rem;
margin-top: 0.5rem;
line-height: 16px;
font-weight: bold;
.autoform__label__required {
color: var(--vscode-inputValidation-errorBorder);
margin-left: 0.25rem;
}
}

View File

@@ -0,0 +1,20 @@
import * as React from 'react';
import { ReactNode } from 'react';
import './LabelField.css';
export interface ILabelFieldProps {
id: string;
label: string | ReactNode;
required?: boolean;
}
export const LabelField: React.FunctionComponent<ILabelFieldProps> = ({ label, id, required }: React.PropsWithChildren<ILabelFieldProps>) => {
return (
label ? (
<label className="autoform__label" htmlFor={id}>
{label}
{required && <span title='Required field' className='autoform__label__required'>*</span>}
</label>
) : null
);
};

View File

@@ -0,0 +1,21 @@
.autoform__list_add_field {
display: flex;
padding: 5px;
border: 1px dashed var(--frontmatter-field-border, var(--vscode-editor-foreground));
width: 100%;
justify-content: center;
margin-top: .5rem;
&:hover {
border-color: var(--frontmatter-field-borderActive, var(--vscode-button-background));
color: var(--frontmatter-field-borderActive, var(--vscode-button-background));
cursor: pointer;
}
svg {
height: 1rem;
width: 1rem;
}
}

View File

@@ -0,0 +1,63 @@
import { PlusIcon } from '@heroicons/react/outline';
import * as React from 'react';
import {
HTMLFieldProps,
connectField,
filterDOMProps,
joinName,
useField,
} from 'uniforms';
import './ListAddField.css';
export type ListAddFieldProps = HTMLFieldProps<
unknown,
HTMLSpanElement,
{ initialCount?: number }
>;
function ListAdd({
disabled,
initialCount,
name,
readOnly,
value,
...props
}: ListAddFieldProps) {
const nameParts = joinName(null, name);
const parentName = joinName(nameParts.slice(0, -1));
const parent = useField<
{ initialCount?: number; maxCount?: number },
unknown[]
>(parentName, { initialCount }, { absoluteName: true })[0];
const limitNotReached =
!disabled && !(parent.maxCount! <= parent.value!.length);
function onAction(event: React.KeyboardEvent | React.MouseEvent) {
if (
limitNotReached &&
!readOnly &&
(!('key' in event) || event.key === 'Enter')
) {
parent.onChange(parent.value!.concat([Object.assign({}, value)]));
}
}
return (
<span
className='autoform__list_add_field'
{...filterDOMProps(props as any)}
onClick={onAction}
onKeyDown={onAction}
role="button"
tabIndex={0}
>
<PlusIcon />
</span>
);
}
export default connectField<ListAddFieldProps>(ListAdd, {
initialValue: false,
kind: 'leaf',
});

View File

@@ -0,0 +1,27 @@
.autoform__list_del_field {
display: flex;
width: 100%;
justify-content: center;
margin-top: .5rem;
&:hover {
border-color: var(--vscode-button-background);
color: var(--vscode-button-background);
cursor: pointer;
}
.line {
height: 1px;
background: var(--frontmatter-list-border, var(--vscode-editor-foreground));
width: 100%;
margin-right: .5rem;
margin-top: .5rem;
}
svg {
height: 1.25rem;
width: 1.25rem;
}
}

View File

@@ -0,0 +1,63 @@
import { TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import {
HTMLFieldProps,
connectField,
filterDOMProps,
joinName,
useField,
} from 'uniforms';
import './ListDelField.css';
export type ListDelFieldProps = HTMLFieldProps<unknown, HTMLSpanElement>;
function ListDel({ disabled, name, readOnly, ...props }: ListDelFieldProps) {
const nameParts = joinName(null, name);
const nameIndex = +nameParts[nameParts.length - 1];
const parentName = joinName(nameParts.slice(0, -1));
const parent = useField<{ minCount?: number }, unknown[]>(
parentName,
{},
{ absoluteName: true },
)[0];
const limitNotReached =
!disabled && !(parent.minCount! >= parent.value!.length);
function onAction(
event:
| React.KeyboardEvent<HTMLSpanElement>
| React.MouseEvent<HTMLSpanElement, MouseEvent>,
) {
if (
limitNotReached &&
!readOnly &&
(!('key' in event) || event.key === 'Enter')
) {
const value = parent.value!.slice();
value.splice(nameIndex, 1);
parent.onChange(value);
}
}
return (
<span
className='autoform__list_del_field'
{...filterDOMProps(props)}
onClick={onAction}
onKeyDown={onAction}
role="button"
tabIndex={0}
>
<div className='line'></div>
<TrashIcon />
</span>
);
}
export default connectField<ListDelFieldProps>(ListDel, {
initialValue: false,
kind: 'leaf',
});

View File

@@ -0,0 +1,8 @@
.autoform__list_field {
margin-bottom: 1rem;
margin-top: 1rem;
padding: 10px;
border: 1px solid var(--frontmatter-list-border, rgba(255, 255, 255, 0.2));
}

View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import { Children, cloneElement, isValidElement } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import ListAddField from './ListAddField';
import ListItemField from './ListItemField';
import './ListField.css';
import { LabelField } from './LabelField';
export type ListFieldProps = HTMLFieldProps<
unknown[],
HTMLDivElement,
{ initialCount?: number; itemProps?: object }
>;
function List({
children = <ListItemField name="$" />,
initialCount,
itemProps,
label,
value,
...props
}: ListFieldProps) {
return (
<div className="autoform__list_field" {...filterDOMProps(props)}>
<LabelField label={label} id={props.id} required={props.required} />
{value?.map((item, itemIndex) =>
Children.map(children, (child, childIndex) =>
isValidElement(child)
? cloneElement(child, {
key: `${itemIndex}-${childIndex}`,
name: (child.props.name || "").replace('$', '' + itemIndex),
...itemProps,
})
: child,
),
)}
<ListAddField initialCount={initialCount} name="$" />
</div>
);
}
export default connectField<ListFieldProps>(List);

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { connectField } from 'uniforms';
import AutoField from './AutoField';
import ListDelField from './ListDelField';
export type ListItemFieldProps = { children?: ReactNode; value?: unknown };
function ListItem({
children = <AutoField label={null} name="" />,
}: ListItemFieldProps) {
return (
<div>
<ListDelField name="" />
{children}
</div>
);
}
export default connectField<ListItemFieldProps>(ListItem, {
initialValue: false,
});

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
export type LongTextFieldProps = HTMLFieldProps<
string,
HTMLDivElement,
{ inputRef?: Ref<HTMLTextAreaElement> }
>;
function LongText({
disabled,
id,
inputRef,
label,
name,
onChange,
placeholder,
readOnly,
value,
...props
}: LongTextFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id}>{label}</label>}
<textarea
disabled={disabled}
id={id}
name={name}
onChange={event => onChange(event.target.value)}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
value={value ?? ''}
/>
</div>
);
}
export default connectField<LongTextFieldProps>(LongText, { kind: 'leaf' });

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import AutoField from './AutoField';
import { LabelField } from './LabelField';
export type NestFieldProps = HTMLFieldProps<
object,
HTMLDivElement,
{ itemProps?: object }
>;
function Nest({
children,
fields,
itemProps,
label,
...props
}: NestFieldProps) {
return (
<div {...filterDOMProps(props)}>
<LabelField label={label} id={props.id} required={props.required} />
{children ||
fields.map(field => (
<AutoField key={field} name={field} {...itemProps} />
))}
</div>
);
}
export default connectField<NestFieldProps>(Nest);

View File

@@ -0,0 +1,54 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelField } from './LabelField';
export type NumFieldProps = HTMLFieldProps<
number,
HTMLDivElement,
{ decimal?: boolean; inputRef?: Ref<HTMLInputElement> }
>;
function Num({
decimal,
disabled,
id,
inputRef,
label,
max,
min,
name,
onChange,
placeholder,
readOnly,
step,
value,
...props
}: NumFieldProps) {
return (
<div {...filterDOMProps(props)}>
<LabelField label={label} id={id} required={props.required} />
<input
disabled={disabled}
id={id}
max={max}
min={min}
name={name}
onChange={event => {
const parse = decimal ? parseFloat : parseInt;
const value = parse(event.target.value);
onChange(isNaN(value) ? undefined : value);
}}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
step={step || (decimal ? 0.01 : 1)}
type="number"
value={value ?? ''}
/>
</div>
);
}
export default connectField<NumFieldProps>(Num, { kind: 'leaf' });

View File

@@ -0,0 +1,28 @@
import { QuickForm } from 'uniforms';
import AutoField from './AutoField';
import BaseForm from './BaseForm';
import ErrorsField from './ErrorsField';
import SubmitField from './SubmitField';
function Quick(parent: any) {
class _ extends QuickForm.Quick(parent) {
static Quick = Quick;
getAutoField() {
return AutoField;
}
getErrorsField() {
return ErrorsField;
}
getSubmitField() {
return SubmitField;
}
}
return _ as unknown as QuickForm;
}
export default Quick(BaseForm);

View File

@@ -0,0 +1,62 @@
import omit = require('lodash.omit');
import * as React from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelField } from './LabelField';
const base64: typeof btoa =
typeof btoa === 'undefined'
? /* istanbul ignore next */ x => Buffer.from(x).toString('base64')
: btoa;
const escape = (x: string) => base64(encodeURIComponent(x)).replace(/=+$/, '');
export type RadioFieldProps = HTMLFieldProps<
string,
HTMLDivElement,
{
allowedValues?: string[];
checkboxes?: boolean;
transform?: (value: string) => string;
}
>;
function Radio({
allowedValues,
disabled,
id,
label,
name,
onChange,
readOnly,
transform,
value,
...props
}: RadioFieldProps) {
return (
<div {...omit(filterDOMProps(props), ['checkboxes'])}>
<LabelField label={label} id={id} required={props.required} />
{allowedValues?.map(item => (
<div key={item}>
<input
checked={item === value}
disabled={disabled}
id={`${id}-${escape(item)}`}
name={name}
onChange={() => {
if (!readOnly) {
onChange(item);
}
}}
type="radio"
/>
<label htmlFor={`${id}-${escape(item)}`}>
{transform ? transform(item) : item}
</label>
</div>
))}
</div>
);
}
export default connectField<RadioFieldProps>(Radio, { kind: 'leaf' });

View File

@@ -0,0 +1,5 @@
.autoform__select_field {
color: var(--frontmatter-select-foreground, var(--vscode-editor-foreground));
}

View File

@@ -0,0 +1,110 @@
import xor = require('lodash.xor');
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelField } from './LabelField';
import './SelectField.css';
const base64: typeof btoa =
typeof btoa === 'undefined'
? /* istanbul ignore next */ x => Buffer.from(x).toString('base64')
: btoa;
const escape = (x: string) => base64(encodeURIComponent(x)).replace(/=+$/, '');
export type SelectFieldProps = HTMLFieldProps<
string | string[],
HTMLDivElement,
{
allowedValues?: string[];
checkboxes?: boolean;
disableItem?: (value: string) => boolean;
inputRef?: Ref<HTMLSelectElement>;
transform?: (value: string) => string;
}
>;
function Select({
allowedValues,
checkboxes,
disabled,
fieldType,
id,
inputRef,
label,
name,
onChange,
placeholder,
readOnly,
required,
disableItem,
transform,
value,
...props
}: SelectFieldProps) {
const multiple = fieldType === Array;
return (
<div className='autoform__select_field' {...filterDOMProps(props)}>
<LabelField label={label} id={id} required={required} />
{checkboxes ? (
allowedValues!.map(item => (
<div key={item}>
<input
checked={
fieldType === Array ? value!.includes(item) : value === item
}
disabled={disableItem?.(item) ?? disabled}
id={`${id}-${escape(item)}`}
name={name}
onChange={() => {
if (!readOnly) {
onChange(fieldType === Array ? xor([item], value) : item);
}
}}
type="checkbox"
/>
<label htmlFor={`${id}-${escape(item)}`}>
{transform ? transform(item) : item}
</label>
</div>
))
) : (
<select
disabled={disabled}
id={id}
multiple={multiple}
name={name}
onChange={event => {
if (!readOnly) {
const item = event.target.value;
if (multiple) {
const clear = event.target.selectedIndex === -1;
onChange(clear ? [] : xor([item], value));
} else {
onChange(item !== '' ? item : undefined);
}
}
}}
ref={inputRef}
value={value ?? ''}
style={{ width: "100%", padding: "0.5rem" }}
>
{(!!placeholder || !required || value === undefined) && !multiple && (
<option value="" disabled={required} hidden={required}>
{placeholder || label}
</option>
)}
{allowedValues?.map(value => (
<option disabled={disableItem?.(value)} key={value} value={value}>
{transform ? transform(value) : value}
</option>
))}
</select>
)}
</div>
);
}
export default connectField<SelectFieldProps>(Select, { kind: 'leaf' });

View File

@@ -0,0 +1,29 @@
import { HTMLProps, Ref } from 'react';
import * as React from 'react';
import { Override, filterDOMProps, useForm } from 'uniforms';
export type SubmitFieldProps = Override<
HTMLProps<HTMLInputElement>,
{ inputRef?: Ref<HTMLInputElement>; value?: string }
>;
export default function SubmitField({
disabled,
inputRef,
readOnly,
value,
...props
}: SubmitFieldProps) {
const { error, state } = useForm();
return (
<input
disabled={disabled === undefined ? !!(error || state.disabled) : disabled}
readOnly={readOnly}
ref={inputRef}
type="submit"
{...(value ? { value } : {})}
{...filterDOMProps(props)}
/>
);
}

View File

@@ -0,0 +1,49 @@
import { Ref } from 'react';
import * as React from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelField } from './LabelField';
export type TextFieldProps = HTMLFieldProps<
string,
HTMLDivElement,
{ inputRef?: Ref<HTMLInputElement> }
>;
function Text({
autoComplete,
disabled,
id,
inputRef,
label,
name,
onChange,
placeholder,
readOnly,
type,
value,
...props
}: TextFieldProps) {
return (
<div {...filterDOMProps(props)}>
<LabelField label={label} id={id} required={props.required} />
<input
autoComplete={autoComplete}
disabled={disabled}
id={id}
name={name}
onChange={event => onChange(event.target.value)}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
type={type}
value={value ?? ''}
/>
</div>
);
}
Text.defaultProps = { type: 'text' };
export default connectField<TextFieldProps>(Text, { kind: 'leaf' });

View File

@@ -0,0 +1,13 @@
import { ValidatedForm } from 'uniforms';
import BaseForm from './BaseForm';
function Validated(parent: any) {
class _ extends ValidatedForm.Validated(parent) {
static Validated = Validated;
}
return _ as unknown as ValidatedForm;
}
export default Validated(BaseForm);

View File

@@ -0,0 +1,5 @@
import BaseForm from './BaseForm';
import QuickForm from './QuickForm';
import ValidatedForm from './ValidatedForm';
export default ValidatedForm.Validated(QuickForm.Quick(BaseForm));

View File

@@ -0,0 +1,23 @@
export { default as AutoField, AutoFieldProps } from './AutoField';
export { default as AutoFields, AutoFieldsProps } from './AutoFields';
export { default as AutoForm } from './AutoForm';
export { default as BaseForm } from './BaseForm';
export { default as BoolField, BoolFieldProps } from './BoolField';
export { default as DateField, DateFieldProps } from './DateField';
export { default as ErrorField, ErrorFieldProps } from './ErrorField';
export { default as ErrorsField, ErrorsFieldProps } from './ErrorsField';
export { default as HiddenField, HiddenFieldProps } from './HiddenField';
export { default as ListAddField, ListAddFieldProps } from './ListAddField';
export { default as ListDelField, ListDelFieldProps } from './ListDelField';
export { default as ListField, ListFieldProps } from './ListField';
export { default as ListItemField, ListItemFieldProps } from './ListItemField';
export { default as LongTextField, LongTextFieldProps } from './LongTextField';
export { default as NestField, NestFieldProps } from './NestField';
export { default as NumField, NumFieldProps } from './NumField';
export { default as QuickForm } from './QuickForm';
export { default as RadioField, RadioFieldProps } from './RadioField';
export { default as SelectField, SelectFieldProps } from './SelectField';
export { default as SubmitField, SubmitFieldProps } from './SubmitField';
export { default as TextField, TextFieldProps } from './TextField';
export { default as ValidatedForm } from './ValidatedForm';
export { default as ValidatedQuickForm } from './ValidatedQuickForm';

View File

@@ -0,0 +1,28 @@
export const TelemetryEvent = {
activate: 'activate',
initialization: 'initialization',
openContentDashboard: 'openContentDashboard',
openMediaDashboard: 'openMediaDashboard',
openDataDashboard: 'openDataDashboard',
closeDashboard: 'closeDashboard',
generateSlug: 'generateSlug',
createContentFromTemplate: 'createContentFromTemplate',
createContentFromContentType: 'createContentFromContentType',
registerFolder: 'registerFolder',
unregisterFolder: 'unregisterFolder',
addMediaFolder: 'addMediaFolder',
promoteSettings: 'promoteSettings',
openPreview: 'openPreview',
uploadMedia: 'uploadMedia',
refreshMedia: 'refreshMedia',
deleteMedia: 'deleteMedia',
insertMediaToContent: 'insertMediaToContent',
updateMediaMetadata: 'updateMediaMetadata',
openExplorerView: 'openExplorerView',
// Webviews
webviewWelcomeScreen: 'webviewWelcomeScreen',
webviewMediaView: 'webviewMediaView',
webviewDataView: 'webviewDataView',
webviewContentsView: 'webviewContentsView',
};

View File

@@ -1,10 +1,13 @@
export * from './ContentType';
export * from './DefaultFields';
export * from './DefaultFileTypes';
export * from './Extension';
export * from './ExtensionState';
export * from './FrameworkDetectors';
export * from './Links';
export * from './LocalStore';
export * from './Navigation';
export * from './TelemetryEvent';
export * from './charMap';
export * from './context';
export * from './settings';

View File

@@ -7,6 +7,7 @@ export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
export const SETTING_TAXONOMY_TAGS = "taxonomy.tags";
export const SETTING_TAXONOMY_CATEGORIES = "taxonomy.categories";
export const SETTING_TAXONOMY_CUSTOM = "taxonomy.customTaxonomy";
export const SETTING_TAXONOMY_FIELD_GROUPS = "taxonomy.fieldGroups";
export const SETTING_DATE_FORMAT = "taxonomy.dateFormat";
export const SETTING_COMMA_SEPARATED_FIELDS = "taxonomy.commaSeparatedFields";
@@ -32,6 +33,8 @@ export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
export const SETTING_TEMPLATES_FOLDER = "templates.folder";
export const SETTING_TEMPLATES_PREFIX = "templates.prefix";
export const SETTING_TELEMETRY_DISABLE = "telemetry.disable";
export const SETTING_PANEL_FREEFORM = "panel.freeform";
export const SETTING_PREVIEW_HOST = "preview.host";
@@ -61,6 +64,8 @@ export const SETTINGS_DATA_FILES = "data.files";
export const SETTINGS_DATA_FOLDERS = "data.folders";
export const SETTINGS_DATA_TYPES = "data.types";
export const SETTINGS_FILE_PRESERVE_CASING = "file.preserveCasing";
export const SETTINGS_FRAMEWORK_ID = "framework.id";
export const SETTINGS_FRAMEWORK_START = "framework.startCommand";

View File

@@ -24,4 +24,5 @@ export enum DashboardMessage {
runCustomScript = 'runCustomScript',
getDataEntries = 'getDataEntries',
putDataEntries = 'putDataEntries',
sendTelemetry = 'sendTelemetry',
}

View File

@@ -7,6 +7,10 @@ import { Overview } from './Overview';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
import usePages from '../../hooks/usePages';
import { useEffect } from 'react';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { TelemetryEvent } from '../../../constants';
export interface IContentsProps {
pages: Page[];
@@ -19,6 +23,12 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loadin
const pageFolders = [...new Set(pageItems.map(page => page.fmFolder))];
useEffect(() => {
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewContentsView
});
}, []);
return (
<div className="flex flex-col h-full overflow-auto">
<Header

View File

@@ -3,21 +3,18 @@ import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models/Page';
import { SettingsSelector, ViewSelector } from '../../state';
import { ViewSelector } from '../../state';
import { DateField } from '../DateField';
import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
import useContentType from '../../../hooks/useContentType';
import { DashboardViewType } from '../../models';
export interface IItemProps extends Page {}
const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
const view = useRecoilValue(ViewSelector);
const settings = useRecoilValue(SettingsSelector);
const contentType = useContentType(settings, { type });
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
const openFile = () => {
Messenger.send(DashboardMessage.openFile, fmFilePath);
@@ -30,8 +27,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
{
previewField && pageData[previewField] ? (
<img src={`${pageData[previewField]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
pageData[PREVIEW_IMAGE_FIELD] ? (
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={title} 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`} />

View File

@@ -2,7 +2,8 @@ import * as React from 'react';
import Ajv from 'ajv';
import { useEffect, useState } from 'react';
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';
import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
import { AutoFields, AutoForm, ErrorsField } from '../../../components/uniforms-frontmatter';
// import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
import { ErrorBoundary } from '@sentry/react';
import { DataFormControls } from './DataFormControls';
@@ -52,7 +53,7 @@ export const DataForm: React.FunctionComponent<IDataFormProps> = ({ schema, mode
schema={bridge}
model={model || {}}
onSubmit={onSubmit}
ref={form => form?.reset()}>
ref={(form: any) => form?.reset()}>
<div className={`fields`}>
<AutoFields />
</div>

View File

@@ -19,6 +19,7 @@ import { ChevronRightIcon } from '@heroicons/react/outline';
import { ToastContainer, toast, Slide } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { DataType } from '../../../models/DataType';
import { TelemetryEvent } from '../../../constants';
export interface IDataViewProps {}
@@ -102,6 +103,10 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
useEffect(() => {
Messenger.listen(messageListener);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewDataView
});
return () => {
Messenger.unlisten(messageListener);

View File

@@ -3,4 +3,4 @@ import { SortableContainer } from 'react-sortable-hoc';
export interface ISortableContainerProps {}
export const Container = SortableContainer(({ children }: React.PropsWithChildren<ISortableContainerProps>) => ( <ul className={`-mx-4 divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>{children}</ul> ));
export const Container = SortableContainer(({ children }: React.PropsWithChildren<ISortableContainerProps>) => ( <ul className={`-mx-4 divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>{children}</ul>));

View File

@@ -23,8 +23,10 @@ export const SortableItem = SortableElement(({ value, selectedIndex, crntIndex,
return (
<>
<li data-test={`${selectedIndex}-${crntIndex}`} className={`py-2 px-2 w-full flex justify-between content-center hover:bg-gray-200 dark:hover:bg-vulcan-400 ${selectedIndex === crntIndex ? `bg-gray-300 dark:bg-vulcan-300` : ``}`}>
<div className='flex items-center'>
<li data-test={`${selectedIndex}-${crntIndex}`} className={`sortable_item py-2 px-2 w-full flex justify-between content-center hover:bg-gray-200 dark:hover:bg-vulcan-400 ${selectedIndex === crntIndex ? `bg-gray-300 dark:bg-vulcan-300` : ``}`}>
<div
className='flex items-center w-full'
onClick={() => onSelectedIndexChange(crntIndex)}>
<DragHandle />
<span>{value}</span>
</div>

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
@@ -81,6 +81,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
alt: alt || "",
caption: caption || ""
});
@@ -94,6 +95,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
snippet = snippet?.replace("{alt}", alt || "");
snippet = snippet?.replace("{caption}", caption || "");
snippet = snippet?.replace("{filename}", basename(relPath || ""));
snippet = snippet?.replace("{mediaWidth}", media?.dimensions?.width?.toString() || "");
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",

View File

@@ -1,11 +1,8 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import {UploadIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
import { DashboardCommand } from '../../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { useRecoilValue } from 'recoil';
import { LoadingAtom, MediaFoldersAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { Header } from '../Header';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
@@ -13,11 +10,12 @@ import { Item } from './Item';
import { Lightbox } from './Lightbox';
import { List } from './List';
import { useDropzone } from 'react-dropzone'
import { useCallback } from 'react';
import { useCallback, useEffect } 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';
export interface IMediaProps {}
@@ -46,6 +44,12 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
});
}, [selectedFolder]);
useEffect(() => {
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewMediaView
});
}, []);
const {getRootProps, isDragActive} = useDropzone({
onDrop,
accept: 'image/*'

View File

@@ -1,6 +1,6 @@
import {HeartIcon, StarIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants';
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK, TelemetryEvent } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { FrontMatterIcon } from '../../panelWebView/components/Icons/FrontMatterIcon';
import { GitHubIcon } from '../../panelWebView/components/Icons/GitHubIcon';
@@ -15,10 +15,15 @@ export interface IWelcomeScreenProps {
export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({settings}: React.PropsWithChildren<IWelcomeScreenProps>) => {
React.useEffect(() => {
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewWelcomeScreen
});
return () => {
Messenger.send(DashboardMessage.reload)
};
}, ['']);
}, []);
return (
<div className={`h-full overflow-auto py-24`}>

View File

@@ -100,12 +100,12 @@ export default function usePages(pages: Page[]) {
// Filter by tag
if (tag) {
pagesSorted = pagesSorted.filter(page => page.tags && page.tags.includes(tag));
pagesSorted = pagesSorted.filter(page => page.fmTags && page.fmTags.includes(tag));
}
// Filter by category
if (category) {
pagesSorted = pagesSorted.filter(page => page.categories && page.categories.includes(category));
pagesSorted = pagesSorted.filter(page => page.fmCategories && page.fmCategories.includes(category));
}
setPageItems(pagesSorted);

View File

@@ -7,6 +7,9 @@ export interface Page {
fmModified: number;
fmDraft: "Draft" | "Published",
fmYear: number | null | undefined;
fmPreviewImage: string;
fmTags: string[];
fmCategories: string[];
title: string;
slug: string;

View File

@@ -2,6 +2,40 @@
@import "tailwindcss/components";
@import "tailwindcss/utilities";
:root {
/* Bool field */
--frontmatter-toggle-background: #15c2cb;
--frontmatter-toggle-secondaryBackground: #ADADAD;
/* Errors field */
--frontmatter-error-background: rgba(255, 85, 0, 0.2);
--frontmatter-error-border: #f50;
--frontmatter-error-foreground: #0e131f;
.vscode-dark {
--frontmatter-error-foreground: #fff;
}
/* List add field */
--frontmatter-field-border: #222733;
--frontmatter-field-borderActive: #15c2cb;
.vscode-dark {
--frontmatter-field-border: rgba(255, 255, 255, 0.5);
--frontmatter-field-borderActive: #009aa3;
}
/* List field */
--frontmatter-list-border: #ADADAD;
.vscode-dark {
--frontmatter-list-border: #404551;
}
/* Select field */
--frontmatter-select-foreground: #0e131f;
}
.loader {
border-top-color: #15c2cb;
@@ -80,15 +114,11 @@
.errors {
> div {
@apply border border-red-400 !important;
@apply border;
}
ul {
@apply list-disc pl-6 pr-4 py-4 bg-opacity-50 text-vulcan-500;
}
li {
@apply capitalize text-gray-900;
@apply list-disc pl-6 pr-4 py-4 bg-opacity-50;
}
}
@@ -276,12 +306,6 @@
input[type="submit"] {
@apply text-vulcan-500
}
.errors {
li {
@apply text-white;
}
}
}
.ant-list.ant-list-bordered {

View File

@@ -1,30 +1,12 @@
import { DashboardData } from '../models/DashboardData';
import { Template } from '../commands/Template';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CUSTOM, CONTEXT, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START } from '../constants';
import * as os from 'os';
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv, ThemeIcon } from "vscode";
import { ArticleHelper, Settings } from "../helpers";
import { ArticleListener, ExtensionListener, MediaListener, ScriptListener, TaxonomyListener, DataListener, SettingsListener } from './../listeners/panel';
import { TelemetryEvent } from '../constants';
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window } from "vscode";
import { Logger, Settings } from "../helpers";
import { Command } from "../panelWebView/Command";
import { CommandToCode } from '../panelWebView/CommandToCode';
import { Article } from '../commands';
import { TagType } from '../panelWebView/TagType';
import { CustomTaxonomyData, DraftField, Field, ScriptType, TaxonomyType } from '../models';
import { exec } from 'child_process';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { Content } from 'mdast';
import { COMMAND_NAME, EXTENSION_BETA_ID, EXTENSION_ID } from '../constants/Extension';
import { Folders } from '../commands/Folders';
import { Preview } from '../commands/Preview';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { WebviewHelper } from '@estruyf/vscode';
import { Extension } from '../helpers/Extension';
import { Dashboard } from '../commands/Dashboard';
import { ImageHelper } from '../helpers/ImageHelper';
import { CustomScript } from '../helpers/CustomScript';
import { Link, Parent } from 'mdast-util-from-markdown/lib';
const FILE_LIMIT = 10;
import { Telemetry } from '../helpers/Telemetry';
export class ExplorerView implements WebviewViewProvider, Disposable {
public static readonly viewType = "frontMatter.explorer";
@@ -63,6 +45,10 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
}
}
public getWebview() {
return this.panel?.webview;
}
/**
* Default resolve webview panel
* @param webviewView
@@ -85,192 +71,43 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
);
webviewView.webview.onDidReceiveMessage(async (msg) => {
switch(msg.command) {
case CommandToCode.getData:
this.getSettings();
this.getFoldersAndFiles();
this.getFileData();
break;
case CommandToCode.updateSlug:
Article.generateSlug();
break;
case CommandToCode.updateLastMod:
Article.setLastModifiedDate();
break;
case CommandToCode.publish:
Article.toggleDraft();
break;
case CommandToCode.updateTags:
this.updateTags(TagType.tags, msg.data?.values || [], msg.data?.parents || []);
break;
case CommandToCode.updateCategories:
this.updateTags(TagType.categories, msg.data?.values || [], msg.data?.parents || []);
break;
case CommandToCode.updateKeywords:
this.updateTags(TagType.keywords, msg.data?.values || [], msg.data?.parents || []);
break;
case CommandToCode.updateCustomTaxonomy:
this.updateCustomTaxonomy(msg.data);
break;
case CommandToCode.addTagToSettings:
this.addTags(TagType.tags, msg.data);
break;
case CommandToCode.addCategoryToSettings:
this.addTags(TagType.categories, msg.data);
break;
case CommandToCode.addToCustomTaxonomy:
this.addCustomTaxonomy(msg.data);
break;
case CommandToCode.openSettings:
const isBeta = Extension.getInstance().isBetaVersion();
commands.executeCommand('workbench.action.openSettings', `@ext:${isBeta ? EXTENSION_BETA_ID : EXTENSION_ID}`);
break;
case CommandToCode.openFile:
if (os.type() === "Linux" && vscodeEnv.remoteName?.toLowerCase() === "wsl") {
commands.executeCommand('remote-wsl.revealInExplorer');
} else {
commands.executeCommand('revealFileInOS');
}
break;
case CommandToCode.runCustomScript:
this.runCustomScript(msg);
break;
case CommandToCode.openProject:
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
const wsPath = wsFolder.fsPath;
if (os.type() === "Darwin") {
exec(`open ${wsPath}`);
} else if (os.type() === "Windows_NT") {
exec(`explorer ${wsPath}`);
} else if (os.type() === "Linux" && vscodeEnv.remoteName?.toLowerCase() === "wsl") {
exec('explorer.exe `wslpath -w "$PWD"`');
} else {
exec(`xdg-open ${wsPath}`);
}
}
break;
case CommandToCode.initProject:
await commands.executeCommand(COMMAND_NAME.init);
this.getSettings();
break;
case CommandToCode.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case CommandToCode.createTemplate:
await commands.executeCommand(COMMAND_NAME.createTemplate);
break;
case CommandToCode.updateModifiedUpdating:
this.updateSetting(SETTING_AUTO_UPDATE_DATE, msg.data || false);
break;
case CommandToCode.toggleWritingSettings:
this.toggleWritingSettings();
break;
case CommandToCode.updateFmHighlight:
this.updateSetting(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false);
break;
case CommandToCode.toggleCenterMode:
await commands.executeCommand(`workbench.action.toggleCenteredLayout`);
break;
case CommandToCode.openPreview:
await commands.executeCommand(COMMAND_NAME.preview);
break;
case CommandToCode.openDashboard:
await commands.executeCommand(COMMAND_NAME.dashboard);
break;
case CommandToCode.updatePreviewUrl:
this.updateSetting(SETTING_PREVIEW_HOST, msg.data || "");
break;
case CommandToCode.openInEditor:
openFileInEditor(msg.data);
break;
case CommandToCode.updateMetadata:
this.updateMetadata(msg.data);
break;
case CommandToCode.selectImage:
await commands.executeCommand(COMMAND_NAME.dashboard, {
type: "media",
data: msg.data
} as DashboardData);
this.getMediaSelection();
break;
case CommandToCode.frameworkCommand:
this.openTerminalWithCommand(msg.data.command);
break;
case CommandToCode.updateStartCommand:
await this.updateSetting(SETTINGS_FRAMEWORK_START, msg.data || "");
break;
}
Logger.info(`Receiving message from webview to panel: ${msg.command}`);
ArticleListener.process(msg);
DataListener.process(msg);
ExtensionListener.process(msg);
MediaListener.process(msg);
ScriptListener.process(msg);
SettingsListener.process(msg);
TaxonomyListener.process(msg);
});
webviewView.onDidChangeVisibility(() => {
if (this.visible) {
// this.getFileData();
Telemetry.send(TelemetryEvent.openExplorerView);
DataListener.getFileData();
}
});
window.onDidChangeActiveTextEditor(() => {
this.postWebviewMessage({ command: Command.loading, data: true });
this.sendMessage({ command: Command.loading, data: true });
if (this.visible) {
this.getFileData();
DataListener.getFileData();
}
}, this);
Settings.onConfigChange((global?: any) => {
this.getSettings();
SettingsListener.getSettings();
});
}
/**
* Triggers a metadata change in the panel
* @param metadata
* Post data to the panel
* @param msg
*/
public pushMetadata(metadata: any) {
const wsFolder = Folders.getWorkspaceFolder();
const filePath = window.activeTextEditor?.document.uri.fsPath;
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
const contentTypes = Settings.get<string>(SETTING_TAXONOMY_CONTENT_TYPES);
const articleDetails = this.getArticleDetails();
if (articleDetails) {
metadata.articleDetails = articleDetails;
}
let updatedMetadata = Object.assign({}, metadata);
if (commaSeparated) {
for (const key of commaSeparated) {
if (updatedMetadata[key] && typeof updatedMetadata[key] === "string") {
updatedMetadata[key] = updatedMetadata[key].split(",").map((s: string) => s.trim());
}
}
}
const keys = Object.keys(updatedMetadata);
if (keys.length > 0) {
updatedMetadata.filePath = filePath;
}
if (keys.length > 0 && contentTypes && wsFolder) {
// Get the current content type
const contentType = ArticleHelper.getContentType(updatedMetadata);
if (contentType) {
this.processImageFields(updatedMetadata, contentType.fields)
}
}
// Check slug
if (!updatedMetadata[DefaultFields.Slug]) {
const slug = Article.getSlug();
if (slug) {
updatedMetadata[DefaultFields.Slug] = slug;
}
}
this.postWebviewMessage({ command: Command.metadata, data: {
...updatedMetadata
}});
public sendMessage(msg: { command: Command, data?: any }) {
this.panel?.webview?.postMessage(msg);
}
/**
@@ -279,9 +116,9 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
*/
public triggerInputFocus(tagType: TagType) {
if (tagType === TagType.tags) {
this.postWebviewMessage({ command: Command.focusOnTags });
this.sendMessage({ command: Command.focusOnTags });
} else {
this.postWebviewMessage({ command: Command.focusOnCategories });
this.sendMessage({ command: Command.focusOnCategories });
}
}
@@ -289,395 +126,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
* Trigger all sections to close
*/
public collapseAll() {
this.postWebviewMessage({ command: Command.closeSections });
}
/**
* Update the metadata of the article
*/
public async updateMetadata({field, parents, value }: { field: string, value: any, parents?: string[], fieldData?: { multiple: boolean, value: string[] } }) {
if (!field) {
return;
}
const editor = window.activeTextEditor;
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
const contentType = ArticleHelper.getContentType(article.data);
const dateFields = contentType.fields.filter((field) => field.type === "datetime");
const imageFields = contentType.fields.filter((field) => field.type === "image" && field.multiple);
// Support multi-level fields
let parentObj = article.data;
for (const parent of parents || []) {
parentObj = parentObj[parent];
}
for (const dateField of dateFields) {
if ((field === dateField.name) && value) {
parentObj[field] = Article.formatDate(new Date(value));
} else if (!imageFields.find(f => f.name === field)) {
// Only override the field data if it is not an multiselect image field
parentObj[field] = value;
}
}
for (const imageField of imageFields) {
if (field === imageField.name) {
// If value is an array, it means it comes from the explorer view itself (deletion)
if (Array.isArray(value)) {
parentObj[field] = value || [];
} else { // Otherwise it is coming from the media dashboard (addition)
let fieldValue = parentObj[field];
if (fieldValue && !Array.isArray(fieldValue)) {
fieldValue = [fieldValue];
}
const crntData = Object.assign([], fieldValue);
const allRelPaths = [...(crntData || []), value];
parentObj[field] = [...new Set(allRelPaths)].filter(f => f);
}
}
}
ArticleHelper.update(editor, article);
this.pushMetadata(article.data);
}
/**
* Open a terminal and run the passed command
* @param command
*/
private openTerminalWithCommand(command: string) {
if (command) {
let terminal = window.activeTerminal;
if (!terminal || (terminal && terminal.state.isInteractedWith === true)) {
terminal = window.createTerminal({
name: `Starting local server`,
iconPath: new ThemeIcon('server-environment'),
message: `Starting local server`,
});
}
if (terminal) {
terminal.sendText(command);
terminal.show(false);
}
}
}
/**
* Run a custom script
* @param msg
*/
private runCustomScript(msg: { command: string, data: any}) {
const scripts: ICustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
if (msg?.data?.title && msg?.data?.script && scripts) {
const customScript = scripts.find((s: ICustomScript) => s.title === msg.data.title);
if (customScript?.script && customScript?.title) {
CustomScript.run(customScript);
}
}
}
/**
* Return the media selection
*/
public async getMediaSelection() {
this.postWebviewMessage({
command: Command.mediaSelectionData,
data: Dashboard.viewData
});
}
/**
* Retrieve the extension settings
*/
public async getSettings() {
this.postWebviewMessage({
command: Command.settings,
data: {
seo: {
title: Settings.get(SETTING_SEO_TITLE_LENGTH) as number || -1,
slug: Settings.get(SETTING_SEO_SLUG_LENGTH) as number || -1,
description: Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1,
content: Settings.get(SETTING_SEO_CONTENT_MIN_LENGTH) as number || -1,
descriptionField: Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description
},
slug: {
prefix: Settings.get(SETTING_SLUG_PREFIX) || "",
suffix: Settings.get(SETTING_SLUG_SUFFIX) || "",
updateFileName: !!Settings.get<boolean>(SETTING_SLUG_UPDATE_FILE_NAME),
},
date: {
format: Settings.get(SETTING_DATE_FORMAT)
},
tags: Settings.get(SETTING_TAXONOMY_TAGS, true) || [],
categories: Settings.get(SETTING_TAXONOMY_CATEGORIES, true) || [],
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
freeform: Settings.get(SETTING_PANEL_FREEFORM),
scripts: (Settings.get<ICustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type === ScriptType.Content || !s.type),
isInitialized: await Template.isInitialized(),
modifiedDateUpdate: Settings.get(SETTING_AUTO_UPDATE_DATE) || false,
writingSettingsEnabled: this.isWritingSettingsEnabled() || false,
fmHighlighting: Settings.get(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT),
preview: Preview.getSettings(),
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
dashboardViewData: Dashboard.viewData,
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
isBacker: await Extension.getInstance().getState<boolean | undefined>(CONTEXT.backer, 'global'),
framework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
commands: {
start: Settings.get<string>(SETTINGS_FRAMEWORK_START)
}
} as PanelSettings
});
}
/**
* Retrieve the information about the registered folders and its files
*/
public async getFoldersAndFiles() {
this.postWebviewMessage({
command: Command.folderInfo,
data: await Folders.getInfo(FILE_LIMIT) || null
});
}
/**
* Process the image fields in the content type
* @param updatedMetadata
* @param fields
* @param parents
*/
private processImageFields(updatedMetadata: any, fields: Field[], parents: string[] = []) {
const imageFields = fields.filter((field) => field.type === "image");
// Support multi-level fields
let parentObj = updatedMetadata;
for (const parent of parents || []) {
parentObj = parentObj[parent];
}
// Process image fields
if (parentObj) {
for (const field of imageFields) {
if (parentObj[field.name]) {
const imageData = ImageHelper.allRelToAbs(field, parentObj[field.name])
if (imageData) {
if (field.multiple && imageData instanceof Array) {
const preview = imageData.map(preview => preview && preview.absPath ? ({
...preview,
webviewUrl: this.panel?.webview.asWebviewUri(preview.absPath).toString()
}) : null);
parentObj[field.name] = preview || [];
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
parentObj[field.name] = {
...imageData,
webviewUrl: preview ? preview.toString() : null
};
}
} else {
parentObj[field.name] = field.multiple ? [] : "";
}
}
}
// Check if there are sub-fields to process
const subFields = fields.filter((field) => field.type === "fields");
if (subFields?.length > 0) {
for (const field of subFields) {
this.processImageFields(updatedMetadata, field.fields || [], [...parents, field.name]);
}
}
}
}
/**
* Retrieve the file its front matter
*/
private getFileData() {
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article?.data) {
this.pushMetadata(article!.data);
}
}
/**
* Update the tags in the current document
* @param tagType
* @param values
*/
private updateTags(tagType: TagType, values: string[], parents: string[]) {
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.data) {
// Support multi-level fields
let parentObj = article.data;
for (const parent of parents || []) {
parentObj = parentObj[parent];
}
parentObj[tagType.toLowerCase()] = values || [];
ArticleHelper.update(editor, article);
this.pushMetadata(article!.data);
}
}
/**
* Update the tags in the current document
* @param data
*/
private updateCustomTaxonomy(data: CustomTaxonomyData) {
if (!data?.id || !data?.name) {
return;
}
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.data) {
// Support multi-level fields
let parentObj = article.data;
for (const parent of data.parents || []) {
parentObj = parentObj[parent];
}
parentObj[data.name] = data.options || [];
ArticleHelper.update(editor, article);
this.pushMetadata(article!.data);
}
}
/**
* Add tag to the settings
* @param data
*/
private async addCustomTaxonomy(data: CustomTaxonomyData) {
if (!data?.id || !data?.option) {
return;
}
await Settings.updateCustomTaxonomy(data.id, data.option);
}
/**
* Add tag to the settings
* @param tagType
* @param value
*/
private async addTags(tagType: TagType, value: string) {
if (value) {
let options = tagType === TagType.tags ? Settings.get<string[]>(SETTING_TAXONOMY_TAGS, true) : Settings.get<string[]>(SETTING_TAXONOMY_CATEGORIES, true);
if (!options) {
options = [];
}
options.push(value);
const taxType = tagType === TagType.tags ? TaxonomyType.Tag : TaxonomyType.Category;
await Settings.updateTaxonomy(taxType, options);
}
}
/**
* Get article details
*/
private getArticleDetails() {
const baseUrl = Settings.get<string>(SETTING_SITE_BASEURL);
const editor = window.activeTextEditor;
if (!editor) {
return null;
}
if (!ArticleHelper.isMarkdownFile()) {
return null;
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.content) {
let content = article.content;
content = content.replace(/({{(.*?)}})/g, ''); // remove hugo shortcodes
const mdTree = fromMarkdown(content);
const elms: Parent[] | Link[] = this.getAllElms(mdTree);
const headings = elms.filter(node => node.type === 'heading');
const paragraphs = elms.filter(node => node.type === 'paragraph').length;
const images = elms.filter(node => node.type === 'image').length;
const links: string[] = elms.filter(node => node.type === 'link').map(node => (node as Link).url);
const internalLinks = links.filter(link => !link.startsWith('http') || (baseUrl && link.toLowerCase().includes((baseUrl || "").toLowerCase()))).length;
let externalLinks = links.filter(link => link.startsWith('http'));
if (baseUrl) {
externalLinks = externalLinks.filter(link => !link.toLowerCase().includes(baseUrl.toLowerCase()));
}
const headers = [];
for (const header of headings) {
const text = header?.children?.filter((node: any) => node.type === 'text').map((node: any) => node.value).join(" ");
if (text) {
headers.push(text);
}
}
const wordCount = this.wordCount(0, mdTree);
return {
headings: headings.length,
headingsText: headers,
paragraphs,
images,
internalLinks,
externalLinks: externalLinks.length,
wordCount,
content: article.content
};
}
return null;
}
private getAllElms(node: Content | any, allElms?: any[]): any[] {
if (!allElms) {
allElms = [];
}
if (node.children?.length > 0) {
for (const child of node.children) {
allElms.push(Object.assign({}, child));
this.getAllElms(child, allElms);
}
}
return allElms;
this.sendMessage({ command: Command.closeSections });
}
private counts(acc: any, node: any) {
@@ -691,69 +140,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
);
}
/**
* Get the word count for the current document
*/
private wordCount(count: number, node: Content | any) {
if (node.type === "text") {
return count + node.value.split(" ").length;
} else {
return (node.children || []).reduce((childCount: number, childNode: any) => this.wordCount(childCount, childNode), count);
}
}
/**
* Toggle the writing settings
*/
private async toggleWritingSettings() {
const config = workspace.getConfiguration("", { languageId: "markdown" });
const enabled = this.isWritingSettingsEnabled();
await config.update("editor.fontSize", enabled ? undefined : 14, false, true);
await config.update("editor.lineHeight", enabled ? undefined : 26, false, true);
await config.update("editor.wordWrap", enabled ? undefined : "wordWrapColumn", false, true);
await config.update("editor.wordWrapColumn", enabled ? undefined : 64, false, true);
await config.update("editor.lineNumbers", enabled ? undefined : "off", false, true);
await config.update("editor.quickSuggestions", enabled ? undefined : false, false, true);
await config.update("editor.minimap.enabled", enabled ? undefined : false, false, true);
this.getSettings();
}
/**
* Check if the writing settings are enabled
*/
private isWritingSettingsEnabled() {
const config = workspace.getConfiguration("", { languageId: "markdown" });
const fontSize = config.get("editor.fontSize");
const lineHeight = config.get("editor.lineHeight");
const wordWrap = config.get("editor.wordWrap");
const wordWrapColumn = config.get("editor.wordWrapColumn");
const lineNumbers = config.get("editor.lineNumbers");
const quickSuggestions = config.get<boolean>("editor.quickSuggestions");
return fontSize && lineHeight && wordWrap && wordWrapColumn && lineNumbers && quickSuggestions !== undefined;
}
/**
* Updates a setting and refreshes the retrieved settings
* @param setting
* @param value
*/
private async updateSetting(setting: string, value: any) {
await Settings.update(setting, value);
this.getSettings();
}
/**
* Post data to the panel
* @param msg
*/
private postWebviewMessage(msg: { command: Command, data?: any }) {
this.panel?.webview?.postMessage(msg);
}
/**
* Retrieve the webview HTML contents
* @param webView
@@ -785,7 +171,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
`script-src 'unsafe-eval' ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`font-src ${webView.cspSource}`,
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`

View File

@@ -1,3 +1,4 @@
import { Telemetry } from './helpers/Telemetry';
import { ContentType } from './helpers/ContentType';
import { Dashboard } from './commands/Dashboard';
import * as vscode from 'vscode';
@@ -6,7 +7,7 @@ import { Folders } from './commands/Folders';
import { Preview } from './commands/Preview';
import { Project } from './commands/Project';
import { Template } from './commands/Template';
import { COMMAND_NAME } from './constants';
import { COMMAND_NAME, TelemetryEvent } from './constants';
import { TaxonomyType } from './models';
import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider';
import { TagType } from './panelWebView/TagType';
@@ -18,16 +19,15 @@ import { Content } from './commands/Content';
import ContentProvider from './providers/ContentProvider';
import { Wysiwyg } from './commands/Wysiwyg';
import { Diagnostics } from './commands/Diagnostics';
import { PagesListener } from './listeners';
import { PagesListener } from './listeners/dashboard';
import { Backers } from './commands/Backers';
import { DataListener, SettingsListener } from './listeners/panel';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
let editDebounce: { (fnc: any, time: number): void; };
let collection: vscode.DiagnosticCollection;
const mdSelector: vscode.DocumentSelector = { language: 'markdown', scheme: 'file' };
export async function activate(context: vscode.ExtensionContext) {
const { subscriptions, extensionUri, extensionPath } = context;
@@ -43,6 +43,9 @@ export async function activate(context: vscode.ExtensionContext) {
SettingsHelper.checkToPromote();
// Sends the activation event
Telemetry.send(TelemetryEvent.activate);
// Start listening to the folders for content changes.
// This will make sure the dashboard is up to date
PagesListener.startWatchers();
@@ -52,6 +55,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Pages dashboard
Dashboard.init();
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openContentDashboard);
if (!data) {
Dashboard.open({ type: "contents" });
} else {
@@ -60,14 +64,17 @@ export async function activate(context: vscode.ExtensionContext) {
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardMedia, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openMediaDashboard);
Dashboard.open({ type: "media" });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardData, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openDataDashboard);
Dashboard.open({ type: "data" });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.closeDashboard);
Dashboard.close();
}));
@@ -84,7 +91,7 @@ export async function activate(context: vscode.ExtensionContext) {
});
// Folding the front matter of markdown files
vscode.languages.registerFoldingRangeProvider(mdSelector, new MarkdownFoldingProvider());
MarkdownFoldingProvider.register();
const insertTags = vscode.commands.registerCommand(COMMAND_NAME.insertTags, async () => {
await vscode.commands.executeCommand('workbench.view.extension.frontmatter-explorer');
@@ -151,7 +158,7 @@ export async function activate(context: vscode.ExtensionContext) {
});
// Settings promotion command
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, SettingsHelper.promote ));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, SettingsHelper.promote));
// Collapse all sections in the webview
const collapseAll = vscode.commands.registerCommand(COMMAND_NAME.collapseSections, () => {
@@ -163,9 +170,8 @@ export async function activate(context: vscode.ExtensionContext) {
Template.init();
Preview.init();
const exView = ExplorerView.getInstance();
exView.getSettings();
exView.getFoldersAndFiles();
SettingsListener.getSettings();
DataListener.getFoldersAndFiles();
MarkdownFoldingProvider.triggerHighlighting();
});
@@ -189,7 +195,7 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.workspace.onDidSaveTextDocument((doc: vscode.TextDocument) => {
if (doc.languageId === 'markdown') {
// Optimize the list of recently changed files
ExplorerView.getInstance().getFoldersAndFiles();
DataListener.getFoldersAndFiles();
}
}));
@@ -234,7 +240,9 @@ export async function activate(context: vscode.ExtensionContext) {
);
}
export function deactivate() {}
export function deactivate() {
Telemetry.dispose();
}
const handleAutoDateUpdate = (e: vscode.TextDocumentWillSaveEvent) => {
Article.autoUpdate(e);

View File

@@ -1,11 +1,10 @@
import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
import * as vscode from 'vscode';
import * as matter from "gray-matter";
import * as fs from "fs";
import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTINGS_CONTENT_PLACEHOLDERS, SETTINGS_CONTENT_SUPPORTED_FILETYPES, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants';
import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTINGS_CONTENT_PLACEHOLDERS, SETTINGS_CONTENT_SUPPORTED_FILETYPES, SETTINGS_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 } from '../constants';
import { DumpOptions } from 'js-yaml';
import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine';
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
import { Extension, Logger, Settings, SlugHelper } from '.';
import { format, parse } from 'date-fns';
import { Notifications } from './Notifications';
@@ -18,6 +17,9 @@ import { ContentType } from '../models';
import { DateHelper } from './DateHelper';
import { DiagnosticSeverity, Position, window, Range } from 'vscode';
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { Link, Parent } from 'mdast-util-from-markdown/lib';
import { Content } from 'mdast';
export class ArticleHelper {
private static notifiedFiles: string[] = [];
@@ -56,7 +58,7 @@ export class ArticleHelper {
* @param editor
* @param article
*/
public static async update(editor: vscode.TextEditor, article: matter.GrayMatterFile<string>) {
public static async update(editor: vscode.TextEditor, article: ParsedFrontMatter) {
const update = this.generateUpdate(editor.document, article);
await editor.edit(builder => builder.replace(update.range, update.newText));
@@ -66,7 +68,7 @@ export class ArticleHelper {
* Generate the update to be applied to the article.
* @param article
*/
public static generateUpdate(document: vscode.TextDocument, article: matter.GrayMatterFile<string>): vscode.TextEdit {
public static generateUpdate(document: vscode.TextDocument, article: ParsedFrontMatter): vscode.TextEdit {
const nrOfLines = document.lineCount as number;
const range = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(nrOfLines, 0));
const removeQuotes = Settings.get(SETTING_REMOVE_QUOTES) as string[];
@@ -117,9 +119,6 @@ export class ArticleHelper {
const indentArray = Settings.get(SETTING_INDENT_ARRAY) as boolean;
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
const language = getFmLanguage();
const langOpts = getFormatOpts(language);
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
if (commaSeparated) {
@@ -130,9 +129,7 @@ export class ArticleHelper {
}
}
return matter.stringify(content, data, ({
...TomlEngine,
...langOpts,
return FrontMatterParser.toFile(content, data, ({
noArrayIndent: !indentArray,
skipInvalid: true,
noCompatMode: true,
@@ -169,7 +166,7 @@ export class ArticleHelper {
/**
* Get date from front matter
*/
public static getDate(article: matter.GrayMatterFile<string> | null) {
public static getDate(article: ParsedFrontMatter | null) {
if (!article) {
return;
}
@@ -230,7 +227,8 @@ export class ArticleHelper {
* @returns
*/
public static sanitize(value: string): string {
return sanitize(value.toLowerCase().replace(/ /g, "-"));
const preserveCasing = Settings.get(SETTINGS_FILE_PRESERVE_CASING) as boolean;
return sanitize((preserveCasing ? value : value.toLowerCase()).replace(/ /g, "-"));
}
/**
@@ -353,22 +351,109 @@ export class ArticleHelper {
return value;
}
/**
* Get the details of the current article
* @returns
*/
public static getDetails() {
const baseUrl = Settings.get<string>(SETTING_SITE_BASEURL);
const editor = window.activeTextEditor;
if (!editor) {
return null;
}
if (!ArticleHelper.isMarkdownFile()) {
return null;
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.content) {
let content = article.content;
content = content.replace(/({{(.*?)}})/g, ''); // remove hugo shortcodes
const mdTree = fromMarkdown(content);
const elms: Parent[] | Link[] = this.getAllElms(mdTree);
const headings = elms.filter(node => node.type === 'heading');
const paragraphs = elms.filter(node => node.type === 'paragraph').length;
const images = elms.filter(node => node.type === 'image').length;
const links: string[] = elms.filter(node => node.type === 'link').map(node => (node as Link).url);
const internalLinks = links.filter(link => !link.startsWith('http') || (baseUrl && link.toLowerCase().includes((baseUrl || "").toLowerCase()))).length;
let externalLinks = links.filter(link => link.startsWith('http'));
if (baseUrl) {
externalLinks = externalLinks.filter(link => !link.toLowerCase().includes(baseUrl.toLowerCase()));
}
const headers = [];
for (const header of headings) {
const text = header?.children?.filter((node: any) => node.type === 'text').map((node: any) => node.value).join(" ");
if (text) {
headers.push(text);
}
}
const wordCount = this.wordCount(0, mdTree);
return {
headings: headings.length,
headingsText: headers,
paragraphs,
images,
internalLinks,
externalLinks: externalLinks.length,
wordCount,
content: article.content
};
}
return null;
}
/**
* Retrieve all the elements from the markdown content
* @param node
* @param allElms
* @returns
*/
private static getAllElms(node: Content | any, allElms?: any[]): any[] {
if (!allElms) {
allElms = [];
}
if (node.children?.length > 0) {
for (const child of node.children) {
allElms.push(Object.assign({}, child));
this.getAllElms(child, allElms);
}
}
return allElms;
}
/**
* Get the word count for the current document
*/
private static wordCount(count: number, node: Content | any) {
if (node.type === "text") {
return count + node.value.split(" ").length;
} else {
return (node.children || []).reduce((childCount: number, childNode: any) => this.wordCount(childCount, childNode), count);
}
}
/**
* Parse a markdown file and its front matter
* @param fileContents
* @returns
*/
private static parseFile(fileContents: string, fileName: string): matter.GrayMatterFile<string> | null {
private static parseFile(fileContents: string, fileName: string): ParsedFrontMatter | null {
try {
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
if (fileContents) {
const language: string = getFmLanguage();
const langOpts = getFormatOpts(language);
let article: matter.GrayMatterFile<string> | null = matter(fileContents, {
...TomlEngine,
...langOpts
});
let article = FrontMatterParser.fromFile(fileContents);
if (article?.data) {
if (commaSeparated) {

View File

@@ -1,13 +1,14 @@
import { PagesListener } from './../listeners/PagesListener';
import { PagesListener } from './../listeners/dashboard';
import { ArticleHelper, Settings } from ".";
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field } from '../models';
import { Uri, workspace, window, commands } from 'vscode';
import { Uri, commands } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { writeFileSync } from "fs";
import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { Telemetry } from './Telemetry';
export class ContentType {
@@ -125,6 +126,8 @@ export class ContentType {
Notifications.info(`Your new content has been created.`);
Telemetry.send(TelemetryEvent.createContentFromContentType);
// Trigger a refresh for the dashboard
PagesListener.refresh();
}
@@ -138,7 +141,7 @@ export class ContentType {
if (obj.fields) {
for (const field of obj.fields) {
if (field.name === "title") {
if (field.name === "title") {
if (field.default) {
data[field.name] = ArticleHelper.processKnownPlaceholders(field.default, titleValue);
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);

View File

@@ -3,13 +3,13 @@ import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
import { ArticleHelper } from '.';
import { Folders } from '../commands/Folders';
import { exec } from 'child_process';
import matter = require('gray-matter');
import * as os from 'os';
import { join } from 'path';
import { Notifications } from './Notifications';
import ContentProvider from '../providers/ContentProvider';
import { Dashboard } from '../commands/Dashboard';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { ParsedFrontMatter } from '../parsers';
export class CustomScript {
@@ -117,7 +117,7 @@ export class CustomScript {
});
}
private static async runScript(wsPath: string, article: matter.GrayMatterFile<string> | null, contentPath: string, script: ICustomScript): Promise<string | null> {
private static async runScript(wsPath: string, article: ParsedFrontMatter | null, contentPath: string, script: ICustomScript): Promise<string | null> {
return new Promise((resolve, reject) => {
let articleData = "";
if (os.type() === "Windows_NT") {

View File

@@ -84,6 +84,13 @@ export class Extension {
return this.ctx.extension.packageJSON.name;
}
/**
* Returns the extension's version
*/
public get version(): string {
return this.ctx.extension.packageJSON.version;
}
/**
* Check if the extension is in production/development mode
*/

View File

@@ -1,3 +1,4 @@
import { ExplorerView } from './../explorerView/ExplorerView';
import { Uri, window } from 'vscode';
import { dirname, join } from "path";
import { Field } from '../models';
@@ -54,11 +55,14 @@ export class ImageHelper {
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
const workspaceFolderPath = wsFolder ? join(wsFolder.fsPath, value) : null;
if (existsSync(staticPath)) {
return Uri.file(staticPath);
} else if (contentFolderPath && existsSync(contentFolderPath)) {
return Uri.file(contentFolderPath);
} else if (workspaceFolderPath && existsSync(workspaceFolderPath)) {
return Uri.file(workspaceFolderPath);
}
}
@@ -78,4 +82,57 @@ export class ImageHelper {
}
return relPath;
}
/**
* Process the image fields in the content type
* @param updatedMetadata
* @param fields
* @param parents
*/
public static processImageFields(updatedMetadata: any, fields: Field[], parents: string[] = []) {
const imageFields = fields.filter((field) => field.type === "image");
const panel = ExplorerView.getInstance();
// Support multi-level fields
let parentObj = updatedMetadata;
for (const parent of parents || []) {
parentObj = parentObj[parent];
}
// Process image fields
if (parentObj) {
for (const field of imageFields) {
if (parentObj[field.name]) {
const imageData = ImageHelper.allRelToAbs(field, parentObj[field.name]);
if (imageData) {
if (field.multiple && imageData instanceof Array) {
const preview = imageData.map(preview => preview && preview.absPath ? ({
...preview,
webviewUrl: panel.getWebview()?.asWebviewUri(preview.absPath).toString()
}) : null);
parentObj[field.name] = preview || [];
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
const preview = panel.getWebview()?.asWebviewUri(imageData.absPath);
parentObj[field.name] = {
...imageData,
webviewUrl: preview ? preview.toString() : null
};
}
} else {
parentObj[field.name] = field.multiple ? [] : "";
}
}
}
// Check if there are sub-fields to process
const subFields = fields.filter((field) => field.type === "fields");
if (subFields?.length > 0) {
for (const field of subFields) {
this.processImageFields(updatedMetadata, field.fields || [], [...parents, field.name]);
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ import imageSize from "image-size";
import { EditorHelper } from "@estruyf/vscode";
import { ExplorerView } from "../explorerView/ExplorerView";
import { SortOption } from "../dashboardWebView/constants/SortOption";
import { DataListener, MediaListener } from "../listeners/panel";
export class MediaHelpers {
@@ -301,10 +302,15 @@ export class MediaHelpers {
}
});
}
panel.getMediaSelection();
MediaListener.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image, parents: data.parents });
MediaListener.getMediaSelection();
DataListener.updateMetadata({
field: data.fieldName,
value: data.image,
parents: data.parents,
blockData: data.blockData
});
}
}
}

View File

@@ -14,15 +14,30 @@ export class MessageHelper {
private static vscode: ClientVsCode<any>;
public static getVsCodeAPI() {
MessageHelper.vscode = acquireVsCodeApi();
if (!MessageHelper.vscode) {
MessageHelper.vscode = acquireVsCodeApi();
}
return MessageHelper.vscode;
}
public static sendMessage = (command: CommandToCode | DashboardMessage, data?: any) => {
const vscode = MessageHelper.getVsCodeAPI();
if (data) {
MessageHelper.vscode.postMessage({ command, data });
vscode.postMessage({ command, data });
} else {
MessageHelper.vscode.postMessage({ command });
vscode.postMessage({ command });
}
}
public static getState = () => {
const vscode = MessageHelper.getVsCodeAPI();
return vscode.getState();
}
public static setState = (data: any) => {
const vscode = MessageHelper.getVsCodeAPI();
vscode.setState({
...data
});
}
}

View File

@@ -0,0 +1,67 @@
import { workspace } from "vscode"
import { Extension, Settings } from "."
import { Dashboard } from "../commands/Dashboard"
import { Preview } from "../commands/Preview"
import { Template } from "../commands/Template"
import { CONTEXT, DefaultFields, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS } from "../constants"
import { CustomScript, DataType, DraftField, FieldGroup, PanelSettings as IPanelSettings, ScriptType } from "../models"
export class PanelSettings {
public static async get(): Promise<IPanelSettings> {
return {
seo: {
title: Settings.get(SETTING_SEO_TITLE_LENGTH) as number || -1,
slug: Settings.get(SETTING_SEO_SLUG_LENGTH) as number || -1,
description: Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1,
content: Settings.get(SETTING_SEO_CONTENT_MIN_LENGTH) as number || -1,
descriptionField: Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description
},
slug: {
prefix: Settings.get(SETTING_SLUG_PREFIX) || "",
suffix: Settings.get(SETTING_SLUG_SUFFIX) || "",
updateFileName: !!Settings.get<boolean>(SETTING_SLUG_UPDATE_FILE_NAME),
},
date: {
format: Settings.get<string>(SETTING_DATE_FORMAT) || ""
},
tags: Settings.get(SETTING_TAXONOMY_TAGS, true) || [],
categories: Settings.get(SETTING_TAXONOMY_CATEGORIES, true) || [],
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
freeform: Settings.get(SETTING_PANEL_FREEFORM),
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type === ScriptType.Content || !s.type),
isInitialized: await Template.isInitialized(),
modifiedDateUpdate: Settings.get(SETTING_AUTO_UPDATE_DATE) || false,
writingSettingsEnabled: this.isWritingSettingsEnabled() || false,
fmHighlighting: Settings.get(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT),
preview: Preview.getSettings(),
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
dashboardViewData: Dashboard.viewData,
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
isBacker: await Extension.getInstance().getState<boolean | undefined>(CONTEXT.backer, 'global'),
framework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
commands: {
start: Settings.get<string>(SETTINGS_FRAMEWORK_START)
},
dataTypes: Settings.get<DataType[]>(SETTINGS_DATA_TYPES),
fieldGroups: Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS),
}
}
/**
* Check if the writing settings are enabled
*/
public static isWritingSettingsEnabled(): boolean {
const config = workspace.getConfiguration("", { languageId: "markdown" });
const fontSize = config.get("editor.fontSize");
const lineHeight = config.get("editor.lineHeight");
const wordWrap = config.get("editor.wordWrap");
const wordWrapColumn = config.get("editor.wordWrapColumn");
const lineNumbers = config.get("editor.lineNumbers");
const quickSuggestions = config.get<boolean>("editor.quickSuggestions");
return( fontSize && lineHeight && wordWrap && wordWrapColumn && lineNumbers && quickSuggestions !== undefined) as boolean;
}
}

View File

@@ -1,10 +1,10 @@
import * as vscode from 'vscode';
import { ArticleHelper } from '.';
import matter = require('gray-matter');
import { ParsedFrontMatter } from '../parsers';
export class SeoHelper {
public static checkLength(editor: vscode.TextEditor, collection: vscode.DiagnosticCollection, article: matter.GrayMatterFile<string>, fieldName: string, length: number) {
public static checkLength(editor: vscode.TextEditor, collection: vscode.DiagnosticCollection, article: ParsedFrontMatter, fieldName: string, length: number) {
const value = article.data[fieldName];
if (value.length > length) {
const text = editor.document.getText();

View File

@@ -1,8 +1,9 @@
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 } from '../constants';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent } from '../constants';
import { Folders } from '../commands/Folders';
import { join, basename } from 'path';
import { existsSync, readFileSync, watch, writeFileSync } from 'fs';
@@ -249,6 +250,8 @@ export class Settings {
}
Notifications.info(`All settings promoted to team level.`);
Telemetry.send(TelemetryEvent.promoteSettings);
}
/**

41
src/helpers/Telemetry.ts Normal file
View File

@@ -0,0 +1,41 @@
import TelemetryReporter, { TelemetryEventMeasurements, TelemetryEventProperties } from '@vscode/extension-telemetry';
import { Extension, Settings } from '.';
import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_TELEMETRY_DISABLE } from '../constants';
export class Telemetry {
private static instance: Telemetry;
private static reporter: TelemetryReporter | null = null;
private constructor() {
const extension = Extension.getInstance();
const extTitle = extension.isBetaVersion() ? EXTENSION_BETA_ID : EXTENSION_ID;
const extVersion = extension.version;
const appKey = `525037e5-70ff-4620-8e52-30e1aef8deee`;
Telemetry.reporter = new TelemetryReporter(extTitle, extVersion, appKey);
}
public static getInstance(): Telemetry {
if (!Telemetry.instance) {
Telemetry.instance = new Telemetry();
}
return Telemetry.instance;
}
public static send(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements) {
if (!Telemetry.reporter) {
Telemetry.getInstance();
}
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
if (isDisabled) {
return;
}
Telemetry.reporter?.sendTelemetryEvent(eventName, properties, measurements);
}
public static dispose() {
Telemetry.reporter?.dispose();
}
}

View File

@@ -1,31 +0,0 @@
import * as toml from '@iarna/toml';
import { SETTING_FRONTMATTER_TYPE } from '../constants';
import { Settings } from './SettingsHelper';
export const getFmLanguage = (): string => {
const language = Settings.get(SETTING_FRONTMATTER_TYPE) as string || "YAML";
return language.toLowerCase();
};
export const getFormatOpts = (format: string): { language: string, delimiters: string | [string, string] | undefined } => {
const formats: { [prop: string]: { language: string, delimiters: string | [string, string] | undefined }} = {
yaml: { language: 'yaml', delimiters: '---' },
toml: { language: 'toml', delimiters: '+++' },
json: { language: 'json', delimiters: '---' },
};
return formats[format];
};
export const TomlEngine = {
engines: {
toml: {
parse: (value: string) => {
return toml.parse(value);
},
stringify: (value: any) => {
return toml.stringify(value);
}
}
}
};

View File

@@ -9,6 +9,7 @@ export * from './FrameworkDetector';
export * from './GroupBy';
export * from './ImageHelper';
export * from './Logger';
export * from './MediaHelpers';
export * from './MediaLibrary';
export * from './MessageHelper';
export * from './Notifications';
@@ -19,7 +20,7 @@ export * from './SettingsHelper';
export * from './SlugHelper';
export * from './Sorting';
export * from './StringHelpers';
export * from './TomlEngine';
export * from './Telemetry';
export * from './decodeBase64Image';
export * from './getNonce';
export * from './isValidFile';

View File

@@ -1,3 +1,3 @@
export const parseWinPath = (path: string | undefined): string => {
return path?.split(`\\`).join(`/`) || '';
}
return path?.split(`\\`).join(`/`) || '';
}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
import { Settings } from '../dashboardWebView/models';
import { ContentType, PanelSettings } from '../models';
import { ContentType, Field, PanelSettings } from '../models';
export default function useContentType(settings: PanelSettings | Settings | undefined | null, metadata: any) {
const [contentType, setContentType] = useState<ContentType>(DEFAULT_CONTENT_TYPE);

View File

@@ -1,7 +1,7 @@
import { Dashboard } from "../commands/Dashboard";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Logger } from "../helpers/Logger";
import { Dashboard } from "../../commands/Dashboard";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Logger } from "../../helpers/Logger";
export abstract class BaseListener {

View File

@@ -1,8 +1,8 @@
import { Dashboard } from "../commands/Dashboard";
import { ExtensionState } from "../constants";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Extension } from "../helpers";
import { Dashboard } from "../../commands/Dashboard";
import { ExtensionState } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Extension } from "../../helpers";
import { BaseListener } from "./BaseListener";

View File

@@ -1,12 +1,12 @@
import { DataFile } from './../models/DataFile';
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { DataFile } from './../../models/DataFile';
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { BaseListener } from "./BaseListener";
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { Folders } from '../commands/Folders';
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
import { Folders } from '../../commands/Folders';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { dirname } from 'path';
import * as yaml from 'js-yaml';
import { Logger, Notifications } from '../helpers';
import { Logger, Notifications } from '../../helpers';
import { commands } from 'vscode';

View File

@@ -1,9 +1,9 @@
import { commands, env } from "vscode";
import { SettingsListener } from ".";
import { COMMAND_NAME } from "../constants";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { CustomScript, Extension } from "../helpers";
import { openFileInEditor } from "../helpers/openFileInEditor";
import { COMMAND_NAME } from "../../constants";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { CustomScript, Extension } from "../../helpers";
import { openFileInEditor } from "../../helpers/openFileInEditor";
import { BaseListener } from "./BaseListener";

View File

@@ -1,10 +1,11 @@
import { MediaHelpers } from './../helpers/MediaHelpers';
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Telemetry } from '../../helpers/Telemetry';
import { MediaHelpers } from '../../helpers/MediaHelpers';
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { BaseListener } from "./BaseListener";
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { SortingOption } from '../dashboardWebView/models';
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
import { SortingOption } from '../../dashboardWebView/models';
import { commands, env, Uri } from 'vscode';
import { COMMAND_NAME } from '../constants';
import { COMMAND_NAME, TelemetryEvent } from '../../constants';
import * as os from 'os';
@@ -20,22 +21,27 @@ export class MediaListener extends BaseListener {
this.sendMediaFiles(page, folder, sorting);
break;
case DashboardMessage.refreshMedia:
Telemetry.send(TelemetryEvent.refreshMedia);
MediaHelpers.resetMedia();
this.sendMediaFiles(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
Telemetry.send(TelemetryEvent.uploadMedia);
this.store(msg?.data);
break;
case DashboardMessage.deleteMedia:
Telemetry.send(TelemetryEvent.deleteMedia);
this.delete(msg?.data);
break;
case DashboardMessage.revealMedia:
this.openFileInFinder(msg?.data?.file);
break;
case DashboardMessage.insertPreviewImage:
Telemetry.send(TelemetryEvent.insertMediaToContent);
MediaHelpers.insertMediaToMarkdown(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
Telemetry.send(TelemetryEvent.updateMediaMetadata);
this.update(msg.data);
break;
case DashboardMessage.createMediaFolder:

View File

@@ -1,18 +1,19 @@
import { isValidFile } from './../helpers/isValidFile';
import { isValidFile } from '../../helpers/isValidFile';
import { existsSync } from "fs";
import { basename, dirname, join } from "path";
import { commands, FileSystemWatcher, RelativePattern, Uri, workspace } from "vscode";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { COMMAND_NAME, DefaultFields, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD } from "../constants";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Page } from "../dashboardWebView/models";
import { ArticleHelper, Logger, Settings } from "../helpers";
import { ContentType } from "../helpers/ContentType";
import { DateHelper } from "../helpers/DateHelper";
import { Notifications } from "../helpers/Notifications";
import { Dashboard } from "../../commands/Dashboard";
import { Folders } from "../../commands/Folders";
import { COMMAND_NAME, DefaultFields, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Page } from "../../dashboardWebView/models";
import { ArticleHelper, Logger, Settings } from "../../helpers";
import { ContentType } from "../../helpers/ContentType";
import { DateHelper } from "../../helpers/DateHelper";
import { Notifications } from "../../helpers/Notifications";
import { BaseListener } from "./BaseListener";
import { Field, FieldType } from '../../models';
export class PagesListener extends BaseListener {
@@ -150,6 +151,9 @@ export class PagesListener extends BaseListener {
fmFileName: fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
fmPreviewImage: "",
fmTags: [],
fmCategories: [],
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
@@ -159,35 +163,69 @@ export class PagesListener extends BaseListener {
};
const contentType = ArticleHelper.getContentType(article.data);
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
if (article?.data[previewField] && wsFolder) {
let fieldValue = article?.data[previewField];
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
let previewFieldParents = this.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 = this.findFieldByType(contentType.fields, "tags");
if (tagParents.length !== 0) {
page.fmTags = this.getFieldValue(article.data, tagParents);
}
let categoryParents = this.findFieldByType(contentType.fields, "categories");
if (categoryParents.length !== 0) {
page.fmCategories = this.getFieldValue(article.data, categoryParents);
}
// 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 {
fieldValue = undefined;
if (!crntPageData[previewField]) {
continue;
}
crntPageData = crntPageData[previewField];
}
}
// 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 (fieldValue && wsFolder) {
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
if (previewUri) {
const preview = Dashboard.getWebview()?.asWebviewUri(previewUri);
page[previewField] = preview?.toString() || "";
} else {
page[previewField] = "";
// 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() || "";
}
}
}
}
@@ -197,4 +235,76 @@ export class PagesListener extends BaseListener {
return;
}
/**
* Retrieve the field value
* @param data
* @param parents
* @returns
*/
private static getFieldValue(data: any, parents: string[]): string[] {
let fieldValue = [];
let crntPageData = data;
for (let i = 0; i < parents.length; i++) {
const crntField = parents[i];
if (i === parents.length - 1) {
fieldValue = crntPageData[crntField];
} else {
if (!crntPageData[crntField]) {
continue;
}
crntPageData = crntPageData[crntField];
}
}
return fieldValue;
}
/**
* Find the field by its type
* @param fields
* @param type
* @param parents
* @returns
*/
private static findFieldByType(fields: Field[], type: FieldType, parents: string[] = []) {
for (const field of fields) {
if (field.type === type) {
parents = [...parents, field.name];
return parents;
} else if (field.type === "fields" && field.fields) {
const subFields = this.findPreviewField(field.fields);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
/**
* Find the preview field in the fields
* @param ctFields
* @param parents
* @returns
*/
private static findPreviewField(ctFields: Field[], parents: string[] = []): string[] {
for (const field of ctFields) {
if (field.isPreviewImage && field.type === "image") {
parents = [...parents, field.name];
return parents;
} else if (field.type === "fields" && field.fields) {
const subFields = this.findPreviewField(field.fields);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
}

View File

@@ -1,9 +1,9 @@
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_FRAMEWORK_ID } from "../constants";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { DashboardSettings, Settings } from "../helpers";
import { FrameworkDetector } from "../helpers/FrameworkDetector";
import { Framework } from "../models";
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_FRAMEWORK_ID } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { DashboardSettings, Settings } from "../../helpers";
import { FrameworkDetector } from "../../helpers/FrameworkDetector";
import { Framework } from "../../models";
import { BaseListener } from "./BaseListener";

View File

@@ -0,0 +1,21 @@
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Telemetry } from "../../helpers/Telemetry";
import { BaseListener } from "./BaseListener";
export class TelemetryListener 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.sendTelemetry:
Telemetry.send(msg.data.event, msg.data.properties, msg.data.metrics);
break;
}
}
}

View File

@@ -1,5 +1,7 @@
export * from './DashboardListener';
export * from './DataListener';
export * from './ExtensionListener';
export * from './MediaListener';
export * from './PagesListener';
export * from './SettingsListener';
export * from './TelemetryListener';

View File

@@ -0,0 +1,27 @@
import { Article } from "../../commands";
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
export class ArticleListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: any, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.updateSlug:
Article.generateSlug();
break;
case CommandToCode.updateLastMod:
Article.setLastModifiedDate();
break;
case CommandToCode.publish:
Article.toggleDraft();
break;
}
}
}

View File

@@ -0,0 +1,28 @@
import { Extension } from './../../helpers/Extension';
import { ExplorerView } from './../../explorerView/ExplorerView';
import { Logger } from "../../helpers";
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { Command } from '../../panelWebView/Command';
export abstract class BaseListener {
public static process(msg: { command: CommandToCode, data: any }) {}
/**
* Send a message to the webview
* @param command
* @param data
*/
public static sendMsg(command: Command, data: any) {
Logger.info(`Sending message to panel: ${command}`);
const extPath = Extension.getInstance().extensionPath;
const panel = ExplorerView.getInstance(extPath);
panel.sendMessage({
command,
data
});
}
}

View File

@@ -0,0 +1,292 @@
import { BlockFieldData } from './../../models/BlockFieldData';
import { ImageHelper } from './../../helpers/ImageHelper';
import { Folders } from "../../commands/Folders";
import { Command } from "../../panelWebView/Command";
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
import { commands, ThemeIcon, window } from 'vscode';
import { ArticleHelper, Logger, Settings } from "../../helpers";
import { COMMAND_NAME, DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES } from "../../constants";
import { Article } from '../../commands';
import { ParsedFrontMatter } from '../../parsers';
const FILE_LIMIT = 10;
export class DataListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: CommandToCode, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.getData:
this.getFoldersAndFiles();
this.getFileData();
break;
case CommandToCode.createContent:
commands.executeCommand(COMMAND_NAME.createContent);
break;
case CommandToCode.createTemplate:
commands.executeCommand(COMMAND_NAME.createTemplate);
break;
case CommandToCode.updateMetadata:
this.updateMetadata(msg.data);
break;
case CommandToCode.frameworkCommand:
this.openTerminalWithCommand(msg.data.command);
break;
case CommandToCode.updatePlaceholder:
this.updatePlaceholder(msg?.data?.field, msg?.data?.value, msg?.data?.title);
break;
}
}
/**
* Retrieve the information about the registered folders and its files
*/
public static async getFoldersAndFiles() {
const folders = await Folders.getInfo(FILE_LIMIT) || null;
this.sendMsg(Command.folderInfo, folders);
}
/**
* Triggers a metadata change in the panel
* @param metadata
*/
public static pushMetadata(metadata: any) {
const wsFolder = Folders.getWorkspaceFolder();
const filePath = window.activeTextEditor?.document.uri.fsPath;
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
const contentTypes = Settings.get<string>(SETTING_TAXONOMY_CONTENT_TYPES);
let articleDetails = null;
try {
articleDetails = ArticleHelper.getDetails();
} catch (e) {
Logger.error(`DataListener::pushMetadata: ${(e as Error).message}`);
}
if (articleDetails) {
metadata.articleDetails = articleDetails;
}
let updatedMetadata = Object.assign({}, metadata);
if (commaSeparated) {
for (const key of commaSeparated) {
if (updatedMetadata[key] && typeof updatedMetadata[key] === "string") {
updatedMetadata[key] = updatedMetadata[key].split(",").map((s: string) => s.trim());
}
}
}
const keys = Object.keys(updatedMetadata);
if (keys.length > 0) {
updatedMetadata.filePath = filePath;
}
if (keys.length > 0 && contentTypes && wsFolder) {
// Get the current content type
const contentType = ArticleHelper.getContentType(updatedMetadata);
if (contentType) {
ImageHelper.processImageFields(updatedMetadata, contentType.fields)
}
}
// Check slug
if (!updatedMetadata[DefaultFields.Slug]) {
const slug = Article.getSlug();
if (slug) {
updatedMetadata[DefaultFields.Slug] = slug;
}
}
this.sendMsg(Command.metadata, updatedMetadata);
}
/**
* Update the metadata of the article
*/
public static async updateMetadata({
field,
parents,
value,
blockData
}: {
field: string,
value: any,
parents?: string[],
blockData?: BlockFieldData,
fieldData?: { multiple: boolean, value: string[] }
}) {
if (!field) {
return;
}
const editor = window.activeTextEditor;
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
const contentType = ArticleHelper.getContentType(article.data);
const dateFields = contentType.fields.filter((f) => f.type === "datetime");
const imageFields = contentType.fields.filter((f) => f.type === "image" && f.multiple);
// Support multi-level fields
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
for (const dateField of dateFields) {
if ((field === dateField.name) && value) {
parentObj[field] = Article.formatDate(new Date(value));
} else if (!imageFields.find(f => f.name === field)) {
// Only override the field data if it is not an multiselect image field
parentObj[field] = value;
}
}
for (const imageField of imageFields) {
if (field === imageField.name) {
// If value is an array, it means it comes from the explorer view itself (deletion)
if (Array.isArray(value)) {
parentObj[field] = value || [];
} else { // Otherwise it is coming from the media dashboard (addition)
let fieldValue = parentObj[field];
if (fieldValue && !Array.isArray(fieldValue)) {
fieldValue = [fieldValue];
}
const crntData = Object.assign([], fieldValue);
const allRelPaths = [...(crntData || []), value];
parentObj[field] = [...new Set(allRelPaths)].filter(f => f);
}
}
}
ArticleHelper.update(editor, article);
this.pushMetadata(article.data);
}
/**
* Retrieve the parent object to update
* @param data
* @param article
* @param parents
* @param blockData
* @returns
*/
public static getParentObject(data: any, article: ParsedFrontMatter, parents: string[] | undefined, blockData?: BlockFieldData) {
let parentObj = data;
let allParents = Object.assign([], parents);
const contentType = ArticleHelper.getContentType(article.data);
// Add support for block fields
if (blockData?.parentFields) {
let crntField = null;
parentObj = article.data;
// Loop through the parents of the block field
for (const parent of blockData?.parentFields) {
if (!parentObj[parent]) {
parentObj[parent] = {};
}
if (allParents[0] && allParents[0] === parent) {
allParents.shift();
}
parentObj = parentObj[parent];
crntField = contentType.fields.find(f => f.name === parent);
}
// Fetches the current block
if (blockData && crntField && crntField.type === 'block') {
if (typeof blockData.selectedIndex !== 'undefined') {
parentObj = parentObj[blockData.selectedIndex];
} else {
parentObj.push({
fieldGroup: blockData.blockType
});
parentObj = parentObj[parentObj.length - 1];
}
}
// Check if there are parents left
if (allParents.length > 0) {
for (const parent of allParents) {
if (!parentObj[parent]) {
parentObj[parent] = {};
}
parentObj = parentObj[parent];
}
}
} else {
for (const parent of parents || []) {
// If parent doesn't yet exists, it needs to be created
if (!parentObj[parent]) {
parentObj[parent] = {};
}
parentObj = parentObj[parent];
}
}
return parentObj;
}
/**
* Retrieve the file its front matter
*/
public static async getFileData() {
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article?.data) {
this.pushMetadata(article!.data);
}
}
/**
* Open a terminal and run the passed command
* @param command
*/
private static openTerminalWithCommand(command: string) {
if (command) {
let terminal = window.activeTerminal;
if (!terminal || (terminal && terminal.state.isInteractedWith === true)) {
terminal = window.createTerminal({
name: `Starting local server`,
iconPath: new ThemeIcon('server-environment'),
message: `Starting local server`,
});
}
if (terminal) {
terminal.sendText(command);
terminal.show(false);
}
}
}
private static updatePlaceholder(field: string, value: string, title: string) {
if (field && value) {
value = ArticleHelper.processKnownPlaceholders(value, title || "");
value = ArticleHelper.processCustomPlaceholders(value, title || "");
}
this.sendMsg(Command.updatePlaceholder, { field, value });
}
}

View File

@@ -0,0 +1,83 @@
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
import { commands, env as vscodeEnv } from 'vscode';
import * as os from 'os';
import { exec } from 'child_process';
import { Folders } from "../../commands/Folders";
import { COMMAND_NAME } from "../../constants";
import { SettingsListener } from ".";
import { openFileInEditor } from "../../helpers";
export class ExtensionListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: any, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.openFile:
this.openFile();
break;
case CommandToCode.openProject:
this.openFolder();
break;
case CommandToCode.openInEditor:
openFileInEditor(msg.data);
break;
case CommandToCode.initProject:
this.initialize();
break;
case CommandToCode.toggleCenterMode:
commands.executeCommand(`workbench.action.toggleCenteredLayout`);
break;
case CommandToCode.openPreview:
commands.executeCommand(COMMAND_NAME.preview);
break;
case CommandToCode.openDashboard:
commands.executeCommand(COMMAND_NAME.dashboard);
break;
}
}
/**
* Initialize project
*/
private static async initialize() {
await commands.executeCommand(COMMAND_NAME.init);
SettingsListener.getSettings();
}
/**
* Open the file in your explorer
*/
private static openFile() {
if (os.type() === "Linux" && vscodeEnv.remoteName?.toLowerCase() === "wsl") {
commands.executeCommand('remote-wsl.revealInExplorer');
} else {
commands.executeCommand('revealFileInOS');
}
}
/**
* Opens the project folder
*/
private static openFolder() {
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
const wsPath = wsFolder.fsPath;
if (os.type() === "Darwin") {
exec(`open ${wsPath}`);
} else if (os.type() === "Windows_NT") {
exec(`explorer ${wsPath}`);
} else if (os.type() === "Linux" && vscodeEnv.remoteName?.toLowerCase() === "wsl") {
exec('explorer.exe `wslpath -w "$PWD"`');
} else {
exec(`xdg-open ${wsPath}`);
}
}
}
}

View File

@@ -0,0 +1,63 @@
import { ExplorerView } from './../../explorerView/ExplorerView';
import { commands, window } from "vscode";
import { Dashboard } from "../../commands/Dashboard";
import { COMMAND_NAME } from "../../constants";
import { ImageHelper } from "../../helpers";
import { DashboardData } from "../../models";
import { Command } from "../../panelWebView/Command";
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
export class MediaListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: any, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.selectImage:
this.selectMedia(msg);
break;
case CommandToCode.getImageUrl:
this.generateUrl(msg.data);
break;
}
}
private static generateUrl(data: string) {
const filePath = window.activeTextEditor?.document.uri.fsPath;
const imgUrl = ImageHelper.relToAbs(filePath || "", data);
if (imgUrl) {
const viewUrl = ExplorerView.getInstance().getWebview()?.asWebviewUri(imgUrl);
if (viewUrl) {
this.sendMsg(Command.sendMediaUrl, {
original: data,
url: viewUrl.toString()
});
}
}
}
/**
* Select a media file
*/
private static async selectMedia(msg: { data: any }) {
await commands.executeCommand(COMMAND_NAME.dashboard, {
type: "media",
data: msg.data
} as DashboardData);
this.getMediaSelection();
}
/**
* Return the media selection
*/
public static async getMediaSelection() {
this.sendMsg(Command.mediaSelectionData, Dashboard.viewData);
}
}

View File

@@ -0,0 +1,38 @@
import { SETTING_CUSTOM_SCRIPTS } from "../../constants";
import { CustomScript, Settings } from "../../helpers";
import { CustomScript as ICustomScript } from "../../models";
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
export class ScriptListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: any, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.runCustomScript:
this.runCustomScript(msg);
break;
}
}
/**
* Run a custom script
* @param msg
*/
private static runCustomScript(msg: { command: string, data: any}) {
const scripts: ICustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
if (msg?.data?.title && msg?.data?.script && scripts) {
const customScript = scripts.find((s: ICustomScript) => s.title === msg.data.title);
if (customScript?.script && customScript?.title) {
CustomScript.run(customScript);
}
}
}
}

View File

@@ -0,0 +1,87 @@
import { commands, workspace } from "vscode";
import { EXTENSION_BETA_ID, EXTENSION_ID, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_PREVIEW_HOST } from "../../constants";
import { Extension, Settings } from "../../helpers";
import { PanelSettings } from "../../helpers/PanelSettings";
import { Command } from "../../panelWebView/Command";
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
export class SettingsListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: CommandToCode, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.getData:
this.getSettings();
break;
case CommandToCode.openSettings:
this.openVSCodeSettings();
break;
case CommandToCode.toggleWritingSettings:
this.toggleWritingSettings();
break;
case CommandToCode.updateModifiedUpdating:
this.updateSetting(SETTING_AUTO_UPDATE_DATE, msg.data || false);
break;
case CommandToCode.updateFmHighlight:
this.updateSetting(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false);
break;
case CommandToCode.updatePreviewUrl:
this.updateSetting(SETTING_PREVIEW_HOST, msg.data || "");
break;
case CommandToCode.updateStartCommand:
this.updateSetting(SETTINGS_FRAMEWORK_START, msg.data || "");
break;
}
}
/**
* Retrieve the extension settings required to render the panel
*/
public static async getSettings() {
const panelSettings = await PanelSettings.get();
this.sendMsg(Command.settings, panelSettings);
}
/**
* Open the settings view of VS Code
*/
public static openVSCodeSettings() {
const isBeta = Extension.getInstance().isBetaVersion();
commands.executeCommand('workbench.action.openSettings', `@ext:${isBeta ? EXTENSION_BETA_ID : EXTENSION_ID}`);
}
/**
* Updates a setting and refreshes the retrieved settings
* @param setting
* @param value
*/
private static async updateSetting(setting: string, value: any) {
await Settings.update(setting, value);
this.getSettings();
}
/**
* Toggle the writing settings
*/
private static async toggleWritingSettings() {
const config = workspace.getConfiguration("", { languageId: "markdown" });
const enabled = PanelSettings.isWritingSettingsEnabled();
await config.update("editor.fontSize", enabled ? undefined : 14, false, true);
await config.update("editor.lineHeight", enabled ? undefined : 26, false, true);
await config.update("editor.wordWrap", enabled ? undefined : "wordWrapColumn", false, true);
await config.update("editor.wordWrapColumn", enabled ? undefined : 64, false, true);
await config.update("editor.lineNumbers", enabled ? undefined : "off", false, true);
await config.update("editor.quickSuggestions", enabled ? undefined : false, false, true);
await config.update("editor.minimap.enabled", enabled ? undefined : false, false, true);
this.getSettings();
}
}

View File

@@ -0,0 +1,122 @@
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { TagType } from "../../panelWebView/TagType";
import { BaseListener } from "./BaseListener";
import { window } from "vscode";
import { ArticleHelper, Settings } from "../../helpers";
import { BlockFieldData, CustomTaxonomyData, TaxonomyType } from "../../models";
import { DataListener } from ".";
import { SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from "../../constants";
export class TaxonomyListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: any, data: any }) {
super.process(msg);
switch(msg.command) {
case CommandToCode.updateTags:
this.updateTags(msg.data?.fieldName, msg.data?.values || [], msg.data?.parents || [], msg.data?.blockData);
break;
case CommandToCode.updateCategories:
this.updateTags(msg.data?.fieldName, msg.data?.values || [], msg.data?.parents || [], msg.data?.blockData);
break;
case CommandToCode.updateKeywords:
this.updateTags(TagType.keywords.toLowerCase(), msg.data?.values || [], msg.data?.parents || [], msg.data?.blockData);
break;
case CommandToCode.updateCustomTaxonomy:
this.updateCustomTaxonomy(msg.data);
break;
case CommandToCode.addTagToSettings:
this.addTags(TagType.tags, msg.data);
break;
case CommandToCode.addCategoryToSettings:
this.addTags(TagType.categories, msg.data);
break;
case CommandToCode.addToCustomTaxonomy:
this.addCustomTaxonomy(msg.data);
break;
}
}
/**
* Update the tags in the current document
* @param tagType
* @param values
*/
private static updateTags(fieldName: string, values: string[], parents: string[], blockData?: BlockFieldData) {
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.data) {
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
parentObj[fieldName] = values || [];
ArticleHelper.update(editor, article);
DataListener.pushMetadata(article!.data);
}
}
/**
* Update the tags in the current document
* @param data
*/
private static updateCustomTaxonomy(data: CustomTaxonomyData) {
if (!data?.id || !data?.name) {
return;
}
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.data) {
const parentObj = DataListener.getParentObject(article.data, article, data.parents, data.blockData);
parentObj[data.name] = data.options || [];
ArticleHelper.update(editor, article);
DataListener.pushMetadata(article!.data);
}
}
/**
* Add tag to the settings
* @param data
*/
private static async addCustomTaxonomy(data: CustomTaxonomyData) {
if (!data?.id || !data?.option) {
return;
}
await Settings.updateCustomTaxonomy(data.id, data.option);
}
/**
* Add tag to the settings
* @param tagType
* @param value
*/
private static async addTags(tagType: TagType, value: string) {
if (value) {
let options = tagType === TagType.tags ? Settings.get<string[]>(SETTING_TAXONOMY_TAGS, true) : Settings.get<string[]>(SETTING_TAXONOMY_CATEGORIES, true);
if (!options) {
options = [];
}
options.push(value);
const taxType = tagType === TagType.tags ? TaxonomyType.Tag : TaxonomyType.Category;
await Settings.updateTaxonomy(taxType, options);
}
}
}

View File

@@ -0,0 +1,8 @@
export * from './ArticleListener';
export * from './BaseListener';
export * from './DataListener';
export * from './ExtensionListener';
export * from './MediaListener';
export * from './ScriptListener';
export * from './SettingsListener';
export * from './TaxonomyListener';

View File

@@ -0,0 +1,7 @@
export interface BlockFieldData {
parentFields: string[] | undefined;
blockType: string | undefined;
selectedIndex: number | undefined;
}

View File

@@ -1,3 +1,4 @@
import { BlockFieldData } from './BlockFieldData';
export interface CustomTaxonomyData {
id: string | undefined;
@@ -5,4 +6,5 @@ export interface CustomTaxonomyData {
options?: string[] | undefined;
option?: string | undefined;
parents?: string[];
blockData?: BlockFieldData;
}

View File

@@ -2,6 +2,7 @@ import { FileStat } from "vscode";
import { DraftField } from ".";
import { Choice } from "./Choice";
import { DashboardData } from "./DashboardData";
import { DataType } from "./DataType";
export interface PanelSettings {
seo: SEO;
@@ -10,19 +11,28 @@ export interface PanelSettings {
date: DateInfo;
categories: string[];
customTaxonomy: CustomTaxonomy[];
freeform: boolean;
freeform: boolean | undefined;
scripts: CustomScript[];
isInitialized: boolean;
modifiedDateUpdate: boolean;
writingSettingsEnabled: boolean;
fmHighlighting: boolean;
fmHighlighting: boolean | undefined;
preview: PreviewSettings;
contentTypes: ContentType[];
dashboardViewData: DashboardData | undefined;
draftField: DraftField;
draftField: DraftField | undefined;
isBacker: boolean | undefined;
framework: string | undefined;
commands: FrameworkCommands;
dataTypes: DataType[] | undefined;
fieldGroups: FieldGroup[] | undefined;
commaSeparatedFields: string[];
}
export interface FieldGroup {
id: string;
labelField?: string;
fields: Field[];
}
export interface FrameworkCommands {
@@ -38,10 +48,12 @@ export interface ContentType {
pageBundle?: boolean;
}
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block";
export interface Field {
title?: string;
name: string;
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields";
type: FieldType;
choices?: string[] | Choice[];
single?: boolean;
multiple?: boolean;
@@ -50,6 +62,9 @@ export interface Field {
taxonomyId?: string;
default?: string;
fields?: Field[];
fieldGroup?: string | string[];
dataType?: string | string[];
taxonomyLimit?: number;
}
export interface DateInfo {
@@ -65,8 +80,9 @@ export interface SEO {
}
export interface Slug {
prefix: number;
suffix: number;
prefix: number | string;
suffix: number | string;
updateFileName?: boolean;
}
export interface FolderInfo {

View File

@@ -1,7 +1,11 @@
export * from './BlockFieldData';
export * from './Choice';
export * from './ContentFolder';
export * from './CustomTaxonomyData';
export * from './DashboardData';
export * from './DataFile';
export * from './DataFolder';
export * from './DataType';
export * from './DraftField';
export * from './Framework';
export * from './MediaPaths';

Some files were not shown because too many files have changed in this diff Show More