Compare commits

..

127 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
Elio Struyf
09c48db957 Merge pull request #238 from estruyf/dev 2022-01-25 17:44:14 +01:00
Elio Struyf
d7658852b0 Reveal media file 2022-01-24 22:27:55 +01:00
Elio Struyf
0a0efba37b Updated changelog 2022-01-24 18:34:58 +01:00
Elio Struyf
a16c0c6355 #235 - Fix for reselecting the previously removed value 2022-01-24 18:34:24 +01:00
Elio Struyf
8dcbe67152 updated changelog 2022-01-22 19:11:25 +01:00
Elio Struyf
2c20621071 #234 - Fix for multi-word keywords 2022-01-22 19:10:42 +01:00
Elio Struyf
48c4c0b8e4 Updated supporters 2022-01-22 16:15:17 +01:00
Elio Struyf
2900777ffb updated changelog 2022-01-22 10:37:21 +01:00
Elio Struyf
0ccd428852 updating the docs 2022-01-22 10:35:57 +01:00
Elio Struyf
368ade6b44 Updated changelog 2022-01-21 17:03:03 +01:00
Elio Struyf
5f6b6e3b4a Fix for multi-dimensional fields + authentication improvement 2022-01-21 16:22:30 +01:00
Elio Struyf
43554a4303 #226 - Update the local server message 2022-01-21 14:00:29 +01:00
Elio Struyf
2b0007c21a Update changelog 2022-01-21 13:55:28 +01:00
Elio Struyf
5ab0bdaa69 #233 - Partial dashboard update on file change 2022-01-21 13:53:24 +01:00
Elio Struyf
1c74df0266 #225 - removed slug validation in button for dependent placeholders 2022-01-20 17:02:32 +01:00
Elio Struyf
469a1aaaf8 updated readme 2022-01-19 19:35:09 +01:00
Elio Struyf
c5523f7aaf Update overflow on data view 2022-01-19 14:08:17 +01:00
Elio Struyf
61b46bc5ac Update comments 2022-01-19 14:00:05 +01:00
Elio Struyf
d7a0f71552 #225 - Optimize slug will now process all fields with {{slug}} 2022-01-19 13:53:19 +01:00
Elio Struyf
7d6d60039e Optimized styling for data dashboard 2022-01-19 12:59:25 +01:00
Elio Struyf
42cc53cefc #226 - Allow to start the local server for the framework or SSG 2022-01-19 10:15:58 +01:00
Elio Struyf
e68daa8ac2 Styling updates 2022-01-18 18:57:06 +01:00
Elio Struyf
333cc1f9df Update FUNDING.yml 2022-01-18 13:24:12 +01:00
Elio Struyf
00bb8c6385 #227 - File type updates 2022-01-18 11:02:05 +01:00
Elio Struyf
1deb969c20 #227 - Added new supported file types setting 2022-01-17 14:48:46 +01:00
Elio Struyf
928afceca7 #231 - Backer support added 2022-01-17 14:11:04 +01:00
Elio Struyf
179b31f67c #230 - front matter json support added 2022-01-16 19:49:18 +01:00
Elio Struyf
7d4fe9ca0f Updated changelog 2022-01-15 20:35:31 +01:00
Elio Struyf
3e33383eb1 #225 - Placeholder support for front matter field values (template and content type) 2022-01-15 20:35:06 +01:00
Elio Struyf
66324fd292 #193 - support added for data folders 2022-01-15 19:31:15 +01:00
Elio Struyf
8b1fbcabaa Fix keys 2022-01-14 20:35:48 +01:00
Elio Struyf
90519488c1 #228 - Added action icon 2022-01-14 20:31:04 +01:00
Elio Struyf
1012e10ddc #228 - Add bulk actions 2022-01-14 20:23:28 +01:00
Elio Struyf
2dd129d9bd Remove unused references 2022-01-14 17:52:06 +01:00
Elio Struyf
6af5458082 #193 - YAML support for data files added 2022-01-14 15:22:51 +01:00
Elio Struyf
9744cf0117 #193 - Added data type support 2022-01-14 13:00:27 +01:00
Elio Struyf
01921c799c #193 - Support for list/array fields 2022-01-14 11:26:30 +01:00
Elio Struyf
b1674b4b84 #193 - Data file dashboard added 2022-01-13 20:18:32 +01:00
Elio Struyf
9f7f803e25 #198 - Additional media sort options 2022-01-12 14:44:45 +01:00
Elio Struyf
b83c565e29 HMR support for panel development 2022-01-12 14:09:59 +01:00
Elio Struyf
dee30923ff #197 - Update fields type + support for taxonomy and images 2022-01-12 11:25:44 +01:00
Elio Struyf
9a91be8025 #197 - Support for multi-dimensional content type fields 2022-01-11 12:23:46 +01:00
Elio Struyf
46a9d6e602 6.0.0 2022-01-11 08:55:06 +01:00
Elio Struyf
511fd48081 Merge pull request #224 from estruyf/dev 2022-01-10 13:01:03 +01:00
Elio Struyf
0039fc1555 Release date added 2022-01-10 13:00:32 +01:00
Elio Struyf
98044187cd Update changelog 2022-01-07 13:03:26 +01:00
Elio Struyf
a6dcc1ea79 Merge pull request #222 from farmerau/onWillSaveTextDocument 2022-01-07 12:55:34 +01:00
Elio Struyf
32a686227e #221 - Logic to remove new line by gray matter 2022-01-07 12:54:39 +01:00
Austin Farmer
faa74132e5 Adjust Automatic Date Updates to Run on Save
Prefers `onWillSaveTextDocument` over `onDidChangeTextDocument` for the triggering event.
2022-01-06 15:44:36 -05:00
Elio Struyf
3a847f7e42 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-01-05 18:49:40 +01:00
Elio Struyf
66c978891e updated changelog 2022-01-05 18:49:18 +01:00
Elio Struyf
2f31230e07 Merge pull request #220 from farmerau/markdown-file-heuristics-mdx 2022-01-05 18:40:01 +01:00
Austin Farmer
c4225c0011 Support detection by file extension 2022-01-05 11:38:32 -05:00
Elio Struyf
4edc7a0280 Updated filetype 2022-01-05 10:17:34 +01:00
Elio Struyf
a60fe5204b Updated config 2022-01-04 11:57:16 +01:00
Elio Struyf
bb980b4afe #218 - Added MDX support for template and content creation 2022-01-04 11:51:11 +01:00
Elio Struyf
504658d87a 5.10.0 2022-01-04 11:24:40 +01:00
186 changed files with 10550 additions and 2353 deletions

View File

@@ -0,0 +1 @@
{}

1
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,5 @@
# These are supported funding model platforms
github: [estruyf]
open_collective: frontmatter
custom: ["https://www.buymeacoffee.com/zMeFRy9"]

View File

@@ -1,5 +1,80 @@
# Change Log
## [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
- [#193](https://github.com/estruyf/vscode-front-matter/issues/193): Support added for editing data files.
- [#197](https://github.com/estruyf/vscode-front-matter/issues/197): Support for multi-dimensional content type fields on content creation and editing.
- [#225](https://github.com/estruyf/vscode-front-matter/issues/225): Placeholder support for front matter field values (template and content type).
- [#226](https://github.com/estruyf/vscode-front-matter/issues/226): Ability to specify the local server start command and trigger it from the UI.
- [#227](https://github.com/estruyf/vscode-front-matter/issues/227): Specify the file types to support with the new `frontMatter.content.supportedFileTypes` setting.
- [#228](https://github.com/estruyf/vscode-front-matter/issues/228): Show bulk button actions in panel and dashboard view.
- [#231](https://github.com/estruyf/vscode-front-matter/issues/231): Once you authenticate via GitHub as a supporter, the support links will be hidden from the UI.
### 🎨 Enhancements
- Added default field value for content type fields
- HMR support for panel webview development
- Added reveal media file action
- [#187](https://github.com/estruyf/vscode-front-matter/issues/187): Svelte support with the [#227](https://github.com/estruyf/vscode-front-matter/issues/227) features has been added.
- [#198](https://github.com/estruyf/vscode-front-matter/issues/198): Additional media sort options (alt, caption, and size).
- [#230](https://github.com/estruyf/vscode-front-matter/issues/230): JSON front matter support added.
- [#233](https://github.com/estruyf/vscode-front-matter/issues/233): Partial update when a page is updated.
### 🐞 Fixes
- [#234](https://github.com/estruyf/vscode-front-matter/issues/234): Fix for multi-word keywords
- [#235](https://github.com/estruyf/vscode-front-matter/issues/235): Fix for reselecting the previously removed value from a choice field
## [5.10.0] - 2022-01-10
### 🎨 Enhancements
- [#218](https://github.com/estruyf/vscode-front-matter/issues/218): Add support for creating `mdx` files from templates and content types. This introduced a new setting: `frontMatter.content.defaultFileType`.
- [#220](https://github.com/estruyf/vscode-front-matter/issues/220): Add support DateTime updates in `mdx` files when the `mdx extension` is not installed.
### 🐞 Fixes
- [#221](https://github.com/estruyf/vscode-front-matter/issues/221): Automatic DateTime switch from on text change to on save to prevent multiple updates.
## [5.9.0] - 2022-01-01 - 🎇🎆
### 🎨 Enhancements

View File

@@ -28,30 +28,50 @@
</a>
</h2>
## What is Front Matter?
## What is Front Matter?
Front Matter BETA is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
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. Jump right into editing and creating content with Front Matter and be able to preview it straight in VS Code.
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
The extension supports various static-site generators and frameworks like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
Our main extension features are:
A couple of our extension highlights that hopefully get you interested in giving Front Matter a try:
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
- Site preview within Visual Studio Code
- Content, data, and media management
- Search, filter, sort, etc. all your content
- Create new content
- Supporting tools to edit content and media
- Preview your site/content straight in Visual Studio Code
- SEO checks for title, description, and keywords
- Support for custom actions/scripts
- and many more
- Extensibility
- As we know, we cannot support all use cases. We provide a way to extend the functionality of the extension to your needs
- and many more features ...
> Missing something? Let us know by opening an issue on the [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
<p align="center">
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
<img src="./assets/v6.0.0/content-preview.png" alt="Site preview" style="display: inline-block" />
</p>
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 6**
In this version, we introduced the new data files/folders dashboard. You can find more information about the release in our [v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
<p align="center">
<img src="./assets/v6.0.0/data-dashboard.png" alt="Data dashboard" style="display: inline-block" />
</p>
> Data files/folders are pieces of content that do not belong to any markdown content, but live on their own. Most of the time, these data files are used to store additional information about your project/blog/website that will be used to render the content.
**Version 5**
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
<p align="center">
<img src="./assets/v5.9.0/media-dashboard.png" alt="Data dashboard" style="display: inline-block" />
</p>
**Version 4**
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
@@ -70,7 +90,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
</a>
</p>
## Installation
## ⚙️ Installation
You can get the extension via:
@@ -80,23 +100,54 @@ You can get the extension via:
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
### Beta version
### 🧪 Beta version
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
- Uninstall the main Front Matter version
- Install the beta version
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
## Documentation
## 📖 Documentation
<h2 align="center">
<a href="https://beta.frontmatter.codes" title="Documentation @ beta.frontmatter.codes">
Check out the extension documentation at beta.frontmatter.codes
</a>
</h2>
All documentation can be found on [frontmatter.codes](https://frontmatter.codes).
Documentation repository: [GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
## 💪 Contributing
Pull requests are welcome. Please open an issue first to discuss what you would like to change, or which problem you would like to fix. This makes it easier for us to follow-up and plan for future releases.
You can always help us improve the extension in varous ways like:
- Testing out the extension and providing feedback
- Reporting issues and bugs
- Suggesting new features
- Fixing an issue
- Updating documentation
- UI improvements
- Tutorials
- etc.
Eager to start contributing? Great 🤩, you can contribute to the following projects:
- [Extension](https://github.com/estruyf/vscode-front-matter)
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
- [Sample Projects](https://github.com/FrontMatter/project-samples)
## 👀 Show the work you are using Front Matter
Are you using Front Matter and are you interested in showing for which websites you use it? You can show your work by opening a [showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
You can open showcase issues for the following things:
- Show the website for which you use Front Matter;
- Share an article/video/webcast/... that explains how you use Front Matter;
- Got something else to share? Open an issue and we can see where it fits on our website.
## 👉 Contributors 🤘
@@ -109,6 +160,9 @@ If you have the courage to test out the beta features, we made available a beta
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/apowell656" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
</a>
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
@@ -128,6 +182,9 @@ If you have the courage to test out the beta features, we made available a beta
</a>
</p>
## 🔑 License
[MIT](./LICENSE)
<br />
<br />

View File

@@ -26,30 +26,50 @@
</a>
</h2>
## What is Front Matter?
## What is Front Matter?
Front Matter is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
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. Jump right into editing and creating content with Front Matter and be able to preview it straight in VS Code.
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
The extension supports various static-site generators and frameworks like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
Our main extension features are:
A couple of our extension highlights that hopefully get you interested in giving Front Matter a try:
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
- Site preview within Visual Studio Code
- Content, data, and media management
- Search, filter, sort, etc. all your content
- Create new content
- Supporting tools to edit content and media
- Preview your site/content straight in Visual Studio Code
- SEO checks for title, description, and keywords
- Support for custom actions/scripts
- and many more
- Extensibility
- As we know, we cannot support all use cases. We provide a way to extend the functionality of the extension to your needs
- and many more features ...
> Missing something? Let us know by opening an issue on the [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
<p align="center">
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
<img src="./assets/v6.0.0/content-preview.png" alt="Site preview" style="display: inline-block" />
</p>
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 6**
In this version, we introduced the new data files/folders dashboard. You can find more information about the release in our [v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
<p align="center">
<img src="./assets/v6.0.0/data-dashboard.png" alt="Data dashboard" style="display: inline-block" />
</p>
> Data files/folders are pieces of content that do not belong to any markdown content, but live on their own. Most of the time, these data files are used to store additional information about your project/blog/website that will be used to render the content.
**Version 5**
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
<p align="center">
<img src="./assets/v5.9.0/media-dashboard.png" alt="Data dashboard" style="display: inline-block" />
</p>
**Version 4**
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
@@ -68,7 +88,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
</a>
</p>
## Installation
## ⚙️ Installation
You can get the extension via:
@@ -78,23 +98,54 @@ You can get the extension via:
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
### Beta version
### 🧪 Beta version
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
- Uninstall the main Front Matter version
- Install the beta version
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
## Documentation
## 📖 Documentation
<h2 align="center">
<a href="https://frontmatter.codes" title="Documentation @ frontmatter.codes">
Check out the extension documentation at frontmatter.codes
</a>
</h2>
All documentation can be found on [frontmatter.codes](https://frontmatter.codes).
Documentation repository: [GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
## 💪 Contributing
Pull requests are welcome. Please open an issue first to discuss what you would like to change, or which problem you would like to fix. This makes it easier for us to follow-up and plan for future releases.
You can always help us improve the extension in varous ways like:
- Testing out the extension and providing feedback
- Reporting issues and bugs
- Suggesting new features
- Fixing an issue
- Updating documentation
- UI improvements
- Tutorials
- etc.
Eager to start contributing? Great 🤩, you can contribute to the following projects:
- [Extension](https://github.com/estruyf/vscode-front-matter)
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
- [Sample Projects](https://github.com/FrontMatter/project-samples)
## 👀 Show the work you are using Front Matter
Are you using Front Matter and are you interested in showing for which websites you use it? You can show your work by opening a [showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
You can open showcase issues for the following things:
- Show the website for which you use Front Matter;
- Share an article/video/webcast/... that explains how you use Front Matter;
- Got something else to share? Open an issue and we can see where it fits on our website.
## 👉 Contributors 🤘
@@ -107,6 +158,9 @@ If you have the courage to test out the beta features, we made available a beta
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/apowell656" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
</a>
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
@@ -126,6 +180,10 @@ If you have the courage to test out the beta features, we made available a beta
</a>
</p>
## 🔑 License
[MIT](./LICENSE)
<br />
<br />

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;
}
@@ -446,12 +451,33 @@ input:checked + .field__toggle__slider:before {
margin-bottom: 1rem;
}
.vscode-dark .metadata_field__box {
background: rgba(255, 255, 255, 0.1);
border: 1px dashed rgba(255, 255, 255, 0.2);
}
.vscode-light .metadata_field__box {
background: rgba(0, 0, 0, 0.1);
border: 1px dashed rgba(0, 0, 0, 0.2);
}
.metadata_field__box {
background: rgba(255, 255, 255, 0.1);
border: 1px dashed rgba(255, 255, 255, 0.2);
margin-bottom: .5rem;
padding: .5rem 1rem;
}
.metadata_field__label {
display: flex;
align-items: center;
margin-bottom: .5rem;
}
.metadata_field__label.metadata_field__label_parent {
justify-content: center;
}
.metadata_field__label svg {
margin-right: .5rem;
}
@@ -613,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%);
}
@@ -638,6 +664,14 @@ input:checked + .field__toggle__slider:before {
margin-top: .5rem;
}
.vscode-light .metadata_field__preview_image__preview {
background: rgba(0, 0, 0, 0.1);
}
.vscode-dark .metadata_field__preview_image__preview {
background: rgba(255, 255, 255, 0.1);
}
.metadata_field__preview_image__preview {
background-color: var(--vscode-button-secondaryBackground);
display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

3930
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": "5.9.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"
}
@@ -97,6 +86,23 @@
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
"scope": "Content"
},
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"oneOf": [
{
"enum": [
"md",
"mdx"
]
},
{
"type": "string"
}
],
"markdownDescription": "Specify the default file type for the content to create. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultfiletype)",
"scope": "Content"
},
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
@@ -188,6 +194,30 @@
},
"scope": "Content"
},
"frontMatter.content.placeholders": {
"type": "array",
"default": [],
"markdownDescription": "This array of placeholders defines the placeholders that you can use in your content types and templates for automatically populating your content its front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.placeholders)",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the placeholder, in your content type or template, use it as follows: {{placeholder}}"
},
"value": {
"type": "string",
"description": "The placeholder its value"
}
},
"additionalProperties": false,
"required": [
"id",
"value"
]
},
"scope": "Content"
},
"frontMatter.content.publicFolder": {
"type": "string",
"default": "",
@@ -240,6 +270,19 @@
},
"scope": "Content"
},
"frontMatter.content.supportedFileTypes": {
"type": "array",
"default": [
"md",
"mdx",
"markdown"
],
"markdownDescription": "Specify the file types that you want to use in Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.supportedfiletypes)",
"items": {
"type": "string"
},
"scope": "Content"
},
"frontMatter.content.wysiwyg": {
"type": "boolean",
"default": true,
@@ -306,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"
},
@@ -319,11 +362,187 @@
"markdownDescription": "Specify if you want to open the dashboard when you start VS Code. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.openonstart)",
"scope": "Dashboard"
},
"frontMatter.data.files": {
"type": "array",
"default": [],
"markdownDescription": "Specify the data files you want to use for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.data.files)",
"items": {
"type": "object",
"default": {},
"properties": {
"id": {
"type": "string",
"description": "Your unique ID you want to use for your data file."
},
"title": {
"type": "string",
"description": "Title you want to give to your data file."
},
"labelField": {
"type": "string",
"description": "The field you want to use as label for your data entries."
},
"file": {
"type": "string",
"description": "Path to the file to load. Only JSON or YAML files are supported."
},
"fileType": {
"type": "string",
"default": "json",
"enum": [
"json",
"yaml"
],
"description": "Defines how you want to parse the file. JSON is the default."
},
"schema": {
"type": "object",
"default": {},
"description": "The JSON schema for your data which will be used to render the data form.",
"additionalProperties": true
},
"type": {
"type": "string",
"default": "content",
"description": "If you are using data types, you can specify your type ID."
}
},
"additionalProperties": false,
"required": [
"id",
"title",
"file"
],
"anyOf": [
{
"required": [
"schema"
]
},
{
"required": [
"type"
]
}
]
},
"scope": "Data"
},
"frontMatter.data.folders": {
"type": "array",
"default": [],
"markdownDescription": "Specify the data files you want to use for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.data.files)",
"items": {
"type": "object",
"default": {},
"properties": {
"id": {
"type": "string",
"description": "Your unique ID you want to use for your data folder."
},
"labelField": {
"type": "string",
"description": "The field you want to use as label for your data entries."
},
"path": {
"type": "string",
"description": "Path to the folder to load files."
},
"schema": {
"type": "object",
"default": {},
"description": "The JSON schema for your data which will be used to render the data form.",
"additionalProperties": true
},
"type": {
"type": "string",
"default": "content",
"description": "If you are using data types, you can specify your type ID."
}
},
"additionalProperties": false,
"required": [
"id",
"path"
],
"anyOf": [
{
"required": [
"schema"
]
},
{
"required": [
"type"
]
}
]
},
"scope": "Data"
},
"frontMatter.data.types": {
"type": "array",
"default": [],
"markdownDescription": "Specify the data types. These types can be used in for your data files. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.data.types)",
"items": {
"type": "object",
"default": {},
"properties": {
"id": {
"type": "string",
"description": "Your unique ID you want to use for your data type."
},
"schema": {
"type": "object",
"default": {},
"description": "The JSON schema for your data which will be used to render the data form.",
"additionalProperties": true
}
},
"required": [
"id",
"schema"
]
},
"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": "",
"markdownDescription": "Specify the ID of your static site generator or framework you are using for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.id)"
},
"frontMatter.framework.startCommand": {
"type": [
"string",
"null"
],
"default": null,
"markdownDescription": "Specify the command you want to use to start your static site generator or framework. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.startcommand)"
},
"frontMatter.global.notifications": {
"type": "array",
"items": {
"type": "string",
"enum": [
"info",
"warning",
"error"
]
},
"default": [
"info",
"warning",
"error"
],
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
"scope": "Templates"
},
"frontMatter.media.defaultSorting": {
"type": "string",
"default": "",
@@ -397,7 +616,17 @@
"type": "string",
"description": "Define the type of field"
},
"fileType": {
"type": "string",
"default": "",
"enum": [
"md",
"mdx"
],
"description": "Specifies the type of content you want to create."
},
"fields": {
"$id": "#contenttypefield",
"type": "array",
"description": "Define the fields of the content type",
"items": {
@@ -416,7 +645,10 @@
"taxonomy",
"tags",
"categories",
"draft"
"draft",
"fields",
"json",
"block"
],
"description": "Define the type of field"
},
@@ -428,6 +660,10 @@
"type": "string",
"description": "Title to show in the UI"
},
"default": {
"type": "string",
"description": "Default value"
},
"choices": {
"type": "array",
"description": "Define your choices",
@@ -475,6 +711,36 @@
"type": "string",
"default": "",
"description": "The ID of your taxonomy field"
},
"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,
@@ -510,6 +776,48 @@
"choices"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "fields"
}
}
},
"then": {
"required": [
"fields"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "block"
}
}
},
"then": {
"required": [
"fieldGroup"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "json"
}
}
},
"then": {
"required": [
"dataType"
]
}
}
]
}
@@ -552,7 +860,8 @@
{
"title": "Publishing date",
"name": "date",
"type": "datetime"
"type": "datetime",
"default": "{{now}}"
},
{
"title": "Content preview",
@@ -615,17 +924,41 @@
"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",
"enum": [
"YAML",
"TOML"
"TOML",
"JSON"
],
"markdownDescription": "Specify the type of Front Matter to use. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.frontmattertype)",
"enumDescriptions": [
"Specifies you want to use YAML markup for the front matter (default)",
"Specifies you want to use TOML markup for the front matter"
"Specifies you want to use TOML markup for the front matter",
"Specifies you want to use JSON markup for the front matter"
],
"scope": "Taxonomy"
},
@@ -698,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",
@@ -709,148 +1047,13 @@
"default": "yyyy-MM-dd",
"markdownDescription": "Specify the prefix you want to add for your new article filenames. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.prefix)",
"scope": "Templates"
},
"frontMatter.global.notifications": {
"type": "array",
"items": {
"type": "string",
"enum": [
"info",
"warning",
"error"
]
},
"default": [
"info",
"warning",
"error"
],
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
"scope": "Templates"
}
}
},
"commands": [
{
"command": "frontMatter.collapseSections",
"title": "Collapse sections",
"category": "Front matter",
"icon": {
"light": "assets/icons/close-light.svg",
"dark": "assets/icons/close-dark.svg"
}
},
{
"command": "frontMatter.createTemplate",
"title": "Create a template from current file",
"category": "Front matter"
},
{
"command": "frontMatter.createCategory",
"title": "Create category",
"category": "Front matter"
},
{
"command": "frontMatter.createTag",
"title": "Create tag",
"category": "Front matter"
},
{
"command": "frontMatter.exportTaxonomy",
"title": "Export all tags & categories to your settings",
"category": "Front matter"
},
{
"command": "frontMatter.createFromTemplate",
"title": "Front Matter: New article from template"
},
{
"command": "frontMatter.registerFolder",
"title": "Front Matter: Register folder"
},
{
"command": "frontMatter.unregisterFolder",
"title": "Front Matter: Unregister folder"
},
{
"command": "frontMatter.generateSlug",
"title": "Generate slug based on content title",
"category": "Front matter"
},
{
"command": "frontMatter.init",
"title": "Initialize project",
"category": "Front matter"
},
{
"command": "frontMatter.insertCategories",
"title": "Insert categories",
"category": "Front matter"
},
{
"command": "frontMatter.insertImage",
"title": "Insert image into your content",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/media-dark.svg",
"light": "/assets/icons/media-light.svg"
}
},
{
"command": "frontMatter.insertTags",
"title": "Insert tags",
"category": "Front matter"
},
{
"command": "frontMatter.createContent",
"title": "Create new content from defined content type or template",
"category": "Front matter"
},
{
"command": "frontMatter.dashboard",
"title": "Open dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.close",
"title": "Close dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-teal.svg",
"light": "/assets/icons/frontmatter-small-teal.svg"
}
},
{
"command": "frontMatter.preview",
"title": "Preview content",
"category": "Front matter"
},
{
"command": "frontMatter.promoteSettings",
"title": "Promote settings from local to team level",
"category": "Front matter"
},
{
"command": "frontMatter.remap",
"title": "Remap or remove tag/category in all articles",
"category": "Front matter"
},
{
"command": "frontMatter.setLastModifiedDate",
"title": "Set lastmod date",
"command": "frontMatter.authenticate",
"title": "Authenticate",
"category": "Front matter"
},
{
@@ -863,21 +1066,12 @@
}
},
{
"command": "frontMatter.markup.italic",
"title": "Italic",
"command": "frontMatter.dashboard.close",
"title": "Close dashboard",
"category": "Front matter",
"icon": {
"light": "assets/icons/italic-light.svg",
"dark": "assets/icons/italic-dark.svg"
}
},
{
"command": "frontMatter.markup.strikethrough",
"title": "Strikethrough",
"category": "Front matter",
"icon": {
"light": "assets/icons/strikethrough-light.svg",
"dark": "assets/icons/strikethrough-dark.svg"
"dark": "/assets/icons/frontmatter-small-teal.svg",
"light": "/assets/icons/frontmatter-small-teal.svg"
}
},
{
@@ -907,6 +1101,62 @@
"dark": "assets/icons/blockquote-dark.svg"
}
},
{
"command": "frontMatter.collapseSections",
"title": "Collapse sections",
"category": "Front matter",
"icon": {
"light": "assets/icons/close-light.svg",
"dark": "assets/icons/close-dark.svg"
}
},
{
"command": "frontMatter.createTemplate",
"title": "Create a template from current file",
"category": "Front matter"
},
{
"command": "frontMatter.createCategory",
"title": "Create category",
"category": "Front matter"
},
{
"command": "frontMatter.createContent",
"title": "Create new content from defined content type or template",
"category": "Front matter"
},
{
"command": "frontMatter.createTag",
"title": "Create tag",
"category": "Front matter"
},
{
"command": "frontMatter.diagnostics",
"title": "Diagnostic logging",
"category": "Front matter"
},
{
"command": "frontMatter.exportTaxonomy",
"title": "Export all tags & categories to your settings",
"category": "Front matter"
},
{
"command": "frontMatter.createFromTemplate",
"title": "Front Matter: New article from template"
},
{
"command": "frontMatter.registerFolder",
"title": "Front Matter: Register folder"
},
{
"command": "frontMatter.unregisterFolder",
"title": "Front Matter: Unregister folder"
},
{
"command": "frontMatter.generateSlug",
"title": "Generate slug based on content title",
"category": "Front matter"
},
{
"command": "frontMatter.markup.heading",
"title": "Heading",
@@ -917,12 +1167,63 @@
}
},
{
"command": "frontMatter.markup.unorderedlist",
"title": "Unordered list",
"command": "frontMatter.init",
"title": "Initialize project",
"category": "Front matter"
},
{
"command": "frontMatter.insertCategories",
"title": "Insert categories",
"category": "Front matter"
},
{
"command": "frontMatter.insertImage",
"title": "Insert image into your content",
"category": "Front matter",
"icon": {
"light": "assets/icons/unordered-list-light.svg",
"dark": "assets/icons/unordered-list-dark.svg"
"dark": "/assets/icons/media-dark.svg",
"light": "/assets/icons/media-light.svg"
}
},
{
"command": "frontMatter.insertTags",
"title": "Insert tags",
"category": "Front matter"
},
{
"command": "frontMatter.markup.italic",
"title": "Italic",
"category": "Front matter",
"icon": {
"light": "assets/icons/italic-light.svg",
"dark": "assets/icons/italic-dark.svg"
}
},
{
"command": "frontMatter.dashboard",
"title": "Open dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.data",
"title": "Open data dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
@@ -934,11 +1235,6 @@
"dark": "assets/icons/ordered-list-dark.svg"
}
},
{
"command": "frontMatter.markup.tasklist",
"title": "Task list",
"category": "Front matter"
},
{
"command": "frontMatter.markup.options",
"title": "Other markup options",
@@ -949,9 +1245,47 @@
}
},
{
"command": "frontMatter.diagnostics",
"title": "Diagnostic logging",
"command": "frontMatter.preview",
"title": "Preview content",
"category": "Front matter"
},
{
"command": "frontMatter.promoteSettings",
"title": "Promote settings from local to team level",
"category": "Front matter"
},
{
"command": "frontMatter.remap",
"title": "Remap or remove tag/category in all articles",
"category": "Front matter"
},
{
"command": "frontMatter.setLastModifiedDate",
"title": "Set lastmod date",
"category": "Front matter"
},
{
"command": "frontMatter.markup.strikethrough",
"title": "Strikethrough",
"category": "Front matter",
"icon": {
"light": "assets/icons/strikethrough-light.svg",
"dark": "assets/icons/strikethrough-dark.svg"
}
},
{
"command": "frontMatter.markup.tasklist",
"title": "Task list",
"category": "Front matter"
},
{
"command": "frontMatter.markup.unorderedlist",
"title": "Unordered list",
"category": "Front matter",
"icon": {
"light": "assets/icons/unordered-list-light.svg",
"dark": "assets/icons/unordered-list-dark.svg"
}
}
],
"menus": {
@@ -1145,10 +1479,13 @@
"build:ext": "npm run clean && npm-run-all --parallel dev:build:*",
"watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js",
"watch:dashboard": "webpack serve --mode development --config ./webpack/dashboard.config.js",
"watch:panel": "webpack serve --mode development --config ./webpack/panel.config.js",
"dev:build:ext": "webpack --mode development --config ./webpack/extension.config.js",
"dev:build:dashboard": "webpack --mode development --config ./webpack/dashboard.config.js",
"dev:build:panel": "webpack --mode development --config ./webpack/panel.config.js",
"prod:ext": "webpack --mode production --config ./webpack/extension.config.js",
"prod:dashboard": "webpack --mode production --config ./webpack/dashboard.config.js",
"prod:panel": "webpack --mode production --config ./webpack/panel.config.js",
"test-compile": "tsc -p ./",
"clean": "rimraf dist",
"start:site": "cd ./docs && npm run dev"
@@ -1159,21 +1496,29 @@
"@headlessui/react": "^1.4.1",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@octokit/rest": "^18.12.0",
"@sentry/react": "^6.13.3",
"@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",
"@types/react": "17.0.0",
"@types/react-datepicker": "^4.1.7",
"@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",
"autoprefixer": "^10.3.2",
"css-loader": "5.2.7",
"date-fns": "2.23.0",
@@ -1184,30 +1529,45 @@
"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",
"path-browserify": "^1.0.1",
"postcss": "^8.3.6",
"postcss-loader": "4.3.0",
"postcss-nested": "^5.0.6",
"react": "17.0.1",
"react-datepicker": "4.2.1",
"react-dom": "17.0.1",
"react-dropzone": "^11.3.4",
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^8.1.0",
"recoil": "^0.4.1",
"rimraf": "^3.0.2",
"style-loader": "2.0.0",
"tailwindcss": "^2.2.7",
"ts-loader": "8.0.3",
"tslint": "6.1.3",
"typescript": "4.0.2",
"typescript": "^4.5.4",
"uniforms": "^3.7.0",
"uniforms-antd": "^3.7.0",
"uniforms-bridge-json-schema": "^3.7.0",
"uniforms-unstyled": "^3.7.0",
"url-join-ts": "^1.0.5",
"wc-react": "github:estruyf/wc-react",
"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

@@ -2,6 +2,7 @@ const tailwindcss = require('tailwindcss');
module.exports = {
plugins: [
require('postcss-nested'),
tailwindcss('./tailwind.config.js'),
require('autoprefixer'),
],

View File

@@ -1,23 +1,21 @@
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 } 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 { TaxonomyType } from "../models";
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 {
private static prevContent = "";
/**
* Insert taxonomy
*
@@ -105,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;
}
@@ -119,7 +117,37 @@ export class Article {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
const updatedArticle = this.setLastModifiedDateInner(editor.document);
if (typeof updatedArticle === "undefined") {
return;
}
ArticleHelper.update(
editor,
updatedArticle as ParsedFrontMatter
);
}
public static async setLastModifiedDateOnSave(
document: vscode.TextDocument
): Promise<vscode.TextEdit[]> {
const updatedArticle = this.setLastModifiedDateInner(document);
if (typeof updatedArticle === "undefined") {
return [];
}
const update = ArticleHelper.generateUpdate(document, updatedArticle);
return [update];
}
private static setLastModifiedDateInner(
document: vscode.TextDocument
): ParsedFrontMatter | undefined {
const article = ArticleHelper.getFrontMatterFromDocument(document);
if (!article) {
return;
}
@@ -128,8 +156,7 @@ export class Article {
const dateField = Settings.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified;
try {
cloneArticle.data[dateField] = Article.formatDate(new Date());
ArticleHelper.update(editor, cloneArticle);
return cloneArticle;
} catch (e: any) {
Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
}
@@ -139,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;
@@ -154,11 +183,34 @@ export class Article {
return;
}
const articleTitle: string = article.data["title"];
let slug = SlugHelper.createSlug(articleTitle);
const contentType = ArticleHelper.getContentType(article.data);
const titleField = "title";
const articleTitle: string = article.data[titleField];
const slug = SlugHelper.createSlug(articleTitle);
if (slug) {
slug = `${prefix}${slug}${suffix}`;
article.data["slug"] = slug;
let slugFieldValue = `${prefix}${slug}${suffix}`;
article.data["slug"] = slugFieldValue;
if (contentType) {
// Update the fields containing the slug placeholder
let fieldsToUpdate: Field[] = contentType.fields.filter(f => f.default === "{{slug}}");
for (const field of fieldsToUpdate) {
article.data[field.name] = slug;
}
// Update the fields containing a custom placeholder that depends on slug
const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS);
const customPlaceholders = placeholders?.filter(p => p.value.includes("{{slug}}"));
for (const customPlaceholder of (customPlaceholders || [])) {
const customPlaceholderFields = contentType.fields.filter(f => f.default === `{{${customPlaceholder.id}}}`);
for (const pField of customPlaceholderFields) {
article.data[pField.name] = customPlaceholder.value;
article.data[pField.name] = ArticleHelper.processKnownPlaceholders(article.data[pField.name], articleTitle);
}
}
}
ArticleHelper.update(editor, article);
// Check if the file name should be updated by the slug
@@ -238,30 +290,17 @@ export class Article {
/**
* Article auto updater
* @param fileChanges
* @param event
*/
public static async autoUpdate(fileChanges: vscode.TextDocumentChangeEvent) {
const txtChanges = fileChanges.contentChanges.map(c => c.text);
const editor = vscode.window.activeTextEditor;
public static async autoUpdate(event: vscode.TextDocumentWillSaveEvent) {
const document = event.document;
if (document && ArticleHelper.isMarkdownFile(document)) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (txtChanges.length > 0 && editor && ArticleHelper.isMarkdownFile()) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (autoUpdate) {
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
if (article.content === Article.prevContent) {
return;
}
Article.prevContent = article.content;
Article.setLastModifiedDate();
if (autoUpdate) {
event.waitUntil(Article.setLastModifiedDateOnSave(document));
}
}
}
}
/**
@@ -298,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;
@@ -325,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());
}

75
src/commands/Backers.ts Normal file
View File

@@ -0,0 +1,75 @@
import { commands, ExtensionContext } from 'vscode';
import { CONTEXT } from '../constants';
import { Extension } from '../helpers';
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;
public static async init(context: ExtensionContext) {
Backers.creds = new Credentials();
await Backers.creds.initialize(context, Backers.tryUsernameCheck);
Backers.tryUsernameCheck();
context.subscriptions.push(
commands.registerCommand('frontMatter.authenticate', async () => {
Backers.tryUsernameCheck();
})
);
}
public static async tryUsernameCheck() {
try {
const username = await Backers.getUsername();
Backers.validate(username || "");
} catch (e) {
Backers.validate("");
}
}
public static async getUsername() {
const octokit = await Backers.creds?.getOctokit();
const user = await octokit?.users.getAuthenticated();
if (user?.data?.login) {
return user?.data?.login;
}
return;
}
public static async validate(username: string) {
const ext = Extension.getInstance();
if (!username) {
ext.setState(CONTEXT.backer, undefined, 'global');
}
const isBeta = ext.isBetaVersion();
const response = await fetch(`https://${isBeta ? `beta.` : ``}frontmatter.codes/api/backers?backer=${username}`);
if (response.ok) {
const prevData = await ext.getState<boolean>(CONTEXT.backer, 'global');
await ext.setState(CONTEXT.backer, true, 'global');
if (!prevData) {
const explorerView = ExplorerView.getInstance();
if (explorerView.visible) {
SettingsListener.getSettings();
}
if (Dashboard.isOpen) {
Dashboard.reload();
}
}
} else {
ext.setState(CONTEXT.backer, false, 'global');
}
}
}

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,7 +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 { 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;
@@ -115,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 });
}
@@ -127,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);
});
@@ -144,6 +141,8 @@ export class Dashboard {
MediaListener.process(msg);
PagesListener.process(msg);
SettingsListener.process(msg);
DataListener.process(msg);
TelemetryListener.process(msg);
});
}
@@ -175,14 +174,15 @@ export class Dashboard {
*/
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
const dashboardFile = "dashboardWebView.js";
const localServerUrl = "http://localhost:9000";
const localPort = `9000`;
const localServerUrl = `localhost:${localPort}`;
let scriptUri = "";
const isProd = Extension.getInstance().isProductionMode;
if (isProd) {
scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', dashboardFile)).toString();
} else {
scriptUri = `${localServerUrl}/${dashboardFile}`;
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}
const nonce = WebviewHelper.getNonce();
@@ -194,10 +194,10 @@ export class Dashboard {
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://localhost:9000 http://0.0.0.0:9000"}`,
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`} 'unsafe-eval'`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`font-src ${webView.cspSource}`,
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : "ws://localhost:9000 ws://0.0.0.0:9000 http://localhost:9000 http://0.0.0.0:9000"}`
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`
];
return `

View File

@@ -1,5 +1,5 @@
import { Questions } from './../helpers/Questions';
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER } 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,7 +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]]`;
@@ -67,6 +69,8 @@ export class Folders {
MediaHelpers.resetMedia();
MediaListener.sendMediaFiles(0, folderName);
}
Telemetry.send(TelemetryEvent.addMediaFolder);
}
/**
@@ -121,6 +125,8 @@ export class Folders {
await Folders.update(folders);
Notifications.info(`Folder registered`);
Telemetry.send(TelemetryEvent.registerFolder);
}
}
@@ -133,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);
}
}
@@ -202,6 +210,7 @@ export class Folders {
* Get the registered folders information
*/
public static async getInfo(limit?: number): Promise<FolderInfo[] | null> {
const supportedFiles = Settings.get<string[]>(SETTINGS_CONTENT_SUPPORTED_FILETYPES);
const folders = Folders.get();
if (folders && folders.length > 0) {
let folderInfo: FolderInfo[] = [];
@@ -214,10 +223,15 @@ export class Folders {
if (projectStart) {
projectStart = projectStart.replace(/\\/g, '/');
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
const markdownFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.markdown'));
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
let files = [...mdFiles, ...markdownFiles, ...mdxFiles];
let files: Uri[] = [];
for (const fileType of (supportedFiles || DEFAULT_FILE_TYPES)) {
const filePath = join(projectStart, folder.excludeSubdir ? '/' : '**', `*${fileType.startsWith('.') ? '' : '.'}${fileType}`);
const foundFiles = await workspace.findFiles(filePath, '**/node_modules/**');
files = [...files, ...foundFiles];
}
if (files) {
let fileStats: FileInfo[] = [];
@@ -293,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
@@ -301,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;
}
@@ -314,8 +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;
}
}
}

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,12 +6,13 @@ import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Settings } from "../helpers";
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
export class Project {
private static content = `---
title: "{{name}}"
slug: "/{{kebabCase name}}/"
title:
slug:
description:
author:
date: 2019-08-22T15:20:28.000Z
@@ -27,6 +29,7 @@ categories: []
public static async init(sampleTemplate: boolean = true) {
try {
Settings.createTeamSettings();
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
const folder = Template.getSettings();
const templatePath = Project.templatePath();
@@ -35,7 +38,7 @@ categories: []
return;
}
const article = Uri.file(join(templatePath.fsPath, "article.md"));
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
if (!fs.existsSync(templatePath.fsPath)) {
await workspace.fs.createDirectory(templatePath);
@@ -45,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 { 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,7 +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 {
@@ -50,6 +52,7 @@ export class Template {
public static async generate() {
const folder = Template.getSettings();
const editor = vscode.window.activeTextEditor;
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
if (folder && editor && ArticleHelper.isMarkdownFile()) {
const article = ArticleHelper.getFrontMatter(editor);
@@ -83,7 +86,7 @@ export class Template {
if (templatePath) {
let fileContents = ArticleHelper.stringifyFrontMatter(keepContents === "no" ? "" : clonedArticle.content, clonedArticle.data);
const templateFile = path.join(templatePath.fsPath, `${titleValue}.md`);
const templateFile = path.join(templatePath.fsPath, `${titleValue}.${fileType}`);
fs.writeFileSync(templateFile, fileContents, { encoding: "utf-8" });
Notifications.info(`Template created and is now available in your ${folder} folder.`);
@@ -140,7 +143,8 @@ export class Template {
contentType = contentTypes?.find(t => t.name === templateData.data.type);
}
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
const fileExtension = extname(template.fsPath).replace(".", "");
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension);
if (!newFilePath) {
return;
}
@@ -156,13 +160,7 @@ export class Template {
}
if (frontMatter.data) {
const fmData = frontMatter.data;
if (typeof fmData.title !== "undefined") {
fmData.title = titleValue;
}
if (typeof fmData.slug !== "undefined") {
fmData.slug = ArticleHelper.sanitize(titleValue);
}
frontMatter.data = ArticleHelper.updatePlaceholders(frontMatter.data, titleValue);
frontMatter = Article.updateDate(frontMatter);
@@ -178,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,3 @@
export const DEFAULT_FILE_TYPES = [".md", ".markdown", ".mdx"];

View File

@@ -29,6 +29,7 @@ export const COMMAND_NAME = {
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
dashboardMedia: getCommandName("dashboard.media"),
dashboardData: getCommandName("dashboard.data"),
dashboardClose: getCommandName("dashboard.close"),
promote: getCommandName("promoteSettings"),
insertImage: getCommandName("insertImage"),

View File

@@ -2,20 +2,32 @@ export const FrameworkDetectors = [
{
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
"requiredFiles": ["gatsby-config.js"],
"requiredDependencies": ["gatsby"]
"requiredDependencies": ["gatsby"],
"commands": {
"start": "npx gatsby develop"
}
},
{
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
"requiredFiles": ["config.toml", "config.yaml", "config.yml"]
"requiredFiles": ["config.toml", "config.yaml", "config.yml"],
"commands": {
"start": "hugo server -D"
}
},
{
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
"requiredFiles": ["next.config.js"],
"requiredDependencies": ["next"]
"requiredDependencies": ["next"],
"commands": {
"start": "npx next dev"
}
},
{
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
"requiredFiles": ["nuxt.config.js"],
"requiredDependencies": ["nuxt"]
"requiredDependencies": ["nuxt"],
"commands": {
"start": "npx nuxt"
}
}
];

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

@@ -5,4 +5,5 @@ export const CONTEXT = {
isEnabled: "frontMatter:enabled",
isDashboardOpen: "frontMatter:dashboard:open",
wysiwyg: "frontMatter:markdown:wysiwyg",
backer: "frontMatter:backers:supporter",
};

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";
@@ -46,14 +49,25 @@ export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
export const SETTINGS_CONTENT_SORTING = "content.sorting";
export const SETTINGS_CONTENT_WYSIWYG = "content.wysiwyg";
export const SETTINGS_CONTENT_PLACEHOLDERS = "content.placeholders";
export const SETTINGS_CONTENT_SORTING_DEFAULT = "content.defaultSorting";
export const SETTINGS_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
export const SETTINGS_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
export const SETTINGS_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
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";
export const SETTING_SITE_BASEURL = "site.baseURL";

View File

@@ -4,5 +4,6 @@ export enum DashboardCommand {
settings = "settings",
media = "media",
viewData = "viewData",
mediaUpdate = "mediaUpdate"
mediaUpdate = "mediaUpdate",
dataFileEntries = "dataFileEntries"
}

View File

@@ -15,10 +15,14 @@ export enum DashboardMessage {
refreshMedia = 'refreshMedia',
uploadMedia = 'uploadMedia',
deleteMedia = 'deleteMedia',
revealMedia = 'revealMedia',
insertPreviewImage = 'insertPreviewImage',
updateMediaMetadata = 'updateMediaMetadata',
createMediaFolder = 'createMediaFolder',
setFramework = 'setFramework',
setState = 'setState',
runCustomScript = 'runCustomScript',
getDataEntries = 'getDataEntries',
putDataEntries = 'putDataEntries',
sendTelemetry = 'sendTelemetry',
}

View File

@@ -1,15 +1,17 @@
import * as React from 'react';
export interface IButtonProps {
secondary?: boolean;
disabled?: boolean;
className?: string;
onClick: () => void;
}
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, disabled, children}: React.PropsWithChildren<IButtonProps>) => {
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, className, disabled, secondary, children}: React.PropsWithChildren<IButtonProps>) => {
return (
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
className={`${className || ""} inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 focus:outline-none disabled:bg-gray-500 ${secondary ? `bg-red-300 hover:bg-red-400` : `bg-teal-600 hover:bg-teal-700`}`}
onClick={onClick}
disabled={disabled}
>

View File

@@ -6,6 +6,7 @@ import { MenuItem, MenuItems } from './Menu';
export interface IChoiceButtonProps {
title: string;
choices: {
icon?: JSX.Element;
title: string;
disabled?: boolean;
onClick: () => void;
@@ -36,10 +37,19 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
<MenuItems widthClass={`w-56`}>
<div className="py-1">
{choices.map((choice) => (
{choices.map((choice, idx) => (
<MenuItem
key={choice.title}
title={choice.title}
key={idx}
title={(
choice.icon ? (
<div className="flex items-center">
{choice.icon}
<span>{choice.title}</span>
</div>
) : (
choice.title
)
)}
value={null}
onClick={choice.onClick}
disabled={choice.disabled} />

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
@@ -30,7 +40,7 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loadin
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
</div>
);
};

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

@@ -8,6 +8,7 @@ import { DashboardViewSelector } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { NavigationType } from '../models';
import { DataView } from './DataView';
export interface IDashboardProps {
showWelcome: boolean;
@@ -30,15 +31,25 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
return <WelcomeScreen settings={settings} />;
}
if (view === NavigationType.Media) {
return (
<main className={`h-full w-full`}>
<Media />
</main>
);
}
if (view === NavigationType.Data) {
return (
<main className={`h-full w-full`}>
<DataView />
</main>
);
}
return (
<main className={`h-full w-full`}>
{
view === NavigationType.Media ? (
<Media />
) : (
<Contents pages={pages} loading={loading} />
)
}
<Contents pages={pages} loading={loading} />
</main>
);
};

View File

@@ -0,0 +1,70 @@
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 '../../../components/uniforms-frontmatter';
// import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
import { ErrorBoundary } from '@sentry/react';
import { DataFormControls } from './DataFormControls';
export interface IDataFormProps {
schema: any;
model: any;
onSubmit: (model: any) => void;
onClear: () => void;
}
export const DataForm: React.FunctionComponent<IDataFormProps> = ({ schema, model, onSubmit, onClear }: React.PropsWithChildren<IDataFormProps>) => {
const [ bridge, setBridge ] = useState<JSONSchemaBridge | null>(null);
const ajv = new Ajv({ allErrors: true, useDefaults: true });
const jsonValidator = (schema: object) => {
const validator = ajv.compile(schema);
return (crntModel: object) => {
validator(crntModel);
return validator.errors?.length ? { details: validator.errors } : null;
};
}
useEffect(() => {
const schemaValidator = jsonValidator(schema);
const bridge = new JSONSchemaBridge(schema, schemaValidator);
setBridge(bridge);
}, [schema]);
if (!bridge) {
return null;
}
return (
<ErrorBoundary>
<div className='autoform'>
{
model ? (
<h2 className='text-gray-500 dark:text-whisper-900'>Modify the data</h2>
) : (
<h2 className='text-gray-500 dark:text-whisper-900'>Add new data</h2>
)
}
<AutoForm
schema={bridge}
model={model || {}}
onSubmit={onSubmit}
ref={(form: any) => form?.reset()}>
<div className={`fields`}>
<AutoFields />
</div>
<div className={`errors`}>
<ErrorsField />
</div>
<DataFormControls model={model} onClear={onClear} />
</AutoForm>
</div>
</ErrorBoundary>
);
};

View File

@@ -0,0 +1,25 @@
import * as React from 'react';
import { useForm } from 'uniforms';
import { SubmitField } from 'uniforms-unstyled';
import { Button } from '../Button';
export interface IDataFormControlsProps {
model: any | null;
onClear: () => void;
}
export const DataFormControls: React.FunctionComponent<IDataFormControlsProps> = ({ model, onClear }: React.PropsWithChildren<IDataFormControlsProps>) => {
const { formRef } = useForm();
return (
<div className='text-right border-t border-gray-200 dark:border-vulcan-300'>
<SubmitField value={model ? `Update` : `Add`} />
<Button className='ml-4' secondary onClick={() => {
if (onClear) {
onClear();
}
formRef.reset();
}}>Cancel</Button>
</div>
);
};

View File

@@ -0,0 +1,235 @@
import * as React from 'react';
import { Header } from '../Header';
import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { DataForm } from './DataForm';
import { useCallback, useEffect, useState } from 'react';
import { DataFile } from '../../../models/DataFile';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { SponsorMsg } from '../SponsorMsg';
import { EventData } from '@estruyf/vscode';
import { DashboardCommand } from '../../DashboardCommand';
import { Button } from '../Button';
import { arrayMoveImmutable } from 'array-move';
import { EmptyView } from './EmptyView';
import { Container } from './SortableContainer';
import { SortableItem } from './SortableItem';
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 {}
export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.PropsWithChildren<IDataViewProps>) => {
const [ selectedData, setSelectedData ] = useState<DataFile | null>(null);
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
const [ dataEntries, setDataEntries ] = useState<any[] | null>(null);
const settings = useRecoilValue(SettingsSelector);
const setSchema = (dataFile: DataFile) => {
setSelectedData(dataFile);
setSelectedIndex(null);
setDataEntries(null);
Messenger.send(DashboardMessage.getDataEntries, { ...dataFile });
};
const messageListener = (message: MessageEvent<EventData<any>>) => {
if (message.data.command === DashboardCommand.dataFileEntries) {
setDataEntries(message.data.data);
}
};
const deleteItem = useCallback((index: number) => {
const dataClone: any[] = Object.assign([], dataEntries);
if (!selectedData) {
return;
}
dataClone.splice(index, 1);
updateData(dataClone);
}, [selectedData, dataEntries]);
const onSubmit = useCallback((data: any) => {
const dataClone: any[] = Object.assign([], dataEntries);
if (selectedIndex !== null && selectedIndex !== undefined) {
dataClone[selectedIndex] = data;
} else {
dataClone.push(data);
}
updateData(dataClone);
}, [selectedData, dataEntries, selectedIndex]);
const onSortEnd = useCallback(({ oldIndex, newIndex }: any) => {
if (!dataEntries || dataEntries.length === 0) {
return null;
}
if (selectedIndex !== null && selectedIndex !== undefined) {
setSelectedIndex(newIndex);
}
const newEntries = arrayMoveImmutable(dataEntries, oldIndex, newIndex);
updateData(newEntries);
}, [selectedData, dataEntries, selectedIndex]);
const updateData = useCallback((data: any) => {
if (!selectedData) {
return;
}
Messenger.send(DashboardMessage.putDataEntries, {
file: selectedData.file,
fileType: selectedData.fileType,
entries: data
});
// Show toast message
toast.success("Updated your data entries", {
position: "top-right",
autoClose: 2000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: false,
transition: Slide
});
}, [selectedData]);
useEffect(() => {
Messenger.listen(messageListener);
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewDataView
});
return () => {
Messenger.unlisten(messageListener);
}
}, []);
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
const dataFiles = (settings?.dataFiles || []).map((dataFile: DataFile) => {
if (!dataFile.schema && !dataFile.id) {
return null;
}
const clonedFile = Object.assign({}, dataFile);
if (clonedFile.type) {
const dataType = settings?.dataTypes?.find((dataType: DataType) => dataType.id === clonedFile.type);
if (!dataType) {
return null;
}
clonedFile.schema = Object.assign({}, dataType.schema);
}
return clonedFile;
}).filter(d => d !== null) as DataFile[];
return (
<div className="flex flex-col h-full overflow-auto inset-y-0">
<Header settings={settings} />
<div className="relative w-full flex-grow mx-auto overflow-hidden">
<div className={`flex w-64 flex-col absolute inset-y-0`}>
<aside className={`flex flex-col flex-grow overflow-y-auto border-r border-gray-200 dark:border-vulcan-300 py-6 px-4 overflow-auto`}>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Select your data type</h2>
<nav className={`flex-1 py-4 -mx-4 `}>
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
{
(dataFiles && dataFiles.length > 0) && (
dataFiles.map((dataFile) => (
<button
key={dataFile.id}
type='button'
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
onClick={() => setSchema(dataFile)}>
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
<span>{dataFile.title}</span>
</button>
)
))
}
</div>
</nav>
</aside>
</div>
<section className={`pl-64 flex min-w-0 h-full`}>
{
selectedData ? (
<>
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData.title.toLowerCase()} data items</h2>
<div className='py-4'>
{
(dataEntries && dataEntries.length > 0) ? (
<>
<Container onSortEnd={onSortEnd} useDragHandle>
{
(dataEntries || []).map((dataEntry, idx) => (
<SortableItem
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
index={idx}
crntIndex={idx}
selectedIndex={selectedIndex}
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
onDeleteItem={deleteItem}
/>
))
}
</Container>
<Button
className='mt-4'
onClick={() => setSelectedIndex(null)}>
Add a new entry
</Button>
</>
) : (
<div className={`flex flex-col items-center justify-center`}>
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
</div>
)
}
</div>
</div>
<div className={`w-2/3 py-6 px-4 overflow-auto`}>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Create or modify your {selectedData.title.toLowerCase()} data</h2>
{
selectedData ? (
<DataForm
schema={selectedData?.schema}
model={(dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null}
onSubmit={onSubmit}
onClear={() => setSelectedIndex(null)} />
) : (
<p>Select a data type to get started</p>
)
}
</div>
</>
) : (
<EmptyView />
)
}
</section>
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
<ToastContainer />
</div>
);
};

View File

@@ -0,0 +1,13 @@
import { ExclamationCircleIcon } from '@heroicons/react/outline';
import * as React from 'react';
export interface IEmptyViewProps {}
export const EmptyView: React.FunctionComponent<IEmptyViewProps> = (props: React.PropsWithChildren<IEmptyViewProps>) => {
return (
<div className='flex flex-col items-center justify-center w-full'>
<ExclamationCircleIcon className={`w-1/12 text-gray-500 dark:text-whisper-900 opacity-90`} />
<h2 className={`text-xl text-gray-500 dark:text-whisper-900`}>Select your date type first</h2>
</div>
);
};

View File

@@ -0,0 +1,6 @@
import * as React from 'react';
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>));

View File

@@ -0,0 +1,70 @@
import { PencilIcon, SelectorIcon, TrashIcon, XIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { SortableHandle, SortableElement } from 'react-sortable-hoc';
import { Alert } from '../Modals/Alert';
export interface ISortableItemProps {
value: string;
index: number;
crntIndex: number;
selectedIndex: number | null;
onSelectedIndexChange: (index: number) => void;
onDeleteItem: (index: number) => void;
}
const DragHandle = SortableHandle(() => <SelectorIcon className={`w-6 h-6 cursor-move`} />);
export const SortableItem = SortableElement(({ value, selectedIndex, crntIndex, onSelectedIndexChange, onDeleteItem }: ISortableItemProps) => {
const [ showAlert, setShowAlert ] = React.useState(false);
const deleteItemConfirm = () => {
setShowAlert(true);
};
return (
<>
<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>
<div className={`space-x-2 flex items-center`}>
<button
type='button'
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
title={`Edit "${value}"`}
onClick={() => onSelectedIndexChange(crntIndex)}>
<PencilIcon className='w-4 h-4' />
<span className='sr-only'>Edit</span>
</button>
<button
type='button'
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
title={`Delete "${value}"`}
onClick={() => deleteItemConfirm()}>
<TrashIcon className='w-4 h-4' />
<span className='sr-only'>Delete</span>
</button>
</div>
</li>
{
showAlert && (
<Alert
title={`Delete data entry`}
description={`Are you sure you want to delete the data entry?`}
okBtnText={`Delete`}
cancelBtnText={`Cancel`}
dismiss={() => setShowAlert(false)}
trigger={() => {
setShowAlert(false);
onDeleteItem(crntIndex);
}} />
)
}
</>
);
});

View File

@@ -0,0 +1 @@
export * from './DataView';

View File

@@ -13,11 +13,12 @@ import { useRecoilState, useResetRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import {PhotographIcon} from '@heroicons/react/outline';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
import { ChoiceButton } from '../ChoiceButton';
import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
import { Tabs } from './Tabs';
import { CustomScript } from '../../../models';
import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline';
export interface IHeaderProps {
settings: Settings | null;
@@ -52,22 +53,25 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
resetSorting();
}
const runBulkScript = (script: CustomScript) => {
Messenger.send(DashboardMessage.runCustomScript, { script });
};
const customActions: any[] = (settings?.scripts || []).filter(s => s.bulk && (s.type === "content" || !s.type)).map((s, idx) => ({
title: (
<div key={idx} className="flex items-center">
<LightningBoltIcon className="w-4 h-4 mr-2" />
<span>{s.title}</span>
</div>
),
onClick: () => runBulkScript(s)
}));
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300 h-12">
<ul className="flex items-center justify-start h-full -mb-px" data-tabs-toggle="#myTabContent" role="tablist">
<li className="mr-2" role="presentation">
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Contents ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="profile" aria-selected="false" onClick={() => updateView(NavigationType.Contents)}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</button>
</li>
<li className="mr-2" role="presentation">
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Media ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="dashboard" aria-selected="true" onClick={() => updateView(NavigationType.Media)}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</button>
</li>
</ul>
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300">
<Tabs onNavigate={updateView} />
</div>
{
@@ -81,15 +85,28 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
<ChoiceButton
title={`Create content`}
choices={[{
title: `Create by content type`,
onClick: createByContentType,
disabled: !settings?.initialized
}, {
title: `Create by template`,
onClick: createByTemplate,
disabled: !settings?.initialized
}]}
choices={[
{
title: (
<div className='flex items-center'>
<PlusIcon className="w-4 h-4 mr-2" />
<span>Create by content type</span>
</div>
),
onClick: createByContentType,
disabled: !settings?.initialized
}, {
title: (
<div className='flex items-center'>
<PlusIcon className="w-4 h-4 mr-2" />
<span>Create by template</span>
</div>
),
onClick: createByTemplate,
disabled: !settings?.initialized
},
...customActions
]}
onClick={createContent}
disabled={!settings?.initialized} />
</div>

View File

@@ -10,6 +10,7 @@ import { NavigationType } from '../../models';
import { SortingOption } from '../../models/SortingOption';
import { SearchSelector, SettingsSelector, SortingAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
import { Sorting as SortingHelpers } from '../../../helpers/Sorting';
export interface ISortingProps {
disableCustomSorting?: boolean;
@@ -23,6 +24,15 @@ export const sortOptions: SortingOption[] = [
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
];
const mediaSortOptions: SortingOption[] = [
{ name: "Size (asc)", id: SortOption.SizeAsc, order: SortOrder.asc, type: SortType.number },
{ name: "Size (desc)", id: SortOption.SizeDesc, order: SortOrder.desc, type: SortType.number },
{ name: "Caption (asc)", id: SortOption.CaptionAsc, order: SortOrder.asc, type: SortType.string },
{ name: "Caption (desc)", id: SortOption.CaptionDesc, order: SortOrder.desc, type: SortType.string },
{ name: "Alt (asc)", id: SortOption.AltAsc, order: SortOrder.asc, type: SortType.string },
{ name: "Alt (desc)", id: SortOption.AltDesc, order: SortOrder.desc, type: SortType.string },
];
export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSorting, view}: React.PropsWithChildren<ISortingProps>) => {
const [ crntSorting, setCrntSorting ] = useRecoilState(SortingAtom);
const searchValue = useRecoilValue(SearchSelector);
@@ -38,6 +48,13 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
};
let allOptions = [...sortOptions];
if (view === NavigationType.Media) {
allOptions = [...allOptions, ...mediaSortOptions];
}
allOptions = allOptions.sort(SortingHelpers.alphabetically("name"))
if (settings?.customSorting && !disableCustomSorting) {
allOptions = [...allOptions, ...settings.customSorting.map((s) => ({
title: s.title || s.name,
@@ -72,7 +89,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
<MenuItems>
<MenuItems widthClass="w-48">
{allOptions.map((option) => (
<MenuItem
key={option.id}

View File

@@ -0,0 +1,25 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { NavigationType } from '../../models';
import { DashboardViewAtom } from '../../state';
export interface ITabProps {
navigationType: NavigationType;
onNavigate: (navigationType: NavigationType) => void;
}
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
const view = useRecoilValue(DashboardViewAtom);
return (
<button
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === navigationType ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
type="button"
role="tab"
aria-controls="profile"
aria-selected="false"
onClick={() => onNavigate(navigationType)}>
{children}
</button>
);
};

View File

@@ -0,0 +1,45 @@
import { DatabaseIcon, PhotographIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { NavigationType } from '../../models';
import { SettingsSelector } from '../../state';
import { Tab } from './Tab';
export interface ITabsProps {
onNavigate: (navigationType: NavigationType) => void;
}
export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.PropsWithChildren<ITabsProps>) => {
const settings = useRecoilValue(SettingsSelector);
return (
<ul className="flex items-center justify-start h-full" data-tabs-toggle="#myTabContent" role="tablist">
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Contents}
onNavigate={onNavigate}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</Tab>
</li>
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Media}
onNavigate={onNavigate}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</Tab>
</li>
{
(settings?.dataFiles && settings.dataFiles.length > 0) && (
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Data}
onNavigate={onNavigate}>
<DatabaseIcon className={`h-6 w-auto mr-2`} /><span>Data</span>
</Tab>
</li>
)
}
</ul>
);
};

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import {FolderAddIcon} from '@heroicons/react/outline';
import {FolderAddIcon, LightningBoltIcon} from '@heroicons/react/outline';
import { useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { SelectedMediaFolderAtom, SettingsSelector } from '../../state';
@@ -31,6 +31,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
title={`Create new folder`}
choices={scripts.map(s => ({
title: s.title,
icon: <LightningBoltIcon className="w-4 h-4 mr-2" />,
onClick: () => runCustomScript(s)
}))}
onClick={onFolderCreation}

View File

@@ -77,9 +77,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
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 || ""
});
@@ -93,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) || "",
@@ -107,6 +111,13 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
setShowAlert(true);
};
const revealMedia = () => {
Messenger.send(DashboardMessage.revealMedia, {
file: media.fsPath,
folder: selectedFolder
});
};
const confirmDeletion = () => {
Messenger.send(DashboardMessage.deleteMedia, {
file: media.fsPath,
@@ -220,7 +231,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
viewData?.data?.filePath ? (
<>
<QuickAction
title='Insert image with markdown markup'
title={(viewData.data.metadataInsert && viewData.data.fieldName) ? `Insert image for your "${viewData.data.fieldName}" field` : `Insert image with markdown markup`}
onClick={insertToArticle}>
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
@@ -242,15 +253,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
onClick={copyToClipboard}>
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
</>
)
}
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
</div>
<Menu as="div" className="relative z-10 inline-block text-left h-5">
@@ -286,13 +297,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
onClick={copyToClipboard} />
{ customScriptActions() }
<MenuItem
title={`Delete`}
onClick={deleteMedia} />
</>
)
}
<MenuItem
title={`Reveal media`}
onClick={revealMedia} />
<MenuItem
title={`Delete`}
onClick={deleteMedia} />
</MenuItems>
</Menu>
</div>

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/*'
@@ -118,7 +122,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
<Lightbox />
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
</div>
);
};

View File

@@ -2,7 +2,7 @@ import { Menu } from '@headlessui/react';
import * as React from 'react';
export interface IMenuItemProps {
title: string;
title: JSX.Element | string;
value?: any;
isCurrent?: boolean;
disabled?: boolean;

View File

@@ -6,18 +6,28 @@ import { VersionInfo } from '../../models';
export interface ISponsorMsgProps {
beta: boolean | undefined;
version: VersionInfo | undefined;
isBacker: boolean | undefined;
}
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isBacker, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
return (
<p className={`w-full max-w-7xl mx-auto px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center justify-between`}>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Support Front Matter">
<span>Support</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
</a>
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={REVIEW_LINK} title="Review Front Matter">
<StarIcon className={`h-5 w-5 group-hover:fill-current`} /> <span>Review</span>
</a>
<p className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
{
isBacker ? (
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
) : (
<>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Support Front Matter">
<span>Support</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
</a>
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={REVIEW_LINK} title="Review Front Matter">
<StarIcon className={`h-5 w-5 group-hover:fill-current`} /> <span>Review</span>
</a>
</>
)
}
</p>
);
};

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

@@ -2,5 +2,11 @@ export enum SortOption {
LastModifiedAsc = "LastModifiedAsc",
LastModifiedDesc = "LastModifiedDesc",
FileNameAsc = "FileNameAsc",
FileNameDesc = "FileNameDesc"
FileNameDesc = "FileNameDesc",
SizeAsc = "SizeAsc",
SizeDesc = "SizeDesc",
CaptionAsc = "CaptionAsc",
CaptionDesc = "CaptionDesc",
AltAsc = "AltAsc",
AltDesc = "AltDesc",
}

View File

@@ -26,6 +26,8 @@ export default function useMessages() {
setView(NavigationType.Media);
} else if (message.data.data?.type === NavigationType.Contents) {
setView(NavigationType.Contents);
} else if (message.data.data?.type === NavigationType.Data) {
setView(NavigationType.Data);
}
break;
case DashboardCommand.settings:

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

@@ -1,4 +1,5 @@
export enum NavigationType {
Contents = "contents",
Media = "media"
Media = "media",
Data = "data",
}

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

@@ -1,8 +1,10 @@
import { DataType } from './../../models/DataType';
import { VersionInfo } from '../../models/VersionInfo';
import { ContentFolder } from '../../models/ContentFolder';
import { ContentType, CustomScript, DraftField, Framework, SortingSetting } from '../../models';
import { SortingOption } from './SortingOption';
import { DashboardViewType } from '.';
import { DataFile } from '../../models/DataFile';
export interface Settings {
beta: boolean;
@@ -24,6 +26,9 @@ export interface Settings {
customSorting: SortingSetting[] | undefined;
dashboardState: DashboardState;
scripts: CustomScript[];
dataFiles: DataFile[] | undefined;
dataTypes: DataType[] | undefined;
isBacker: boolean | undefined;
}
export interface DashboardState {

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;
@@ -29,4 +63,260 @@
top: -1px;
left: 2px;
color: white;
}
.autoform {
@apply py-4;
h2 {
@apply text-sm mb-2;
}
form {
label {
@apply block;
@apply text-gray-500;
@apply my-2;
}
input {
@apply w-full text-vulcan-500 px-2 py-1;
&::placeholder {
@apply text-gray-500;
}
}
.ant-form-item-has-error .ant-form-item-control-input {
@apply relative;
}
.ant-form-item-has-error {
label {
&::after {
content: ' *';
@apply text-red-400;
}
}
}
.ant-form-item-has-error input {
@apply border border-red-400;
}
.ant-form-item-has-error .ant-form-item-children-icon {
@apply text-red-400 absolute right-1 top-1;
svg {
@apply w-4 h-4;
}
}
.errors {
> div {
@apply border;
}
ul {
@apply list-disc pl-6 pr-4 py-4 bg-opacity-50;
}
}
input[type="submit"] {
@apply w-auto mt-4 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white bg-teal-600 cursor-pointer;
&:hover {
@apply bg-teal-700;
}
&:focus {
@apply outline-none;
}
&:disabled {
@apply bg-gray-500 opacity-50;
}
}
}
.fields {}
.ant-list.ant-list-bordered {
@apply border border-gray-300;
}
.ant-btn-dashed {
@apply border border-gray-300 border-dashed flex items-center justify-center py-1 mt-2;
&:hover {
@apply text-teal-900 border-teal-900;
}
}
.ant-input:hover, .ant-input-focused, .ant-input:focus {
@apply border-teal-600;
}
.ant-btn-ghost:focus, .ant-btn-ghost:hover {
@apply text-teal-600 border-teal-700;
}
.ant-switch-checked {
@apply bg-teal-500;
}
.ant-switch {
@apply bg-gray-400;
margin: 0;
padding: 0;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
font-feature-settings: "tnum";
position: relative;
display: inline-block;
box-sizing: border-box;
min-width: 44px;
height: 22px;
line-height: 22px;
vertical-align: middle;
border: 0;
border-radius: 100px;
cursor: pointer;
transition: all .2s;
user-select: none;
&.ant-switch-checked {
@apply bg-teal-500;
.ant-switch-handle {
left: calc(100% - 20px);
}
}
.ant-switch-handle {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
transition: all .2s ease-in-out;
&::before {
position: absolute;
inset: 0;
background-color: #fff;
border-radius: 9px;
box-shadow: 0 2px 4px #00230b33;
transition: all .2s ease-in-out;
content: "";
}
}
.ant-switch-inner {
display: block;
margin: 0 7px 0 25px;
color: #fff;
font-size: 12px;
transition: margin .2s;
svg {
display: none;
}
}
}
.ant-input-number {
@apply border;
box-sizing: border-box;
font-variant: tabular-nums;
list-style: none;
font-feature-settings: "tnum";
position: relative;
width: 100%;
min-width: 0;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
transition: all .3s;
display: inline-block;
width: 90px;
margin: 0;
padding: 0;
&.ant-input-number-focused {
@apply border-teal-600;
}
}
.ant-input-number-handler {
position: relative;
display: block;
width: 100%;
height: 50%;
overflow: hidden;
color: #00000073;
font-weight: 700;
line-height: 0;
text-align: center;
border-left: 1px solid #d9d9d9;
transition: all .1s linear;
}
.ant-input-number-handler-wrap {
position: absolute;
top: 0;
right: 0;
width: 22px;
height: 100%;
background: #fff;
opacity: 0;
transition: opacity .24s linear .1s;
}
.ant-input-number-input {
@apply px-2 py-1;
width: 100%;
height: 30px;
text-align: left;
background-color: transparent;
border: 0;
outline: 0;
transition: all .3s linear;
appearance: textfield !important;
}
.ant-input-number:hover .ant-input-number-handler-wrap, .ant-input-number-focused .ant-input-number-handler-wrap {
opacity: 1;
}
}
.vscode-dark .autoform {
form {
label {
@apply text-whisper-900;
}
input[type="submit"] {
@apply text-vulcan-500
}
}
.ant-list.ant-list-bordered {
@apply border-vulcan-100;
}
.ant-btn-dashed {
@apply border-vulcan-50;
&:hover {
@apply text-teal-400 border-teal-900;
}
}
}

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 } 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 } 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, 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,211 +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 || []);
break;
case CommandToCode.updateCategories:
this.updateTags(TagType.categories, msg.data || []);
break;
case CommandToCode.updateKeywords:
this.updateTags(TagType.keywords, msg.data || []);
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.updateModifiedUpdating(msg.data || false);
break;
case CommandToCode.toggleWritingSettings:
this.toggleWritingSettings();
break;
case CommandToCode.updateFmHighlight:
this.updateFmHighlight((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.updatePreviewUrl(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;
}
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) {
const imageFields = contentType.fields.filter((field) => field.type === "image");
for (const field of imageFields) {
if (updatedMetadata[field.name]) {
const imageData = ImageHelper.allRelToAbs(field, updatedMetadata[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);
updatedMetadata[field.name] = preview || [];
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
updatedMetadata[field.name] = {
...imageData,
webviewUrl: preview ? preview.toString() : null
};
}
} else {
updatedMetadata[field.name] = field.multiple ? [] : "";
}
}
}
}
}
// 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);
}
/**
@@ -298,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 });
}
}
@@ -308,295 +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, value }: { field: string, value: any, 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);
for (const dateField of dateFields) {
if ((field === dateField.name) && value) {
article.data[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
article.data[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)) {
article.data[field] = value || [];
} else { // Otherwise it is coming from the media dashboard (addition)
let fieldValue = article.data[field];
if (fieldValue && !Array.isArray(fieldValue)) {
fieldValue = [fieldValue];
}
const crntData = Object.assign([], fieldValue);
const allRelPaths = [...(crntData || []), value];
article.data[field] = [...new Set(allRelPaths)].filter(f => f);
}
}
}
ArticleHelper.update(editor, article);
this.pushMetadata(article.data);
}
/**
* 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)
} 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
});
}
/**
* 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[]) {
const editor = window.activeTextEditor;
if (!editor) {
return "";
}
const article = ArticleHelper.getFrontMatter(editor);
if (article && article.data) {
article.data[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) {
article.data[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) {
@@ -610,117 +140,61 @@ 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;
}
/**
* Update the preview URL
*/
private async updatePreviewUrl(previewUrl: string) {
await Settings.update(SETTING_PREVIEW_HOST, previewUrl);
this.getSettings();
}
/**
* Toggle the Front Matter highlighting
*/
private async updateFmHighlight(autoUpdate: boolean) {
await Settings.update(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, autoUpdate);
this.getSettings();
}
/**
* Toggle the modified auto-update setting
*/
private async updateModifiedUpdating(autoUpdate: boolean) {
await Settings.update(SETTING_AUTO_UPDATE_DATE, autoUpdate);
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
*/
private getWebviewContent(webView: Webview): string {
const ext = Extension.getInstance();
const dashboardFile = "panelWebView.js";
const localPort = `9001`;
const localServerUrl = `localhost:${localPort}`;
const extensionPath = ext.extensionPath;
const styleVSCodeUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'vscode.css'));
const styleResetUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'reset.css'));
const stylesUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'styles.css'));
const scriptUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'dist', 'panelWebView.js'));
const nonce = WebviewHelper.getNonce();
const ext = Extension.getInstance();
const version = ext.getVersion();
const isBeta = ext.isBetaVersion();
let scriptUri = "";
const isProd = Extension.getInstance().isProductionMode;
if (isProd) {
scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', dashboardFile)).toString();
} else {
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}
const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
`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}`}`
];
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleResetUri}" rel="stylesheet">
<link href="${styleVSCodeUri}" rel="stylesheet">
<link href="${stylesUri}" rel="stylesheet">
<title>Front Matter</title>
<title>Front Matter Panel</title>
</head>
<body>
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" ></div>
<div id="app" data-isProd="${isProd}" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" ></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`panel-${version.installedVersion}`}" alt="Daily usage" />
<script nonce="${nonce}" src="${scriptUri}"></script>
<script ${isProd ? `nonce="${nonce}"` : ""} src="${scriptUri}"></script>
</body>
</html>
`;

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,19 +19,20 @@ 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;
const extension = Extension.getInstance(context);
Backers.init(context);
if (!extension.checkIfExtensionCanRun()) {
return undefined;
@@ -41,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();
@@ -50,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 {
@@ -58,10 +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();
}));
@@ -78,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');
@@ -145,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, () => {
@@ -157,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();
});
@@ -177,14 +189,13 @@ export async function activate(context: vscode.ExtensionContext) {
triggerShowDraftStatus();
// Listener for file edit changes
editDebounce = debounceCallback();
subscriptions.push(vscode.workspace.onDidChangeTextDocument(triggerFileChange));
subscriptions.push(vscode.workspace.onWillSaveTextDocument(handleAutoDateUpdate));
// Listener for file saves
subscriptions.push(vscode.workspace.onDidSaveTextDocument((doc: vscode.TextDocument) => {
if (doc.languageId === 'markdown') {
// Optimize the list of recently changed files
ExplorerView.getInstance().getFoldersAndFiles();
DataListener.getFoldersAndFiles();
}
}));
@@ -229,10 +240,12 @@ export async function activate(context: vscode.ExtensionContext) {
);
}
export function deactivate() {}
export function deactivate() {
Telemetry.dispose();
}
const triggerFileChange = (e: vscode.TextDocumentChangeEvent) => {
editDebounce(() => Article.autoUpdate(e), 1000);
const handleAutoDateUpdate = (e: vscode.TextDocumentWillSaveEvent) => {
Article.autoUpdate(e);
};
const triggerShowDraftStatus = () => {

View File

@@ -1,22 +1,25 @@
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, 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 { Extension, Settings } from '.';
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
import { Extension, Logger, Settings, SlugHelper } from '.';
import { format, parse } from 'date-fns';
import { Notifications } from './Notifications';
import { Article } from '../commands';
import { basename, join } from 'path';
import { join } from 'path';
import { EditorHelper } from '@estruyf/vscode';
import sanitize from '../helpers/Sanitize';
import { existsSync, mkdirSync } from 'fs';
import { ContentType } from '../models';
import { DateHelper } from './DateHelper';
import { Diagnostic, DiagnosticSeverity, Position, window, Range } from 'vscode';
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[] = [];
@@ -27,8 +30,17 @@ export class ArticleHelper {
* @param editor
*/
public static getFrontMatter(editor: vscode.TextEditor) {
const fileContents = editor.document.getText();
return ArticleHelper.parseFile(fileContents, editor.document.fileName);
return ArticleHelper.getFrontMatterFromDocument(editor.document);
}
/**
* Get the contents of the specified document
*
* @param document The document to parse.
*/
public static getFrontMatterFromDocument(document: vscode.TextDocument) {
const fileContents = document.getText();
return ArticleHelper.parseFile(fileContents, document.fileName);
}
/**
@@ -46,12 +58,38 @@ 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));
}
/**
* Generate the update to be applied to the article.
* @param article
*/
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[];
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
// Check if there is a line ending
const lines = article.content.split("\n");
const lastLine = lines.pop();
const endsWithNewLine = lastLine !== undefined && lastLine.trim() === "";
let newMarkdown = this.stringifyFrontMatter(article.content, Object.assign({}, article.data));
// Logic to not include a new line at the end of the file
if (!endsWithNewLine) {
const lines = newMarkdown.split("\n");
const lastLine = lines.pop();
if (lastLine !== undefined && lastLine?.trim() === "") {
newMarkdown = lines.join("\n");
}
}
// Check for field where quotes need to be removed
if (removeQuotes && removeQuotes.length) {
for (const toRemove of removeQuotes) {
@@ -68,8 +106,7 @@ export class ArticleHelper {
}
}
const nrOfLines = editor.document.lineCount as number;
await editor.edit(builder => builder.replace(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(nrOfLines, 0)), newMarkdown));
return vscode.TextEdit.replace(range, newMarkdown);
}
/**
@@ -82,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) {
@@ -95,9 +129,7 @@ export class ArticleHelper {
}
}
return matter.stringify(content, data, ({
...TomlEngine,
...langOpts,
return FrontMatterParser.toFile(content, data, ({
noArrayIndent: !indentArray,
skipInvalid: true,
noCompatMode: true,
@@ -109,15 +141,32 @@ export class ArticleHelper {
/**
* Checks if the current file is a markdown file
*/
public static isMarkdownFile() {
const editor = vscode.window.activeTextEditor;
return (editor && editor.document && (editor.document.languageId.toLowerCase() === "markdown" || editor.document.languageId.toLowerCase() === "mdx"));
public static isMarkdownFile(document: vscode.TextDocument | undefined | null = null) {
const supportedLanguages = ["markdown", "mdx"];
const fileTypes = Settings.get<string[]>(SETTINGS_CONTENT_SUPPORTED_FILETYPES);
const supportedFileExtensions = fileTypes ? fileTypes.map(f => f.startsWith(`.`) ? f : `.${f}`) : DEFAULT_FILE_TYPES;
const languageId = document?.languageId?.toLowerCase();
const isSupportedLanguage = languageId && supportedLanguages.includes(languageId);
document ??= vscode.window.activeTextEditor?.document;
/**
* It's possible that the file is a file type we support but the user hasn't installed
* language support for. In that case, we'll manually check the extension as a proxy
* for whether or not we support the file.
*/
if (!isSupportedLanguage) {
const fileName = document?.fileName?.toLowerCase();
return fileName && supportedFileExtensions.findIndex(fileExtension => fileName.endsWith(fileExtension)) > -1;
}
return isSupportedLanguage;
}
/**
* Get date from front matter
*/
public static getDate(article: matter.GrayMatterFile<string> | null) {
public static getDate(article: ParsedFrontMatter | null) {
if (!article) {
return;
}
@@ -178,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, "-"));
}
/**
@@ -188,8 +238,9 @@ export class ArticleHelper {
* @param titleValue
* @returns The new file path
*/
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string): string | undefined {
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined {
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
// Name of the file or folder to create
const sanitizedName = ArticleHelper.sanitize(titleValue);
@@ -203,10 +254,10 @@ export class ArticleHelper {
return;
} else {
mkdirSync(newFolder);
newFilePath = join(newFolder, `index.md`);
newFilePath = join(newFolder, `index.${fileExtension || contentType.fileType || fileType}`);
}
} else {
let newFileName = `${sanitizedName}.md`;
let newFileName = `${sanitizedName}.${fileExtension || contentType?.fileType || fileType}`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;
@@ -223,22 +274,186 @@ export class ArticleHelper {
return newFilePath;
}
/**
* Update placeholder values in the front matter content
* @param data
* @param title
* @returns
*/
public static updatePlaceholders(data: any, title: string) {
const fmData = Object.assign({}, data);
for (const fieldName of Object.keys(fmData)) {
const fieldValue = fmData[fieldName];
if (fieldName === "title" && (fieldValue === null || fieldValue === "")) {
fmData[fieldName] = title;
}
if (fieldName === "slug" && (fieldValue === null || fieldValue === "")) {
fmData[fieldName] = SlugHelper.createSlug(title);
}
fmData[fieldName] = this.processKnownPlaceholders(fmData[fieldName], title);
fmData[fieldName] = this.processCustomPlaceholders(fmData[fieldName], title);
}
return fmData;
}
/**
* Replace the known placeholders
* @param value
* @param title
* @returns
*/
public static processKnownPlaceholders(value: string, title: string) {
if (value && typeof value === "string") {
if (value.includes("{{title}}")) {
const regex = new RegExp("{{title}}", "g");
value = value.replace(regex, title);
}
if (value.includes("{{slug}}")) {
const regex = new RegExp("{{slug}}", "g");
value = value.replace(regex, SlugHelper.createSlug(title) || "");
}
if (value.includes("{{now}}")) {
const regex = new RegExp("{{now}}", "g");
value = value.replace(regex, Article.formatDate(new Date()));
}
}
return value;
}
/**
* Replace the custom placeholders
* @param value
* @param title
* @returns
*/
public static processCustomPlaceholders(value: string, title: string) {
if (value && typeof value === "string") {
const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS);
if (placeholders && placeholders.length > 0) {
for (const placeholder of placeholders) {
if (value.includes(`{{${placeholder.id}}}`)) {
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
const updatedValue = this.processKnownPlaceholders(placeholder.value, title);
value = value.replace(regex, updatedValue);
}
}
}
}
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) {
@@ -265,6 +480,8 @@ export class ArticleHelper {
await EditorHelper.showFile(fileName)
}
}];
Logger.error(error.message);
const editor = window.activeTextEditor;
if (editor?.document.uri) {
@@ -278,7 +495,6 @@ export class ArticleHelper {
fmRange = MarkdownFoldingProvider.getFrontMatterRange(editor.document);
}
if (fmRange) {
Extension.getInstance().diagnosticCollection.set(editor.document.uri, [{
severity: DiagnosticSeverity.Error,

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 { ContentType as IContentType, DraftField } from '../models';
import { Uri, workspace, window, commands } from 'vscode';
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field } from '../models';
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 {
@@ -91,6 +92,12 @@ export class ContentType {
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
}
/**
* Create a new file with the specified content type
* @param contentType
* @param folderPath
* @returns
*/
private static async create(contentType: IContentType, folderPath: string) {
const titleValue = await Questions.ContentTitle();
@@ -103,15 +110,7 @@ export class ContentType {
return;
}
let data: any = {};
for (const field of contentType.fields) {
if (field.name === "title") {
data[field.name] = titleValue;
} else {
data[field.name] = null;
}
}
let data: any = this.processFields(contentType, titleValue, {});
data = ArticleHelper.updateDates(Object.assign({}, data));
@@ -127,7 +126,39 @@ export class ContentType {
Notifications.info(`Your new content has been created.`);
Telemetry.send(TelemetryEvent.createContentFromContentType);
// Trigger a refresh for the dashboard
PagesListener.refresh();
}
/**
* Process all content type fields
* @param contentType
* @param data
*/
private static processFields(obj: IContentType | Field, titleValue: string, data: any) {
if (obj.fields) {
for (const field of obj.fields) {
if (field.name === "title") {
if (field.default) {
data[field.name] = ArticleHelper.processKnownPlaceholders(field.default, titleValue);
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
} else {
data[field.name] = titleValue;
}
} else {
if (field.type === "fields") {
data[field.name] = this.processFields(field, titleValue, {});
} else {
data[field.name] = field.default ? ArticleHelper.processKnownPlaceholders(field.default, titleValue) : "";
data[field.name] = field.default ? ArticleHelper.processCustomPlaceholders(data[field.name], titleValue) : "";
}
}
}
}
return data;
}
}

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

@@ -1,8 +1,13 @@
import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Template } from "../commands/Template";
import { ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
import { CONTEXT, ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DATA_FILES, SETTINGS_DATA_FOLDERS, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, ScriptType, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
import { DataFolder } from "../models/DataFolder";
import { DataType } from "../models/DataType";
import { Extension } from "./Extension";
import { FrameworkDetector } from "./FrameworkDetector";
import { Settings } from "./SettingsHelper";
@@ -33,7 +38,7 @@ export class DashboardSettings {
contentFolders: Folders.get(),
crntFramework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type && s.type !== ScriptType.Content),
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []),
dashboardState: {
contents: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
@@ -44,7 +49,58 @@ export class DashboardSettings {
defaultSorting: Settings.get<string>(SETTINGS_MEDIA_SORTING_DEFAULT),
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace")
}
}
},
dataFiles: await this.getDataFiles(),
dataTypes: Settings.get<DataType[]>(SETTINGS_DATA_TYPES),
isBacker: await ext.getState<boolean | undefined>(CONTEXT.backer, 'global')
} as ISettings
}
/**
* Retrieve all the data files
* @returns
*/
private static async getDataFiles(): Promise<DataFile[]> {
const wsPath = Folders.getWorkspaceFolder()?.fsPath;
const files = Settings.get<DataFile[]>(SETTINGS_DATA_FILES);
const folders = Settings.get<DataFolder[]>(SETTINGS_DATA_FOLDERS);
let clonedFiles = Object.assign([], files);
if (folders) {
for (let folder of folders) {
if (!folder.path) {
continue;
}
const folderPath = Folders.getAbsFilePath(folder.path);
if (!folderPath) {
continue;
}
let dataFolderPath = join(folderPath.replace((wsPath || ''), ''));
if (dataFolderPath.startsWith('/')) {
dataFolderPath = dataFolderPath.substring(1);
}
const dataJsonFiles = await workspace.findFiles(join(dataFolderPath, '*.json'));
const dataYmlFiles = await workspace.findFiles(join(dataFolderPath, '*.yml'));
const dataYamlFiles = await workspace.findFiles(join(dataFolderPath, '*.yaml'));
const dataFiles = [...dataJsonFiles, ...dataYmlFiles, ...dataYamlFiles];
for (let dataFile of dataFiles) {
clonedFiles.push({
id: basename(dataFile.fsPath),
title: basename(dataFile.fsPath),
file: dataFile.fsPath,
fileType: dataFile.fsPath.endsWith('.json') ? 'json' : 'yaml',
labelField: folder.labelField,
schema: folder.schema,
type: folder.type
} as DataFile)
}
}
}
return clonedFiles;
}
}

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

@@ -25,4 +25,12 @@ export class Logger {
Logger.channel?.appendLine(`["${type}" - ${format(new Date(), "HH:MM:ss")}] ${message}`);
}
public static warning(message: string): void {
Logger.info(message, "WARNING");
}
public static error(message: string): void {
Logger.info(message, "ERROR");
}
}

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