Compare commits

...

46 Commits

Author SHA1 Message Date
Elio Struyf
a5fbf6991c Merge pull request #318 from estruyf/dev 2022-04-11 13:07:16 +02:00
Elio Struyf
a5f8017ab7 7.1.2 2022-04-11 13:05:56 +02:00
Elio Struyf
201fa4d564 #316 - Fix draft tab navigation 2022-04-11 13:05:48 +02:00
Elio Struyf
ef4e3fe28e Added sponsor image 2022-04-11 09:55:44 +02:00
Elio Struyf
6d6d86692a Merge pull request #313 from estruyf/dev 2022-04-08 10:03:42 +02:00
Elio Struyf
ada9724e54 update changelog 2022-04-08 10:03:13 +02:00
Elio Struyf
6347b728e4 Merge branch 'dev' 2022-04-08 10:01:09 +02:00
Elio Struyf
5079812745 7.1.1 2022-04-08 10:00:53 +02:00
Elio Struyf
c2ed9b2577 Fix for stopPropagation 2022-04-08 10:00:50 +02:00
Elio Struyf
20a5178326 Merge pull request #312 from estruyf/dev 2022-04-07 16:34:10 +02:00
Elio Struyf
81ad61f89d Update changelog for 7.1.0 release 2022-04-07 16:26:13 +02:00
Elio Struyf
5fc030b4dc Update attributes 2022-04-07 16:24:52 +02:00
Elio Struyf
bea6b181db Update changelog 2022-04-07 12:37:12 +02:00
Elio Struyf
e17819f458 #311 - Fix for updating snippets 2022-04-07 12:36:42 +02:00
Elio Struyf
db52e9c29a Style fix for tagpicker 2022-04-07 10:09:30 +02:00
Elio Struyf
e06fcb8ced Update settings + commends + changelog 2022-04-06 17:27:07 +02:00
Elio Struyf
51f43e8bfa #310 - Update changelog 2022-04-06 16:19:35 +02:00
Elio Struyf
0f66935385 #303 - updated card tags setting 2022-04-06 12:03:28 +02:00
Elio Struyf
29a6b121bb #308 - image check and if unknown, show file icon 2022-04-05 15:46:23 +02:00
Elio Struyf
6ed37f9ac2 Updated logo 2022-04-05 15:19:53 +02:00
Elio Struyf
f7c53b9afb updated changelog 2022-04-04 17:25:23 +02:00
Elio Struyf
e6e5c5c881 #308 - Supported mime types for media dashboard 2022-04-04 17:24:55 +02:00
Elio Struyf
f059b89fa5 #306 - Default value fix 2022-04-04 08:35:18 +02:00
Elio Struyf
8d5a678bb8 #240 - fix snippet action 2022-04-04 08:17:39 +02:00
Elio Struyf
8d901105bf Updated readme 2022-04-02 19:42:24 +02:00
Elio Struyf
637394156f #305 - fix for taxonomy picker styling 2022-04-02 10:08:31 +02:00
Elio Struyf
a79a987f5e Fix for onenter with selection 2022-04-02 09:34:28 +02:00
Elio Struyf
b22335d38e Updated changelog 2022-04-01 17:00:00 +02:00
Elio Struyf
757ccbddcf Fix duplicate snippets reference 2022-04-01 15:54:32 +02:00
Elio Struyf
39a30b320d #304 - Fix yaml stringify 2022-04-01 15:42:59 +02:00
Elio Struyf
912e436ca8 Fix for delete alert dialog 2022-04-01 15:41:57 +02:00
Elio Struyf
1b0853e23d #240 - Add the ability to define display modes 2022-03-31 17:05:04 +02:00
Elio Struyf
07c4e69319 Update media menu 2022-03-31 10:15:47 +02:00
Elio Struyf
4b3808a9ec #303 - new menu position 2022-03-30 20:42:51 +02:00
Elio Struyf
bd09a10fd8 #303 - Content card actions 2022-03-30 11:47:25 +02:00
Elio Struyf
9dbdd092f6 update changelog 2022-03-30 10:00:43 +02:00
Elio Struyf
ce331f12fb #302 - Spinner fix 2022-03-30 09:59:49 +02:00
Elio Struyf
cb93370f59 #301 - Visualize tags on content cards 2022-03-30 09:53:30 +02:00
Elio Struyf
7c77dca821 SEO Keywords key fix 2022-03-29 18:18:58 +02:00
Elio Struyf
17ffed418f #246 - Support to add multiple tags/keywords/taxonomy via comma separated values 2022-03-29 18:18:48 +02:00
Elio Struyf
7abb97e681 #293 - enhancement for preview images in block fields 2022-03-28 17:48:07 +02:00
Elio Struyf
33b1acddd0 7.1.0 2022-03-23 10:17:45 +01:00
Elio Struyf
e73803f927 #294 - Allow full text search 2022-03-23 10:17:32 +01:00
Elio Struyf
bbd257e650 Layout snippet fix 2022-03-23 10:17:07 +01:00
Elio Struyf
491b32baaa #296 - State caching for content dashboard 2022-03-23 08:49:43 +01:00
Elio Struyf
70d8bfe273 Small padding fixes 2022-03-22 16:46:26 +01:00
97 changed files with 1655 additions and 545 deletions

View File

@@ -1,5 +1,42 @@
# Change Log
## [7.1.2] - 2022-04-11
### 🐞 Fixes
- [#316](https://github.com/estruyf/vscode-front-matter/issues/316): Fix draft tab navigation
## [7.1.1] - 2022-04-08
### 🐞 Fixes
- Fix in menu item with `stopPropagation` not defined.
## [7.1.0] - 2022-04-07 - [Release notes](https://beta.frontmatter.codes/updates/v7.1.0)
### 🎨 Enhancements
- [#240](https://github.com/estruyf/vscode-front-matter/issues/240): Capability added to define display modes
- [#246](https://github.com/estruyf/vscode-front-matter/issues/246): Support to add multiple tags/keywords/taxonomy via comma separated values
- [#293](https://github.com/estruyf/vscode-front-matter/issues/293): Support added for setting preview images in block fields
- [#294](https://github.com/estruyf/vscode-front-matter/issues/294): Full-text search allows you to search through all your page content
- [#297](https://github.com/estruyf/vscode-front-matter/issues/297): SEO Keywords input got moved to the SEO section
- [#301](https://github.com/estruyf/vscode-front-matter/issues/301): Show tags on the content cards
- [#303](https://github.com/estruyf/vscode-front-matter/issues/303): Content card actions to quickly view, delete, or run custom scripts
- [#310](https://github.com/estruyf/vscode-front-matter/issues/310): Supported mime types for media dashboard
### ⚡️ Optimizations
- [#296](https://github.com/estruyf/vscode-front-matter/issues/296): Loading optimization of the content dashboard
### 🐞 Fixes
- [#302](https://github.com/estruyf/vscode-front-matter/issues/302): Fix for spinner when navigating between tabs
- [#304](https://github.com/estruyf/vscode-front-matter/issues/304): Fix yaml stringify which caused additional fields to be added
- [#305](https://github.com/estruyf/vscode-front-matter/issues/305): Fix for overflow issue in taxonomy picker
- [#306](https://github.com/estruyf/vscode-front-matter/issues/306): Fix for default value of content type fields
- [#311](https://github.com/estruyf/vscode-front-matter/issues/311): Fix for updating snippets
## [7.0.0] - 2022-03-21 - [Release notes](https://beta.frontmatter.codes/updates/v7.0.0)
### ✨ New Features

View File

@@ -166,24 +166,7 @@ You can open showcase issues for the following things:
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/jmatthewpryor" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/850570" />
</a>
<a href="https://github.com/apowell656" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
</a>
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
<a href="https://github.com/grahampcharles" title="Graham Charles">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/3606679?v=4" />
</a>
<a href="https://github.com/zivbk1" title="Bryan Klein">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
</a>
<a href="https://github.com/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>
<img src="https://frontmatter.codes/api/img-sponsors" />
</p>
<br />

View File

@@ -164,24 +164,7 @@ You can open showcase issues for the following things:
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/jmatthewpryor" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/850570" />
</a>
<a href="https://github.com/apowell656" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
</a>
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
<a href="https://github.com/grahampcharles" title="Graham Charles">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/3606679?v=4" />
</a>
<a href="https://github.com/zivbk1" title="Bryan Klein">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
</a>
<a href="https://github.com/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>
<img src="https://frontmatter.codes/api/img-sponsors" />
</p>
<br />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#AD0670;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#AD0670"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#AD0670;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#AD0670"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 26.1.0, 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">
<style type="text/css">
.st0{fill:none;stroke:#02AEB7;stroke-width:50;stroke-miterlimit:10;}
.st1{fill:#02AEB7;}
</style>
<rect x="25" y="25" class="st0" width="1200" height="1200"/>
<path class="st1" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path class="st1" 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
<path fill="#C5C5C5" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#C5C5C5" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6s15,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"/>
s-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.8s-14.2-97.6-20.3-149.9
l-34.3,641.6H529.6L602.2,151.2z"/>
<rect x="119.4" y="0.1" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="395.7" y="0.1" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="675.3" y="0.1" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="119.4" y="1184.7" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="395.7" y="1184.7" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="675.3" y="1184.7" fill="#C5C5C5" width="184" height="64.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#02aeb7;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#02aeb7"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#000000;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#000000"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#000000"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#000000"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#000000"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#000000"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#000000"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#000000"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#000000"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#000000"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#000000"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#000000"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#C5C5C5"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#C5C5C5" stroke-miterlimit="10" stroke-width="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="#C5C5C5"/></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="#424242"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="#424242"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="#424242"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="#424242"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#424242"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#424242" stroke-miterlimit="10" stroke-width="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="#424242"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="#424242"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#424242"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#424242"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#424242"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="#424242"/></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 26.1.0, 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
<path fill="#C5C5C5" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#C5C5C5" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6s15,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"/>
s-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.8s-14.2-97.6-20.3-149.9
l-34.3,641.6H529.6L602.2,151.2z"/>
<rect x="119.4" y="0.1" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="395.7" y="0.1" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="675.3" y="0.1" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="119.4" y="1184.7" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="395.7" y="1184.7" fill="#C5C5C5" width="184" height="64.7"/>
<rect x="675.3" y="1184.7" fill="#C5C5C5" width="184" height="64.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 26.1.0, 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
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6s15,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"/>
s-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.8s-14.2-97.6-20.3-149.9
l-34.3,641.6H529.6L602.2,151.2z"/>
<rect x="119.4" y="0.1" fill="#424242" width="184" height="64.7"/>
<rect x="395.7" y="0.1" fill="#424242" width="184" height="64.7"/>
<rect x="675.3" y="0.1" fill="#424242" width="184" height="64.7"/>
<rect x="119.4" y="1184.7" fill="#424242" width="184" height="64.7"/>
<rect x="395.7" y="1184.7" fill="#424242" width="184" height="64.7"/>
<rect x="675.3" y="1184.7" fill="#424242" width="184" height="64.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -8,4 +8,4 @@
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>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -30,7 +30,7 @@
}
.inherit {
position: relative !important;
position: inherit !important;
}
.z-10 { z-index: 10 !important; }
@@ -143,12 +143,13 @@
}
.article__tags {
position: relative;
/* position: relative; */
margin-bottom: 1rem;
}
.article__tags__dropbox {
width: 90%;
/* Minus the twice the padding */
width: calc(100% - 2.5rem);
margin: 0;
padding: 0;
z-index: 1;
@@ -172,20 +173,21 @@
.article__tags__input.freeform {
position: relative;
outline: 1px solid var(--vscode-inputValidation-infoBorder);
outline-offset: -1px;
}
.article__tags__input.freeform input {
padding-right: 35px;
border: 0;
}
.article__tags__input button {
position: absolute;
bottom: 1px;
top: 1px;
right: 1px;
bottom: 0;
top: 0;
right: 0;
width: 30px;
padding-bottom: 2px;
padding-top: 2px;
display: inline-flex;
align-items: center;
justify-content: center;

39
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "vscode-front-matter-beta",
"version": "7.0.0",
"version": "7.1.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -55,9 +55,9 @@
"dev": true
},
"@estruyf/vscode": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@estruyf/vscode/-/vscode-0.0.2.tgz",
"integrity": "sha512-Qb2UGiARR0Pxeknjzwq925kxyxWMsASIcCOfwOJGZed7EPhDLc6Zd1v6AReYpkQFyxSUjBVg38dT7dn5bh19fQ==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@estruyf/vscode/-/vscode-0.0.3.tgz",
"integrity": "sha512-Mak13ZHj/TcyL0snPHsrqELlhpvV1JszZFScCcm1G2dp6UdP4dSk32MlNH5FusGhTIEOI6APE8krldMcZjS/3w==",
"dev": true,
"requires": {
"@types/vscode-webview": "1.57.0"
@@ -584,6 +584,12 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"@types/mime-types": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -3806,12 +3812,20 @@
"dev": true
},
"mime-types": {
"version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": {
"mime-db": "1.51.0"
"mime-db": "1.52.0"
},
"dependencies": {
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true
}
}
},
"mimic-fn": {
@@ -5554,6 +5568,15 @@
}
}
},
"tailwindcss-nested-groups": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss-nested-groups/-/tailwindcss-nested-groups-1.2.4.tgz",
"integrity": "sha512-SrW9VTgH5/PmW/E0q1+TC7TckhVfonDpiYiLKsRYX57FEcM3LemiH7JYcsTlubM+eAVkd9Jjj03pTiyfnDBibQ==",
"dev": true,
"requires": {
"postcss-selector-parser": "^6.0.4"
}
},
"tapable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",

View File

@@ -3,7 +3,7 @@
"displayName": "Front Matter",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "7.0.0",
"version": "7.1.2",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -379,6 +379,12 @@
},
"scope": "Custom scripts"
},
"frontMatter.dashboard.content.cardTags": {
"type": "string",
"default": "tags",
"markdownDescription": "Specify the name of the metadata field that will be used to show the tags on the content card. When empty or null, it will hide the tags from the card. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.dashboard.content.cardTags)",
"scope": "Dashboard"
},
"frontMatter.dashboard.mediaSnippet": {
"type": "array",
"default": [],
@@ -561,6 +567,54 @@
"default": null,
"markdownDescription": "Specify the command you want to use to start your static site generator or framework. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.startcommand)"
},
"frontMatter.global.activeMode": {
"type": [
"string",
"null"
],
"default": "",
"markdownDescription": "Specify the activated mode of Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.activemode)",
"scope": "Global"
},
"frontMatter.global.modes": {
"type": "array",
"default": [],
"markdownDescription": "Specify the modes you want to use for Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.modes)",
"items": {
"type": "object",
"default": {},
"properties": {
"id": {
"type": "string",
"description": "The ID of your mode."
},
"features": {
"type": "array",
"default": [],
"description": "The features you want to use for your mode.",
"items": {
"type": "string",
"enum": [
"panel.globalSettings",
"panel.seo",
"panel.actions",
"panel.metadata",
"panel.recentlyModified",
"panel.otherActions",
"dashboard.snippets.view",
"dashboard.snippets.manage",
"dashboard.data.view"
]
}
}
},
"required": [
"id",
"features"
]
},
"scope": "Global"
},
"frontMatter.global.notifications": {
"type": "array",
"items": {
@@ -591,6 +645,19 @@
"markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.media.defaultsorting)",
"scope": "Content"
},
"frontMatter.media.supportedMimeTypes": {
"type": "array",
"default": [
"image/*",
"video/*",
"audio/*"
],
"markdownDescription": "Specify the mime types to support for the media files. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.media.supportedMimeTypes)",
"items": {
"type": "string"
},
"scope": "Media"
},
"frontMatter.panel.freeform": {
"type": "boolean",
"default": true,
@@ -697,7 +764,14 @@
"description": "Title to show in the UI"
},
"default": {
"type": "string",
"type": [
"string",
"number",
"boolean",
"array",
"object",
"null"
],
"description": "Default value"
},
"choices": {
@@ -1105,6 +1179,15 @@
"title": "Authenticate",
"category": "Front matter"
},
{
"command": "frontMatter.markup.blockquote",
"title": "Blockquote",
"category": "Front matter",
"icon": {
"light": "assets/icons/blockquote-light.svg",
"dark": "assets/icons/blockquote-dark.svg"
}
},
{
"command": "frontMatter.markup.bold",
"title": "Bold",
@@ -1141,15 +1224,6 @@
"dark": "assets/icons/codeblock-dark.svg"
}
},
{
"command": "frontMatter.markup.blockquote",
"title": "Blockquote",
"category": "Front matter",
"icon": {
"light": "assets/icons/blockquote-light.svg",
"dark": "assets/icons/blockquote-dark.svg"
}
},
{
"command": "frontMatter.collapseSections",
"title": "Collapse sections",
@@ -1276,8 +1350,8 @@
}
},
{
"command": "frontMatter.dashboard.snippets",
"title": "Open snippets dashboard",
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
@@ -1285,8 +1359,8 @@
}
},
{
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"command": "frontMatter.dashboard.snippets",
"title": "Open snippets dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
@@ -1340,6 +1414,12 @@
"dark": "assets/icons/strikethrough-dark.svg"
}
},
{
"command": "frontMatter.mode.switch",
"title": "Switch mode",
"category": "Front matter",
"icon": "$(preview)"
},
{
"command": "frontMatter.markup.tasklist",
"title": "Task list",
@@ -1380,7 +1460,7 @@
{
"command": "frontMatter.insertSnippet",
"group": "navigation@-129",
"when": "frontMatter:file:isValid == true"
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
@@ -1463,6 +1543,14 @@
"command": "frontMatter.preview",
"when": "frontMatterCanOpenPreview"
},
{
"command": "frontMatter.dashboard.data",
"when": "frontMatter:dashboard:data:enabled"
},
{
"command": "frontMatter.dashboard.snippets",
"when": "frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.collapseSections",
"when": "false"
@@ -1529,7 +1617,7 @@
},
{
"command": "frontMatter.insertSnippet",
"when": "frontMatter:file:isValid == true"
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
@@ -1575,9 +1663,14 @@
"when": "view == frontMatter.explorer"
},
{
"command": "frontMatter.dashboard",
"command": "frontMatter.mode.switch",
"group": "navigation@1",
"when": "view == frontMatter.explorer"
},
{
"command": "frontMatter.dashboard",
"group": "navigation@2",
"when": "view == frontMatter.explorer"
}
]
},
@@ -1652,7 +1745,7 @@
},
"devDependencies": {
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "0.0.2",
"@estruyf/vscode": "0.0.3",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
@@ -1666,6 +1759,7 @@
"@types/lodash.omit": "^4.5.6",
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^5.2.6",
"@types/mustache": "^4.1.2",
"@types/node": "10.17.48",
@@ -1696,6 +1790,7 @@
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"mime-types": "^2.1.35",
"mustache": "^4.2.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
@@ -1713,6 +1808,7 @@
"rimraf": "^3.0.2",
"style-loader": "2.0.0",
"tailwindcss": "^2.2.7",
"tailwindcss-nested-groups": "^1.2.4",
"ts-loader": "8.0.3",
"tslint": "6.1.3",
"typescript": "^4.5.4",

View File

@@ -9,6 +9,7 @@ import { DashboardData } from '../models/DashboardData';
import { MediaLibrary } from '../helpers/MediaLibrary';
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener } from '../listeners/dashboard';
import { MediaListener as PanelMediaListener } from '../listeners/panel'
import { ModeListener } from '../listeners/general';
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -143,6 +144,7 @@ export class Dashboard {
DataListener.process(msg);
TelemetryListener.process(msg);
SnippetListener.process(msg);
ModeListener.process(msg);
});
}

View File

@@ -71,8 +71,8 @@ export class Preview {
);
webView.iconPath = {
dark: Uri.file(join(extensionPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionPath, 'assets/frontmatter.svg'))
dark: Uri.file(join(extensionPath, 'assets/icons/frontmatter-short-dark.svg')),
light: Uri.file(join(extensionPath, 'assets/icons/frontmatter-short-light.svg'))
}
const localhostUrl = await env.asExternalUri(

View File

@@ -0,0 +1,26 @@
import * as React from 'react';
export interface IFeatureFlagProps {
flag: string;
features: string[] | null;
alternative?: JSX.Element;
}
export const FeatureFlag: React.FunctionComponent<IFeatureFlagProps> = ({ flag, features, alternative, children }: React.PropsWithChildren<IFeatureFlagProps>) => {
if (!features ||( features.length > 0 && !features.includes(flag))) {
if (alternative) {
return alternative;
}
return null;
}
return (
<>
{children}
</>
);
};

View File

@@ -35,6 +35,7 @@ export const COMMAND_NAME = {
promote: getCommandName("promoteSettings"),
createFolder: getCommandName("createFolder"),
diagnostics: getCommandName("diagnostics"),
modeSwitch: getCommandName("mode.switch"),
// Insert dashboards
insertImage: getCommandName("insertImage"),

View File

@@ -12,6 +12,10 @@ export const ExtensionState = {
},
Media: {
Sorting: `frontMatter:Dashboard:Media:Sorting`,
},
Pages: {
Cache: `frontMatter:Dashboard:Pages:Cache`,
Index: `frontMatter:Dashboard:Pages:Index`,
}
},

21
src/constants/Features.ts Normal file
View File

@@ -0,0 +1,21 @@
export const FEATURE_FLAG = {
panel: {
globalSettings: "panel.globalSettings",
seo: "panel.seo",
actions: "panel.actions",
metadata: "panel.metadata",
recentlyModified: "panel.recentlyModified",
otherActions: "panel.otherActions",
},
dashboard: {
snippets: {
view: "dashboard.snippets.view",
manage: "dashboard.snippets.manage",
},
data: {
view: "dashboard.data.view",
}
}
};

View File

@@ -0,0 +1,5 @@
export enum GeneralCommands{
setMode = "setMode"
};

View File

@@ -8,4 +8,7 @@ export const CONTEXT = {
wysiwyg: "frontMatter:markdown:wysiwyg",
backer: "frontMatter:backers:supporter",
isValidFile: "frontMatter:file:isValid",
isSnippetsDashboardEnabled: "frontMatter:dashboard:snippets:enabled",
isDataDashboardEnabled: "frontMatter:dashboard:data:enabled",
};

View File

@@ -3,7 +3,9 @@ export * from './DefaultFields';
export * from './DefaultFileTypes';
export * from './Extension';
export * from './ExtensionState';
export * from './Features';
export * from './FrameworkDetectors';
export * from './GeneralCommands';
export * from './Links';
export * from './LocalStore';
export * from './Navigation';

View File

@@ -3,6 +3,8 @@ export const EXTENSION_NAME = "Front Matter";
export const CONFIG_KEY = "frontMatter";
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
export const SETTING_GLOBAL_MODES = "global.modes";
export const SETTING_GLOBAL_ACTIVE_MODE = "global.activeMode";
export const SETTING_TAXONOMY_TAGS = "taxonomy.tags";
export const SETTING_TAXONOMY_CATEGORIES = "taxonomy.categories";
@@ -56,8 +58,11 @@ export const SETTING_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
export const SETTING_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
export const SETTING_DATA_FILES = "data.files";
export const SETTING_DATA_FOLDERS = "data.folders";

View File

@@ -1,9 +1,11 @@
export enum DashboardCommand {
loading = "loading",
pages = "pages",
searchPages = "searchPages",
settings = "settings",
media = "media",
viewData = "viewData",
mediaUpdate = "mediaUpdate",
dataFileEntries = "dataFileEntries"
dataFileEntries = "dataFileEntries",
searchReady = "searchReady",
}

View File

@@ -1,32 +1,46 @@
export enum DashboardMessage {
getViewType = 'getViewType',
reload = 'reload',
setPageViewType = 'setPageViewType',
getMode = 'getMode',
showWarning = 'showWarning',
// Content dashboard
getData = 'getData',
openFile = 'openFile',
getTheme = 'getTheme',
createContent = 'createContent',
createByContentType = 'createByContentType',
createByTemplate = 'createByTemplate',
updateSetting = 'updateSetting',
initializeProject = 'initializeProject',
reload = 'reload',
setPageViewType = 'setPageViewType',
refreshPages = 'refreshPages',
searchPages = 'searchPages',
openFile = 'openFile',
deleteFile = 'deleteFile',
// Media Dashboard
getMedia = 'getMedia',
copyToClipboard = 'copyToClipboard',
refreshMedia = 'refreshMedia',
refreshPages = 'refreshPages',
uploadMedia = 'uploadMedia',
deleteMedia = 'deleteMedia',
revealMedia = 'revealMedia',
insertPreviewImage = 'insertPreviewImage',
updateMediaMetadata = 'updateMediaMetadata',
createMediaFolder = 'createMediaFolder',
setFramework = 'setFramework',
setState = 'setState',
runCustomScript = 'runCustomScript',
// Data dashboard
getDataEntries = 'getDataEntries',
putDataEntries = 'putDataEntries',
sendTelemetry = 'sendTelemetry',
// Snippets dashboard
insertSnippet = 'insertSnippet',
addSnippet = 'addSnippet',
updateSnippet = 'updateSnippet',
// Other
getTheme = 'getTheme',
updateSetting = 'updateSetting',
initializeProject = 'initializeProject',
setFramework = 'setFramework',
setState = 'setState',
runCustomScript = 'runCustomScript',
sendTelemetry = 'sendTelemetry',
}

View File

@@ -0,0 +1,102 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { EyeIcon, TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { CustomScript, ScriptType } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
import { Alert } from '../Modals/Alert';
export interface IContentActionsProps {
title: string;
path: string;
scripts: CustomScript[] | undefined;
onOpen: () => void;
}
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen }: React.PropsWithChildren<IContentActionsProps>) => {
const [ showDeletionAlert, setShowDeletionAlert ] = React.useState(false);
const onView = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onOpen();
};
const onDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setShowDeletionAlert(true);
};
const onDeleteConfirm = () => {
if (path) {
Messenger.send(DashboardMessage.deleteFile, path);
}
setShowDeletionAlert(false);
}
const runCustomScript = React.useCallback((e: React.MouseEvent<HTMLButtonElement>, script: CustomScript) => {
e.stopPropagation();
Messenger.send(DashboardMessage.runCustomScript, {script, path});
}, [path]);
const customScriptActions = React.useMemo(() => {
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
<MenuItem
key={script.title}
title={script.title}
onClick={(value, e) => runCustomScript(e, script)} />
))
}, [scripts]);
return (
<>
<div className={`group-scope absolute top-6 right-0 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full p-2 -mt-4">
<Menu as="div" className="relative z-10 flex text-left">
<div className='hidden group-scope-hover:flex'>
<QuickAction
title={`View content`}
onClick={onView}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete content`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
<ActionMenuButton title={`Menu`} />
<MenuItems widthClass='w-40' marginTopClass='mt-6'>
<MenuItem
title={`View`}
onClick={(value, e) => onView(e)} />
{ customScriptActions }
<MenuItem
title={`Delete`}
onClick={(value, e) => onDelete(e)} />
</MenuItems>
</Menu>
</div>
</div>
{
showDeletionAlert && (
<Alert
title={`Delete: ${title}`}
description={`Are you sure you want to delete the "${title}" content?`}
okBtnText={`Delete`}
cancelBtnText={`Cancel`}
dismiss={() => setShowDeletionAlert(false)}
trigger={onDeleteConfirm} />
)
}
</>
);
};

View File

@@ -33,7 +33,7 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loadin
<PageLayout
folders={pageFolders}
totalPages={pageItems.length}>
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
<div className="w-full flex-grow max-w-7xl mx-auto pb-6 px-4">
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
</div>

View File

@@ -3,11 +3,12 @@ import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models/Page';
import { ViewSelector } from '../../state';
import { SettingsSelector, ViewSelector } from '../../state';
import { DateField } from '../DateField';
import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardViewType } from '../../models';
import { ContentActions } from './ContentActions';
export interface IItemProps extends Page {}
@@ -15,16 +16,27 @@ const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
const view = useRecoilValue(ViewSelector);
const settings = useRecoilValue(SettingsSelector);
const openFile = () => {
Messenger.send(DashboardMessage.openFile, fmFilePath);
};
const tags: string[] | undefined = React.useMemo(() => {
if (!settings?.dashboardState?.contents?.tags) {
return undefined;
}
const tagField = settings.dashboardState.contents.tags;
return pageData[tagField] || [];
}, [settings, pageData]);
if (view === DashboardViewType.Grid) {
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50`}
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
{
pageData[PREVIEW_IMAGE_FIELD] ? (
@@ -37,16 +49,38 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
}
</div>
<div className="p-4 w-full">
<div className="relative p-4 w-full">
<div className={`flex justify-between items-center`}>
<Status draft={draft} />
<DateField value={date} />
<DateField className={`mr-4`} value={date} />
<ContentActions
title={title}
path={fmFilePath}
scripts={settings?.scripts}
onOpen={openFile} />
</div>
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p>
{
tags && tags.length > 0 && (
<div className="mt-2">
{
tags.map((tag, index) => (
<span
key={index}
className="inline-block mr-1 mt-1 text-[#5D561D] dark:text-[#F0ECD0] text-xs">
#{tag}
</span>
))
}
</div>
)
}
</div>
</button>
</li>

View File

@@ -4,12 +4,14 @@ import useMessages from '../hooks/useMessages';
import useDarkMode from '../../hooks/useDarkMode';
import { WelcomeScreen } from './WelcomeScreen';
import { useRecoilValue } from 'recoil';
import { DashboardViewSelector } from '../state';
import { DashboardViewSelector, ModeAtom } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { NavigationType } from '../models';
import { DataView } from './DataView';
import { Snippets } from './SnippetsView/Snippets';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants';
export interface IDashboardProps {
showWelcome: boolean;
@@ -18,6 +20,7 @@ export interface IDashboardProps {
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome}: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages, settings } = useMessages();
const view = useRecoilValue(DashboardViewSelector);
const mode = useRecoilValue(ModeAtom);
useDarkMode();
if (!settings) {
@@ -50,9 +53,11 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
if (view === NavigationType.Data) {
return (
<main className={`h-full w-full`}>
<DataView />
</main>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.data.view}>
<main className={`h-full w-full`}>
<DataView />
</main>
</FeatureFlag>
);
}

View File

@@ -3,10 +3,11 @@ import * as React from 'react';
import { DateHelper } from '../../helpers/DateHelper';
export interface IDateFieldProps {
className?: string;
value: Date | string;
}
export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: React.PropsWithChildren<IDateFieldProps>) => {
export const DateField: React.FunctionComponent<IDateFieldProps> = ({className, value}: React.PropsWithChildren<IDateFieldProps>) => {
const [ dateValue, setDateValue ] = React.useState<string>("");
React.useEffect(() => {
@@ -24,6 +25,6 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: Rea
}
return (
<span className={`text-vulcan-100 dark:text-whisper-900 text-xs`}>{dateValue}</span>
<span className={`${className || ""} text-vulcan-100 dark:text-whisper-900 text-xs`}>{dateValue}</span>
);
};

View File

@@ -1,18 +1,13 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom, SearchAtom, SelectedMediaFolderSelector } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { RefreshIcon } from '@heroicons/react/outline';
import { useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom } from '../../state';
import { LIMIT } from '../../hooks/useMedia';
export interface IPaginationStatusProps {}
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
const totalMedia = useRecoilValue(MediaTotalSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ page, setPage ] = useRecoilState(PageAtom);
const [ , setSearch ] = useRecoilState(SearchAtom);
const page = useRecoilValue(PageAtom);
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
@@ -22,21 +17,8 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
return mediaItems;
};
const refresh = () => {
setPage(0);
setSearch('');
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
return (
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<div className="hidden sm:flex">
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results

View File

@@ -0,0 +1,57 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { NavigationType } from '../../models';
import { CategoryAtom, DashboardViewAtom, FolderAtom, LoadingAtom, PageAtom, SearchAtom, SelectedMediaFolderSelector, SortingAtom, TagAtom } from '../../state';
export interface IRefreshDashboardDataProps {}
export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardDataProps> = (props: React.PropsWithChildren<IRefreshDashboardDataProps>) => {
const view = useRecoilValue(DashboardViewAtom);
const [, setLoading] = useRecoilState(LoadingAtom);
const resetSearch = useResetRecoilState(SearchAtom);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
// Media
const resetPage = useResetRecoilState(PageAtom);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const refreshPages = () => {
setLoading(true);
resetSearch();
resetSorting();
resetFolder();
resetTag();
resetCategory();
Messenger.send(DashboardMessage.refreshPages);
};
const refreshMedia = () => {
setLoading(true);
resetPage();
resetSearch();
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
};
const refresh = useCallback(() => {
if (view === NavigationType.Contents) {
refreshPages();
} else if (view === NavigationType.Media) {
refreshMedia();
}
}, [view]);
return (
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh dashboard"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh dashboard</span>
</button>
);
};

View File

@@ -1,36 +0,0 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { CategoryAtom, FolderAtom, LoadingAtom, SearchAtom, SortingAtom, TagAtom } from '../../state';
export interface IRefreshPagesProps {}
export const RefreshPages: React.FunctionComponent<IRefreshPagesProps> = (props: React.PropsWithChildren<IRefreshPagesProps>) => {
const [, setLoading] = useRecoilState(LoadingAtom);
const resetSearch = useResetRecoilState(SearchAtom);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
const refresh = () => {
setLoading(true);
resetSearch();
resetSorting();
resetFolder();
resetTag();
resetCategory();
Messenger.send(DashboardMessage.refreshPages);
}
return (
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh dashboard"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh dashboard</span>
</button>
);
};

View File

@@ -1,9 +1,9 @@
import {SearchIcon} from '@heroicons/react/solid';
import {SearchIcon, XCircleIcon} from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { SearchAtom } from '../../state';
import { RefreshPages } from './RefreshPages';
import { SearchAtom, SearchReadyAtom } from '../../state';
import { RefreshDashboardData } from './RefreshDashboardData';
export interface ISearchboxProps {
placeholder?: string;
@@ -12,12 +12,18 @@ export interface ISearchboxProps {
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({placeholder}: React.PropsWithChildren<ISearchboxProps>) => {
const [ value, setValue ] = React.useState('');
const [ debounceSearchValue, setDebounceValue ] = useRecoilState(SearchAtom);
const searchReady = useRecoilValue(SearchReadyAtom);
const debounceSearch = useDebounce<string>(value, 500);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
const reset = React.useCallback(() => {
setValue('');
setDebounceValue('');
}, [setValue, setDebounceValue]);
React.useEffect(() => {
if (!debounceSearchValue && value) {
setValue('');
@@ -38,17 +44,26 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({placeholder
</div>
<input
type="search"
type="text"
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`}
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none appearance-none disabled:opacity-50`}
placeholder={placeholder || "Search"}
value={value}
onChange={handleChange}
disabled={!searchReady}
/>
{
value && (
<button onClick={reset} className="absolute inset-y-0 right-0 pr-3 flex items-center">
<XCircleIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</button>
)
}
</div>
</div>
<RefreshPages />
<RefreshDashboardData />
</div>
);
};

View File

@@ -1,9 +1,11 @@
import { DatabaseIcon, PhotographIcon, ScissorsIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { NavigationType } from '../../models';
import { SettingsSelector } from '../../state';
import { ModeAtom } from '../../state';
import { Tab } from './Tab';
export interface ITabsProps {
@@ -11,7 +13,7 @@ export interface ITabsProps {
}
export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.PropsWithChildren<ITabsProps>) => {
const settings = useRecoilValue(SettingsSelector);
const mode = useRecoilValue(ModeAtom);
return (
<ul className="flex items-center justify-start h-full" data-tabs-toggle="#myTabContent" role="tablist">
@@ -29,20 +31,24 @@ export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</Tab>
</li>
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Snippets}
onNavigate={onNavigate}>
<ScissorsIcon className={`h-6 w-auto mr-2`} /><span>Snippets</span>
</Tab>
</li>
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Data}
onNavigate={onNavigate}>
<DatabaseIcon className={`h-6 w-auto mr-2`} /><span>Data</span>
</Tab>
</li>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.snippets.view}>
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Snippets}
onNavigate={onNavigate}>
<ScissorsIcon className={`h-6 w-auto mr-2`} /><span>Snippets</span>
</Tab>
</li>
</FeatureFlag>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.data.view}>
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Data}
onNavigate={onNavigate}>
<DatabaseIcon className={`h-6 w-auto mr-2`} /><span>Data</span>
</Tab>
</li>
</FeatureFlag>
</ul>
);
};

View File

@@ -1,21 +1,22 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { ScriptType } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { ActionMenuButton } from '../Menu/ActionMenuButton';
import { QuickAction } from '../Menu/QuickAction';
import { Alert } from '../Modals/Alert';
import { DetailsSlideOver } from './DetailsSlideOver';
import { MenuButton } from './MenuButton'
import { QuickAction } from './QuickAction';
export interface IItemProps {
media: MediaInfo;
@@ -166,9 +167,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
setShowDetails(true);
};
const openLightbox = () => {
setLightbox(media.vsPath || "");
};
const openLightbox = useCallback(() => {
if (!isVideoFile() && !isAudioFile()) {
setLightbox(media.vsPath || "");
}
}, [media.vsPath]);
const updateMetadata = () => {
setShowForm(true);
@@ -184,6 +187,55 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
))
}
const isVideoFile = useCallback(() => {
if (media.mimeType?.startsWith("video/")) {
return true;
}
return false;
}, [media]);
const isAudioFile = useCallback(() => {
if (media.mimeType?.startsWith("audio/")) {
return true;
}
return false;
}, [media]);
const isImageFile = useCallback(() => {
if (media.mimeType?.startsWith("image/")) {
return true;
}
return false;
}, [media]);
const renderMediaIcon = useMemo(() => {
if (isVideoFile()) {
return <VideoCameraIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
if (isAudioFile()) {
return <MusicNoteIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
if (isImageFile()) {
return <PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
return <DocumentIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}, [media]);
const renderMedia = useMemo(() => {
if (isVideoFile() || isAudioFile()) {
return null;
}
if (isImageFile()) {
return <img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />;
}
return null;
}, [media]);
useEffect(() => {
if (media.alt !== alt) {
setAlt(media.alt);
@@ -208,69 +260,71 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50">
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
{
renderMediaIcon
}
</div>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />
{ renderMedia }
</div>
</button>
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className={`group-scope absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className="flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='hidden group-hover:inline-block h-5'>
<QuickAction
title='View media details'
onClick={viewMediaDetails}>
<EyeIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
<Menu as="div" className="relative z-10 flex text-left">
<div className='hidden group-scope-hover:flex'>
<QuickAction
title='View media details'
onClick={viewMediaDetails}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Edit metadata'
onClick={updateMetadata}>
<PencilIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Edit metadata'
onClick={updateMetadata}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{
viewData?.data?.filePath ? (
<>
<QuickAction
title={(viewData.data.metadataInsert && viewData.data.fieldName) ? `Insert image for your "${viewData.data.fieldName}" field` : `Insert image with markdown markup`}
onClick={insertToArticle}>
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
{
viewData?.data?.filePath ? (
<>
<QuickAction
title={(viewData.data.metadataInsert && viewData.data.fieldName) ? `Insert image for your "${viewData.data.fieldName}" field` : `Insert image with markdown markup`}
onClick={insertToArticle}>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<QuickAction
title='Insert snippet'
onClick={insertSnippet}>
<CodeIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
)
}
</>
) : (
<>
<QuickAction
title='Copy media path'
onClick={copyToClipboard}>
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
</>
)
}
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<QuickAction
title='Insert snippet'
onClick={insertSnippet}>
<CodeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
</>
) : (
<>
<QuickAction
title='Copy media path'
onClick={copyToClipboard}>
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</>
)
}
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
</div>
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
<Menu as="div" className="relative z-10 inline-block text-left h-5">
<MenuButton title={`Menu`} />
<ActionMenuButton title={`Menu`} />
<MenuItems widthClass='w-40'>
<MenuItem

View File

@@ -73,12 +73,15 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
const {getRootProps, isDragActive} = useDropzone({
onDrop,
accept: 'image/*'
accept: settings?.dashboardState.media.mimeTypes || ['image/*', 'video/*', 'audio/*'],
onDropRejected: () => {
Messenger.send(DashboardMessage.showWarning, 'Unsupported file type');
}
});
return (
<PageLayout>
<div className="w-full h-full" {...getRootProps()}>
<div className="w-full h-full pb-6" {...getRootProps()}>
{
viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>

View File

@@ -1,20 +0,0 @@
import { Menu } from '@headlessui/react';
import {DotsVerticalIcon} from '@heroicons/react/outline';
import * as React from 'react';
export interface IMenuButtonProps {
title: string;
disabled?: boolean;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`inline-flex items-center ${disabled ? 'opacity-50' : ''}`}>
<Menu.Button disabled={disabled} className="group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600">
<span className="sr-only">{title}</span>
<DotsVerticalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
);
};

View File

@@ -0,0 +1,18 @@
import { Menu } from '@headlessui/react';
import {DotsVerticalIcon} from '@heroicons/react/outline';
import * as React from 'react';
export interface IActionMenuButtonProps {
title: string;
disabled?: boolean;
}
export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IActionMenuButtonProps>) => {
return (
<Menu.Button disabled={disabled} className={`group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600 ${disabled ? 'opacity-50' : ''}`}>
<span className="sr-only">{title}</span>
<DotsVerticalIcon className="w-4 h-4" aria-hidden="true" />
</Menu.Button>
);
};

View File

@@ -6,7 +6,7 @@ export interface IMenuItemProps {
value?: any;
isCurrent?: boolean;
disabled?: boolean;
onClick: (value: any) => void;
onClick: (value: any, e: React.MouseEvent<HTMLButtonElement>) => void;
}
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, disabled, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
@@ -14,7 +14,7 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value,
<Menu.Item>
<button
disabled={disabled}
onClick={() => onClick(value)}
onClick={(e) => onClick(value, e)}
className={`${!isCurrent ? `font-normal` : `font-bold`} text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
>
{title}

View File

@@ -4,9 +4,10 @@ import { Fragment } from 'react';
export interface IMenuItemsProps {
widthClass?: string;
marginTopClass?: string;
}
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, children}: React.PropsWithChildren<IMenuItemsProps>) => {
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, marginTopClass, children}: React.PropsWithChildren<IMenuItemsProps>) => {
return (
<Transition
as={Fragment}
@@ -17,7 +18,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className={`${widthClass || ""} origin-top-right absolute right-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`}>
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-10 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">
{children}
</div>

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
export interface IQuickActionProps {
title: string;
onClick: () => void;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({title, onClick, children}: React.PropsWithChildren<IQuickActionProps>) => {

View File

@@ -1,3 +1,5 @@
export * from './ActionMenuButton';
export * from './MenuButton';
export * from './MenuItem';
export * from './MenuItems';
export * from './QuickAction';

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Tab } from '../constants/Tab';
import { SettingsAtom, TabAtom } from '../state';
import { SettingsAtom, TabAtom, TabInfoAtom } from '../state';
export interface INavigationProps {
totalPages: number;
@@ -15,8 +15,9 @@ export const tabs = [
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
const tabInfo = useRecoilValue(TabInfoAtom);
const settings = useRecoilValue(SettingsAtom);
return (
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{
@@ -28,7 +29,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPage
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
{tab.name}{(tabInfo && tabInfo[tab.id]) ? ` (${tabInfo[tab.id]})` : ''}
</button>
))
) : (
@@ -38,7 +39,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPage
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tabs[0].id)}
>
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
{tabs[0].name}{(tabInfo && tabInfo[tabs[0].id]) ? ` (${tabInfo[tabs[0].id]})` : ''}
</button>
{
@@ -49,7 +50,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPage
aria-current={value === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(value)}
>
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
{value}{(tabInfo && tabInfo[value]) ? ` (${tabInfo[value]})` : ''}
</button>
))
}

View File

@@ -3,10 +3,13 @@ import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { Snippet, SnippetField, Snippets } from '../../../models';
import { Snippet, Snippets } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsSelector, ViewDataSelector } from '../../state';
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { QuickAction } from '../Menu';
import { Alert } from '../Modals/Alert';
import { FormDialog } from '../Modals/FormDialog';
import { NewForm } from './NewForm';
@@ -20,6 +23,7 @@ export interface IItemProps {
export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: React.PropsWithChildren<IItemProps>) => {
const viewData = useRecoilValue(ViewDataSelector);
const settings = useRecoilValue(SettingsSelector);
const mode = useRecoilValue(ModeAtom);
const [ showInsertDialog, setShowInsertDialog ] = useState(false);
const [ showEditDialog, setShowEditDialog ] = useState(false);
const [ showAlert, setShowAlert ] = React.useState(false);
@@ -100,36 +104,63 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='group-hover:hidden'>
<DotsHorizontalIcon className="w-4 h-4" />
</div>
<FeatureFlag
features={mode?.features || []}
flag={FEATURE_FLAG.dashboard.snippets.manage}
alternative={(
viewData?.data?.filePath ? (
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='group-hover:hidden'>
<DotsHorizontalIcon className="w-4 h-4" />
</div>
<div className='hidden group-hover:flex space-x-2'>
{
viewData?.data?.filePath && (
<>
<button onClick={() => setShowInsertDialog(true)}>
<PlusIcon className='w-4 h-4' />
<span className='sr-only'>Insert snippet</span>
</button>
</>
)
}
<div className='hidden group-hover:flex'>
<QuickAction
title={`Insert snippet`}
onClick={() => setShowInsertDialog(true)}>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
</div>
</div>
) : undefined
)}
>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='group-hover:hidden'>
<DotsHorizontalIcon className="w-4 h-4" />
</div>
<button onClick={onOpenEdit}>
<PencilIcon className='w-4 h-4' />
<span className='sr-only'>Edit snippet</span>
</button>
<div className='hidden group-hover:flex'>
{
viewData?.data?.filePath && (
<>
<QuickAction
title={`Insert snippet`}
onClick={() => setShowInsertDialog(true)}>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</>
)
}
<button onClick={() => setShowAlert(true)}>
<TrashIcon className='w-4 h-4' />
<span className='sr-only'>Delete snippet</span>
</button>
<QuickAction
title={`Edit snippet`}
onClick={onOpenEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete snippet`}
onClick={() => setShowAlert(true)}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
</div>
</div>
</div>
</FeatureFlag>
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{snippet.description}</p>
</li>

View File

@@ -3,10 +3,12 @@ import { CodeIcon, PlusSmIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { TelemetryEvent } from '../../../constants/TelemetryEvent';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsSelector, ViewDataSelector } from '../../state';
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { PageLayout } from '../Layout/PageLayout';
import { FormDialog } from '../Modals/FormDialog';
import { SponsorMsg } from '../SponsorMsg';
@@ -18,6 +20,7 @@ export interface ISnippetsProps {}
export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.PropsWithChildren<ISnippetsProps>) => {
const settings = useRecoilValue(SettingsSelector);
const viewData = useRecoilValue(ViewDataSelector);
const mode = useRecoilValue(ModeAtom);
const [ snippetTitle, setSnippetTitle ] = useState<string>('');
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
const [ snippetBody, setSnippetBody ] = useState<string>('');
@@ -60,74 +63,78 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
return (
<PageLayout
header={(
<div
className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
aria-label="Pagination"
>
<div className="flex flex-1 justify-end">
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
title={`Create new snippet`}
onClick={() => setShowCreateDialog(true)}>
<PlusSmIcon className={`mr-2 h-6 w-6`} />
<span className={`text-sm`}>Create new snippet</span>
</button>
</div>
</div>
)}>
{
viewData?.data?.filePath && (
<div className={`text-xl text-center mb-6`}>
<p>Select the snippet to add to your content.</p>
</div>
)
}
{
snippetKeys && snippetKeys.length > 0 ? (
<ul role="list" className={`grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}>
{
snippetKeys.map((snippetKey: any, index: number) => (
<Item
key={index}
title={snippetKey}
snippet={snippets[snippetKey]} />
))
}
</ul>
) : (
<div className='w-full h-full flex items-center justify-center'>
<div className='flex flex-col items-center text-gray-500 dark:text-whisper-900'>
<CodeIcon className='w-32 h-32' />
<p className='text-3xl'>No snippets found</p>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.snippets.manage}>
<div
className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
aria-label="snippets header"
>
<div className="flex flex-1 justify-end">
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
title={`Create new snippet`}
onClick={() => setShowCreateDialog(true)}>
<PlusSmIcon className={`mr-2 h-6 w-6`} />
<span className={`text-sm`}>Create new snippet</span>
</button>
</div>
</div>
)
}
</FeatureFlag>
)}>
{
showCreateDialog && (
<FormDialog
title={`Create a snippet`}
description={``}
isSaveDisabled={!snippetTitle || !snippetBody}
trigger={onSnippetAdd}
dismiss={reset}
okBtnText='Save'
cancelBtnText='Cancel'>
<div className="flex flex-col">
{
viewData?.data?.filePath && (
<div className={`text-xl text-center mb-6`}>
<p>Select the snippet to add to your content.</p>
</div>
)
}
<NewForm
title={snippetTitle}
description={snippetDescription}
body={snippetBody}
onTitleUpdate={(value: string) => setSnippetTitle(value)}
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
onBodyUpdate={(value: string) => setSnippetBody(value)} />
</FormDialog>
)
}
{
snippetKeys && snippetKeys.length > 0 ? (
<ul role="list" className={`grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}>
{
snippetKeys.map((snippetKey: any, index: number) => (
<Item
key={index}
title={snippetKey}
snippet={snippets[snippetKey]} />
))
}
</ul>
) : (
<div className='w-full h-full flex items-center justify-center'>
<div className='flex flex-col items-center text-gray-500 dark:text-whisper-900'>
<CodeIcon className='w-32 h-32' />
<p className='text-3xl'>No snippets found</p>
</div>
</div>
)
}
{
showCreateDialog && (
<FormDialog
title={`Create a snippet`}
description={``}
isSaveDisabled={!snippetTitle || !snippetBody}
trigger={onSnippetAdd}
dismiss={reset}
okBtnText='Save'
cancelBtnText='Cancel'>
<NewForm
title={snippetTitle}
description={snippetDescription}
body={snippetBody}
onTitleUpdate={(value: string) => setSnippetTitle(value)}
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
onBodyUpdate={(value: string) => setSnippetBody(value)} />
</FormDialog>
)
}
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
</PageLayout>

View File

@@ -4,7 +4,7 @@ export interface ISpinnerProps {}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
return (
<div className={`fixed top-12 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
</div>
);

View File

@@ -3,42 +3,53 @@ import { useRecoilState } from 'recoil';
import { DashboardCommand } from '../DashboardCommand';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
import { DashboardViewAtom, LoadingAtom, SettingsAtom, ViewDataAtom } from '../state';
import { DashboardViewAtom, LoadingAtom, SettingsAtom, ViewDataAtom, SearchReadyAtom, ModeAtom } from '../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { NavigationType } from '../models';
import { GeneralCommands } from '../../constants';
export default function useMessages() {
const [loading, setLoading] = useRecoilState(LoadingAtom);
const [pages, setPages] = useState<Page[]>([]);
const [settings, setSettings] = useRecoilState(SettingsAtom);
const [viewData, setViewData] = useRecoilState(ViewDataAtom);
const [, setMode] = useRecoilState(ModeAtom);
const [, setView] = useRecoilState(DashboardViewAtom);
const [, setSearchReady] = useRecoilState(SearchReadyAtom);
Messenger.listen((message: MessageEvent<EventData<any>>) => {
switch (message.data.command) {
Messenger.listen((event: MessageEvent<EventData<any>>) => {
const message = event.data;
switch (message.command) {
case DashboardCommand.loading:
setLoading(message.data.data);
setLoading(message.data);
break;
case DashboardCommand.viewData:
setViewData(message.data.data);
if (message.data.data?.type === NavigationType.Media) {
setViewData(message.data);
if (message.data?.type === NavigationType.Media) {
setView(NavigationType.Media);
} else if (message.data.data?.type === NavigationType.Contents) {
} else if (message.data?.type === NavigationType.Contents) {
setView(NavigationType.Contents);
} else if (message.data.data?.type === NavigationType.Data) {
} else if (message.data?.type === NavigationType.Data) {
setView(NavigationType.Data);
} else if (message.data.data?.type === NavigationType.Snippets) {
} else if (message.data?.type === NavigationType.Snippets) {
setView(NavigationType.Snippets);
}
break;
case DashboardCommand.settings:
setSettings(message.data.data);
setSettings(message.data);
break;
case DashboardCommand.pages:
setPages(message.data.data);
setPages(message.data);
setLoading(false);
break;
case DashboardCommand.searchReady:
setSearchReady(true);
break;
case GeneralCommands.setMode:
setMode(message.data);
break;
}
});
@@ -47,6 +58,7 @@ export default function useMessages() {
Messenger.send(DashboardMessage.getViewType);
Messenger.send(DashboardMessage.getTheme);
Messenger.send(DashboardMessage.getData);
Messenger.send(DashboardMessage.getMode);
}, ['']);
return {

View File

@@ -1,26 +1,20 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { SortOption } from '../constants/SortOption';
import { Tab } from '../constants/Tab';
import { Page } from '../models/Page';
import Fuse from 'fuse.js';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabSelector, TagSelector } from '../state';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabInfoAtom, TabSelector, TagSelector } from '../state';
import { SortOrder, SortType } from '../../models';
import { Sorting } from '../../helpers/Sorting';
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [
{ name: 'title', weight: 0.8 },
{ name: 'slug', weight: 0.8 },
{ name: 'description', weight: 0.5 }
],
includeScore: true,
threshold: 0.1
};
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../DashboardMessage';
import { EventData } from '@estruyf/vscode/dist/models';
export default function usePages(pages: Page[]) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
const [ sorting, setSorting ] = useRecoilState(SortingAtom);
const [ tabInfo , setTabInfo ] = useRecoilState(TabInfoAtom);
const settings = useRecoilValue(SettingsSelector);
const tab = useRecoilValue(TabSelector);
const folder = useRecoilValue(FolderSelector);
@@ -28,30 +22,24 @@ export default function usePages(pages: Page[]) {
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
useEffect(() => {
const processPages = useCallback((searchedPages: Page[]) => {
const draftField = settings?.draftField;
let usedSorting = sorting;
if (!usedSorting) {
const lastSort = settings?.dashboardState.contents.sorting;
if (lastSort) {
setSorting(lastSort);
return;
}
}
// Check if search needs to be performed
let searchedPages = pages;
if (search) {
const fuse = new Fuse(pages, fuseOptions);
const results = fuse.search(search);
searchedPages = results.map(page => page.item);
}
// Filter the pages
let pagesToShow: Page[] = Object.assign([], searchedPages);
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesToShow.length;
// Filter by draft status
if (draftField && draftField.type === 'choice') {
const draftChoices = settings?.draftField?.choices;
for (const choice of (draftChoices || [])) {
if (choice) {
draftTypes[choice] = pagesToShow.filter(page => page.fmDraft === choice).length;
}
}
if (tab !== Tab.All) {
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
} else {
@@ -59,6 +47,10 @@ export default function usePages(pages: Page[]) {
}
} else {
const draftFieldName = draftField?.name || "draft";
draftTypes[Tab.Draft] = pagesToShow.filter(page => !!page[draftFieldName]).length;
draftTypes[Tab.Published] = pagesToShow.filter(page => !page[draftFieldName]).length;
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
} else if (tab === Tab.Draft) {
@@ -68,6 +60,9 @@ export default function usePages(pages: Page[]) {
}
}
// Set the tab information
setTabInfo(draftTypes);
// Sort the pages
let pagesSorted: Page[] = Object.assign([], pagesToShow);
if (!search) {
@@ -115,7 +110,48 @@ export default function usePages(pages: Page[]) {
}
setPageItems(pagesSorted);
}, [ settings?.draftField, pages, tab, sorting, folder, search, tag, category ]);
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
const searchListener = (message: MessageEvent<EventData<any>>) => {
switch (message.data.command) {
case DashboardMessage.searchPages:
processPages(message.data.data);
break;
}
};
useEffect(() => {
let usedSorting = sorting;
if (!usedSorting) {
const lastSort = settings?.dashboardState.contents.sorting;
if (lastSort) {
setSorting(lastSort);
return;
}
}
// Check if search needs to be performed
let searchedPages = pages;
if (search) {
// const fuse = new Fuse(pages, fuseOptions);
// const results = fuse.search(search);
// searchedPages = results.map(page => page.item);
Messenger.send(DashboardMessage.searchPages, { query: search });
} else {
processPages(searchedPages);
}
}, [ settings?.draftField, pages, sorting, search, tab ]);
useEffect(() => {
Messenger.listen(searchListener);
return () => {
Messenger.unlisten(searchListener);
}
}, []);
return {
pageItems

View File

@@ -34,15 +34,17 @@ export interface Settings {
}
export interface DashboardState {
contents: ViewState;
contents: ContentsViewState;
media: MediaViewState;
}
export interface ViewState {
export interface ContentsViewState {
sorting: SortingOption | null | undefined;
defaultSorting: string | null | undefined;
tags: string | null | undefined;
}
export interface MediaViewState extends ViewState {
export interface MediaViewState extends ContentsViewState {
selectedFolder: string | null | undefined;
mimeTypes: string[] | null | undefined;
}

View File

@@ -0,0 +1,7 @@
import { atom } from 'recoil';
import { Mode } from '../../../models';
export const ModeAtom = atom<Mode | undefined>({
key: 'ModeAtom',
default: undefined
});

View File

@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const SearchReadyAtom = atom<boolean>({
key: 'SearchReadyAtom',
default: false
});

View File

@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const TabInfoAtom = atom<{ [tab: string]: number } | null>({
key: 'TabInfoAtom',
default: {}
});

View File

@@ -6,12 +6,15 @@ export * from './LightboxAtom';
export * from './LoadingAtom';
export * from './MediaFoldersAtom';
export * from './MediaTotalAtom';
export * from './ModeAtom';
export * from './PageAtom';
export * from './SearchAtom';
export * from './SearchReadyAtom';
export * from './SelectedMediaFolderAtom';
export * from './SettingsAtom';
export * from './SortingAtom';
export * from './TabAtom';
export * from './TabInfoAtom';
export * from './TagAtom';
export * from './ViewAtom';
export * from './ViewDataAtom';

View File

@@ -7,6 +7,7 @@ import { TagType } from '../panelWebView/TagType';
import { WebviewHelper } from '@estruyf/vscode';
import { Extension } from '../helpers/Extension';
import { Telemetry } from '../helpers/Telemetry';
import { ModeListener } from '../listeners/general';
export class ExplorerView implements WebviewViewProvider, Disposable {
public static readonly viewType = "frontMatter.explorer";
@@ -80,6 +81,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
ScriptListener.process(msg);
SettingsListener.process(msg);
TaxonomyListener.process(msg);
ModeListener.process(msg);
});
webviewView.onDidChangeVisibility(() => {

View File

@@ -23,6 +23,7 @@ import { PagesListener } from './listeners/dashboard';
import { Backers } from './commands/Backers';
import { DataListener, SettingsListener } from './listeners/panel';
import { NavigationType } from './dashboardWebView/models';
import { ModeSwitch } from './services/ModeSwitch';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
@@ -179,6 +180,7 @@ export async function activate(context: vscode.ExtensionContext) {
SettingsListener.getSettings();
DataListener.getFoldersAndFiles();
MarkdownFoldingProvider.triggerHighlighting();
ModeSwitch.register();
});
// Create the status bar
@@ -220,6 +222,9 @@ export async function activate(context: vscode.ExtensionContext) {
// What you see, is what you get
Wysiwyg.registerCommands(subscriptions);
// Mode switching
ModeSwitch.register();
// Diagnostics
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.diagnostics, Diagnostics.show));

View File

@@ -289,6 +289,8 @@ export class ArticleHelper {
* @returns The new file path
*/
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined {
FrontMatterParser.currentContent = null;
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);

View File

@@ -11,7 +11,6 @@ import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { Telemetry } from './Telemetry';
import { processKnownPlaceholders } from './PlaceholderHelper';
export class ContentType {
/**
@@ -100,7 +99,6 @@ export class ContentType {
* @returns
*/
private static async create(contentType: IContentType, folderPath: string) {
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
return;
@@ -154,8 +152,14 @@ export class ContentType {
if (field.type === "fields") {
data[field.name] = this.processFields(field, titleValue, {});
} else {
data[field.name] = field.default ? processKnownPlaceholders(field.default, titleValue, dateFormat) : "";
data[field.name] = field.default ? ArticleHelper.processCustomPlaceholders(data[field.name], titleValue) : "";
const defaultValue = field.default;
if (typeof defaultValue === "string") {
data[field.name] = processKnownPlaceholders(defaultValue, titleValue, dateFormat);
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
} else {
data[field.name] = typeof defaultValue !== "undefined" ? defaultValue : "";
}
}
}
}

View File

@@ -30,6 +30,9 @@ export class CustomScript {
if (script.bulk) {
// Run script on all files
CustomScript.bulkRun(wsPath, script);
} else if (path) {
// Run script for provided path
CustomScript.singleRun(wsPath, script, path);
} else {
// Run script on current file.
CustomScript.singleRun(wsPath, script);
@@ -38,18 +41,31 @@ export class CustomScript {
}
}
private static async singleRun(wsPath: string, script: ICustomScript): Promise<void> {
const editor = window.activeTextEditor;
if (!editor) return;
private static async singleRun(wsPath: string, script: ICustomScript, path: string | null = null): Promise<void> {
let articlePath: string | null = path;
let article: ParsedFrontMatter | null = null;
const article = ArticleHelper.getFrontMatter(editor);
if (!path) {
const editor = window.activeTextEditor;
if (!editor) return;
if (article) {
const output = await CustomScript.runScript(wsPath, article, editor.document.uri.fsPath, script);
CustomScript.showOutput(output, script);
articlePath = editor.document.uri.fsPath;
article = ArticleHelper.getFrontMatter(editor);
} else {
Notifications.warning(`${script.title}: Current article couldn't be retrieved.`);
article = ArticleHelper.getFrontMatterByPath(path);
}
if (articlePath && article) {
window.withProgress({
location: ProgressLocation.Notification,
title: `Executing: ${script.title}`,
cancellable: false
}, async () => {
const output = await CustomScript.runScript(wsPath, article, articlePath as string, script);
CustomScript.showOutput(output, script);
});
} else {
Notifications.warning(`${script.title}: Article couldn't be retrieved.`);
}
}

View File

@@ -2,7 +2,7 @@ import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Template } from "../commands/Template";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT } from "../constants";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
@@ -45,12 +45,14 @@ export class DashboardSettings {
dashboardState: {
contents: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTING_CONTENT_SORTING_DEFAULT)
defaultSorting: Settings.get<string>(SETTING_CONTENT_SORTING_DEFAULT),
tags: Settings.get<string>(SETTING_DASHBOARD_CONTENT_TAGS),
},
media: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTING_MEDIA_SORTING_DEFAULT),
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace")
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace"),
mimeTypes: Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES)
}
},
dataFiles: await this.getDataFiles(),

View File

@@ -82,6 +82,13 @@ export class Extension {
return this.ctx.extension.packageJSON.name;
}
/**
* Get the displayName of the extension
*/
public get displayName(): string {
return this.ctx.extension.packageJSON.displayName;
}
/**
* Returns the extension's version
*/

View File

@@ -7,8 +7,8 @@ export class Logger {
private static channel: OutputChannel | null = null;
private constructor() {
const title = Extension.getInstance().title;
Logger.channel = window.createOutputChannel(title);
const displayName = Extension.getInstance().displayName;
Logger.channel = window.createOutputChannel(displayName);
}
public static getInstance(): Logger {

View File

@@ -1,7 +1,7 @@
import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER } from "../constants";
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { SortingOption } from "../dashboardWebView/models";
import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models";
import { basename, extname, join, parse, dirname, relative } from "path";
@@ -12,6 +12,7 @@ import { EditorHelper } from "@estruyf/vscode";
import { SortOption } from "../dashboardWebView/constants/SortOption";
import { DataListener, MediaListener } from "../listeners/panel";
import { ArticleHelper } from "./ArticleHelper";
import { lookup } from "mime-types";
export class MediaHelpers {
@@ -109,10 +110,12 @@ export class MediaHelpers {
files = files.map((file) => {
try {
const metadata = MediaLibrary.getInstance().get(file.fsPath);
const mimeType = lookup(file.fsPath);
return {
...file,
dimensions: imageSize(file.fsPath),
dimensions: mimeType && mimeType.startsWith('image/') ? imageSize(file.fsPath) : undefined,
mimeType: lookup(file.fsPath) || '',
...metadata
};
} catch (e) {
@@ -356,9 +359,16 @@ export class MediaHelpers {
* Filter the media files
*/
private static filterMedia(files: Uri[]) {
let mimeTypes = Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES) || ["image/*", "video/*", "audio/*"];
mimeTypes = mimeTypes.map(type => type.toLowerCase());
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext.toLowerCase());
const type = lookup(file.fsPath);
if (type) {
const isValid = this.acceptMimeType(type, mimeTypes);
return isValid;
}
return false;
}).map((file) => ({
filename: basename(file.fsPath),
fsPath: file.fsPath,
@@ -379,4 +389,27 @@ export class MediaHelpers {
return Object.assign([], files);
}
/**
* Validate mimetype of the selected file
* @param crntType
* @param supportedTypes
* @returns
*/
private static acceptMimeType(crntType: string, supportedTypes: string[]) {
if (crntType && supportedTypes) {
const mimeType = crntType.toLowerCase();
const baseMimeType = mimeType.replace(/\/.*$/, '')
return supportedTypes.some(type => {
const validType = type.trim().toLowerCase()
if (validType.endsWith('/*')) {
// This is something like a image/* mime type
return baseMimeType === validType.replace(/\/.*$/, '')
}
return mimeType === validType
})
}
return true
}
}

View File

@@ -1,4 +1,5 @@
import { Uri, workspace, window } from "vscode";
import { Logger } from "./Logger";
import { Notifications } from "./Notifications";
export const openFileInEditor = async (filePath: string) => {
@@ -8,6 +9,7 @@ export const openFileInEditor = async (filePath: string) => {
await window.showTextDocument(doc, 1, false);
} catch (e) {
Notifications.error(`Couldn't open the file.`);
Logger.error(`${filePath}: ${(e as Error).message}`);
}
}
};

View File

@@ -2,7 +2,7 @@ import { Dashboard } from "../../commands/Dashboard";
import { ExtensionState } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Extension } from "../../helpers";
import { Extension, Notifications } from "../../helpers";
import { BaseListener } from "./BaseListener";
@@ -27,6 +27,9 @@ export class DashboardListener extends BaseListener {
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
break;
case DashboardMessage.showWarning:
Notifications.warning(msg.data);
break;
}
}
}

View File

@@ -1,26 +1,63 @@
import { isValidFile } from '../../helpers/isValidFile';
import { existsSync } from "fs";
import { existsSync, unlinkSync } from "fs";
import { basename, dirname, join } from "path";
import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode";
import { Dashboard } from "../../commands/Dashboard";
import { Folders } from "../../commands/Folders";
import { COMMAND_NAME, DefaultFields, SETTING_CONTENT_STATIC_FOLDER, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants";
import { COMMAND_NAME, DefaultFields, ExtensionState, SETTING_CONTENT_STATIC_FOLDER, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_FIELD_GROUPS } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Page } from "../../dashboardWebView/models";
import { ArticleHelper, Logger, Settings } from "../../helpers";
import { ArticleHelper, Extension, Logger, Settings } from "../../helpers";
import { ContentType } from "../../helpers/ContentType";
import { DateHelper } from "../../helpers/DateHelper";
import { Notifications } from "../../helpers/Notifications";
import { BaseListener } from "./BaseListener";
import { Field, FieldType } from '../../models';
import { Field, FieldGroup, FieldType } from '../../models';
import { DataListener } from '../panel';
import Fuse from 'fuse.js';
export class PagesListener extends BaseListener {
private static watchers: { [path: string]: FileSystemWatcher } = {};
private static lastPages: Page[] = [];
/**
* Process the messages for the dashboard views
* @param msg
*/
public static async process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.getData:
this.getPagesData();
break;
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.createByContentType:
await commands.executeCommand(COMMAND_NAME.createByContentType);
break;
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
case DashboardMessage.refreshPages:
this.getPagesData(true);
break;
case DashboardMessage.searchPages:
this.searchPages(msg.data);
break;
case DashboardMessage.deleteFile:
this.deletePage(msg.data);
break;
}
}
/**
* Saved file watcher
* @returns
*/
public static saveFileWatcher() {
return workspace.onDidSaveTextDocument((doc: TextDocument) => {
if (ArticleHelper.isSupportedFile(doc)) {
@@ -63,33 +100,32 @@ export class PagesListener extends BaseListener {
}
/**
* Process the messages for the dashboard views
* @param msg
* Delete a page
* @param path
*/
public static async process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.getData:
this.getPagesData();
break;
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.createByContentType:
await commands.executeCommand(COMMAND_NAME.createByContentType);
break;
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
case DashboardMessage.refreshPages:
this.getPagesData();
break;
private static async deletePage(path: string) {
if (!path) {
return;
}
Logger.info(`Deleting file: ${path}`)
unlinkSync(path);
this.lastPages = this.lastPages.filter(p => p.fmFilePath !== path);
this.sendPageData(this.lastPages);
const ext = Extension.getInstance();
await ext.setState(ExtensionState.Dashboard.Pages.Cache, this.lastPages, "workspace");
}
/**
* Watcher for processing page updates
* @param file
*/
private static async watcherExec(file: Uri) {
if (Dashboard.isOpen) {
const ext = Extension.getInstance();
Logger.info(`File watcher execution for: ${file.fsPath}`)
const pageIdx = this.lastPages.findIndex(p => p.fmFilePath === file.fsPath);
@@ -99,10 +135,11 @@ export class PagesListener extends BaseListener {
const updatedPage = this.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder);
if (updatedPage) {
this.lastPages[pageIdx] = updatedPage;
this.sendMsg(DashboardCommand.pages, this.lastPages);
this.sendPageData(this.lastPages);
await ext.setState(ExtensionState.Dashboard.Pages.Cache, this.lastPages, "workspace");
}
} else {
this.getPagesData();
this.getPagesData(true);
}
}
}
@@ -110,7 +147,18 @@ export class PagesListener extends BaseListener {
/**
* Retrieve all the markdown pages
*/
private static async getPagesData() {
private static async getPagesData(clear: boolean = false) {
const ext = Extension.getInstance();
// Get data from the cache
if (!clear) {
const cachedPages = await ext.getState<Page[]>(ExtensionState.Dashboard.Pages.Cache, "workspace");
if (cachedPages) {
this.sendPageData(cachedPages);
}
}
// Update the dashboard with the fresh data
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
@@ -135,11 +183,63 @@ export class PagesListener extends BaseListener {
}
this.lastPages = pages;
this.sendMsg(DashboardCommand.pages, pages);
this.sendPageData(pages);
this.sendMsg(DashboardCommand.searchReady, true);
await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace");
await this.createSearchIndex(pages);
}
/**
* Send the page data without the body
*/
private static sendPageData(pages: Page[]) {
// Omit the body content
this.sendMsg(DashboardCommand.pages, pages.map(p => {
const { fmBody, ...rest } = p;
return rest;
}));
}
/**
* Create the search index for the pages
* @param pages
*/
private static async createSearchIndex(pages: Page[]) {
const pagesIndex = Fuse.createIndex([ 'title', 'slug', 'description', 'fmBody' ], pages);
await Extension.getInstance().setState(ExtensionState.Dashboard.Pages.Index, pagesIndex, "workspace");
}
/**
* Search the pages
*/
private static async searchPages(data: { query: string }) {
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [
{ name: 'title', weight: 1 },
{ name: 'fmBody', weight: 1, },
{ name: 'slug', weight: 0.5 },
{ name: 'description', weight: 0.5 },
],
includeScore: true,
ignoreLocation: true,
threshold: 0.1
};
const pagesIndex = await Extension.getInstance().getState<Fuse.FuseIndex<Page>>(ExtensionState.Dashboard.Pages.Index, "workspace");
const fuse = new Fuse(this.lastPages, fuseOptions, pagesIndex);
const results = fuse.search(data.query || "");
const pageResults = results.map(page => page.item);
this.sendMsg(DashboardCommand.searchPages, pageResults);
}
/**
* Get fresh page data
*/
public static refresh() {
this.getPagesData();
this.getPagesData(true);
}
/**
@@ -178,6 +278,7 @@ export class PagesListener extends BaseListener {
fmPreviewImage: "",
fmTags: [],
fmCategories: [],
fmBody: article?.content || "",
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
@@ -222,6 +323,17 @@ export class PagesListener extends BaseListener {
}
crntPageData = crntPageData[previewField];
// Check for preview image in block data
if (crntPageData instanceof Array && crntPageData.length > 0) {
// Get the first field block that contains the next field data
const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]);
if (fieldData) {
crntPageData = fieldData;
} else {
continue;
}
}
}
}
@@ -326,9 +438,52 @@ export class PagesListener extends BaseListener {
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
} else if (field.type === "block") {
const subFields = this.findPreviewInBlockField(field);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
/**
* Look for the preview image in the block field
* @param field
* @param parents
* @returns
*/
private static findPreviewInBlockField(field: Field) {
const groups = field.fieldGroup && Array.isArray(field.fieldGroup) ? field.fieldGroup : [field.fieldGroup];
if (!groups) {
return [];
}
const blocks = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS);
if (!blocks) {
return [];
}
let found = false;
for (const group of groups) {
const block = blocks.find(block => block.id === group);
if (!block) {
continue;
}
let newParents: string[] = [];
if (!found) {
newParents = this.findPreviewField(block?.fields, []);
}
if (newParents.length > 0) {
found = true;
return newParents;
}
}
return [];
}
}

View File

@@ -5,6 +5,7 @@ import { SETTING_CONTENT_SNIPPETS } from "../../constants";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Notifications, Settings } from "../../helpers";
import { BaseListener } from "./BaseListener";
import { SettingsListener } from "./SettingsListener";
export class SnippetListener extends BaseListener {
@@ -47,7 +48,8 @@ export class SnippetListener extends BaseListener {
fields: fields || []
};
Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
SettingsListener.getSettings();
}
private static async updateSnippet(data: any) {
@@ -58,7 +60,8 @@ export class SnippetListener extends BaseListener {
return;
}
Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
SettingsListener.getSettings();
}
private static async insertSnippet(data: any) {

View File

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

View File

@@ -0,0 +1,58 @@
import { ModeSwitch } from './../../services/ModeSwitch';
import { CONTEXT, FEATURE_FLAG, GeneralCommands, SETTING_GLOBAL_MODES } from '../../constants';
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Mode } from '../../models';
import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
import { Settings } from "../../helpers";
import { commands } from 'vscode';
export class ModeListener extends BaseListener {
/**
* Process the messages
* @param msg
*/
public static process(msg: { command: DashboardMessage | CommandToCode, data: any }) {
super.process(msg as any);
switch(msg.command) {
case DashboardMessage.getMode:
case CommandToCode.getMode:
this.getMode();
break;
}
}
public static async getMode() {
const modes = Settings.get<Mode[]>(SETTING_GLOBAL_MODES);
if (!modes || modes.length === 0) {
await this.resetEnablement();
return;
}
const activeMode = ModeSwitch.getMode();
if (activeMode) {
const mode = modes.find(m => m.id === activeMode);
this.sendMsg(GeneralCommands.setMode as any, mode);
// Check the commands that need to be enabled/disabled
const snippetsView = mode?.features.find(f => f === FEATURE_FLAG.dashboard.snippets.view);
const dataView = mode?.features.find(f => f === FEATURE_FLAG.dashboard.data.view);
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, !!snippetsView);
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, !!dataView);
} else {
this.sendMsg(GeneralCommands.setMode as any, undefined);
// Enable dashboards
await this.resetEnablement();
}
}
public static async resetEnablement() {
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, true);
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, true);
}
}

View File

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

View File

@@ -108,9 +108,9 @@ export class DataListener extends BaseListener {
}
}
if (JSON.stringify(DataListener.lastMetadataUpdate) !== JSON.stringify(updatedMetadata)) {
// if (JSON.stringify(DataListener.lastMetadataUpdate) !== JSON.stringify(updatedMetadata)) {
this.sendMsg(Command.metadata, updatedMetadata);
}
// }
DataListener.lastMetadataUpdate = updatedMetadata;
}

View File

@@ -14,6 +14,7 @@ export interface MediaInfo {
dimensions?: ISizeCalculationResult | undefined;
caption?: string | undefined;
alt?: string | undefined;
mimeType?: string | undefined;
mtime?: Date;
ctime?: Date;
size?: number;

6
src/models/Mode.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface Mode {
id: string;
features: string[];
}

View File

@@ -9,6 +9,7 @@ export * from './DataType';
export * from './DraftField';
export * from './Framework';
export * from './MediaPaths';
export * from './Mode';
export * from './PanelSettings';
export * from './Snippets';
export * from './SortOrder';

View File

@@ -32,4 +32,5 @@ export enum CommandToCode {
updateStartCommand = "update-start-command",
getImageUrl = "get-image-url",
updatePlaceholder = "update-placeholder",
getMode = "get-mode",
}

View File

@@ -9,13 +9,15 @@ import { FolderAndFiles } from './components/FolderAndFiles';
import { Metadata } from './components/Metadata';
import { SponsorMsg } from './components/SponsorMsg';
import useMessages from './hooks/useMessages';
import { FeatureFlag } from '../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../constants/Features';
export interface IViewPanelProps {
}
export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React.PropsWithChildren<IViewPanelProps>) => {
const { loading, mediaSelecting, metadata, settings, folderAndFiles, focusElm, unsetFocus } = useMessages();
const { loading, mediaSelecting, metadata, settings, folderAndFiles, focusElm, unsetFocus, mode } = useMessages();
if (mediaSelecting) {
return (
<div className="frontmatter media_selection">
@@ -32,31 +34,52 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React
if (!metadata || Object.keys(metadata || {}).length === 0) {
return (
<BaseView settings={settings} folderAndFiles={folderAndFiles} />
<BaseView mode={mode} settings={settings} folderAndFiles={folderAndFiles} />
);
}
return (
<div className="frontmatter">
<div className={`ext_actions`}>
<GlobalSettings settings={settings} />
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.globalSettings}>
<GlobalSettings settings={settings} />
</FeatureFlag>
{
settings && settings.seo && <SeoStatus seo={settings.seo} data={metadata} />
settings && settings.seo && (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.seo}>
<SeoStatus
seo={settings.seo}
data={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus} />
</FeatureFlag>
)
}
{
settings && metadata && <Actions metadata={metadata} settings={settings} />
settings && metadata && (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.actions}>
<Actions metadata={metadata} settings={settings} />
</FeatureFlag>
)
}
<Metadata
settings={settings}
metadata={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus} />
<FolderAndFiles data={folderAndFiles} />
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.metadata}>
<Metadata
settings={settings}
metadata={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus} />
</FeatureFlag>
<OtherActions settings={settings} isFile={true} />
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.recentlyModified}>
<FolderAndFiles data={folderAndFiles} />
</FeatureFlag>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.otherActions}>
<OtherActions settings={settings} isFile={true} />
</FeatureFlag>
</div>
<SponsorMsg isBacker={settings?.isBacker} />

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { CustomScript, FolderInfo, PanelSettings } from '../../models';
import { CustomScript, FolderInfo, Mode, PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Collapsible } from './Collapsible';
@@ -8,13 +8,16 @@ import { OtherActions } from './OtherActions';
import { FolderAndFiles } from './FolderAndFiles';
import { SponsorMsg } from './SponsorMsg';
import { StartServerButton } from './StartServerButton';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants/Features';
export interface IBaseViewProps {
settings: PanelSettings | undefined;
folderAndFiles: FolderInfo[] | undefined;
mode: Mode | undefined;
}
const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles}: React.PropsWithChildren<IBaseViewProps>) => {
const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles, mode}: React.PropsWithChildren<IBaseViewProps>) => {
const openDashboard = () => {
MessageHelper.sendMessage(CommandToCode.openDashboard);
@@ -41,26 +44,34 @@ const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndF
return (
<div className="frontmatter">
<div className={`ext_actions`}>
<GlobalSettings settings={settings} isBase />
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.globalSettings}>
<GlobalSettings settings={settings} isBase />
</FeatureFlag>
<Collapsible id={`base_actions`} title="Actions">
<div className={`base__actions`}>
<button onClick={openDashboard}>Open dashboard</button>
<StartServerButton settings={settings} />
<button onClick={initProject} disabled={settings?.isInitialized}>Initialize project</button>
<button onClick={createContent} disabled={!settings?.isInitialized}>Create new content</button>
<button onClick={openPreview} disabled={!settings?.preview?.host}>Open site preview</button>
{
customActions.map((script) => (
<button key={script.title} onClick={() => runBulkScript(script)}>{ script.title }</button>
))
}
</div>
</Collapsible>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.actions}>
<Collapsible id={`base_actions`} title="Actions">
<div className={`base__actions`}>
<button onClick={openDashboard}>Open dashboard</button>
<StartServerButton settings={settings} />
<button onClick={initProject} disabled={settings?.isInitialized}>Initialize project</button>
<button onClick={createContent} disabled={!settings?.isInitialized}>Create new content</button>
<button onClick={openPreview} disabled={!settings?.preview?.host}>Open site preview</button>
{
customActions.map((script) => (
<button key={script.title} onClick={() => runBulkScript(script)}>{ script.title }</button>
))
}
</div>
</Collapsible>
</FeatureFlag>
<FolderAndFiles data={folderAndFiles} isBase />
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.recentlyModified}>
<FolderAndFiles data={folderAndFiles} isBase />
</FeatureFlag>
<OtherActions settings={settings} isFile={false} isBase />
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.otherActions}>
<OtherActions settings={settings} isFile={false} isBase />
</FeatureFlag>
</div>
<SponsorMsg isBacker={settings?.isBacker} />

View File

@@ -1,5 +1,6 @@
import * as React from 'react';
import { RocketIcon } from '../Icons/RocketIcon';
import { ToggleIcon } from '../Icons/ToggleIcon';
import { VsLabel } from '../VscodeComponents';
export interface IToggleProps {
@@ -26,7 +27,7 @@ export const Toggle: React.FunctionComponent<IToggleProps> = ({label, checked, o
<div className={`metadata_field`}>
<VsLabel>
<div className={`metadata_field__label`}>
<RocketIcon /> <span style={{ lineHeight: "16px"}}>{label}</span>
<ToggleIcon /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>

View File

@@ -6,6 +6,45 @@ export interface IFrontMatterIconProps {
export const FrontMatterIcon: React.FunctionComponent<IFrontMatterIconProps> = ({className}: React.PropsWithChildren<IFrontMatterIconProps>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" className={className || ""} viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="currentcolor"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="currentcolor"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="currentcolor"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="currentcolor"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="currentcolor"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="currentcolor" strokeMiterlimit="10" strokeWidth="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="currentcolor"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="currentcolor"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="currentcolor"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="currentcolor"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="currentcolor"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="currentcolor"/></svg>
<svg className={className || ""} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1201 1201" enableBackground="new 0 0 1201 1201">
<rect x="1" y="1" display="none" fill="none" stroke="#01AEB7" strokeWidth="50" strokeMiterlimit="10" width="1200" height="1200"/>
<g enableBackground="new ">
<path fill="currentcolor" d="M173.4,576.8H96.9V214.5h135.2v85.2h-58.7v52.8H228v81.9h-54.7V576.8z"/>
<path fill="currentcolor" d="M463.3,576.8h-82.7l-37.8-129.1c-0.4-2.2-1-5.1-1.9-8.7c-0.9-3.6-2-7.9-3.4-12.9l0.6,23.9v126.7h-76.1
V214.5h78.3c34.5,0,60.3,8.7,77.4,26.1c21.8,22.2,32.7,53.8,32.7,94.8c0,43.8-14.6,74.1-43.9,90.8L463.3,576.8z M338.3,382.3h5.1
c8.8,0,16.1-4.3,22-12.9c5.9-8.6,8.8-19.6,8.8-32.9c0-27.1-11.1-40.6-33.2-40.6h-2.8V382.3z"/>
<path fill="currentcolor" d="M693.2,396.8c0,55.2-9.8,100.1-29.5,134.7c-19.6,34.4-45.1,51.6-76.5,51.6c-27.9,0-51.8-13.2-71.7-39.7
c-25.1-33.5-37.6-83.7-37.6-150.7c0-64.1,13.1-113.2,39.3-147.1c19.4-25.2,42.5-37.8,69.2-37.8c32.7,0,58.6,17.1,78,51.2
C683.5,293.2,693.2,339.1,693.2,396.8z M613.7,397.5c0-65.6-9.1-98.3-27.2-98.3c-9.1,0-16.3,8.6-21.7,25.8
c-5.3,16-7.9,39.4-7.9,70.4c0,30.5,2.5,54.2,7.4,71c5,16.8,11.8,25.2,20.6,25.2c9.1,0,16-8.3,20.9-24.9
C611.1,450.2,613.7,427.1,613.7,397.5z"/>
<path fill="currentcolor" d="M725.8,576.8V214.5h73l38.3,127.2c2.1,7.5,4.4,16.1,7.1,25.8c2.6,9.7,5.5,20.9,8.7,33.6l7.9,31.9
c-2.8-31.4-5-58.4-6.4-80.7c-1.5-22.4-2.2-41.1-2.2-56.3v-81.4h73v362.4h-73l-38.5-133.3c-4-14.2-7.7-27.6-10.9-40
c-3.2-12.4-6.1-24.3-8.5-35.6c1.6,25.5,2.8,47.7,3.5,66.7c0.7,18.9,1.1,35.5,1.1,49.8v92.5H725.8z"/>
<path fill="currentcolor" d="M1063.8,576.8h-76.5V301.3h-42.2v-86.8h162.6v86.8h-43.9V576.8z"/>
</g>
<g enableBackground="new ">
<path fill="currentcolor" d="M123,657.8h35.8l27.4,133.7c3.4,16.7,6.4,33.4,9.2,50.2c2.7,16.8,5.3,34.4,7.5,52.7
c0.3-2.4,0.5-4.3,0.6-5.6c0.1-1.3,0.3-2.3,0.4-2.9l5.8-37.6l5.2-35.7l4.6-23.6L244,657.8h36.2l28.6,327.7h-40l-7-111.2
c-0.3-5.7-0.5-10.5-0.7-14.6c-0.2-4.1-0.3-7.5-0.3-10.2l-1.8-43.9l-1-40.3c0-0.3,0-0.9-0.1-1.8c-0.1-0.9-0.2-2.2-0.3-3.7l-1,6.6
c-1.1,7.8-2,14.6-2.9,20.4c-0.9,5.8-1.6,10.7-2.3,14.6l-5.2,29.9l-2,11.2l-26.2,143.1h-28.2L165.1,862c-3.4-16.3-6.3-32.8-9-49.6
c-2.6-16.8-5-34.3-7.1-52.7l-12.1,225.8H97.5L123,657.8z"/>
<path fill="currentcolor" d="M395.5,657.8h32.6l66.6,327.7h-41.5l-11.7-63.9H380l-11.9,63.9h-40.2L395.5,657.8z M435.5,887.5l-9.3-52.2
c-6.2-35.5-11.2-73.4-15.1-113.8c-1.9,19.7-4.3,39.6-7.2,59.8c-3,20.2-6.4,41.6-10.5,64.2l-7.6,42H435.5z"/>
<path fill="currentcolor" d="M495.7,657.8h136.2v38.6H585v289.1H544V696.4h-48.3V657.8z"/>
<path fill="currentcolor" d="M638.1,657.8h136.2v38.6h-46.9v289.1h-41.1V696.4h-48.3V657.8z"/>
<path fill="currentcolor" d="M805.4,657.8h111.3v37.4h-69.4V799h61.6v37.4h-61.6v111.9h69.4v37.4H805.4V657.8z"/>
<path fill="currentcolor" d="M962.9,657.8h55.1c22.8,0,39.8,5.7,50.9,17.2c14.1,14.9,21.1,37.6,21.1,68.1c0,23.5-3.7,42.3-11.2,56.6
c-7.4,14.2-18.1,23-31.9,26.4l57.2,159.4h-42.5l-57-160.5v160.5h-41.9V657.8z M1004.8,803c16.2,0,27.7-4,34.4-11.9
c6.7-7.9,10.1-21.3,10.1-40.1c0-10.2-0.7-18.9-2.1-26.1c-1.4-7.2-3.6-13.1-6.6-17.6c-3-4.5-6.9-7.9-11.7-10
c-4.8-2.1-10.4-3.2-17-3.2h-7V803z"/>
</g>
<rect x="97" y="93.9" fill="currentcolor" width="169.7" height="43"/>
<rect x="351.9" y="93.9" fill="currentcolor" width="169.7" height="43"/>
<rect x="609.8" y="93.9" fill="currentcolor" width="169.7" height="43"/>
<rect x="97" y="1060.7" fill="currentcolor" width="169.7" height="43"/>
<rect x="351.9" y="1060.7" fill="currentcolor" width="169.7" height="43"/>
<rect x="609.8" y="1060.7" fill="currentcolor" width="169.7" height="43"/>
</svg>
);
};

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
export interface IToggleIconProps {}
export const ToggleIcon: React.FunctionComponent<IToggleIconProps> = (props: React.PropsWithChildren<IToggleIconProps>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentcolor" fill="none" strokeLinecap="round" strokeLinejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="16" cy="12" r="2" />
<rect x="2" y="6" width="20" height="12" rx="6" />
</svg>
);
};

View File

@@ -80,24 +80,9 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
return (
<Collapsible id={`tags`} title="Metadata" className={`inherit z-20`}>
{
renderFields(contentType?.fields || [], metadata)
}
{
<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>
);
};

View File

@@ -15,6 +15,7 @@ export interface ISeoKeywordsProps {
}
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
const [ isReady, setIsReady ] = React.useState(false);
const validateKeywords = () => {
if (!keywords) {
@@ -32,7 +33,15 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...d
return [];
}
if (!keywords || keywords.length === 0) {
// Workaround for lit components not updating render
React.useEffect(() => {
setIsReady(false);
setTimeout(() => {
setIsReady(true);
}, 0);
}, [keywords]);
if (!isReady || !keywords || keywords.length === 0) {
return null;
}
@@ -49,7 +58,7 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...d
{
validateKeywords().map((keyword, index) => {
return (
<ErrorBoundary fallback={<div />}>
<ErrorBoundary key={keyword} fallback={<div />}>
<SeoKeywordInfo key={index} keyword={keyword} {...data} />
</ErrorBoundary>
);

View File

@@ -1,18 +1,23 @@
import * as React from 'react';
import { SEO } from '../../models/PanelSettings';
import { TagType } from '../TagType';
import { ArticleDetails } from './ArticleDetails';
import { Collapsible } from './Collapsible';
import FieldBoundary from './ErrorBoundary/FieldBoundary';
import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
import { SeoFieldInfo } from './SeoFieldInfo';
import { SeoKeywords } from './SeoKeywords';
import { TagPicker } from './TagPicker';
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
export interface ISeoStatusProps {
seo: SEO;
data: any;
focusElm: TagType | null;
unsetFocus: () => void;
}
const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
const { data, seo } = props;
const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({ data, seo, focusElm, unsetFocus }: React.PropsWithChildren<ISeoStatusProps>) => {
const { title, slug } = data;
const [ isOpen, setIsOpen ] = React.useState(true);
const tableRef = React.useRef<HTMLElement>();
@@ -93,6 +98,18 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsW
headings={data?.articleDetails?.headingsText}
wordCount={data?.articleDetails?.wordCount}
content={data?.articleDetails?.content} />
<FieldBoundary fieldName={`Keywords`}>
<TagPicker
type={TagType.keywords}
icon={<SymbolKeywordIcon />}
crntSelected={data.keywords as string[] || []}
options={[]}
freeform={true}
focussed={focusElm === TagType.keywords}
unsetFocus={unsetFocus}
disableConfigurable />
</FieldBoundary>
<ArticleDetails details={data.articleDetails} />
</div>

View File

@@ -149,6 +149,43 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
return !selected.includes(option) && option.toLowerCase().includes((inputValue || "").toLowerCase());
};
/**
* Add the new item to the data
* @param e
*/
const onEnterData = useCallback((e: React.KeyboardEvent<HTMLInputElement>, closeMenu: Function, highlightedIndex: any) => {
if (e.key === "Enter" && e.type === "keydown" && (highlightedIndex === null || highlightedIndex === undefined)) {
const value = inputRef.current?.value.trim();
if (!value) {
return;
}
// Split the value by comma
const newValues: string[] = [];
const values = value.split(",");
for (let crntValue of values) {
crntValue = crntValue.trim();
if (crntValue) {
const item = options.find(o => o?.toLowerCase() === crntValue?.toLowerCase());
if (item) {
newValues.push(item);
} else if (freeform) {
newValues.push(crntValue);
}
}
}
const uniqValues = Array.from(new Set([...selected, ...newValues]));
setSelected(uniqValues);
sendUpdate(uniqValues);
setInputValue("");
closeMenu();
}
}, [options, inputRef, selected, freeform]);
/**
* Check if the input is disabled
*/
const checkIsDisabled = useCallback(() => {
if (!limit) {
return false;
@@ -191,7 +228,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
inputValue={inputValue}
onInputValueChange={(value) => setInputValue(value)}>
{
({ getInputProps, getItemProps, getMenuProps, isOpen, inputValue, getRootProps, openMenu, closeMenu, clearSelection }) => (
({ getInputProps, getItemProps, getMenuProps, isOpen, inputValue, getRootProps, openMenu, closeMenu, clearSelection, highlightedIndex }) => (
<>
<div {...getRootProps(undefined, {suppressRefError: true})} className={`article__tags__input ${freeform ? 'freeform' : ''}`}>
<input {
@@ -199,6 +236,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
ref: inputRef,
onFocus: openMenu as any,
onClick: openMenu as any,
onKeyDown: (e) => onEnterData(e, closeMenu, highlightedIndex),
onBlur: () => {
closeMenu();
unsetFocus();

View File

@@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import { GeneralCommands } from '../../constants';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Mode } from '../../models/Mode';
import { DashboardData } from '../../models/DashboardData';
import { FolderInfo, PanelSettings } from '../../models/PanelSettings';
import { Command } from '../Command';
@@ -15,6 +17,7 @@ export default function useMessages() {
const [focusElm, setFocus] = useState<TagType | null>(null);
const [folderAndFiles, setFolderAndFiles] = useState<FolderInfo[] | undefined>(undefined);
const [mediaSelecting, setMediaSelecting] = useState<DashboardData | undefined>(undefined);
const [mode, setMode] = useState<Mode | undefined>(undefined);
window.addEventListener('message', event => {
const message = event.data;
@@ -43,9 +46,20 @@ export default function useMessages() {
case Command.mediaSelectionData:
setMediaSelecting(message.data);
break;
case GeneralCommands.setMode:
setMode(message.data);
break;
}
});
useEffect(() => {
if (loading) {
window.setTimeout(() => {
setLoading(false);
}, 5000);
}
}, [loading])
useEffect(() => {
setLoading(true);
@@ -55,6 +69,7 @@ export default function useMessages() {
}, 5000);
vscode.postMessage({ command: CommandToCode.getData });
vscode.postMessage({ command: CommandToCode.getMode });
}, []);
return {
@@ -64,6 +79,7 @@ export default function useMessages() {
focusElm,
loading,
mediaSelecting,
mode,
unsetFocus: () => { setFocus(null) }
};
}

View File

@@ -0,0 +1,78 @@
import { ModeListener } from './../listeners/general/ModeListener';
import { SETTING_GLOBAL_ACTIVE_MODE, SETTING_GLOBAL_MODES } from './../constants/settings';
import { commands, StatusBarAlignment, StatusBarItem, ThemeColor, window } from "vscode";
import { Settings } from "../helpers/SettingsHelper";
import { COMMAND_NAME } from '../constants';
import { Mode } from '../models';
export class ModeSwitch {
private static isInit: boolean = false;
private static statusBarElm: StatusBarItem;
private static currentMode: string;
public static register() {
if (!ModeSwitch.statusBarElm) {
ModeSwitch.statusBarElm = window.createStatusBarItem(StatusBarAlignment.Left, 100);
ModeSwitch.statusBarElm.tooltip = "Switch between the different modes of Front Matter";
ModeSwitch.statusBarElm.command = COMMAND_NAME.modeSwitch;
}
if (!ModeSwitch.isInit) {
commands.registerCommand(COMMAND_NAME.modeSwitch, ModeSwitch.switchMode);
const mode = Settings.get<string | null>(SETTING_GLOBAL_ACTIVE_MODE);
if (mode) {
ModeSwitch.currentMode = mode;
} else {
ModeSwitch.currentMode = "";
}
ModeSwitch.isInit = true;
}
ModeListener.getMode();
const modes = Settings.get<string | null>(SETTING_GLOBAL_MODES);
if (!modes || modes.length === 0) {
ModeSwitch.statusBarElm.hide();
return;
}
ModeSwitch.setText();
}
public static getMode(): string {
return ModeSwitch.currentMode;
}
private static async switchMode() {
const modes = Settings.get<Mode[]>(SETTING_GLOBAL_MODES);
if (!modes || modes.length === 0) {
return;
}
const modePicks = [
"Default",
...modes.map(m => m.id)
]
const mode = await window.showQuickPick(modePicks, {
placeHolder: `Select the mode you want to use`,
ignoreFocusOut: true,
title: `Front Matter: Mode selection`
});
if (mode) {
ModeSwitch.currentMode = mode === "Default" ? "" : mode;
ModeSwitch.setText();
ModeListener.getMode();
}
}
private static setText() {
ModeSwitch.statusBarElm.text = `$(preview) Mode: ${ModeSwitch.currentMode ? ModeSwitch.currentMode : "Default"}`;
ModeSwitch.statusBarElm.show();
}
}

View File

@@ -112,6 +112,7 @@ module.exports = {
extend: {},
},
plugins: [
require("@tailwindcss/forms")
require("@tailwindcss/forms"),
require("tailwindcss-nested-groups"),
],
}