forked from iarv/vscode-front-matter
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 079a13e161 | |||
| 69c1e587d0 | |||
| 3996252531 | |||
| 4fddda65e6 | |||
| 5916344092 | |||
| b96722dd69 | |||
| 263ccab311 | |||
| 3571af82c7 | |||
| c60520c0ff | |||
| b473431eae | |||
| cbf434f741 | |||
| 04c401207f | |||
| 7291e6aac6 | |||
| a7aab96f0e | |||
| f500749644 | |||
| 47e59bc54c | |||
| 8902e25021 | |||
| 33093e1eb4 | |||
| d36178c44f | |||
| 15b09ccc75 | |||
| dffa6c87a0 | |||
| c4a1caee09 | |||
| 1d9f07b86d | |||
| a794a95bb8 | |||
| 40a56f6057 | |||
| 82353f7b64 | |||
| 82a22da90a | |||
| 380e40ea05 | |||
| 2bedb23341 | |||
| 1110b76364 | |||
| d19e632f80 | |||
| 4e040b5f7a | |||
| 7a2a0934c2 | |||
| d3eb7b223c | |||
| f74eec954f | |||
| 864c4e7aa6 | |||
| 5667906caf | |||
| 2fe7c524e7 | |||
| 5cc83526ad | |||
| 76b5e99a08 | |||
| 7d5505d421 | |||
| d97a11f8b5 | |||
| 0590cec684 | |||
| b4cdc4feb9 | |||
| 986fd95524 | |||
| f51fec5fb9 | |||
| 8198ce2af3 | |||
| defffc4c8e | |||
| 8f47cbfb0b | |||
| 0be91c17d0 | |||
| fae7ab8417 | |||
| df239f2cc0 | |||
| 70099dc97f | |||
| c11be0e3ec | |||
| f8b7870180 | |||
| c58a5c62d9 | |||
| ce92444bf2 | |||
| b2709ebffd | |||
| 2b20cf9d24 | |||
| f4a499ad0f | |||
| 4494b158c0 | |||
| 3416e55264 |
@@ -1,5 +1,60 @@
|
||||
# Change Log
|
||||
|
||||
## [5.3.1] - 2021-10-29
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#163](https://github.com/estruyf/vscode-front-matter/issues/163): Setting workspace state instead of global state for the media view
|
||||
|
||||
## [5.3.0] - 2021-10-28 - [Release Notes](https://beta.frontmatter.codes/updates/v5.3.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#158](https://github.com/estruyf/vscode-front-matter/issues/158): Add support for non-boolean draft/publish status fields
|
||||
- [#159](https://github.com/estruyf/vscode-front-matter/issues/159): Enhancements to SEO checks: Slug check, keyword details, more article information
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Value check when generating slug from title
|
||||
- Fix for date time formatting with `DD` and `YYYY` tokens
|
||||
- Fix in tag space replacing when object is passed
|
||||
|
||||
## [5.2.0] - 2021-10-19
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#151](https://github.com/estruyf/vscode-front-matter/issues/151): Detect which site-generator or framework is used
|
||||
- [#152](https://github.com/estruyf/vscode-front-matter/issues/152): Automatically set setting based on the used site-generator or framework
|
||||
- [#154](https://github.com/estruyf/vscode-front-matter/issues/154): Bulk script support added
|
||||
- [#155](https://github.com/estruyf/vscode-front-matter/issues/155): Fallback image added for the images shown in the editor panel
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#153](https://github.com/estruyf/vscode-front-matter/issues/153): Support old date formatting for date-fns
|
||||
- [#156](https://github.com/estruyf/vscode-front-matter/issues/156): Fix for uploading media files into a new folder
|
||||
|
||||
## [5.1.1] - 2021-10-14
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#149](https://github.com/estruyf/vscode-front-matter/issues/149): Fix panel rendering when incorrect type for keywords is provided
|
||||
|
||||
## [5.1.0] - 2021-10-13
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#141](https://github.com/estruyf/vscode-front-matter/issues/141): Allow content creation for page bundles or single files
|
||||
- [#145](https://github.com/estruyf/vscode-front-matter/issues/145): Moved folder registration settings to `frontmatter.json` file
|
||||
- [#147](https://github.com/estruyf/vscode-front-matter/issues/147): Error boundary added for metadata fields
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Rendered more hooks than during the previous render in `FileList`
|
||||
- [#142](https://github.com/estruyf/vscode-front-matter/issues/142): Fix for unknown tags where it throws an error
|
||||
- [#143](https://github.com/estruyf/vscode-front-matter/issues/143): Fix for duplicate values in the file list
|
||||
- [#144](https://github.com/estruyf/vscode-front-matter/issues/144): Fix for `toISOString` does not exist on object
|
||||
- [#146](https://github.com/estruyf/vscode-front-matter/issues/146): Date parsing logic added with fallbacks
|
||||
|
||||
## [5.0.0] - 2021-10-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.0.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
@@ -48,6 +48,10 @@ Our main extension features are:
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**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).
|
||||
|
||||
**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).
|
||||
|
||||
@@ -46,6 +46,10 @@ Our main extension features are:
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**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).
|
||||
|
||||
**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).
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
|
||||
<rect x="25" y="25" fill="none" stroke="#ffffff" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
|
||||
<path fill="#ffffff" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#ffffff" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
|
||||
<rect x="25" y="25" fill="none" stroke="#424242" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
|
||||
<path fill="#424242" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+39
-1
@@ -356,8 +356,18 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.table__cell__seo_details {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table__cell__validation {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table__cell__validation div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.table__cell__validation .valid {
|
||||
@@ -368,6 +378,15 @@
|
||||
color: #E6AF2E;
|
||||
}
|
||||
|
||||
.table__cell__validation div span + span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.seo__status__note {
|
||||
font-size: 10px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
/* Fields */
|
||||
.field__toggle {
|
||||
position: relative;
|
||||
@@ -437,6 +456,25 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__error {
|
||||
color: var(--vscode-errorForeground);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata_field__error button {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.metadata_field__error button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__input, .metadata_field__input:focus,
|
||||
.metadata_field__textarea, .metadata_field__textarea:focus {
|
||||
outline: none;
|
||||
|
||||
Generated
+133
-45
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "5.0.0",
|
||||
"version": "5.3.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -236,6 +236,60 @@
|
||||
"integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/fast-element": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.6.0.tgz",
|
||||
"integrity": "sha512-ePTcBuCA99n7He0BYLIzFr5YOHYPSiBLJeDbDsyyQ5JUs4oXaZD0v54Pq0GAtSZuagnCGTDqcxEcBPUhTqLmQw==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/fast-foundation": {
|
||||
"version": "1.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-1.24.8.tgz",
|
||||
"integrity": "sha512-n4O9jPh8BBliF/Yl9FAVhrSoopsRCnva2L432s/fHwLelY9WUeswjO3DidVBFbzXD5u/gzC4LGWJScNe/ZGU4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@microsoft/fast-element": "^1.4.0",
|
||||
"@microsoft/fast-web-utilities": "^4.8.0",
|
||||
"@microsoft/tsdoc-config": "^0.13.4",
|
||||
"tabbable": "^5.2.0",
|
||||
"tslib": "^1.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@microsoft/fast-web-utilities": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-4.8.1.tgz",
|
||||
"integrity": "sha512-P3xeyUwQ9nPkFrgAdmkOzaXxIq8YqMU5K+LXcoHgJddJCBCKfGWW9OZQOTigLddItTyVyfO8qsJpDQb1TskKHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exenv-es6": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/tsdoc": {
|
||||
"version": "0.12.24",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.24.tgz",
|
||||
"integrity": "sha512-Mfmij13RUTmHEMi9vRUhMXD7rnGR2VvxeNYtaGtaJ4redwwjT4UXYJ+nzmVJF7hhd4pn/Fx5sncDKxMVFJSWPg==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/tsdoc-config": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.9.tgz",
|
||||
"integrity": "sha512-VqqZn+rT9f6XujFPFR2aN9XKF/fuir/IzKVzoxI0vXIzxysp4ee6S2jCakmlGFHEasibifFTsJr7IYmRPxfzYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@microsoft/tsdoc": "0.12.24",
|
||||
"ajv": "~6.12.6",
|
||||
"jju": "~1.4.0",
|
||||
"resolve": "~1.19.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -269,92 +323,92 @@
|
||||
"dev": true
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz",
|
||||
"integrity": "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.3.tgz",
|
||||
"integrity": "sha512-jwlpsk2/u1cofvfYsjmqcnx50JJtf/T6HTgdW+ih8+rqWC5ABEZf4IiB/H+KAyjJ3wVzCOugMq5irL83XDCfqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/core": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"@sentry/core": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
|
||||
"integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.3.tgz",
|
||||
"integrity": "sha512-obm3SjgCk8A7nB37b2AU1eq1q7gMoJRrGMv9VRIyfcG0Wlz/5lJ9O3ohUk+YZaaVfZMxXn6hFtsBiOWmlv7IIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/minimal": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"@sentry/hub": "6.13.3",
|
||||
"@sentry/minimal": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
|
||||
"integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.3.tgz",
|
||||
"integrity": "sha512-eYppBVqvhs5cvm33snW2sxfcw6G20/74RbBn+E4WDo15hozis89kU7ZCJDOPkXuag3v1h9igns/kM6PNBb41dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
|
||||
"integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.3.tgz",
|
||||
"integrity": "sha512-63MlYYRni3fs5Bh8XBAfVZ+ctDdWg0fapSTP1ydIC37fKvbE+5zhyUqwrEKBIiclEApg1VKX7bkKxVdu/vsFdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/hub": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/react": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz",
|
||||
"integrity": "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.3.tgz",
|
||||
"integrity": "sha512-fdfmD9XNpGDwdkeLyd+iq+kqtNeghpH3wiez2rD81ZBvrn70uKaO2/yYDE71AXC6fUOwQuJmdfAuqBcNJkYIEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/browser": "6.13.2",
|
||||
"@sentry/minimal": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@sentry/minimal": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
|
||||
"integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.3.tgz",
|
||||
"integrity": "sha512-yyOFIhqlprPM0g4f35Icear3eZk2mwyYcGEzljJfY2iU6pJwj1lzia5PfSwiCW7jFGMmlBJNhOAIpfhlliZi8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/minimal": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"@sentry/hub": "6.13.3",
|
||||
"@sentry/minimal": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
|
||||
"integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.3.tgz",
|
||||
"integrity": "sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A==",
|
||||
"dev": true
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
|
||||
"integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.3.tgz",
|
||||
"integrity": "sha512-zYFuFH3MaYtBZTeJ4Yajg7pDf0pM3MWs3+9k5my9Fd+eqNcl7dYQYJbT9gyC0HXK1QI4CAMNNlHNl4YXhF91ag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/types": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
@@ -586,6 +640,16 @@
|
||||
"integrity": "sha512-LlO6K7nzrIWDCZN1Zi6J6ibxrpMibSAct+zNjAwpkNkwup6cJLx5diYvsOJODMPWOuQlBO21qkxtdkSRzW6+Jw==",
|
||||
"dev": true
|
||||
},
|
||||
"@vscode/webview-ui-toolkit": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-0.8.1.tgz",
|
||||
"integrity": "sha512-SOgeaZ+/6yFDXLsgH+JvKcs4E0ThvuohNhkr9mvnggjl+OfFxc+Yqkyf5B4B5kuu3EppOCzEMiUwPC8APuLEGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@microsoft/fast-element": "^1.2.0",
|
||||
"@microsoft/fast-foundation": "^1.24.7"
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||
@@ -2264,6 +2328,12 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"exenv-es6": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.0.0.tgz",
|
||||
"integrity": "sha512-fcG/TX8Ruv9Ma6PBaiNsUrHRJzVzuFMP6LtPn/9iqR+nr9mcLeEOGzXQGLC5CVQSXGE98HtzW2mTZkrCA3XrDg==",
|
||||
"dev": true
|
||||
},
|
||||
"expand-brackets": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
|
||||
@@ -3512,6 +3582,12 @@
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||
"dev": true
|
||||
},
|
||||
"jju": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
|
||||
"integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3645,6 +3721,12 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.topath": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
|
||||
@@ -5774,6 +5856,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tabbable": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.1.tgz",
|
||||
"integrity": "sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==",
|
||||
"dev": true
|
||||
},
|
||||
"tailwindcss": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.7.tgz",
|
||||
|
||||
+83
-4
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter",
|
||||
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "5.0.0",
|
||||
"version": "5.3.1",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -97,6 +97,43 @@
|
||||
"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.draftField": {
|
||||
"type": "object",
|
||||
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.draftField)",
|
||||
"default": {
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
},
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"boolean",
|
||||
"choice"
|
||||
],
|
||||
"description": ""
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the field to use"
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "List of choices for the field",
|
||||
"items": {
|
||||
"type": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.fmHighlight": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -151,6 +188,22 @@
|
||||
"nodeBin": {
|
||||
"type": "string",
|
||||
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use."
|
||||
},
|
||||
"bulk": {
|
||||
"type": "boolean",
|
||||
"description": "Run the script for all content files"
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"editor",
|
||||
"notification"
|
||||
],
|
||||
"description": "Define where you want to output your script output. Default is a notification, but you can specify to show it in an editor panel."
|
||||
},
|
||||
"outputType": {
|
||||
"type": "string",
|
||||
"description": "The type of output for the editor panel. Can be used to change it to 'markdown' for example"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -180,6 +233,11 @@
|
||||
"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.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.panel.freeform": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -198,6 +256,12 @@
|
||||
"markdownDescription": "Specify the path you want to add after the host and before your slug. This can be used for instance to include the year/month like: `yyyy/MM`. The date will be generated based on the article its date field value. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.preview.pathname)",
|
||||
"scope": "Site preview"
|
||||
},
|
||||
"frontMatter.site.baseURL": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.site.baseURL)",
|
||||
"scope": "Site"
|
||||
},
|
||||
"frontMatter.taxonomy.alignFilename": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -252,7 +316,8 @@
|
||||
"image",
|
||||
"choice",
|
||||
"tags",
|
||||
"categories"
|
||||
"categories",
|
||||
"draft"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -314,6 +379,11 @@
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pageBundle": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if you want to create a folder when creating new content."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -325,6 +395,7 @@
|
||||
"default": [
|
||||
{
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
@@ -429,6 +500,12 @@
|
||||
"markdownDescription": "Specifies the optimal description length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seodescriptionlength)",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.seoSlugLength": {
|
||||
"type": "number",
|
||||
"default": 75,
|
||||
"markdownDescription": "Specifies the optimal slug length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seoSlugLength)",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.seoTitleLength": {
|
||||
"type": "number",
|
||||
"default": 60,
|
||||
@@ -658,8 +735,8 @@
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@sentry/react": "^6.13.2",
|
||||
"@sentry/tracing": "^6.13.2",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"@tailwindcss/forms": "^0.3.3",
|
||||
"@types/glob": "7.1.3",
|
||||
"@types/js-yaml": "3.12.1",
|
||||
@@ -671,6 +748,7 @@
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/vscode": "1.51.0",
|
||||
"@vscode/codicons": "0.0.20",
|
||||
"@vscode/webview-ui-toolkit": "^0.8.1",
|
||||
"autoprefixer": "^10.3.2",
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.23.0",
|
||||
@@ -681,6 +759,7 @@
|
||||
"html-loader": "1.3.2",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"image-size": "^1.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash.uniqby": "4.7.0",
|
||||
"mdast-util-from-markdown": "1.0.0",
|
||||
"node-json-db": "^1.3.0",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { extname, basename } from 'path';
|
||||
import { COMMAND_NAME, DefaultFields } from '../constants';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
|
||||
|
||||
export class Article {
|
||||
@@ -171,7 +172,7 @@ export class Article {
|
||||
|
||||
let newFileName = `${slugName}${ext}`;
|
||||
if (filePrefix && typeof filePrefix === "string") {
|
||||
newFileName = `${format(new Date(), filePrefix)}-${newFileName}`;
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix) as string)}-${newFileName}`;
|
||||
}
|
||||
|
||||
const newPath = editor.document.uri.fsPath.replace(fileName, newFileName);
|
||||
@@ -239,13 +240,13 @@ export class Article {
|
||||
/**
|
||||
* Format the date to the defined format
|
||||
*/
|
||||
public static formatDate(dateValue: Date) {
|
||||
public static formatDate(dateValue: Date): string {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
return format(dateValue, dateFormat);
|
||||
return format(dateValue, DateHelper.formatUpdate(dateFormat) as string);
|
||||
} else {
|
||||
return dateValue.toISOString();
|
||||
return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+86
-23
@@ -1,10 +1,10 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME } from '../constants';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID, SETTINGS_CONTENT_DRAFT_FIELD } from '../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { basename, dirname, extname, join } from "path";
|
||||
import { basename, dirname, extname, join, parse } from "path";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
|
||||
import { Settings as SettingsHelper } from '../helpers';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { DraftField, Framework, TaxonomyType } from '../models';
|
||||
import { Folders } from './Folders';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
|
||||
@@ -14,7 +14,6 @@ import { Template } from './Template';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { Settings } from '../dashboardWebView/models/Settings';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
import { parseJSON } from 'date-fns';
|
||||
import { ViewType } from '../dashboardWebView/state';
|
||||
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
|
||||
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
|
||||
@@ -24,6 +23,9 @@ import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { MediaLibrary } from '../helpers/MediaLibrary';
|
||||
import imageSize from 'image-size';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { FrameworkDetector } from '../helpers/FrameworkDetector';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
@@ -97,8 +99,8 @@ export class Dashboard {
|
||||
Dashboard.isDisposed = false;
|
||||
|
||||
Dashboard.webview.iconPath = {
|
||||
dark: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter-dark.svg')),
|
||||
light: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter.svg'))
|
||||
dark: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-dark.svg')),
|
||||
light: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-light.svg'))
|
||||
};
|
||||
|
||||
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri);
|
||||
@@ -160,7 +162,7 @@ export class Dashboard {
|
||||
}
|
||||
break;
|
||||
case DashboardMessage.setPageViewType:
|
||||
Extension.getInstance().setState(ExtensionState.PagesView, msg.data);
|
||||
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
|
||||
break;
|
||||
case DashboardMessage.getMedia:
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
|
||||
@@ -187,6 +189,9 @@ export class Dashboard {
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
|
||||
break;
|
||||
case DashboardMessage.setFramework:
|
||||
Dashboard.setFramework(msg?.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -220,11 +225,28 @@ export class Dashboard {
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
|
||||
if (data?.position) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const editor = window.activeTextEditor;
|
||||
const line = data.position.line;
|
||||
const character = data.position.character;
|
||||
if (line) {
|
||||
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || ``));
|
||||
let imgPath = data.image;
|
||||
const filePath = data.file;
|
||||
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
|
||||
|
||||
const imgDir = dirname(absImgPath);
|
||||
const fileDir = dirname(filePath);
|
||||
|
||||
if (imgDir === fileDir) {
|
||||
imgPath = join('/', basename(imgPath));
|
||||
|
||||
// Snippets are already parsed, so update the URL of the image
|
||||
if (data.snippet) {
|
||||
data.snippet = data.snippet.replace(data.image, imgPath);
|
||||
}
|
||||
}
|
||||
|
||||
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || ``));
|
||||
}
|
||||
panel.getMediaSelection();
|
||||
} else {
|
||||
@@ -240,6 +262,7 @@ export class Dashboard {
|
||||
private static async getSettings() {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = await Template.isInitialized();
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.settings,
|
||||
@@ -248,19 +271,40 @@ export class Dashboard {
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
|
||||
folders: Folders.get(),
|
||||
initialized: await Template.isInitialized(),
|
||||
initialized: isInitialized,
|
||||
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
|
||||
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
|
||||
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
draftField: SettingsHelper.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
|
||||
contentFolders: Folders.get().map(f => f.path),
|
||||
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
} as Settings
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
*/
|
||||
private static setFramework(frameworkId: string | null) {
|
||||
SettingsHelper.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
|
||||
|
||||
if (frameworkId) {
|
||||
const allFrameworks = FrameworkDetector.getAll();
|
||||
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
|
||||
if (framework) {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
} else {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting from the dashboard
|
||||
*/
|
||||
@@ -272,16 +316,25 @@ export class Dashboard {
|
||||
/**
|
||||
* Retrieve all media files
|
||||
*/
|
||||
private static async getMedia(page: number = 0, selectedFolder: string = '') {
|
||||
private static async getMedia(page: number = 0, requestedFolder: string = '') {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const contentFolders = Folders.get();
|
||||
const viewData = Dashboard.viewData;
|
||||
let selectedFolder = requestedFolder;
|
||||
|
||||
// If the static folder is not set, retreive the last opened location
|
||||
if (!selectedFolder) {
|
||||
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder);
|
||||
if (stateValue && existsSync(stateValue)) {
|
||||
selectedFolder = stateValue;
|
||||
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder, "workspace");
|
||||
|
||||
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
|
||||
// Support for page bundles
|
||||
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
|
||||
const folderPath = parse(viewData.data.filePath).dir;
|
||||
selectedFolder = folderPath;
|
||||
} else if (stateValue && existsSync(stateValue)) {
|
||||
selectedFolder = stateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +343,15 @@ export class Dashboard {
|
||||
selectedFolder = '';
|
||||
}
|
||||
|
||||
const relSelectedFolderPath = selectedFolder ? selectedFolder.substring((parseWinPath(wsFolder?.fsPath || "")).length + 1) : '';
|
||||
let relSelectedFolderPath = selectedFolder;
|
||||
const parsedPath = parseWinPath(wsFolder?.fsPath || "");
|
||||
if (selectedFolder && selectedFolder.startsWith(parsedPath)) {
|
||||
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
|
||||
}
|
||||
|
||||
if (relSelectedFolderPath.startsWith('/')) {
|
||||
relSelectedFolderPath = relSelectedFolderPath.substring(1);
|
||||
}
|
||||
|
||||
let allMedia: MediaInfo[] = [];
|
||||
|
||||
@@ -379,7 +440,7 @@ export class Dashboard {
|
||||
}
|
||||
|
||||
// Store the last opened folder
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, selectedFolder);
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.media,
|
||||
@@ -420,8 +481,8 @@ export class Dashboard {
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
fmDraft: article?.data.draft ? "Draft" : "Published",
|
||||
fmYear: article?.data[dateField] ? parseJSON(article?.data[dateField]).getFullYear() : null,
|
||||
fmDraft: ContentType.getDraftStatus(article?.data),
|
||||
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
@@ -432,7 +493,7 @@ export class Dashboard {
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
|
||||
|
||||
|
||||
if (article?.data[previewField] && wsFolder) {
|
||||
let fieldValue = article?.data[previewField];
|
||||
if (fieldValue && Array.isArray(fieldValue)) {
|
||||
@@ -523,9 +584,9 @@ export class Dashboard {
|
||||
|
||||
if (imgData) {
|
||||
writeFileSync(staticPath, imgData.data);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${staticFolder}/${folder}`);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
|
||||
|
||||
const folderPath = `${staticFolder}/${folder}`;
|
||||
const folderPath = `${folder}`;
|
||||
if (Dashboard.timers[folderPath]) {
|
||||
clearTimeout(Dashboard.timers[folderPath]);
|
||||
delete Dashboard.timers[folderPath];
|
||||
@@ -596,7 +657,9 @@ export class Dashboard {
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const version = Extension.getInstance().getVersion();
|
||||
const ext = Extension.getInstance();
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
@@ -608,7 +671,7 @@ export class Dashboard {
|
||||
<title>Front Matter Dashboard</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
|
||||
<div id="app" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
|
||||
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`dashboard-${version.installedVersion}`}" alt="Daily usage" />
|
||||
|
||||
|
||||
@@ -274,7 +274,11 @@ export class Folders {
|
||||
*/
|
||||
private static async update(folders: ContentFolder[]) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folders.map(folder => ({ title: folder.title, path: Folders.relWsFolder(folder, wsFolder) })));
|
||||
let folderDetails = folders.map(folder => ({
|
||||
title: folder.title,
|
||||
path: Folders.relWsFolder(folder, wsFolder)
|
||||
}));
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import { commands, env, Uri, ViewColumn, window } from "vscode";
|
||||
import { Settings } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
|
||||
|
||||
export class Preview {
|
||||
@@ -34,7 +35,7 @@ export class Preview {
|
||||
if (settings.pathname) {
|
||||
const articleDate = ArticleHelper.getDate(article);
|
||||
try {
|
||||
slug = join(format(articleDate || new Date(), settings.pathname), slug);
|
||||
slug = join(format(articleDate || new Date(), DateHelper.formatUpdate(settings.pathname) as string), slug);
|
||||
} catch (error) {
|
||||
slug = join(settings.pathname, slug);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
|
||||
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -15,6 +16,11 @@ export class StatusListener {
|
||||
public static async verify(frontMatterSB: vscode.StatusBarItem, collection: vscode.DiagnosticCollection) {
|
||||
const draftMsg = "in draft";
|
||||
const publishMsg = "to publish";
|
||||
|
||||
const draft = ContentType.getDraftField();
|
||||
if (!draft || draft.type !== "boolean") {
|
||||
frontMatterSB.hide();
|
||||
}
|
||||
|
||||
let editor = vscode.window.activeTextEditor;
|
||||
if (editor && ArticleHelper.isMarkdownFile()) {
|
||||
|
||||
+10
-12
@@ -3,14 +3,14 @@ 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 { format } from 'date-fns';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { ArticleHelper, Settings } from '../helpers';
|
||||
import { Article } from '.';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { CONTEXT } from '../constants';
|
||||
import { Project } from './Project';
|
||||
import { Folders } from './Folders';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
|
||||
export class Template {
|
||||
|
||||
@@ -95,7 +95,7 @@ export class Template {
|
||||
*/
|
||||
public static async create(folderPath: string) {
|
||||
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
|
||||
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
const contentTypes = ContentType.getAll();
|
||||
|
||||
if (!folderPath) {
|
||||
Notifications.warning(`Incorrect project folder path retrieved.`);
|
||||
@@ -133,16 +133,14 @@ export class Template {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExt = path.parse(selectedTemplate).ext;
|
||||
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
|
||||
let newFileName = `${sanitizedName}${fileExt}`;
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
|
||||
const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath);
|
||||
let contentType: IContentType | undefined;
|
||||
if (templateData && templateData.data && templateData.data.type) {
|
||||
contentType = contentTypes?.find(t => t.name === templateData.data.type);
|
||||
}
|
||||
|
||||
const newFilePath = path.join(folderPath, newFileName);
|
||||
if (fs.existsSync(newFilePath)) {
|
||||
Notifications.warning(`File already exists, please remove it before creating a new one with the same title.`);
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,7 +160,7 @@ export class Template {
|
||||
fmData.title = titleValue;
|
||||
}
|
||||
if (typeof fmData.slug !== "undefined") {
|
||||
fmData.slug = sanitizedName;
|
||||
fmData.slug = ArticleHelper.sanitize(titleValue);
|
||||
}
|
||||
|
||||
frontMatter = Article.updateDate(frontMatter);
|
||||
|
||||
@@ -4,6 +4,7 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default';
|
||||
|
||||
export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
@@ -28,7 +29,7 @@ export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
{
|
||||
"title": "Is in draft",
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
"type": "draft"
|
||||
},
|
||||
{
|
||||
"title": "Tags",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const FrameworkDetectors = [
|
||||
{
|
||||
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
|
||||
"requiredFiles": ["gatsby-config.js"],
|
||||
"requiredDependencies": ["gatsby"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
|
||||
"requiredFiles": ["config.toml", "config.yaml", "config.yml"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
|
||||
"requiredFiles": ["next.config.js"],
|
||||
"requiredDependencies": ["next"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
|
||||
"requiredFiles": ["nuxt.config.js"],
|
||||
"requiredDependencies": ["nuxt"]
|
||||
}
|
||||
];
|
||||
@@ -20,6 +20,7 @@ export const SETTING_REMOVE_QUOTES = "taxonomy.noPropertyValueQuotes";
|
||||
export const SETTING_FRONTMATTER_TYPE = "taxonomy.frontMatterType";
|
||||
|
||||
export const SETTING_SEO_TITLE_LENGTH = "taxonomy.seoTitleLength";
|
||||
export const SETTING_SEO_SLUG_LENGTH = "taxonomy.seoSlugLength";
|
||||
export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength";
|
||||
export const SETTING_SEO_CONTENT_MIN_LENGTH = "taxonomy.seoContentLengh";
|
||||
export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
|
||||
@@ -38,10 +39,15 @@ export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
|
||||
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
|
||||
|
||||
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
export const SETTINGS_FRAMEWORK_ID = "framework.id";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -17,5 +17,6 @@ export enum DashboardMessage {
|
||||
deleteMedia = 'deleteMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder'
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
setFramework = 'setFramework',
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
|
||||
<div className="p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
<Status draft={!!draft} />
|
||||
<Status draft={draft} />
|
||||
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Status draft={!!draft} />
|
||||
<Status draft={draft} />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { format, parseJSON } from 'date-fns';
|
||||
import { format } from 'date-fns';
|
||||
import * as React from 'react';
|
||||
import { DateHelper } from '../../helpers/DateHelper';
|
||||
|
||||
export interface IDateFieldProps {
|
||||
value: Date | string;
|
||||
@@ -10,9 +11,9 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: Rea
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
|
||||
const dateString = format(parsedValue, 'yyyy-MM-dd');
|
||||
setDateValue(dateString);
|
||||
const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value) : value;
|
||||
const dateString = parsedValue ? format(parsedValue, 'yyyy-MM-dd') : parsedValue;
|
||||
setDateValue(dateString || "");
|
||||
} catch (e) {
|
||||
// Date is invalid
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Tab } from '../constants/Tab';
|
||||
import { TabAtom } from '../state';
|
||||
import { SettingsAtom, TabAtom } from '../state';
|
||||
|
||||
export interface INavigationProps {
|
||||
totalPages: number;
|
||||
@@ -15,19 +15,47 @@ export const tabs = [
|
||||
|
||||
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
|
||||
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
return (
|
||||
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))}
|
||||
{
|
||||
settings?.draftField?.type === "boolean" ? (
|
||||
tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`${tabs[0].id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tabs[0].id)}
|
||||
>
|
||||
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
|
||||
{
|
||||
settings?.draftField?.choices?.map((value, idx) => (
|
||||
<button
|
||||
key={`${value}-${idx}`}
|
||||
className={`${value === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm first-letter:uppercase`}
|
||||
aria-current={value === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(value)}
|
||||
>
|
||||
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsAtom } from '../state';
|
||||
|
||||
export interface IStatusProps {
|
||||
draft: boolean;
|
||||
draft: boolean | string;
|
||||
}
|
||||
|
||||
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
if (settings?.draftField && settings.draftField.type === "choice") {
|
||||
if (draft) {
|
||||
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
|
||||
);
|
||||
|
||||
@@ -4,22 +4,17 @@ import { Status } from '../../models/Status';
|
||||
|
||||
export interface IStepProps {
|
||||
name: string;
|
||||
description: string;
|
||||
description: JSX.Element;
|
||||
status: Status;
|
||||
showLine: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: () => void | undefined;
|
||||
}
|
||||
|
||||
export const Step: React.FunctionComponent<IStepProps> = ({name, description, status, showLine, onClick}: React.PropsWithChildren<IStepProps>) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLine ? (
|
||||
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
|
||||
) : null
|
||||
}
|
||||
|
||||
<button className={`relative flex items-start group text-left ${onClick ? "" : "cursor-default"}`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
|
||||
const renderChildren = () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
status === Status.NotStarted && (
|
||||
<span className="h-9 flex items-center" aria-hidden="true">
|
||||
@@ -52,9 +47,31 @@ export const Step: React.FunctionComponent<IStepProps> = ({name, description, st
|
||||
|
||||
<span className="ml-4 min-w-0 flex flex-col">
|
||||
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
|
||||
<span className="text-sm text-vulcan-400 dark:text-whisper-600" dangerouslySetInnerHTML={{__html: description}} />
|
||||
<div className="text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLine ? (
|
||||
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
onClick ? (
|
||||
<button className={`relative flex items-start group text-left`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
|
||||
{renderChildren()}
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative flex items-start group text-left">
|
||||
{renderChildren()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -4,36 +4,99 @@ import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Settings } from '../../models/Settings';
|
||||
import { Status } from '../../models/Status';
|
||||
import { Step } from './Step';
|
||||
import { useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { Framework } from '../../../models';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
const [framework, setFramework] = useState<string | null>(null);
|
||||
|
||||
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
|
||||
const setFrameworkAndSendMessage = (framework: string) => {
|
||||
setFramework(framework);
|
||||
Messenger.send(DashboardMessage.setFramework, framework);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
name: 'Initialize project',
|
||||
description: 'Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.',
|
||||
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
|
||||
status: settings.initialized ? Status.Completed : Status.NotStarted,
|
||||
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
|
||||
},
|
||||
{
|
||||
name: 'Framework presets',
|
||||
description: (
|
||||
<div>
|
||||
<div>Select your site-generator or framework to prefill some of the recommended settings.</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left mt-4">
|
||||
<div>
|
||||
<Menu.Button className="group flex justify-center text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600 p-2 rounded-md border border-vulcan-400 dark:border-white">
|
||||
{framework ? framework : 'Select your framework'}
|
||||
<ChevronDownIcon
|
||||
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Menu.Items className={`w-40 origin-top-left absolute left-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
<MenuItem
|
||||
title={`other`}
|
||||
value={`other`}
|
||||
isCurrent={!framework}
|
||||
onClick={(value) => setFrameworkAndSendMessage(value)} />
|
||||
|
||||
<hr />
|
||||
|
||||
{frameworks.map((f) => (
|
||||
<MenuItem
|
||||
key={f.name}
|
||||
title={f.name}
|
||||
value={f.name}
|
||||
isCurrent={f.name === framework}
|
||||
onClick={(value) => setFrameworkAndSendMessage(value)} />
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
),
|
||||
status: settings.crntFramework ? Status.Completed : Status.NotStarted,
|
||||
onClick: undefined
|
||||
},
|
||||
{
|
||||
name: 'Register content folders (manual action)',
|
||||
description: 'Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.',
|
||||
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
|
||||
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
name: 'Show the dashboard',
|
||||
description: 'Once both actions are completed, click on this action to load the dashboard.',
|
||||
description: <>Once both actions are completed, click on this action to load the dashboard.</>,
|
||||
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
|
||||
}
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings.crntFramework) {
|
||||
setFramework(settings.crntFramework);
|
||||
}
|
||||
}, [settings.crntFramework]);
|
||||
|
||||
return (
|
||||
<nav aria-label="Progress">
|
||||
<ol role="list" className="overflow-hidden">
|
||||
<ol role="list">
|
||||
{steps.map((step, stepIdx) => (
|
||||
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
|
||||
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />
|
||||
|
||||
@@ -33,11 +33,11 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({set
|
||||
</h1>
|
||||
|
||||
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
Thank you for taking the time to test out Front Matter!
|
||||
Thank you for using Front Matter!
|
||||
</p>
|
||||
|
||||
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
We try to aim to make Front Matter as easy to use as possible, but if you have any questions, please don't hesitate to reach out to us on GitHub.
|
||||
We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 w-full sm:mx-auto sm:max-w-lg lg:ml-0">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tab } from '../constants/Tab';
|
||||
import { Page } from '../models/Page';
|
||||
import Fuse from 'fuse.js';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { CategorySelector, FolderSelector, SearchSelector, SortingSelector, TabSelector, TagSelector } from '../state';
|
||||
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingSelector, TabSelector, TagSelector } from '../state';
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
keys: [
|
||||
@@ -16,6 +16,7 @@ const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [ pageItems, setPageItems ] = useState<Page[]>([]);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const tab = useRecoilValue(TabSelector);
|
||||
const sorting = useRecoilValue(SortingSelector);
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
@@ -24,6 +25,8 @@ export default function usePages(pages: Page[]) {
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
|
||||
useEffect(() => {
|
||||
const draftField = settings?.draftField;
|
||||
|
||||
// Check if search needs to be performed
|
||||
let searchedPages = pages;
|
||||
if (search) {
|
||||
@@ -34,12 +37,22 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
// Filter the pages
|
||||
let pagesToShow: Page[] = Object.assign([], searchedPages);
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page.draft);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page.draft);
|
||||
|
||||
if (draftField && draftField.type === 'choice') {
|
||||
if (tab !== Tab.All) {
|
||||
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
const draftFieldName = draftField?.name || "draft";
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the pages
|
||||
@@ -69,7 +82,7 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
|
||||
setPageItems(pagesSorted);
|
||||
}, [ pages, tab, sorting, folder, search, tag, category ]);
|
||||
}, [ settings?.draftField, pages, tab, sorting, folder, search, tag, category ]);
|
||||
|
||||
return {
|
||||
pageItems
|
||||
|
||||
@@ -7,10 +7,17 @@ import { Integrations } from "@sentry/tracing";
|
||||
import { SENTRY_LINK } from "../constants";
|
||||
import './styles.css';
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
const welcome = elm?.getAttribute("data-showWelcome");
|
||||
const version = elm?.getAttribute("data-version");
|
||||
const environment = elm?.getAttribute("data-environment");
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || "",
|
||||
environment: environment || ""
|
||||
});
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
@@ -18,7 +25,4 @@ declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
setState: (data: T) => void;
|
||||
postMessage: (msg: unknown) => void;
|
||||
};
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
const welcome = elm?.getAttribute("data-showWelcome");
|
||||
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
|
||||
@@ -6,7 +6,7 @@ export interface Page {
|
||||
fmFileName: string;
|
||||
fmModified: number;
|
||||
fmDraft: "Draft" | "Published",
|
||||
fmYear: number | null;
|
||||
fmYear: number | null | undefined;
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ViewType } from '../state';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType } from '../../models';
|
||||
import { ContentType, DraftField, Framework } from '../../models';
|
||||
|
||||
export interface Settings {
|
||||
beta: boolean;
|
||||
@@ -17,4 +17,7 @@ export interface Settings {
|
||||
mediaSnippet: string[];
|
||||
contentTypes: ContentType[];
|
||||
contentFolders: string[];
|
||||
crntFramework: string;
|
||||
framework: Framework | null | undefined;
|
||||
draftField: DraftField | null | undefined;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { Tab } from '../../constants/Tab';
|
||||
|
||||
export const TabAtom = atom<Tab>({
|
||||
export const TabAtom = atom<Tab | string>({
|
||||
key: 'TabAtom',
|
||||
default: Tab.All
|
||||
});
|
||||
@@ -1,20 +1,18 @@
|
||||
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, SETTINGS_CONTENT_STATIC_FOLDER, 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 } from '../constants';
|
||||
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 } from '../constants';
|
||||
import * as os from 'os';
|
||||
import { PanelSettings, CustomScript } from '../models/PanelSettings';
|
||||
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 { Command } from "../panelWebView/Command";
|
||||
import { CommandToCode } from '../panelWebView/CommandToCode';
|
||||
import { Article } from '../commands';
|
||||
import { TagType } from '../panelWebView/TagType';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { DraftField, TaxonomyType } from '../models';
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { Content } from 'mdast';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { COMMAND_NAME } from '../constants/Extension';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { Preview } from '../commands/Preview';
|
||||
@@ -23,6 +21,8 @@ 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;
|
||||
|
||||
@@ -352,38 +352,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* @param msg
|
||||
*/
|
||||
private runCustomScript(msg: { command: string, data: any}) {
|
||||
const scripts: CustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
|
||||
const scripts: ICustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
|
||||
|
||||
if (msg?.data?.title && msg?.data?.script && scripts) {
|
||||
const customScript = scripts.find((s: CustomScript) => s.title === msg.data.title);
|
||||
const customScript = scripts.find((s: ICustomScript) => s.title === msg.data.title);
|
||||
if (customScript?.script && customScript?.title) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const wsPath = wsFolder.fsPath;
|
||||
|
||||
let articleData = `'${JSON.stringify(article?.data)}'`;
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
}
|
||||
|
||||
exec(`${customScript.nodeBin || "node"} ${path.join(wsPath, msg.data.script)} "${wsPath}" "${editor?.document.uri.fsPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${msg?.data?.title}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.showInformationMessage(`${msg?.data?.title}: ${stdout || "Executed your custom script."}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
CustomScript.run(customScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,6 +381,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
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
|
||||
@@ -430,7 +405,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
preview: Preview.getSettings(),
|
||||
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
|
||||
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
dashboardViewData: Dashboard.viewData
|
||||
dashboardViewData: Dashboard.viewData,
|
||||
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD)
|
||||
} as PanelSettings
|
||||
});
|
||||
}
|
||||
@@ -502,6 +478,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* Get article details
|
||||
*/
|
||||
private getArticleDetails() {
|
||||
const baseUrl = Settings.get<string>(SETTING_SITE_BASEURL);
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return null;
|
||||
@@ -518,13 +495,36 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
content = content.replace(/({{(.*?)}})/g, ''); // remove hugo shortcodes
|
||||
|
||||
const mdTree = fromMarkdown(content);
|
||||
const headings = mdTree.children.filter(node => node.type === 'heading').length;
|
||||
const paragraphs = mdTree.children.filter(node => node.type === 'paragraph').length;
|
||||
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: headings.length,
|
||||
headingsText: headers,
|
||||
paragraphs,
|
||||
images,
|
||||
internalLinks,
|
||||
externalLinks: externalLinks.length,
|
||||
wordCount,
|
||||
content: article.content
|
||||
};
|
||||
@@ -533,6 +533,21 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
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;
|
||||
}
|
||||
|
||||
private counts(acc: any, node: any) {
|
||||
// add 1 to an initial or existing value
|
||||
acc[node.type] = (acc[node.type] || 0) + 1;
|
||||
@@ -633,7 +648,9 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const version = Extension.getInstance().getVersion();
|
||||
const ext = Extension.getInstance();
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
@@ -648,7 +665,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
<title>Front Matter</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app" 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" />
|
||||
|
||||
|
||||
+5
-1
@@ -15,6 +15,7 @@ import { Extension } from './helpers/Extension';
|
||||
import { DashboardData } from './models/DashboardData';
|
||||
import { Settings as SettingsHelper } from './helpers';
|
||||
import { Content } from './commands/Content';
|
||||
import ContentProvider from './providers/ContentProvider';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
let statusDebouncer: { (fnc: any, time: number): void; };
|
||||
@@ -125,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
// Settings promotion command
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, () => { console.log('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, () => {
|
||||
@@ -175,6 +176,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Inserting an image in Markdown
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
|
||||
|
||||
// Create the editor experience for bulk scripts
|
||||
subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(ContentProvider.scheme, new ContentProvider()));
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(
|
||||
insertTags,
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
|
||||
import { ContentType } from './../models/PanelSettings';
|
||||
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 } from '../constants';
|
||||
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 { DumpOptions } from 'js-yaml';
|
||||
import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine';
|
||||
import { Settings } from '.';
|
||||
import { parse } from 'date-fns';
|
||||
import { format, parse } from 'date-fns';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Article } from '../commands';
|
||||
import { basename } from 'path';
|
||||
import { basename, 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';
|
||||
|
||||
export class ArticleHelper {
|
||||
|
||||
@@ -29,9 +32,9 @@ export class ArticleHelper {
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static getFrontMatterByPath(filePath: string) {
|
||||
public static getFrontMatterByPath(filePath: string, surpressNotification: boolean = false) {
|
||||
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
|
||||
return ArticleHelper.parseFile(file, filePath);
|
||||
return ArticleHelper.parseFile(file, filePath, surpressNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,12 +167,63 @@ export class ArticleHelper {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the value
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
public static sanitize(value: string): string {
|
||||
return sanitize(value.toLowerCase().replace(/ /g, "-"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the file or folder for the new content
|
||||
* @param contentType
|
||||
* @param folderPath
|
||||
* @param titleValue
|
||||
* @returns The new file path
|
||||
*/
|
||||
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string): string | undefined {
|
||||
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
|
||||
// Name of the file or folder to create
|
||||
const sanitizedName = ArticleHelper.sanitize(titleValue);
|
||||
let newFilePath: string | undefined;
|
||||
|
||||
// Create a folder with the `index.md` file
|
||||
if (contentType?.pageBundle) {
|
||||
const newFolder = join(folderPath, sanitizedName);
|
||||
if (existsSync(newFolder)) {
|
||||
Notifications.error(`A page bundle with the name ${sanitizedName} already exists in ${folderPath}`);
|
||||
return;
|
||||
} else {
|
||||
mkdirSync(newFolder);
|
||||
newFilePath = join(newFolder, `index.md`);
|
||||
}
|
||||
} else {
|
||||
let newFileName = `${sanitizedName}.md`;
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;
|
||||
}
|
||||
|
||||
newFilePath = join(folderPath, newFileName);
|
||||
|
||||
if (existsSync(newFilePath)) {
|
||||
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return newFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, surpressNotification: boolean = false): matter.GrayMatterFile<string> | null {
|
||||
try {
|
||||
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
|
||||
|
||||
@@ -197,19 +251,20 @@ export class ArticleHelper {
|
||||
const items = [{
|
||||
title: "Check file",
|
||||
action: async () => {
|
||||
console.log(fileName);
|
||||
await EditorHelper.showFile(fileName)
|
||||
}
|
||||
}];
|
||||
|
||||
Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => {
|
||||
if (result?.title) {
|
||||
const item = items.find(i => i.title === result.title);
|
||||
if (item) {
|
||||
item.action();
|
||||
|
||||
if (!surpressNotification) {
|
||||
Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => {
|
||||
if (result?.title) {
|
||||
const item = items.find(i => i.title === result.title);
|
||||
if (item) {
|
||||
item.action();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+51
-18
@@ -1,19 +1,59 @@
|
||||
import { ArticleHelper, Settings } from ".";
|
||||
import { SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from "../constants";
|
||||
import { ContentType as IContentType } from '../models';
|
||||
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { ContentType as IContentType, DraftField } from '../models';
|
||||
import { Uri, workspace, window } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Questions } from "./Questions";
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { format } from "date-fns";
|
||||
import { join } from "path";
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import { writeFileSync } from "fs";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
|
||||
|
||||
|
||||
export class ContentType {
|
||||
|
||||
/**
|
||||
* Retrieve the draft field
|
||||
* @returns
|
||||
*/
|
||||
public static getDraftField() {
|
||||
const draftField = Settings.get<DraftField | null | undefined>(SETTINGS_CONTENT_DRAFT_FIELD);
|
||||
if (draftField) {
|
||||
return draftField;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the field its status
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public static getDraftStatus(data: { [field: string]: any }) {
|
||||
const contentType = ArticleHelper.getContentType(data);
|
||||
const draftSetting = ContentType.getDraftField();
|
||||
|
||||
const draftField = contentType.fields.find(f => f.type === "draft");
|
||||
|
||||
let fieldValue = null;
|
||||
|
||||
if (draftField) {
|
||||
fieldValue = data[draftField.name];
|
||||
} else if (draftSetting && data && data[draftSetting.name]) {
|
||||
fieldValue = data[draftSetting.name];
|
||||
}
|
||||
|
||||
if (draftSetting && fieldValue) {
|
||||
if (draftSetting.type === "boolean") {
|
||||
return fieldValue ? "Draft" : "Published";
|
||||
} else {
|
||||
return fieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content based on content types
|
||||
* @returns
|
||||
@@ -51,27 +91,18 @@ export class ContentType {
|
||||
}
|
||||
|
||||
private static async create(contentType: IContentType, folderPath: string) {
|
||||
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
|
||||
const titleValue = await Questions.ContentTitle();
|
||||
if (!titleValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
|
||||
let newFileName = `${sanitizedName}.md`;
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
|
||||
}
|
||||
|
||||
const newFilePath = join(folderPath, newFileName);
|
||||
if (existsSync(newFilePath)) {
|
||||
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: any = {};
|
||||
let data: any = {};
|
||||
|
||||
for (const field of contentType.fields) {
|
||||
if (field.name === "title") {
|
||||
@@ -81,6 +112,8 @@ export class ContentType {
|
||||
}
|
||||
}
|
||||
|
||||
data = ArticleHelper.updateDates(Object.assign({}, data));
|
||||
|
||||
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
|
||||
data['type'] = contentType.name;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
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';
|
||||
|
||||
export class CustomScript {
|
||||
|
||||
public static async run(script: ICustomScript): Promise<void> {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
if (wsFolder) {
|
||||
const wsPath = wsFolder.fsPath;
|
||||
|
||||
if (script.bulk) {
|
||||
// Run script on all files
|
||||
CustomScript.bulkRun(wsPath, script);
|
||||
} else {
|
||||
// Run script on current file.
|
||||
CustomScript.singleRun(wsPath, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async singleRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
|
||||
if (article) {
|
||||
const output = await CustomScript.runScript(wsPath, article, editor.document.uri.fsPath, script);
|
||||
|
||||
CustomScript.showOutput(output, script);
|
||||
} else {
|
||||
Notifications.warning(`${script.title}: Current article couldn't be retrieved.`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async bulkRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const folders = await Folders.getInfo();
|
||||
|
||||
if (!folders || folders.length === 0) {
|
||||
Notifications.warning(`${script.title}: No files found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let output: string[] = [];
|
||||
|
||||
window.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: `Executing: ${script.title}`,
|
||||
cancellable: false
|
||||
}, async (progress, token) => {
|
||||
for await (const folder of folders) {
|
||||
if (folder.lastModified.length > 0) {
|
||||
for await (const file of folder.lastModified) {
|
||||
try {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath, true);
|
||||
if (article) {
|
||||
const crntOutput = await CustomScript.runScript(wsPath, article, file.filePath, script);
|
||||
if (crntOutput) {
|
||||
output.push(crntOutput);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skipping file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomScript.showOutput(output.join(`\n`), script);
|
||||
});
|
||||
}
|
||||
|
||||
private static async runScript(wsPath: string, article: matter.GrayMatterFile<string> | null, contentPath: string, script: ICustomScript): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let articleData = "";
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
} else {
|
||||
articleData = JSON.stringify(article?.data).replace(/'/g, "%27");
|
||||
articleData = `'${articleData}'`;
|
||||
}
|
||||
|
||||
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${script.title}: ${error.message}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static showOutput(output: string | null, script: ICustomScript): void {
|
||||
if (output) {
|
||||
if (script.output === "editor") {
|
||||
ContentProvider.show(output, script.title, script.outputType || "text");
|
||||
} else {
|
||||
window.showInformationMessage(`${script.title}: ${output}}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Notifications.info(`${script.title}: Executed your custom script.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { parse, parseISO, parseJSON } from "date-fns";
|
||||
|
||||
|
||||
export class DateHelper {
|
||||
|
||||
public static formatUpdate(value: string | null | undefined): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.replace(/YYYY/g, 'yyyy');
|
||||
value = value.replace(/DD/g, 'dd');
|
||||
return value;
|
||||
}
|
||||
|
||||
public static tryParse(date: any, format?: string): Date | null {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (date instanceof Date) {
|
||||
return date;
|
||||
}
|
||||
|
||||
if (typeof date === 'string') {
|
||||
const jsonParsed = DateHelper.tryParseJson(date);
|
||||
if (DateHelper.isValid(jsonParsed)) {
|
||||
return jsonParsed;
|
||||
}
|
||||
|
||||
const isoParsed = DateHelper.tryParseIso(date);
|
||||
if (DateHelper.isValid(isoParsed)) {
|
||||
return isoParsed;
|
||||
}
|
||||
|
||||
if (format) {
|
||||
const formatParsed = DateHelper.tryFormatParse(date, format);
|
||||
if (DateHelper.isValid(formatParsed)) {
|
||||
return formatParsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static isValid(date: any): boolean {
|
||||
return date instanceof Date && !isNaN(date?.getTime());
|
||||
}
|
||||
|
||||
public static tryFormatParse(date: string, format: string): Date | null {
|
||||
try {
|
||||
return parse(date, format, new Date());
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static tryParseJson(date: string): Date | null {
|
||||
try {
|
||||
return parseJSON(date);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static tryParseIso(date: string): Date | null {
|
||||
try {
|
||||
return parseISO(date);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,10 @@ export class Extension {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!versionInfo.usedVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split semantic version
|
||||
const version = versionInfo.usedVersion.split('.');
|
||||
const major = parseInt(version[0]);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { existsSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
|
||||
import { Extension } from "./Extension";
|
||||
|
||||
export class FrameworkDetector {
|
||||
|
||||
public static get(folder: string) {
|
||||
return this.check(folder);
|
||||
}
|
||||
|
||||
public static getAll() {
|
||||
return FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
}
|
||||
|
||||
private static check(folder: string) {
|
||||
const { dependencies, devDependencies } = Extension.getInstance().packageJson;
|
||||
|
||||
for (const detector of FrameworkDetectors) {
|
||||
if (detector && folder) {
|
||||
|
||||
// Verify by dependencies
|
||||
for (const dependency of detector.requiredDependencies ?? []) {
|
||||
const inDependencies = dependencies && dependencies[dependency]
|
||||
const inDevDependencies = devDependencies && devDependencies[dependency]
|
||||
if (inDependencies || inDevDependencies) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify by files
|
||||
for (const filename of detector.requiredFiles ?? []) {
|
||||
const fileExists = existsSync(resolve(folder, filename));
|
||||
if (fileExists) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,15 @@ interface MediaRecord {
|
||||
}
|
||||
|
||||
export class MediaLibrary {
|
||||
private db: JsonDB;
|
||||
private db: JsonDB | undefined;
|
||||
private static instance: MediaLibrary;
|
||||
|
||||
private constructor() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (!wsFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.db = new JsonDB(join(parseWinPath(wsFolder?.fsPath || ""), LocalStore.rootFolder, LocalStore.contentFolder, LocalStore.mediaDatabaseFile), true, false, '/');
|
||||
|
||||
workspace.onDidRenameFiles(e => {
|
||||
@@ -46,7 +50,7 @@ export class MediaLibrary {
|
||||
public get(id: string): MediaRecord | undefined {
|
||||
try {
|
||||
const fileId = this.parsePath(id);
|
||||
if (this.db.exists(fileId)) {
|
||||
if (this.db?.exists(fileId)) {
|
||||
return this.db.getData(fileId);
|
||||
}
|
||||
return undefined;
|
||||
@@ -57,16 +61,16 @@ export class MediaLibrary {
|
||||
|
||||
public set(id: string, metadata: any): void {
|
||||
const fileId = this.parsePath(id);
|
||||
this.db.push(fileId, metadata, true);
|
||||
this.db?.push(fileId, metadata, true);
|
||||
}
|
||||
|
||||
public rename(oldId: string, newId: string): void {
|
||||
const fileId = this.parsePath(oldId);
|
||||
const newFileId = this.parsePath(newId);
|
||||
const data = this.db.getData(fileId);
|
||||
const data = this.db?.getData(fileId);
|
||||
if (data) {
|
||||
this.db.delete(fileId);
|
||||
this.db.push(newFileId, data, true);
|
||||
this.db?.delete(fileId);
|
||||
this.db?.push(newFileId, data, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,16 @@ import { Notifications } from './Notifications';
|
||||
|
||||
export class Questions {
|
||||
|
||||
/**
|
||||
* Yes/No question
|
||||
* @param placeholder
|
||||
* @returns
|
||||
*/
|
||||
public static async yesOrNo(placeholder: string) {
|
||||
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: false });
|
||||
return answer === "yes";
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the name of the content to create
|
||||
* @param showWarning
|
||||
|
||||
@@ -38,7 +38,7 @@ export class Settings {
|
||||
* Check if the setting is present in the workspace and ask to promote them to the global settings
|
||||
*/
|
||||
public static async checkToPromote() {
|
||||
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.SettingPromoted);
|
||||
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.SettingPromoted, "workspace");
|
||||
if (!isPromoted) {
|
||||
if (Settings.hasSettings()) {
|
||||
window.showInformationMessage(`You have local settings. Would you like to promote them to the global settings ("frontmatter.json")?`, 'Yes', 'No').then(async (result) => {
|
||||
@@ -47,7 +47,7 @@ export class Settings {
|
||||
}
|
||||
|
||||
if (result === "No" || result === "Yes") {
|
||||
Extension.getInstance().setState(ExtensionState.SettingPromoted, true);
|
||||
Extension.getInstance().setState(ExtensionState.SettingPromoted, true, "workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export class Settings {
|
||||
Settings.globalConfig = JSON.parse(localConfig);
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value;
|
||||
writeFileSync(fmConfig, JSON.stringify(Settings.globalConfig, null, 2), 'utf8');
|
||||
|
||||
|
||||
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
|
||||
if (workspaceSettingValue) {
|
||||
await Settings.update(name, undefined);
|
||||
|
||||
+18
-10
@@ -14,14 +14,18 @@ export class SlugHelper {
|
||||
|
||||
// Remove punctuation from input string, and split it into words.
|
||||
let cleanTitle = this.removePunctuation(articleTitle);
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
let words = cleanTitle.split(/\s/);
|
||||
// Removing stop words
|
||||
words = this.removeStopWords(words);
|
||||
cleanTitle = words.join("-");
|
||||
cleanTitle = this.replaceCharacters(cleanTitle);
|
||||
return cleanTitle;
|
||||
if (cleanTitle) {
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
let words = cleanTitle.split(/\s/);
|
||||
// Removing stop words
|
||||
words = this.removeStopWords(words);
|
||||
cleanTitle = words.join("-");
|
||||
cleanTitle = this.replaceCharacters(cleanTitle);
|
||||
return cleanTitle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,9 +34,13 @@ export class SlugHelper {
|
||||
* @param value
|
||||
*/
|
||||
private static removePunctuation(value: string): string {
|
||||
const punctuationless = value.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const punctuationless = value?.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
// Remove double spaces
|
||||
return punctuationless.replace(/\s{2,}/g," ");
|
||||
return punctuationless?.replace(/\s{2,}/g," ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface DraftField {
|
||||
name: string;
|
||||
type: "boolean" | "choice";
|
||||
choices?: string[];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export interface Framework {
|
||||
name: string;
|
||||
dist: string;
|
||||
static: string;
|
||||
build: string;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FileType } from "vscode";
|
||||
import { DraftField } from ".";
|
||||
import { Choice } from "./Choice";
|
||||
import { DashboardData } from "./DashboardData";
|
||||
|
||||
@@ -17,17 +18,20 @@ export interface PanelSettings {
|
||||
preview: PreviewSettings;
|
||||
contentTypes: ContentType[];
|
||||
dashboardViewData: DashboardData | undefined;
|
||||
draftField: DraftField;
|
||||
}
|
||||
|
||||
export interface ContentType {
|
||||
name: string;
|
||||
fields: Field[];
|
||||
|
||||
pageBundle?: boolean;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
title?: string;
|
||||
name: string;
|
||||
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories";
|
||||
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft";
|
||||
choices?: string[] | Choice[];
|
||||
single?: boolean;
|
||||
multiple?: boolean;
|
||||
@@ -41,6 +45,7 @@ export interface DateInfo {
|
||||
|
||||
export interface SEO {
|
||||
title: number;
|
||||
slug: number;
|
||||
description: number;
|
||||
content: number;
|
||||
descriptionField: string;
|
||||
@@ -70,6 +75,9 @@ export interface CustomScript {
|
||||
title: string;
|
||||
script: string;
|
||||
nodeBin?: string;
|
||||
bulk?: boolean;
|
||||
output?: "notification" | "editor";
|
||||
outputType?: string;
|
||||
}
|
||||
|
||||
export interface PreviewSettings {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export * from './Choice';
|
||||
export * from './ContentFolder';
|
||||
export * from './DashboardData';
|
||||
export * from './DraftField';
|
||||
export * from './Framework';
|
||||
export * from './MediaPaths';
|
||||
export * from './PanelSettings';
|
||||
export * from './TaxonomyType';
|
||||
export * from './VersionInfo';
|
||||
|
||||
@@ -7,10 +7,13 @@ export interface IActionButtonProps {
|
||||
onClick: (e: React.SyntheticEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const ActionButton: React.FunctionComponent<IActionButtonProps> = ({className, onClick, disabled,title}: React.PropsWithChildren<IActionButtonProps>) => {
|
||||
const ActionButton: React.FunctionComponent<IActionButtonProps> = ({className, onClick, disabled,title}: React.PropsWithChildren<IActionButtonProps>) => {
|
||||
return (
|
||||
<div className={`article__action`}>
|
||||
<button onClick={onClick} className={className || ""} disabled={disabled}>{title}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ActionButton.displayName = 'ActionButton';
|
||||
export { ActionButton };
|
||||
@@ -10,7 +10,7 @@ export interface IActionsProps {
|
||||
settings: PanelSettings;
|
||||
}
|
||||
|
||||
export const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithChildren<IActionsProps>) => {
|
||||
const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithChildren<IActionsProps>) => {
|
||||
const { metadata, settings } = props;
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0 || !settings) {
|
||||
@@ -27,12 +27,15 @@ export const Actions: React.FunctionComponent<IActionsProps> = (props: React.Pro
|
||||
|
||||
{
|
||||
(settings && settings.scripts && settings.scripts.length > 0) && (
|
||||
settings.scripts.map((value) => (
|
||||
<CustomScript key={value.title.replace(/ /g, '')} {...value} />
|
||||
settings.scripts.map((value, idx) => (
|
||||
<CustomScript key={value?.title?.replace(/ /g, '') || idx} {...value} />
|
||||
))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Actions.displayName = 'Actions';
|
||||
export { Actions };
|
||||
@@ -6,10 +6,13 @@ export interface IArticleDetailsProps {
|
||||
headings: number;
|
||||
paragraphs: number;
|
||||
wordCount: number;
|
||||
internalLinks: number;
|
||||
externalLinks: number;
|
||||
images: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({details}: React.PropsWithChildren<IArticleDetailsProps>) => {
|
||||
const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({details}: React.PropsWithChildren<IArticleDetailsProps>) => {
|
||||
|
||||
if (!details || (details.headings === undefined && details.paragraphs === undefined)) {
|
||||
return null;
|
||||
@@ -42,8 +45,38 @@ export const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({d
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
details?.internalLinks !== undefined && (
|
||||
<VsTableRow>
|
||||
<VsTableCell>Internal links</VsTableCell>
|
||||
<VsTableCell>{details.internalLinks}</VsTableCell>
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
details?.externalLinks !== undefined && (
|
||||
<VsTableRow>
|
||||
<VsTableCell>External links</VsTableCell>
|
||||
<VsTableCell>{details.externalLinks}</VsTableCell>
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
details?.images !== undefined && (
|
||||
<VsTableRow>
|
||||
<VsTableCell>Images</VsTableCell>
|
||||
<VsTableCell>{details.images}</VsTableCell>
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ArticleDetails.displayName = 'ArticleDetails';
|
||||
export { ArticleDetails };
|
||||
@@ -13,7 +13,7 @@ export interface IBaseViewProps {
|
||||
folderAndFiles: FolderInfo[] | undefined;
|
||||
}
|
||||
|
||||
export const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles}: React.PropsWithChildren<IBaseViewProps>) => {
|
||||
const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles}: React.PropsWithChildren<IBaseViewProps>) => {
|
||||
|
||||
const openDashboard = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openDashboard);
|
||||
@@ -48,4 +48,7 @@ export const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, fol
|
||||
<SponsorMsg />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
BaseView.displayName = 'BaseView';
|
||||
export { BaseView };
|
||||
@@ -10,10 +10,26 @@ export interface ICollapsibleProps {
|
||||
sendUpdate?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children, title, sendUpdate, className}: React.PropsWithChildren<ICollapsibleProps>) => {
|
||||
const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children, title, sendUpdate, className}: React.PropsWithChildren<ICollapsibleProps>) => {
|
||||
const [ isOpen, setIsOpen ] = React.useState(false);
|
||||
const collapseKey = `collapse-${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const collapsed = window.localStorage.getItem(collapseKey);
|
||||
if (collapsed === null || collapsed === 'true') {
|
||||
setIsOpen(true);
|
||||
updateStorage(true);
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.command === Command.closeSections) {
|
||||
setIsOpen(false);
|
||||
updateStorage(false);
|
||||
}
|
||||
});
|
||||
}, ['']);
|
||||
|
||||
const updateStorage = (value: boolean) => {
|
||||
window.localStorage.setItem(collapseKey, value.toString());
|
||||
}
|
||||
@@ -32,22 +48,6 @@ export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, chi
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const collapsed = window.localStorage.getItem(collapseKey);
|
||||
if (collapsed === null || collapsed === 'true') {
|
||||
setIsOpen(true);
|
||||
updateStorage(true);
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.command === Command.closeSections) {
|
||||
setIsOpen(false);
|
||||
updateStorage(false);
|
||||
}
|
||||
});
|
||||
}, ['']);
|
||||
|
||||
return (
|
||||
<VsCollapsible title={title} onClick={triggerClick} open={isOpen}>
|
||||
<div className={`section collapsible__body ${className || ""}`} slot="body">
|
||||
@@ -55,4 +55,7 @@ export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, chi
|
||||
</div>
|
||||
</VsCollapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Collapsible.displayName = 'Collapsible';
|
||||
export { Collapsible };
|
||||
@@ -8,7 +8,7 @@ export interface ICustomScriptProps {
|
||||
script: string;
|
||||
}
|
||||
|
||||
export const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title, script}: React.PropsWithChildren<ICustomScriptProps>) => {
|
||||
const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title, script}: React.PropsWithChildren<ICustomScriptProps>) => {
|
||||
|
||||
const runCustomScript = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title, script });
|
||||
@@ -17,4 +17,7 @@ export const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title
|
||||
return (
|
||||
<ActionButton onClick={runCustomScript} title={title} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
CustomScript.displayName = 'CustomScript';
|
||||
export { CustomScript };
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
|
||||
export interface IFieldBoundaryProps {
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export interface IFieldBoundaryState {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export default class FieldBoundary extends React.Component<IFieldBoundaryProps, IFieldBoundaryState> {
|
||||
|
||||
constructor(props: IFieldBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(error: any) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: any, errorInfo: any) {
|
||||
Sentry.captureMessage(`Field boundary: ${error?.message || error}`);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IFieldBoundaryProps> {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<span style={{ lineHeight: "16px"}}>{this.props.fieldName}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<div className={`metadata_field__error`}>
|
||||
<span>Error loading field</span>
|
||||
|
||||
<button onClick={() => this.setState({ hasError: false })}>Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children as any;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { VsLabel } from '../VscodeComponents';
|
||||
import { ClockIcon } from '@heroicons/react/outline';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { forwardRef } from 'react';
|
||||
import { DateHelper } from '../../../helpers/DateHelper';
|
||||
|
||||
export interface IDateTimeFieldProps {
|
||||
label: string;
|
||||
@@ -22,19 +23,22 @@ const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ value, onClick }
|
||||
});
|
||||
|
||||
export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({label, date, format, onChange}: React.PropsWithChildren<IDateTimeFieldProps>) => {
|
||||
const [ dateValue, setDateValue ] = React.useState<Date | null>(date);
|
||||
const [ dateValue, setDateValue ] = React.useState<Date | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const crntValue = DateHelper.tryParse(date, format);
|
||||
const stateValue = DateHelper.tryParse(dateValue, format);
|
||||
|
||||
if (crntValue?.toISOString() !== stateValue?.toISOString()) {
|
||||
setDateValue(date);
|
||||
}
|
||||
}, [ date ]);
|
||||
|
||||
const onDateChange = (date: Date) => {
|
||||
setDateValue(date);
|
||||
onChange(date);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (dateValue?.toISOString() !== date?.toISOString()) {
|
||||
setDateValue(date);
|
||||
}
|
||||
}, [ date ]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
@@ -45,10 +49,10 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
|
||||
|
||||
<div className={`metadata_field__datetime`}>
|
||||
<DatePicker
|
||||
selected={dateValue as Date}
|
||||
selected={dateValue as Date || new Date()}
|
||||
onChange={onDateChange}
|
||||
timeInputLabel="Time:"
|
||||
dateFormat={format || "MM/dd/yyyy HH:mm"}
|
||||
dateFormat={DateHelper.formatUpdate(format) || "MM/dd/yyyy HH:mm"}
|
||||
customInput={(<CustomInput />)}
|
||||
showTimeInput
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { RocketIcon } from '../Icons/RocketIcon';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { ChoiceField } from './ChoiceField';
|
||||
import { Toggle } from './Toggle';
|
||||
|
||||
export interface IDraftFieldProps {
|
||||
label: string;
|
||||
type: "boolean" | "choice";
|
||||
value: boolean | string | null | undefined;
|
||||
|
||||
choices?: string[];
|
||||
|
||||
onChanged: (value: string | boolean) => void;
|
||||
}
|
||||
|
||||
export const DraftField: React.FunctionComponent<IDraftFieldProps> = ({ label, type, value, choices, onChanged }: React.PropsWithChildren<IDraftFieldProps>) => {
|
||||
|
||||
if (type === "boolean") {
|
||||
return (
|
||||
<Toggle
|
||||
label={label}
|
||||
checked={!!value}
|
||||
onChanged={(checked) => onChanged(checked)} />
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "choice") {
|
||||
return (
|
||||
<ChoiceField
|
||||
label={label}
|
||||
selected={value as string}
|
||||
choices={choices as string[]}
|
||||
multiSelect={false}
|
||||
onChange={value => onChanged(value as string)} />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { XCircleIcon } from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IImageFallbackProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export const ImageFallback: React.FunctionComponent<IImageFallbackProps> = ({ src }: React.PropsWithChildren<IImageFallbackProps>) => {
|
||||
|
||||
if (!src) {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '120px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'var(--button-secondary-hover-background)'
|
||||
}}>
|
||||
<XCircleIcon style={{
|
||||
height: '8rem',
|
||||
width: '8rem',
|
||||
color: 'var(--vscode-errorForeground)',
|
||||
}} />
|
||||
|
||||
<p style={{
|
||||
marginBottom: '1rem',
|
||||
color: 'var(--button-secondary-foreground)',
|
||||
}}>The image couldn't be loaded</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img src={src} />
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ImageFallback } from './ImageFallback';
|
||||
import { PreviewImageValue } from './PreviewImageField';
|
||||
|
||||
export interface IPreviewImageProps {
|
||||
@@ -7,9 +8,10 @@ export interface IPreviewImageProps {
|
||||
}
|
||||
|
||||
export const PreviewImage: React.FunctionComponent<IPreviewImageProps> = ({ value, onRemove }: React.PropsWithChildren<IPreviewImageProps>) => {
|
||||
|
||||
return (
|
||||
<div className={`metadata_field__preview_image__preview`}>
|
||||
<img src={value.webviewUrl} />
|
||||
<ImageFallback src={value.webviewUrl} />
|
||||
|
||||
<button type="button" onClick={() => onRemove(value.original)} className={`metadata_field__preview_image__remove`}>Remove image</button>
|
||||
</div>
|
||||
|
||||
@@ -13,17 +13,17 @@ export interface ITextFieldProps {
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
const [ text, setText ] = React.useState<string | null>(value);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
onChange(txtValue);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
onChange(txtValue);
|
||||
};
|
||||
|
||||
let isValid = true;
|
||||
if (limit && limit !== -1) {
|
||||
@@ -59,8 +59,6 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
{
|
||||
limit && limit > 0 && (text || "").length > limit && (
|
||||
<div className={`metadata_field__limit`}>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { FileIcon } from './Icons/FileIcon';
|
||||
import { MarkdownIcon } from './Icons/MarkdownIcon';
|
||||
|
||||
export interface IFileItemProps {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }: React.PropsWithChildren<IFileItemProps>) => {
|
||||
|
||||
const openFile = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openInEditor, path);
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={`file_list__items__item`}
|
||||
onClick={openFile}>
|
||||
{
|
||||
(name.endsWith('.md') || name.endsWith('.mdx')) ? (
|
||||
<MarkdownIcon />
|
||||
) : (
|
||||
<FileIcon />
|
||||
)
|
||||
}
|
||||
|
||||
<span>{name}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
FileItem.displayName = 'FileItem';
|
||||
export { FileItem };
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { FileInfo } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { FileIcon } from './Icons/FileIcon';
|
||||
import { MarkdownIcon } from './Icons/MarkdownIcon';
|
||||
import { FileItem } from './FileItem';
|
||||
import { VsLabel } from './VscodeComponents';
|
||||
|
||||
export interface IFileListProps {
|
||||
@@ -12,11 +9,7 @@ export interface IFileListProps {
|
||||
totalFiles: number;
|
||||
}
|
||||
|
||||
export const FileList: React.FunctionComponent<IFileListProps> = ({files, folderName, totalFiles}: React.PropsWithChildren<IFileListProps>) => {
|
||||
|
||||
const openFile = (filePath: string) => {
|
||||
MessageHelper.sendMessage(CommandToCode.openInEditor, filePath);
|
||||
};
|
||||
const FileList: React.FunctionComponent<IFileListProps> = ({files, folderName, totalFiles}: React.PropsWithChildren<IFileListProps>) => {
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return null;
|
||||
@@ -28,23 +21,14 @@ export const FileList: React.FunctionComponent<IFileListProps> = ({files, folder
|
||||
|
||||
<ul className="file_list__items">
|
||||
{
|
||||
files.map(file => (
|
||||
<li key={file.fileName}
|
||||
className={`file_list__items__item`}
|
||||
onClick={() => openFile(file.filePath)}>
|
||||
{
|
||||
(file.fileName.endsWith('.md') || file.fileName.endsWith('.mdx')) ? (
|
||||
<MarkdownIcon />
|
||||
) : (
|
||||
<FileIcon />
|
||||
)
|
||||
}
|
||||
|
||||
<span>{file.fileName}</span>
|
||||
</li>
|
||||
(files && files.length > 0) && files.map(file => (
|
||||
<FileItem key={file.filePath} name={file.fileName} path={file.filePath} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
FileList.displayName = 'FileList';
|
||||
export { FileList };
|
||||
@@ -9,7 +9,7 @@ export interface IFolderAndFilesProps {
|
||||
isBase?: boolean;
|
||||
}
|
||||
|
||||
export const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({data, isBase}: React.PropsWithChildren<IFolderAndFilesProps>) => {
|
||||
const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({data, isBase}: React.PropsWithChildren<IFolderAndFilesProps>) => {
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
@@ -42,4 +42,7 @@ export const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({d
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
FolderAndFiles.displayName = 'FolderAndFiles';
|
||||
export { FolderAndFiles };
|
||||
@@ -11,7 +11,7 @@ export interface IGlobalSettingsProps {
|
||||
isBase?: boolean;
|
||||
}
|
||||
|
||||
export const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings, isBase}: React.PropsWithChildren<IGlobalSettingsProps>) => {
|
||||
const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings, isBase}: React.PropsWithChildren<IGlobalSettingsProps>) => {
|
||||
const { modifiedDateUpdate, fmHighlighting } = settings || {};
|
||||
const [ previewUrl, setPreviewUrl ] = React.useState<string>("");
|
||||
const [ isDirty, setIsDirty ] = React.useState<boolean>(false);
|
||||
@@ -48,11 +48,11 @@ export const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({s
|
||||
<Collapsible id={`${isBase ? "base_" : ""}settings`} className={`base__actions`} title="Global settings">
|
||||
<div className={`base__action`}>
|
||||
<VsLabel>Modified date</VsLabel>
|
||||
<VsCheckbox label="Auto-update modified date" checked={modifiedDateUpdate} onClick={onDateCheck} />
|
||||
<VsCheckbox checked={modifiedDateUpdate} onClick={onDateCheck}>Auto-update modified date</VsCheckbox>
|
||||
</div>
|
||||
<div className={`base__action`}>
|
||||
<VsLabel>Front Matter highlight</VsLabel>
|
||||
<VsCheckbox label="Highlight Front Matter" checked={fmHighlighting} onClick={onHighlightCheck} />
|
||||
<VsCheckbox checked={fmHighlighting} onClick={onHighlightCheck}>Highlight Front Matter</VsCheckbox>
|
||||
</div>
|
||||
<div className={`base__action`}>
|
||||
<VsLabel>Local preview</VsLabel>
|
||||
@@ -65,4 +65,7 @@ export const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({s
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
GlobalSettings.displayName = 'GlobalSettings';
|
||||
export { GlobalSettings };
|
||||
@@ -4,7 +4,10 @@ export interface IIconProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const Icon: React.FunctionComponent<IIconProps> = ({ name }: React.PropsWithChildren<IIconProps>) => {
|
||||
const Icon: React.FunctionComponent<IIconProps> = ({ name }: React.PropsWithChildren<IIconProps>) => {
|
||||
|
||||
return (<i className={`codicon codicon-${name}`}></i>);
|
||||
};
|
||||
};
|
||||
|
||||
Icon.displayName = 'Icon';
|
||||
export { Icon };
|
||||
@@ -8,7 +8,6 @@ import { Toggle } from './Fields/Toggle';
|
||||
import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
|
||||
import { TagIcon } from './Icons/TagIcon';
|
||||
import { TagPicker } from './TagPicker';
|
||||
import { parseJSON } from 'date-fns';
|
||||
import { DateTimeField } from './Fields/DateTimeField';
|
||||
import { TextField } from './Fields/TextField';
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
@@ -17,6 +16,9 @@ import { ListUnorderedIcon } from './Icons/ListUnorderedIcon';
|
||||
import { NumberField } from './Fields/NumberField';
|
||||
import { ChoiceField } from './Fields/ChoiceField';
|
||||
import useContentType from '../../hooks/useContentType';
|
||||
import { DateHelper } from '../../helpers/DateHelper';
|
||||
import FieldBoundary from './ErrorBoundary/FieldBoundary';
|
||||
import { DraftField } from './Fields/DraftField';
|
||||
|
||||
export interface IMetadataProps {
|
||||
settings: PanelSettings | undefined;
|
||||
@@ -25,7 +27,7 @@ export interface IMetadataProps {
|
||||
unsetFocus: () => void;
|
||||
}
|
||||
|
||||
export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren<IMetadataProps>) => {
|
||||
const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren<IMetadataProps>) => {
|
||||
const contentType = useContentType(settings, metadata);
|
||||
|
||||
const sendUpdate = (field: string | undefined, value: any) => {
|
||||
@@ -39,11 +41,9 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
|
||||
});
|
||||
};
|
||||
|
||||
const getDate = (date: string | Date) => {
|
||||
if (typeof date === 'string') {
|
||||
return parseJSON(date);
|
||||
}
|
||||
return date;
|
||||
const getDate = (date: string | Date): Date | null => {
|
||||
const parsedDate = DateHelper.tryParse(date, settings?.date?.format);
|
||||
return parsedDate || date as Date | null;
|
||||
}
|
||||
|
||||
if (!settings) {
|
||||
@@ -64,20 +64,23 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
|
||||
const dateValue = metadata[field.name] ? getDate(metadata[field.name] as string) : null;
|
||||
|
||||
return (
|
||||
<DateTimeField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
date={dateValue}
|
||||
format={settings?.date?.format}
|
||||
onChange={(date => sendUpdate(field.name, date))} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<DateTimeField
|
||||
label={field.title || field.name}
|
||||
date={dateValue}
|
||||
format={settings?.date?.format}
|
||||
onChange={(date => sendUpdate(field.name, date))} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'boolean') {
|
||||
return (
|
||||
<Toggle
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
checked={!!metadata[field.name] as any}
|
||||
onChanged={(checked) => sendUpdate(field.name, checked)} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<Toggle
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
checked={!!metadata[field.name] as any}
|
||||
onChanged={(checked) => sendUpdate(field.name, checked)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'string') {
|
||||
const textValue = metadata[field.name];
|
||||
@@ -90,14 +93,15 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
singleLine={field.single}
|
||||
limit={limit}
|
||||
rows={3}
|
||||
onChange={(value) => sendUpdate(field.name, value)}
|
||||
value={textValue as string || null} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<TextField
|
||||
label={field.title || field.name}
|
||||
singleLine={field.single}
|
||||
limit={limit}
|
||||
rows={3}
|
||||
onChange={(value) => sendUpdate(field.name, value)}
|
||||
value={textValue as string || null} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'number') {
|
||||
const fieldValue = metadata[field.name];
|
||||
@@ -107,61 +111,81 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
|
||||
}
|
||||
|
||||
return (
|
||||
<NumberField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
onChange={(value) => sendUpdate(field.name, value)}
|
||||
value={nrValue} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<NumberField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
onChange={(value) => sendUpdate(field.name, value)}
|
||||
value={nrValue} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'image') {
|
||||
return (
|
||||
<PreviewImageField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
fieldName={field.name}
|
||||
filePath={metadata.filePath as string}
|
||||
value={metadata[field.name] as PreviewImageValue | PreviewImageValue[] | null}
|
||||
multiple={field.multiple}
|
||||
onChange={(value => sendUpdate(field.name, value))} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<PreviewImageField
|
||||
label={field.title || field.name}
|
||||
fieldName={field.name}
|
||||
filePath={metadata.filePath as string}
|
||||
value={metadata[field.name] as PreviewImageValue | PreviewImageValue[] | null}
|
||||
multiple={field.multiple}
|
||||
onChange={(value => sendUpdate(field.name, value))} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'choice') {
|
||||
const choices = field.choices || [];
|
||||
const choiceValue = metadata[field.name];
|
||||
|
||||
return (
|
||||
<ChoiceField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
selected={choiceValue as string}
|
||||
choices={choices}
|
||||
multiSelect={field.multiple}
|
||||
onChange={(value => sendUpdate(field.name, value))} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<ChoiceField
|
||||
label={field.title || field.name}
|
||||
selected={choiceValue as string}
|
||||
choices={choices}
|
||||
multiSelect={field.multiple}
|
||||
onChange={(value => sendUpdate(field.name, value))} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'tags') {
|
||||
return (
|
||||
<TagPicker
|
||||
key={field.name}
|
||||
type={TagType.tags}
|
||||
label={field.title || field.name}
|
||||
icon={<TagIcon />}
|
||||
crntSelected={metadata[field.name] as string[] || []}
|
||||
options={settings?.tags || []}
|
||||
freeform={settings.freeform}
|
||||
focussed={focusElm === TagType.tags}
|
||||
unsetFocus={unsetFocus} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<TagPicker
|
||||
type={TagType.tags}
|
||||
label={field.title || field.name}
|
||||
icon={<TagIcon />}
|
||||
crntSelected={metadata[field.name] as string[] || []}
|
||||
options={settings?.tags || []}
|
||||
freeform={settings.freeform}
|
||||
focussed={focusElm === TagType.tags}
|
||||
unsetFocus={unsetFocus} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'categories') {
|
||||
return (
|
||||
<TagPicker
|
||||
key={field.name}
|
||||
type={TagType.categories}
|
||||
label={field.title || field.name}
|
||||
icon={<ListUnorderedIcon />}
|
||||
crntSelected={metadata.categories as string[] || []}
|
||||
options={settings.categories}
|
||||
freeform={settings.freeform}
|
||||
focussed={focusElm === TagType.categories}
|
||||
unsetFocus={unsetFocus} />
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<TagPicker
|
||||
type={TagType.categories}
|
||||
label={field.title || field.name}
|
||||
icon={<ListUnorderedIcon />}
|
||||
crntSelected={metadata.categories as string[] || []}
|
||||
options={settings.categories}
|
||||
freeform={settings.freeform}
|
||||
focussed={focusElm === TagType.categories}
|
||||
unsetFocus={unsetFocus} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'draft') {
|
||||
const draftField = settings?.draftField;
|
||||
const value = metadata[field.name];
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<DraftField
|
||||
label={field.title || field.name}
|
||||
type={draftField.type}
|
||||
choices={draftField.choices || []}
|
||||
value={value as boolean | string | null | undefined}
|
||||
onChanged={(value: boolean | string) => sendUpdate(field.name, value)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
@@ -177,15 +201,21 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
|
||||
}
|
||||
|
||||
{
|
||||
<TagPicker type={TagType.keywords}
|
||||
icon={<SymbolKeywordIcon />}
|
||||
crntSelected={metadata.keywords as string[] || []}
|
||||
options={[]}
|
||||
freeform={true}
|
||||
focussed={focusElm === TagType.keywords}
|
||||
unsetFocus={unsetFocus}
|
||||
disableConfigurable />
|
||||
<FieldBoundary fieldName={`Keywords`}>
|
||||
<TagPicker
|
||||
type={TagType.keywords}
|
||||
icon={<SymbolKeywordIcon />}
|
||||
crntSelected={metadata.keywords as string[] || []}
|
||||
options={[]}
|
||||
freeform={true}
|
||||
focussed={focusElm === TagType.keywords}
|
||||
unsetFocus={unsetFocus}
|
||||
disableConfigurable />
|
||||
</FieldBoundary>
|
||||
}
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Metadata.displayName = 'Metadata';
|
||||
export { Metadata };
|
||||
@@ -6,10 +6,13 @@ export interface IOtherActionButtonProps {
|
||||
onClick: (e: React.SyntheticEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const OtherActionButton: React.FunctionComponent<IOtherActionButtonProps> = ({ className, disabled, onClick, children}: React.PropsWithChildren<IOtherActionButtonProps>) => {
|
||||
const OtherActionButton: React.FunctionComponent<IOtherActionButtonProps> = ({ className, disabled, onClick, children}: React.PropsWithChildren<IOtherActionButtonProps>) => {
|
||||
return (
|
||||
<div className={`ext_link_block`}>
|
||||
<button onClick={onClick} className={className || ""} disabled={disabled}>{children}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
OtherActionButton.displayName = 'OtherActionButton';
|
||||
export { OtherActionButton };
|
||||
@@ -19,7 +19,7 @@ export interface IOtherActionsProps {
|
||||
isBase?: boolean;
|
||||
}
|
||||
|
||||
export const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFile, settings, isBase}: React.PropsWithChildren<IOtherActionsProps>) => {
|
||||
const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFile, settings, isBase}: React.PropsWithChildren<IOtherActionsProps>) => {
|
||||
|
||||
const openSettings = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openSettings);
|
||||
@@ -64,4 +64,7 @@ export const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFil
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
OtherActions.displayName = 'OtherActions';
|
||||
export { OtherActions };
|
||||
@@ -7,7 +7,7 @@ export interface IPreviewProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.PropsWithChildren<IPreviewProps>) => {
|
||||
const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.PropsWithChildren<IPreviewProps>) => {
|
||||
|
||||
const open = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openPreview);
|
||||
@@ -20,4 +20,7 @@ export const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.Pr
|
||||
return (
|
||||
<ActionButton onClick={open} title={`Open preview`} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Preview.displayName = 'Preview';
|
||||
export { Preview };
|
||||
@@ -9,7 +9,7 @@ export interface IPublishActionProps {
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export const PublishAction: React.FunctionComponent<IPublishActionProps> = (props: React.PropsWithChildren<IPublishActionProps>) => {
|
||||
const PublishAction: React.FunctionComponent<IPublishActionProps> = (props: React.PropsWithChildren<IPublishActionProps>) => {
|
||||
const { draft } = props;
|
||||
|
||||
const publish = () => {
|
||||
@@ -19,4 +19,7 @@ export const PublishAction: React.FunctionComponent<IPublishActionProps> = (prop
|
||||
return (
|
||||
<ActionButton onClick={publish} className={`${draft ? "" : "secondary"}`} title={draft ? "Publish" : "Revert to draft"} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
PublishAction.displayName = 'PublishAction';
|
||||
export { PublishAction };
|
||||
@@ -9,7 +9,7 @@ export interface ISeoDetailsProps {
|
||||
noValidation?: boolean;
|
||||
}
|
||||
|
||||
export const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: React.PropsWithChildren<ISeoDetailsProps>) => {
|
||||
const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: React.PropsWithChildren<ISeoDetailsProps>) => {
|
||||
const { allowedLength, title, value, valueTitle, noValidation } = props;
|
||||
|
||||
const validate = () => {
|
||||
@@ -38,4 +38,7 @@ export const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: Rea
|
||||
</VsTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoDetails.displayName = 'SeoDetails';
|
||||
export { SeoDetails };
|
||||
@@ -9,14 +9,17 @@ export interface ISeoFieldInfoProps {
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
export const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({ title, value, recommendation, isValid }: React.PropsWithChildren<ISeoFieldInfoProps>) => {
|
||||
const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({ title, value, recommendation, isValid }: React.PropsWithChildren<ISeoFieldInfoProps>) => {
|
||||
return (
|
||||
<VsTableRow>
|
||||
<VsTableCell className={`table__cell table__title`}>{title}</VsTableCell>
|
||||
<VsTableCell className={`table__cell`}>{value}/{recommendation}</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
{ isValid !== undefined ? <ValidInfo isValid={isValid} /> : <span>-</span> }
|
||||
{ isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span> }
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoFieldInfo.displayName = 'SeoFieldInfo';
|
||||
export { SeoFieldInfo };
|
||||
@@ -8,25 +8,78 @@ export interface ISeoKeywordInfoProps {
|
||||
description: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
wordCount?: number;
|
||||
headings?: string[];
|
||||
}
|
||||
|
||||
export const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword, title, description, slug, content}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword, title, description, slug, content, wordCount, headings}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
|
||||
const density = () => {
|
||||
if (!wordCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pattern = new RegExp('\\b' + keyword.toLowerCase() + '\\b', 'ig');
|
||||
const count = (content.match(pattern) || []).length;
|
||||
const density = (count / wordCount) * 100;
|
||||
const densityTitle = `Keyword usage ${density.toFixed(2)}% *`;
|
||||
|
||||
if (density < 0.75) {
|
||||
return <ValidInfo label={densityTitle} isValid={false} />
|
||||
} else if (density >= 0.75 && density < 1.5) {
|
||||
return <ValidInfo label={densityTitle} isValid={true} />
|
||||
} else {
|
||||
return <ValidInfo label={densityTitle} isValid={false} />
|
||||
}
|
||||
};
|
||||
|
||||
const checkHeadings = () => {
|
||||
if (!headings || headings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exists = headings.filter(heading => heading.split(' ').findIndex(word => word.toLowerCase() === keyword.toLowerCase()) !== -1);
|
||||
return <ValidInfo label={`Used in heading(s)`} isValid={exists.length > 0} />;
|
||||
};
|
||||
|
||||
if (!keyword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VsTableRow>
|
||||
<VsTableCell className={`table__cell`}>{keyword}</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!slug && (slug.toLowerCase().includes(keyword.toLowerCase()) || slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase()))} />
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
<VsTableCell className={`table__cell table__cell__validation table__cell__seo_details`}>
|
||||
<div>
|
||||
<ValidInfo label={`Title`} isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</div>
|
||||
<div>
|
||||
<ValidInfo label={`Description`} isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</div>
|
||||
<div>
|
||||
<ValidInfo label={`Slug`} isValid={!!slug && (slug.toLowerCase().includes(keyword.toLowerCase()) || slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase()))} />
|
||||
</div>
|
||||
<div>
|
||||
<ValidInfo label={`Content`} isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</div>
|
||||
{
|
||||
headings && headings.length > 0 && (
|
||||
<div>
|
||||
{checkHeadings()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
wordCount && (
|
||||
<div>
|
||||
{density()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoKeywordInfo.displayName = 'SeoKeywordInfo';
|
||||
export { SeoKeywordInfo };
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { SeoKeywordInfo } from './SeoKeywordInfo';
|
||||
import { VsTable, VsTableBody, VsTableCell, VsTableHeader, VsTableHeaderCell, VsTableRow } from './VscodeComponents';
|
||||
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
|
||||
|
||||
export interface ISeoKeywordsProps {
|
||||
keywords: string[] | null;
|
||||
@@ -9,9 +9,27 @@ export interface ISeoKeywordsProps {
|
||||
description: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
headings?: string[];
|
||||
wordCount?: number;
|
||||
}
|
||||
|
||||
export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
|
||||
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
|
||||
|
||||
const validateKeywords = () => {
|
||||
if (!keywords) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof keywords === 'string') {
|
||||
return [keywords];
|
||||
}
|
||||
|
||||
if (Array.isArray(keywords)) {
|
||||
return keywords;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!keywords || keywords.length === 0) {
|
||||
return null;
|
||||
@@ -21,17 +39,14 @@ export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keyword
|
||||
<div className={`seo__status__keywords`}>
|
||||
<h4>Keywords</h4>
|
||||
|
||||
<VsTable bordered>
|
||||
<VsTable bordered columns={["30%", "auto"]}>
|
||||
<VsTableHeader slot="header">
|
||||
<VsTableHeaderCell className={`table__cell`}>Keyword</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Title</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Description</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Slug</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Content</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Details</VsTableHeaderCell>
|
||||
</VsTableHeader>
|
||||
<VsTableBody slot="body">
|
||||
{
|
||||
keywords.map((keyword, index) => {
|
||||
validateKeywords().map((keyword, index) => {
|
||||
return (
|
||||
<SeoKeywordInfo key={index} keyword={keyword} {...data} />
|
||||
);
|
||||
@@ -39,8 +54,17 @@ export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keyword
|
||||
}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
|
||||
|
||||
|
||||
{
|
||||
data.wordCount && (
|
||||
<div className={`seo__status__note`}>
|
||||
* A keyword density of 1-1.5% is sufficient in most cases.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoKeywords.displayName = 'SeoKeywords';
|
||||
export { SeoKeywords };
|
||||
@@ -11,14 +11,34 @@ export interface ISeoStatusProps {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const { data, seo } = props;
|
||||
const { title } = data;
|
||||
const { title, slug } = data;
|
||||
const [ isOpen, setIsOpen ] = React.useState(true);
|
||||
const tableRef = React.useRef<HTMLElement>();
|
||||
const pushUpdate = React.useRef((value: boolean) => {
|
||||
setTimeout(() => {
|
||||
setIsOpen(value);
|
||||
}, 10);
|
||||
}).current;
|
||||
|
||||
const { descriptionField } = seo;
|
||||
|
||||
// Workaround for lit components not updating render
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
let height = 0;
|
||||
|
||||
tableRef.current?.childNodes.forEach((elm: any) => {
|
||||
height += elm.clientHeight;
|
||||
});
|
||||
|
||||
if (height > 0 && tableRef.current) {
|
||||
tableRef.current.style.height = `${height}px`;
|
||||
}
|
||||
}, 10);
|
||||
}, [title, data[descriptionField], data?.articleDetails?.wordCount]);
|
||||
|
||||
if (!title && !data[descriptionField]) {
|
||||
return null;
|
||||
}
|
||||
@@ -45,6 +65,11 @@ export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React
|
||||
<SeoFieldInfo title={`title`} value={title.length} recommendation={`${seo.title} chars`} isValid={title.length <= seo.title} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(slug && seo.slug > 0) && (
|
||||
<SeoFieldInfo title={`slug`} value={slug.length} recommendation={`${seo.slug} chars`} isValid={slug.length <= seo.slug} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(data[descriptionField] && seo.description > 0) && (
|
||||
@@ -65,6 +90,8 @@ export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React
|
||||
title={title}
|
||||
description={data[descriptionField]}
|
||||
slug={data.slug}
|
||||
headings={data?.articleDetails?.headingsText}
|
||||
wordCount={data?.articleDetails?.wordCount}
|
||||
content={data?.articleDetails?.content} />
|
||||
|
||||
<ArticleDetails details={data.articleDetails} />
|
||||
@@ -72,24 +99,14 @@ export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React
|
||||
);
|
||||
};
|
||||
|
||||
// Workaround for lit components not updating render
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
let height = 0;
|
||||
|
||||
tableRef.current?.childNodes.forEach((elm: any) => {
|
||||
height += elm.clientHeight;
|
||||
});
|
||||
|
||||
if (height > 0 && tableRef.current) {
|
||||
tableRef.current.style.height = `${height}px`;
|
||||
}
|
||||
}, 10);
|
||||
}, [title, data[descriptionField], data?.articleDetails?.wordCount]);
|
||||
|
||||
|
||||
return (
|
||||
<Collapsible id={`seo`} title="SEO Status" sendUpdate={(value) => setIsOpen(value)}>
|
||||
<Collapsible id={`seo`} title="SEO Status" sendUpdate={pushUpdate}>
|
||||
{ renderContent() }
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoStatus.displayName = 'SeoStatus';
|
||||
export { SeoStatus };
|
||||
@@ -11,7 +11,7 @@ export interface ISlugActionProps {
|
||||
slugOpts: Slug;
|
||||
}
|
||||
|
||||
export const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
const { value, crntValue, slugOpts } = props;
|
||||
|
||||
let slug = SlugHelper.createSlug(value);
|
||||
@@ -24,4 +24,7 @@ export const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: Rea
|
||||
return (
|
||||
<ActionButton onClick={optimize} disabled={crntValue === slug} title={`Optimize slug`} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SlugAction.displayName = 'SlugAction';
|
||||
export { SlugAction };
|
||||
@@ -2,8 +2,11 @@ import * as React from 'react';
|
||||
|
||||
export interface ISpinnerProps {}
|
||||
|
||||
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
return (
|
||||
<div className="spinner">Loading...</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Spinner.displayName = 'Spinner';
|
||||
export { Spinner };
|
||||
@@ -4,7 +4,7 @@ import { HeartIcon } from './Icons/HeartIcon';
|
||||
|
||||
export interface ISponsorMsgProps {}
|
||||
|
||||
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
return (
|
||||
<p className={`sponsor`}>
|
||||
<a href={SPONSOR_LINK} title="Sponsor Front Matter">
|
||||
@@ -12,4 +12,7 @@ export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: Rea
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SponsorMsg.displayName = 'SponsorMsg';
|
||||
export { SponsorMsg };
|
||||
@@ -13,7 +13,7 @@ export interface ITagProps {
|
||||
onRemove: (tags: string) => void;
|
||||
}
|
||||
|
||||
export const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithChildren<ITagProps>) => {
|
||||
const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithChildren<ITagProps>) => {
|
||||
const { value, className, title, onRemove, onCreate, disableConfigurable } = props;
|
||||
|
||||
return (
|
||||
@@ -25,4 +25,7 @@ export const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithCh
|
||||
<button title={title} className={`article__tags__items__item_delete ${className}`} onClick={() => onRemove(value)}>{value} <span><ArchiveIcon /></span></button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Tag.displayName = 'Tag';
|
||||
export { Tag };
|
||||
@@ -20,7 +20,7 @@ export interface ITagPickerProps {
|
||||
disableConfigurable?: boolean;
|
||||
}
|
||||
|
||||
export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const { label, icon, type, crntSelected, options, freeform, focussed, unsetFocus, disableConfigurable } = props;
|
||||
const [ selected, setSelected ] = React.useState<string[]>([]);
|
||||
const [ inputValue, setInputValue ] = React.useState<string>("");
|
||||
@@ -83,7 +83,7 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
|
||||
if (selectedItem) {
|
||||
let value = selectedItem || "";
|
||||
|
||||
const item = options.find(o => o.toLowerCase() === selectedItem.toLowerCase());
|
||||
const item = options.find(o => o?.toLowerCase() === selectedItem?.toLowerCase());
|
||||
if (item) {
|
||||
value = item;
|
||||
}
|
||||
@@ -187,11 +187,14 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
|
||||
</Downshift>
|
||||
|
||||
<Tags
|
||||
values={(selected || []).sort((a: string, b: string) => a.toLowerCase() < b.toLowerCase() ? -1 : 1 )}
|
||||
values={(selected || []).sort((a: string, b: string) => a?.toLowerCase() < b?.toLowerCase() ? -1 : 1 )}
|
||||
onRemove={onRemove}
|
||||
onCreate={onCreate}
|
||||
options={options}
|
||||
disableConfigurable={!!disableConfigurable} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
TagPicker.displayName = 'TagPicker';
|
||||
export { TagPicker };
|
||||
@@ -11,24 +11,34 @@ export interface ITagsProps {
|
||||
onRemove: (tags: string) => void;
|
||||
}
|
||||
|
||||
export const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWithChildren<ITagsProps>) => {
|
||||
const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWithChildren<ITagsProps>) => {
|
||||
const { values, options, onCreate, onRemove, disableConfigurable } = props;
|
||||
|
||||
const knownTags = values.filter(v => options.includes(v));
|
||||
const unknownTags = values.filter(v => !options.includes(v));
|
||||
|
||||
const generateKey = (tag: string, idx: number) => {
|
||||
if (tag && typeof tag === 'string') {
|
||||
return `${tag.replace(/ /g, "_")}-${idx}`;
|
||||
}
|
||||
return `tag-${idx}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`article__tags__items`}>
|
||||
{
|
||||
knownTags.map(t => (
|
||||
<Tag key={t.replace(/ /g, "_")} value={t} className={`article__tags__items__pill_exists`} onRemove={onRemove} title={`Remove ${t}`} />
|
||||
knownTags.map((t, idx) => (
|
||||
<Tag key={generateKey(t, idx)} value={t} className={`article__tags__items__pill_exists`} onRemove={onRemove} title={`Remove ${t}`} />
|
||||
))
|
||||
}
|
||||
{
|
||||
unknownTags.map(t => (
|
||||
<Tag key={t.replace(/ /g, "_")} value={t} className={`article__tags__items__pill_notexists`} onRemove={onRemove} onCreate={onCreate} title={`Be aware, this tag "${t}" is not saved in your settings. Once removed, it will be gone forever.`} disableConfigurable={disableConfigurable} />
|
||||
unknownTags.map((t, idx) => (
|
||||
<Tag key={generateKey(t, idx)} value={t} className={`article__tags__items__pill_notexists`} onRemove={onRemove} onCreate={onCreate} title={`Be aware, this tag "${t}" is not saved in your settings. Once removed, it will be gone forever.`} disableConfigurable={disableConfigurable} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Tags.displayName = 'Tags';
|
||||
export { Tags };
|
||||
@@ -3,10 +3,11 @@ import { CheckIcon } from './Icons/CheckIcon';
|
||||
import { WarningIcon } from './Icons/WarningIcon';
|
||||
|
||||
export interface IValidInfoProps {
|
||||
label?: string;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
export const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: React.PropsWithChildren<IValidInfoProps>) => {
|
||||
const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({label, isValid}: React.PropsWithChildren<IValidInfoProps>) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
@@ -16,6 +17,10 @@ export const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: R
|
||||
<span className="warning"><WarningIcon /></span>
|
||||
)
|
||||
}
|
||||
{ label && <span>{label}</span> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ValidInfo.displayName = 'ValidInfo';
|
||||
export { ValidInfo };
|
||||
@@ -1,5 +1,6 @@
|
||||
import {wrapWc} from 'wc-react';
|
||||
|
||||
// @bendera/vscode-webview-elements
|
||||
export const VsTable = wrapWc(`vscode-table`);
|
||||
export const VsTableHeader = wrapWc(`vscode-table-header`);
|
||||
export const VsTableHeaderCell = wrapWc(`vscode-table-header-cell`);
|
||||
@@ -7,5 +8,7 @@ export const VsTableBody = wrapWc(`vscode-table-body`);
|
||||
export const VsTableRow = wrapWc(`vscode-table-row`);
|
||||
export const VsTableCell = wrapWc(`vscode-table-cell`);
|
||||
export const VsCollapsible = wrapWc(`vscode-collapsible`);
|
||||
export const VsCheckbox = wrapWc(`vscode-checkbox`);
|
||||
export const VsLabel = wrapWc(`vscode-label`);
|
||||
export const VsLabel = wrapWc(`vscode-label`);
|
||||
|
||||
// @vscode/webview-ui-toolkit
|
||||
export const VsCheckbox = wrapWc(`vscode-checkbox`);
|
||||
@@ -13,13 +13,20 @@ import '@bendera/vscode-webview-elements/dist/vscode-table-body';
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-table-row';
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-table-cell';
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-collapsible';
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-checkbox';
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-label';
|
||||
|
||||
import '@vscode/webview-ui-toolkit/dist/esm/checkbox';
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
const version = elm?.getAttribute("data-version");
|
||||
const environment = elm?.getAttribute("data-environment");
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || "",
|
||||
environment: environment || ""
|
||||
});
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
@@ -28,5 +35,4 @@ declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
postMessage: (msg: unknown) => void;
|
||||
};
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
render(<ViewPanel />, elm);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { window, Position, TextDocumentContentProvider, Uri, workspace, WorkspaceEdit, Range, languages, ViewColumn } from "vscode";
|
||||
|
||||
|
||||
export default class ContentProvider implements TextDocumentContentProvider {
|
||||
|
||||
public static get scheme() { return "frontmatter" };
|
||||
|
||||
provideTextDocumentContent(uri: Uri): string {
|
||||
return uri.query;
|
||||
}
|
||||
|
||||
public static async show(data: string, title: string, outputType?: string) {
|
||||
const apiData = JSON.stringify(data, null, 2);
|
||||
|
||||
const uri = Uri.parse(`${ContentProvider.scheme}:${title} output`);
|
||||
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
|
||||
await window.showTextDocument(doc, { preview: true, viewColumn: ViewColumn.Beside, preserveFocus: true });
|
||||
|
||||
const workEdits = new WorkspaceEdit();
|
||||
workEdits.replace(doc.uri, new Range(new Position(0, 0), new Position(doc.lineCount, 0)), data);
|
||||
await workspace.applyEdit(workEdits);
|
||||
|
||||
await languages.setTextDocumentLanguage(doc, outputType || "text");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user