Compare commits

..

77 Commits

Author SHA1 Message Date
Elio Struyf
7a2a0934c2 Merge pull request #150 from estruyf/dev
Merge for 5.1.1
2021-10-14 16:24:29 +02:00
Elio Struyf
d3eb7b223c 5.1.1 2021-10-14 16:23:51 +02:00
Elio Struyf
f74eec954f #149 - Fix keywords 2021-10-14 16:23:28 +02:00
Elio Struyf
864c4e7aa6 Merge pull request #148 from estruyf/dev
Merge for v5.1.0
2021-10-13 11:34:29 +02:00
Elio Struyf
5667906caf Update changelog for release 2021-10-13 10:46:06 +02:00
Elio Struyf
2fe7c524e7 #147 - Error boundary added for metadata fields 2021-10-13 09:05:32 +02:00
Elio Struyf
5cc83526ad Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-10-13 08:27:48 +02:00
Elio Struyf
76b5e99a08 File check 2021-10-13 08:27:18 +02:00
Elio Struyf
7d5505d421 Updated changelog 2021-10-12 20:34:15 +02:00
Elio Struyf
d97a11f8b5 #146 - Date parsing logic added with fallbacks 2021-10-12 20:34:06 +02:00
Elio Struyf
0590cec684 Add version and environment details 2021-10-12 19:49:34 +02:00
Elio Struyf
b4cdc4feb9 Version fixes 2021-10-12 15:52:54 +02:00
Elio Struyf
986fd95524 #145 - Moved folder registration settings 2021-10-12 15:22:43 +02:00
Elio Struyf
f51fec5fb9 Fix issue in sorting of taxonomy picker 2021-10-12 10:50:42 +02:00
Elio Struyf
8198ce2af3 Added VSCode webcomponents 2021-10-12 10:23:55 +02:00
Elio Struyf
defffc4c8e Remove unused settings 2021-10-12 09:12:18 +02:00
Elio Struyf
8f47cbfb0b #141 - Merge content creation logic to ArticleHelper 2021-10-12 09:11:47 +02:00
Elio Struyf
0be91c17d0 #141 - Added support for page bundles with templates 2021-10-11 21:40:05 +02:00
Elio Struyf
fae7ab8417 Date to string fix 2021-10-11 17:49:45 +02:00
Elio Struyf
df239f2cc0 #144 - Pass a default value when null 2021-10-11 11:10:47 +02:00
Elio Struyf
70099dc97f #144 - Fix date value issue 2021-10-11 11:08:45 +02:00
Elio Struyf
c11be0e3ec #143 - Fix for recent files unique values 2021-10-11 10:55:58 +02:00
Elio Struyf
f8b7870180 Updated changlog 2021-10-11 10:50:28 +02:00
Elio Struyf
c58a5c62d9 #142 - Fix for unknown tags 2021-10-11 10:49:46 +02:00
Elio Struyf
ce92444bf2 Updated dashboard icon 2021-10-11 10:39:50 +02:00
Elio Struyf
b2709ebffd #141 - Support opening of the page bundle folder 2021-10-11 10:21:40 +02:00
Elio Struyf
2b20cf9d24 Updated changelog 2021-10-11 09:21:56 +02:00
Elio Struyf
f4a499ad0f 5.1.0 2021-10-11 09:20:48 +02:00
Elio Struyf
4494b158c0 #141 - Page bundle functionality 2021-10-11 09:20:42 +02:00
Elio Struyf
3416e55264 Fix rending more hooks 2021-10-08 15:30:56 +02:00
Elio Struyf
9b53e31cd5 Merge pull request #138 from estruyf/dev
Merge for v5.0.0 release
2021-10-07 14:11:47 +02:00
Elio Struyf
f49b93b042 Added release date 2021-10-07 14:06:01 +02:00
Elio Struyf
a005930c14 Added small icon 2021-10-07 14:05:12 +02:00
Elio Struyf
9dea1ee6ed Update logo 2021-10-07 08:46:05 +02:00
Elio Struyf
1ea0999d17 #137 - Ask to move the templates foler into frontmatter 2021-10-06 21:03:17 +02:00
Elio Struyf
2e6a466ba5 New sponsor added 2021-10-06 13:51:33 +02:00
Elio Struyf
fbcd430dc6 Style fix for preview images 2021-10-06 11:42:57 +02:00
Elio Struyf
2bd910db47 Update changelog 2021-10-06 11:35:04 +02:00
Elio Struyf
8158c9a483 #135 - Hidden property added for fields 2021-10-06 11:34:34 +02:00
Elio Struyf
4622fbe757 Version check when migration is running 2021-10-06 09:00:08 +02:00
Elio
97a635c2de Making sure paths are parsed for Windows 2021-10-05 20:20:31 +02:00
Elio
e8c67c75fd Fix file path issues for windows + open file with parsing error 2021-10-05 19:58:25 +02:00
Elio Struyf
6151ecb4c1 #132 - Open only an existing folder 2021-10-05 13:45:42 +02:00
Elio Struyf
1d8c192c07 #134 - Promote settings question 2021-10-05 13:26:11 +02:00
Elio Struyf
014911b7a9 #133 - Fix for overriding default content type settings 2021-10-05 13:23:58 +02:00
Elio Struyf
7b2d7b8aa5 #132 - Drag and drop fix 2021-10-05 09:12:03 +02:00
Elio Struyf
caceed2d4c Updated changelog 2021-10-05 08:46:54 +02:00
Elio Struyf
476ec6c2fd #132 - Persist the last opened folder location 2021-10-05 08:44:30 +02:00
Elio Struyf
5374edfa01 5.0.0 2021-10-05 08:11:35 +02:00
Elio Struyf
6a0cac9dfb Updated version 2021-10-05 08:11:32 +02:00
Elio Struyf
b525a6a211 #131 #132 - Changes to media dashboard 2021-10-04 20:57:54 +02:00
Elio Struyf
c295761560 #128 - support multi image selection with isPreviewImage property 2021-10-04 11:46:07 +02:00
Elio Struyf
6154164b4b #128 - 🚀 multi image selection support 2021-10-04 11:32:52 +02:00
Elio Struyf
05ce2d3537 #126 - Create new content from conent type 2021-10-03 12:55:58 +02:00
Elio Struyf
544f24bcba #127 - Dashboard action 2021-10-02 17:36:14 +02:00
Elio Struyf
1c354ed976 Link positioning issue 2021-10-01 17:04:29 +02:00
Elio Struyf
9b9bf1bfbe Include error tracing 2021-10-01 15:28:00 +02:00
Elio Struyf
29d5f02d10 #124 - Update preview image 2021-10-01 14:50:57 +02:00
Elio Struyf
9bc2fbc141 Remove unused references 2021-10-01 13:15:10 +02:00
Elio Struyf
cac009b773 #110 - Support for workspaces with multiple folders 2021-10-01 10:18:48 +02:00
Elio Struyf
bf1639cac7 Update changelog 2021-09-30 16:27:17 +02:00
Elio Struyf
5f28e145c4 #124 - Add support to specify preview image 2021-09-30 16:26:21 +02:00
Elio Struyf
8199ab964e Placeholder styling 2021-09-30 15:52:54 +02:00
Elio Struyf
14c050e34b #122 - Check if file already exists 2021-09-30 15:52:41 +02:00
Elio Struyf
b96411d1f9 #122 - Update media filename 2021-09-30 12:26:58 +02:00
Elio Struyf
87c469a6c7 Remove log 2021-09-30 10:12:00 +02:00
Elio Struyf
c29aef03f0 Changed img description to caption 2021-09-30 10:05:06 +02:00
Elio Struyf
9eaf94de7a #119 #121 - Choice field enhancements 2021-09-30 09:34:00 +02:00
Elio Struyf
f5e7526fae #120 - Fix for choice and number field 2021-09-30 08:41:35 +02:00
Elio Struyf
579e4925c8 Updated readme 2021-09-29 09:52:33 +02:00
Elio Struyf
4a4c558d9d Fix field type 2021-09-28 15:57:37 +02:00
Elio Struyf
9bb34850e2 Updated changelog 2021-09-28 09:01:44 +02:00
Elio Struyf
c6e37532bc #113 - Move the database init code 2021-09-27 18:23:36 +02:00
Elio Struyf
221f962beb updated changelog 2021-09-27 16:25:38 +02:00
Elio Struyf
17566279e2 Updated dependencies 2021-09-27 16:23:35 +02:00
Elio Struyf
4e7488414d #113 - Implemented local database for media 2021-09-27 16:22:23 +02:00
Elio Struyf
c206817431 #117 - Added single line support 2021-09-27 09:23:09 +02:00
103 changed files with 2798 additions and 692 deletions

View File

@@ -0,0 +1,7 @@
---
name: Feedback
about: Tell more on what you think
title: 'Feedback: '
labels: ''
assignees: ''
---

View File

@@ -1,5 +1,53 @@
# Change Log
## [5.1.1] - 2021-10-14
- [#149](https://github.com/estruyf/vscode-front-matter/issues/149): Fix panel rendering when incorrect type for keywords is provided
## [5.1.0] - 2021-10-13
### 🎨 Enhancements
- [#141](https://github.com/estruyf/vscode-front-matter/issues/141): Allow content creation for page bundles or single files
- [#145](https://github.com/estruyf/vscode-front-matter/issues/145): Moved folder registration settings to `frontmatter.json` file
- [#147](https://github.com/estruyf/vscode-front-matter/issues/147): Error boundary added for metadata fields
### 🐞 Fixes
- Rendered more hooks than during the previous render in `FileList`
- [#142](https://github.com/estruyf/vscode-front-matter/issues/142): Fix for unknown tags where it throws an error
- [#143](https://github.com/estruyf/vscode-front-matter/issues/143): Fix for duplicate values in the file list
- [#144](https://github.com/estruyf/vscode-front-matter/issues/144): Fix for `toISOString` does not exist on object
- [#146](https://github.com/estruyf/vscode-front-matter/issues/146): Date parsing logic added with fallbacks
## [5.0.0] - 2021-10-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.0.0)
### ✨ New features
- [#113](https://github.com/estruyf/vscode-front-matter/issues/113): Integrating a local DB for media metadata (caption, alt)
- [#132](https://github.com/estruyf/vscode-front-matter/issues/132): Major changes to the media dashboard which allows you to navigate through all folders
### 🎨 Enhancements
- [#110](https://github.com/estruyf/vscode-front-matter/issues/110): Add support for workspaces with multiple folders
- [#117](https://github.com/estruyf/vscode-front-matter/issues/117): Allow to specify a singleline of text in the metadata fields
- [#119](https://github.com/estruyf/vscode-front-matter/issues/119): Multi-select support for choice fields
- [#121](https://github.com/estruyf/vscode-front-matter/issues/121): Choice fields support ID/title objects as well as a regular string
- [#122](https://github.com/estruyf/vscode-front-matter/issues/122): Update the filenames of your media
- [#124](https://github.com/estruyf/vscode-front-matter/issues/124): Add new `isPreviewImage` property to the content type field to specify custom preview images
- [#126](https://github.com/estruyf/vscode-front-matter/issues/126): Create new content from the available content types
- [#127](https://github.com/estruyf/vscode-front-matter/issues/127): Title bar action added to open the dashboard
- [#128](https://github.com/estruyf/vscode-front-matter/issues/128): Support for multi-select on image fields added
- [#131](https://github.com/estruyf/vscode-front-matter/issues/131): Folder creation support added on media dashboard
- [#134](https://github.com/estruyf/vscode-front-matter/issues/134): On startup, the extension checks if local settings can be promoted
- [#135](https://github.com/estruyf/vscode-front-matter/issues/135): `Hidden` property added for field configuration
- [#137](https://github.com/estruyf/vscode-front-matter/issues/137): Ask to move the `.templates` folder to the new `.frontmatter` folder
### 🐞 Fixes
- [#120](https://github.com/estruyf/vscode-front-matter/issues/120): Choice and number field not updating when set manually in front matter of the file
- [#133](https://github.com/estruyf/vscode-front-matter/issues/133): Fix for overriding default content type settings
## [4.0.1] - 2021-09-24
- [#114](https://github.com/estruyf/vscode-front-matter/issues/114): Fix for categories/tags provided as string in YAML

View File

@@ -96,18 +96,28 @@ If you have the courage to test out the beta features, we made available a beta
## 👉 Contributors 🤘
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
</a>
<p align="center">
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
</a>
</p>
## 🖤 Sponsors 👇 🤘
<p align="center">
<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/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>
</p>
<br />
<br />
## 🖤 Sponsors
<p align="center">
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
<img src="assets/sponsors/powered-by-vercel.png" />
<img src="assets/sponsors/powered-by-vercel.png" />
</a>
</p>

View File

@@ -94,18 +94,28 @@ If you have the courage to test out the beta features, we made available a beta
## 👉 Contributors 🤘
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
</a>
<p align="center">
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
</a>
</p>
## 🖤 Sponsors 👇 🤘
<p align="center">
<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/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>
</p>
<br />
<br />
## 🖤 Sponsors
<p align="center">
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
<img src="assets/sponsors/powered-by-vercel.png" />
<img src="assets/sponsors/powered-by-vercel.png" />
</a>
</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,40 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
<style type="text/css">
.st0{enable-background:new ;}
.st1{fill:#FFFFFF;}
.st2{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
.st3{fill:none;}
</style>
<g class="st0">
<path class="st1" d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z"/>
<path class="st1" d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8v0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
c0-0.6-0.3-1-0.8-1l0,0V6.8z"/>
<path class="st1" d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2s-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
s0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
c-0.2,0-0.4,0.2-0.5,0.6s-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6s0.4-0.2,0.5-0.6
C14.5,8.4,14.6,7.9,14.6,7.2z"/>
<path class="st1" d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9
s-0.1-1-0.1-1.3V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9S19,6.8,18.9,6.6c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2
h-1.8V11.4z"/>
<path class="st1" d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z"/>
</g>
<rect class="st2" width="28" height="28"/>
<g class="st0">
<path class="st1" d="M2.9,17h0.9l0.6,3c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1v-0.1L5,21.3l0.1-0.8
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2v-1v-0.9l0,0c0,0,0,0,0-0.1v0.2c0,0.2,0,0.3-0.1,0.5
s0,0.2-0.1,0.3l-0.1,0.7v0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2l-0.3,5.2h-1L2.9,17z"/>
<path class="st1" d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6
c0,0.5-0.1,0.9-0.2,1.4c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1L10.3,22.2L10.3,22.2z"/>
<path class="st1" d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z"/>
<path class="st1" d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z"/>
<path class="st1" d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z"/>
<path class="st1" d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6
l1.4,3.7h-1l-1.4-3.7v3.7h-1L22.3,17L22.3,17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6
s-0.1-0.3-0.2-0.4s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2v2.5H23.3z"/>
</g>
<rect x="-33.5" y="14" class="st3" width="8.6" height="14"/>
</svg>
<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:#ffffff;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:#ffffff"/><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:#ffffff"/><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:#ffffff"/><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:#ffffff"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#ffffff"/><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:#ffffff"/><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:#ffffff"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#ffffff"/><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:#ffffff"/></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1 +1 @@
<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="#01aeb7"/><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="#01aeb7"/><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="#01aeb7"/><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="#01aeb7"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#01aeb7"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#01aeb7" 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="#01aeb7"/><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="#01aeb7"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#01aeb7"/><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="#01aeb7"/></svg>
<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.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,44 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.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 28 28" style="color:#ad0670" xml:space="preserve">
<style type="text/css">
.st1 {
fill: none;
stroke: #ad0670;
stroke-width: 2;
stroke-miterlimit: 10;
}
</style>
<g class="st0">
<path fill="currentcolor" d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z" />
<path fill="currentcolor" d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8l0,0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
c0-0.6-0.3-1-0.8-1H8V6.8z" />
<path fill="currentcolor" d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2c-0.7,0-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
c0-1.5,0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
c-0.2,0-0.4,0.2-0.5,0.6c-0.1,0.4-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6c0.2,0,0.4-0.2,0.5-0.6
C14.5,8.4,14.6,7.9,14.6,7.2z" />
<path fill="currentcolor" d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9s-0.1-1-0.1-1.3
V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9s-0.1-0.6-0.2-0.8c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2H17.2z" />
<path fill="currentcolor" d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z" />
</g>
<rect class="st1" width="28" height="28" />
<g class="st0">
<path fill="currentcolor" d="M2.9,17h0.9L4.4,20c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1c0,0,0-0.1,0-0.1L5,21.3l0.1-0.8
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2l0-1l0-0.9c0,0,0,0,0,0c0,0,0,0,0-0.1l0,0.2
c0,0.2,0,0.3-0.1,0.5s0,0.2-0.1,0.3l-0.1,0.7l0,0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2
l-0.3,5.2h-1L2.9,17z" />
<path fill="currentcolor" d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6c0,0.5-0.1,0.9-0.2,1.4
c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1H10.3z" />
<path fill="currentcolor" d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z" />
<path fill="currentcolor" d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z" />
<path fill="currentcolor" d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z" />
<path fill="currentcolor" d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6l1.4,3.7h-1
l-1.4-3.7v3.7h-1V17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6s-0.1-0.3-0.2-0.4
s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2V20.3z" />
</g>
</svg>
<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.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
<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
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,39 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.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 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
<style type="text/css">
.st0{enable-background:new ;}
.st1{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
.st2{fill:none;}
</style>
<g class="st0">
<path d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z"/>
<path d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8l0,0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
c0-0.6-0.3-1-0.8-1H8V6.8z"/>
<path d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2c-0.7,0-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
c0-1.5,0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
c-0.2,0-0.4,0.2-0.5,0.6c-0.1,0.4-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6c0.2,0,0.4-0.2,0.5-0.6
C14.5,8.4,14.6,7.9,14.6,7.2z"/>
<path d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9s-0.1-1-0.1-1.3
V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9s-0.1-0.6-0.2-0.8c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2H17.2z"/>
<path d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z"/>
</g>
<rect class="st1" width="28" height="28"/>
<g class="st0">
<path d="M2.9,17h0.9L4.4,20c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1c0,0,0-0.1,0-0.1L5,21.3l0.1-0.8
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2l0-1l0-0.9c0,0,0,0,0,0c0,0,0,0,0-0.1l0,0.2
c0,0.2,0,0.3-0.1,0.5s0,0.2-0.1,0.3l-0.1,0.7l0,0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2
l-0.3,5.2h-1L2.9,17z"/>
<path d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6c0,0.5-0.1,0.9-0.2,1.4
c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1H10.3z"/>
<path d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z"/>
<path d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z"/>
<path d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z"/>
<path d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6l1.4,3.7h-1
l-1.4-3.7v3.7h-1V17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6s-0.1-0.3-0.2-0.4
s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2V20.3z"/>
</g>
<rect x="-33.5" y="14" class="st2" width="8.6" height="14"/>
</svg>
<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.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,4 +1,4 @@
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#F3EFF5">
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#C5C5C5">
<path d="M9 9H4v1h5V9z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z" />
</svg>

Before

Width:  |  Height:  |  Size: 277 B

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -1,4 +1,4 @@
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentcolor">
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#424242">
<path d="M9 9H4v1h5V9z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z" />
</svg>

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
<rect x="25" y="25" fill="none" stroke="#ffffff" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
<path fill="#ffffff" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#ffffff" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
<rect x="25" y="25" fill="none" stroke="#424242" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
<path fill="#424242" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -437,6 +437,26 @@ input:checked + .field__toggle__slider:before {
margin-right: .5rem;
}
.metadata_field__error {
color: var(--vscode-errorForeground);
display: flex;
justify-content: space-between;
align-items: center;
}
.metadata_field__error button {
color: var(--vscode-button-secondaryForeground);
background-color: var(--vscode-button-secondaryBackground);
padding-left: 1rem;
padding-right: 1rem;
width: auto;
}
.metadata_field__error button:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.metadata_field__input, .metadata_field__input:focus,
.metadata_field__textarea, .metadata_field__textarea:focus {
outline: none;
}
@@ -451,17 +471,92 @@ input:checked + .field__toggle__slider:before {
outline: none !important;
}
.metadata_field__choice {
.metadata_field__choice__toggle {
color: var(--vscode-input-placeholderForeground);
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
outline: none !important;
width: 100%;
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background);
display: flex;
align-items: center;
position: relative;
}
.metadata_field__choice__toggle:hover,
.metadata_field__choice__toggle:focus,
.metadata_field__choice__toggle:active,
.metadata_field__choice__toggle:disabled {
background-color: var(--vscode-input-background);
}
.metadata_field__choice::placeholde {
color: var(--vscode-input-placeholderForeground);
.metadata_field__choice__toggle span {
margin-right: 1rem;
}
.metadata_field__choice__toggle svg.icon {
height: 1rem;
width: 1rem;
margin-left: .25rem;
position: absolute;
right: .25rem;
}
.metadata_field__choice_list {
width: 90%;
margin: 0;
padding: 0;
z-index: 1;
position: absolute;
list-style: none;
overflow: auto;
max-height: 200px;
color: var(--vscode-dropdown-foreground);
background-color: var(--vscode-dropdown-background);
}
.metadata_field__choice_list.open {
border: 1px solid rgba(0, 0, 0, .9);
}
.metadata_field__choice_list li {
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
cursor: pointer;
}
.metadata_field__choice_list li:active {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
}
.metadata_field__choice_list li[aria-selected="true"] {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-hoverBackground);
}
.metadata_field__choice_list li[aria-disabled="true"] {
display: none;
}
.metadata_field__choice_list__item {
opacity: 0.8;
}
.metadata_field__choice__button {
margin-top: .5rem;
display: inline-flex;
align-items: center;
width: auto;
margin-right: .5rem;
}
.metadata_field__choice__button_icon {
height: 1.25rem;
width: 1.25rem;
margin-left: .5rem;
}
.metadata_field__datetime {
@@ -481,12 +576,56 @@ input:checked + .field__toggle__slider:before {
width: auto;
}
.metadata_field__datetime > button:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.metadata_field__multiple_images {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.metadata_field__preview_image img {
display: block;
margin: 0 auto;
max-height: 16rem;
}
.metadata_field__preview_image__button {
background-color: transparent;
border: 2px dashed var(--vscode-button-background);
padding: 1.5rem;
filter: brightness(85%);
}
.metadata_field__preview_image__button:hover {
background-color: rgba(255, 255, 255, .1);
filter: brightness(100%);
}
.metadata_field__preview_image__button svg {
color: var(--vscode-foreground);
display: block;
width: 3rem;
height: 3rem;
margin: 0 auto;
}
.metadata_field__preview_image__button span {
color: var(--vscode-foreground);
display: inline-block;
margin: 0 auto;
margin-top: .5rem;
}
.metadata_field__preview_image__preview {
background-color: var(--vscode-button-secondaryBackground);
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.metadata_field__preview_image__remove {
background-color: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-inputValidation-errorForeground);

224
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "vscode-front-matter-beta",
"version": "4.0.1",
"version": "5.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -236,6 +236,60 @@
"integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==",
"dev": true
},
"@microsoft/fast-element": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.6.0.tgz",
"integrity": "sha512-ePTcBuCA99n7He0BYLIzFr5YOHYPSiBLJeDbDsyyQ5JUs4oXaZD0v54Pq0GAtSZuagnCGTDqcxEcBPUhTqLmQw==",
"dev": true
},
"@microsoft/fast-foundation": {
"version": "1.24.8",
"resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-1.24.8.tgz",
"integrity": "sha512-n4O9jPh8BBliF/Yl9FAVhrSoopsRCnva2L432s/fHwLelY9WUeswjO3DidVBFbzXD5u/gzC4LGWJScNe/ZGU4Q==",
"dev": true,
"requires": {
"@microsoft/fast-element": "^1.4.0",
"@microsoft/fast-web-utilities": "^4.8.0",
"@microsoft/tsdoc-config": "^0.13.4",
"tabbable": "^5.2.0",
"tslib": "^1.13.0"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}
}
},
"@microsoft/fast-web-utilities": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-4.8.1.tgz",
"integrity": "sha512-P3xeyUwQ9nPkFrgAdmkOzaXxIq8YqMU5K+LXcoHgJddJCBCKfGWW9OZQOTigLddItTyVyfO8qsJpDQb1TskKHA==",
"dev": true,
"requires": {
"exenv-es6": "^1.0.0"
}
},
"@microsoft/tsdoc": {
"version": "0.12.24",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.24.tgz",
"integrity": "sha512-Mfmij13RUTmHEMi9vRUhMXD7rnGR2VvxeNYtaGtaJ4redwwjT4UXYJ+nzmVJF7hhd4pn/Fx5sncDKxMVFJSWPg==",
"dev": true
},
"@microsoft/tsdoc-config": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.9.tgz",
"integrity": "sha512-VqqZn+rT9f6XujFPFR2aN9XKF/fuir/IzKVzoxI0vXIzxysp4ee6S2jCakmlGFHEasibifFTsJr7IYmRPxfzYw==",
"dev": true,
"requires": {
"@microsoft/tsdoc": "0.12.24",
"ajv": "~6.12.6",
"jju": "~1.4.0",
"resolve": "~1.19.0"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -268,6 +322,96 @@
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==",
"dev": true
},
"@sentry/browser": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.3.tgz",
"integrity": "sha512-jwlpsk2/u1cofvfYsjmqcnx50JJtf/T6HTgdW+ih8+rqWC5ABEZf4IiB/H+KAyjJ3wVzCOugMq5irL83XDCfqQ==",
"dev": true,
"requires": {
"@sentry/core": "6.13.3",
"@sentry/types": "6.13.3",
"@sentry/utils": "6.13.3",
"tslib": "^1.9.3"
}
},
"@sentry/core": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.3.tgz",
"integrity": "sha512-obm3SjgCk8A7nB37b2AU1eq1q7gMoJRrGMv9VRIyfcG0Wlz/5lJ9O3ohUk+YZaaVfZMxXn6hFtsBiOWmlv7IIA==",
"dev": true,
"requires": {
"@sentry/hub": "6.13.3",
"@sentry/minimal": "6.13.3",
"@sentry/types": "6.13.3",
"@sentry/utils": "6.13.3",
"tslib": "^1.9.3"
}
},
"@sentry/hub": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.3.tgz",
"integrity": "sha512-eYppBVqvhs5cvm33snW2sxfcw6G20/74RbBn+E4WDo15hozis89kU7ZCJDOPkXuag3v1h9igns/kM6PNBb41dw==",
"dev": true,
"requires": {
"@sentry/types": "6.13.3",
"@sentry/utils": "6.13.3",
"tslib": "^1.9.3"
}
},
"@sentry/minimal": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.3.tgz",
"integrity": "sha512-63MlYYRni3fs5Bh8XBAfVZ+ctDdWg0fapSTP1ydIC37fKvbE+5zhyUqwrEKBIiclEApg1VKX7bkKxVdu/vsFdw==",
"dev": true,
"requires": {
"@sentry/hub": "6.13.3",
"@sentry/types": "6.13.3",
"tslib": "^1.9.3"
}
},
"@sentry/react": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.3.tgz",
"integrity": "sha512-fdfmD9XNpGDwdkeLyd+iq+kqtNeghpH3wiez2rD81ZBvrn70uKaO2/yYDE71AXC6fUOwQuJmdfAuqBcNJkYIEw==",
"dev": true,
"requires": {
"@sentry/browser": "6.13.3",
"@sentry/minimal": "6.13.3",
"@sentry/types": "6.13.3",
"@sentry/utils": "6.13.3",
"hoist-non-react-statics": "^3.3.2",
"tslib": "^1.9.3"
}
},
"@sentry/tracing": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.3.tgz",
"integrity": "sha512-yyOFIhqlprPM0g4f35Icear3eZk2mwyYcGEzljJfY2iU6pJwj1lzia5PfSwiCW7jFGMmlBJNhOAIpfhlliZi8Q==",
"dev": true,
"requires": {
"@sentry/hub": "6.13.3",
"@sentry/minimal": "6.13.3",
"@sentry/types": "6.13.3",
"@sentry/utils": "6.13.3",
"tslib": "^1.9.3"
}
},
"@sentry/types": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.3.tgz",
"integrity": "sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A==",
"dev": true
},
"@sentry/utils": {
"version": "6.13.3",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.3.tgz",
"integrity": "sha512-zYFuFH3MaYtBZTeJ4Yajg7pDf0pM3MWs3+9k5my9Fd+eqNcl7dYQYJbT9gyC0HXK1QI4CAMNNlHNl4YXhF91ag==",
"dev": true,
"requires": {
"@sentry/types": "6.13.3",
"tslib": "^1.9.3"
}
},
"@tailwindcss/forms": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.3.tgz",
@@ -496,6 +640,16 @@
"integrity": "sha512-LlO6K7nzrIWDCZN1Zi6J6ibxrpMibSAct+zNjAwpkNkwup6cJLx5diYvsOJODMPWOuQlBO21qkxtdkSRzW6+Jw==",
"dev": true
},
"@vscode/webview-ui-toolkit": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-0.8.1.tgz",
"integrity": "sha512-SOgeaZ+/6yFDXLsgH+JvKcs4E0ThvuohNhkr9mvnggjl+OfFxc+Yqkyf5B4B5kuu3EppOCzEMiUwPC8APuLEGQ==",
"dev": true,
"requires": {
"@microsoft/fast-element": "^1.2.0",
"@microsoft/fast-foundation": "^1.24.7"
}
},
"@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@@ -2174,6 +2328,12 @@
"safe-buffer": "^5.1.1"
}
},
"exenv-es6": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.0.0.tgz",
"integrity": "sha512-fcG/TX8Ruv9Ma6PBaiNsUrHRJzVzuFMP6LtPn/9iqR+nr9mcLeEOGzXQGLC5CVQSXGE98HtzW2mTZkrCA3XrDg==",
"dev": true
},
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -2873,6 +3033,15 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"requires": {
"react-is": "^16.7.0"
}
},
"homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -3085,6 +3254,15 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
},
"image-size": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
"integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==",
"dev": true,
"requires": {
"queue": "6.0.2"
}
},
"import-cwd": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
@@ -3404,6 +3582,12 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"jju": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
"integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3537,6 +3721,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true
},
"lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
@@ -4106,6 +4296,23 @@
"lodash": "^4.17.21"
}
},
"node-json-db": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-json-db/-/node-json-db-1.3.0.tgz",
"integrity": "sha512-3IK9KuqfKdK12zFKtzmnD6Y5J9qL0TY0gnnZ5XKpzdhCP019zOxPxGCaH6cmIqiho2ymFgcTMKQeJvYkbBFraQ==",
"dev": true,
"requires": {
"mkdirp": "~1.0.4"
},
"dependencies": {
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
}
}
},
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@@ -4791,6 +4998,15 @@
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
"dev": true
},
"queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"dev": true,
"requires": {
"inherits": "~2.0.3"
}
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5640,6 +5856,12 @@
}
}
},
"tabbable": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.1.tgz",
"integrity": "sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==",
"dev": true
},
"tailwindcss": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.7.tgz",

View File

@@ -3,7 +3,7 @@
"displayName": "Front Matter",
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "4.0.1",
"version": "5.1.1",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -268,8 +268,44 @@
"type": "array",
"description": "Define your choices",
"items": {
"type": "string"
"type": [
"object",
"string"
],
"properties": {
"id": {
"type": [
"null",
"string"
],
"description": "The choice ID"
},
"title": {
"type": "string",
"description": "The choice title"
}
}
}
},
"single": {
"type": "boolean",
"default": false,
"description": "Is a single line field"
},
"multiple": {
"type": "boolean",
"default": false,
"description": "Do you allow to select multiple values?"
},
"isPreviewImage": {
"type": "boolean",
"default": false,
"description": "Specify if the image field can be used as preview. Be aware, you can only have one preview image per content type."
},
"hidden": {
"type": "boolean",
"default": false,
"description": "Do you want to hide the field from the metadata section?"
}
},
"additionalProperties": false,
@@ -278,6 +314,11 @@
"name"
]
}
},
"pageBundle": {
"type": "boolean",
"default": false,
"description": "Specify if you want to create a folder when creating new content."
}
},
"additionalProperties": false,
@@ -289,6 +330,7 @@
"default": [
{
"name": "default",
"pageBundle": false,
"fields": [
{
"title": "Title",
@@ -306,7 +348,7 @@
"type": "datetime"
},
{
"title": "Article preview",
"title": "Content preview",
"name": "preview",
"type": "image"
},
@@ -419,7 +461,7 @@
},
"frontMatter.templates.folder": {
"type": "string",
"default": ".templates",
"default": ".frontmatter/templates",
"markdownDescription": "Specify the folder to use for your article templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.folder)",
"scope": "Templates"
},
@@ -475,7 +517,7 @@
},
{
"command": "frontMatter.generateSlug",
"title": "Generate slug based on article title",
"title": "Generate slug based on content title",
"category": "Front matter"
},
{
@@ -490,7 +532,7 @@
},
{
"command": "frontMatter.insertImage",
"title": "Insert image into article",
"title": "Insert image into your content",
"category": "Front matter",
"icon": "$(device-camera)"
},
@@ -501,17 +543,18 @@
},
{
"command": "frontMatter.createContent",
"title": "New article from template",
"title": "Create new content from defined content type or template",
"category": "Front matter"
},
{
"command": "frontMatter.dashboard",
"title": "Open dashboard",
"category": "Front matter"
"category": "Front matter",
"icon": "$(preview)"
},
{
"command": "frontMatter.preview",
"title": "Preview article",
"title": "Preview content",
"category": "Front matter"
},
{
@@ -533,9 +576,14 @@
"menus": {
"editor/title": [
{
"when": "resourceLangId == markdown",
"command": "frontMatter.insertImage",
"group": "navigation"
"group": "navigation@-99",
"when": "resourceLangId == markdown"
},
{
"command": "frontMatter.dashboard",
"group": "navigation@-98",
"when": "frontMatter:enabled == true"
}
],
"explorer/context": [
@@ -616,6 +664,8 @@
"@headlessui/react": "^1.4.1",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/glob": "7.1.3",
"@types/js-yaml": "3.12.1",
@@ -627,6 +677,7 @@
"@types/react-dom": "17.0.0",
"@types/vscode": "1.51.0",
"@vscode/codicons": "0.0.20",
"@vscode/webview-ui-toolkit": "^0.8.1",
"autoprefixer": "^10.3.2",
"css-loader": "5.2.7",
"date-fns": "2.23.0",
@@ -636,8 +687,11 @@
"gray-matter": "4.0.2",
"html-loader": "1.3.2",
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
"lodash-es": "^4.17.21",
"lodash.uniqby": "4.7.0",
"mdast-util-from-markdown": "1.0.0",
"node-json-db": "^1.3.0",
"postcss": "^8.3.6",
"postcss-loader": "4.3.0",
"react": "17.0.1",

View File

@@ -1,7 +1,6 @@
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX } from './../constants/settings';
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from './../constants';
import * as vscode from 'vscode';
import { TaxonomyType } from "../models";
import { CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from "../constants/settings";
import { format } from "date-fns";
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
import matter = require('gray-matter');
@@ -240,13 +239,13 @@ export class Article {
/**
* Format the date to the defined format
*/
public static formatDate(dateValue: Date) {
public static formatDate(dateValue: Date): string {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
if (dateFormat && typeof dateFormat === "string") {
return format(dateValue, dateFormat);
} else {
return dateValue.toISOString();
return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString();
}
}

31
src/commands/Content.ts Normal file
View File

@@ -0,0 +1,31 @@
import { commands, QuickPickItem, window } from 'vscode';
import { COMMAND_NAME } from '../constants';
export class Content {
public static async create() {
const options: QuickPickItem[] = [{
label: "Create content by content type",
description: "Select if you want to create new content by the available content type(s)"
}, {
label: "Create content by template",
description: "Select if you want to create new content by the available template(s)"
} as QuickPickItem];
const selectedOption = await window.showQuickPick(options, {
placeHolder: `Select how you want to create your new content`,
canPickMany: false
});
if (selectedOption) {
if (selectedOption.label === options[0].label) {
commands.executeCommand(COMMAND_NAME.createByContentType);
} else {
commands.executeCommand(COMMAND_NAME.createByTemplate);
}
}
return;
}
}

View File

@@ -1,7 +1,7 @@
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET } from './../constants/settings';
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME } from '../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { basename, dirname, extname, join } from "path";
import { existsSync, statSync, unlinkSync, writeFileSync } from "fs";
import { basename, dirname, extname, join, parse } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
import { Settings as SettingsHelper } from '../helpers';
import { TaxonomyType } from '../models';
@@ -10,20 +10,20 @@ import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
import { Page } from '../dashboardWebView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { COMMAND_NAME, EXTENSION_STATE_PAGES_VIEW } from '../constants/Extension';
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Settings } from '../dashboardWebView/models/Settings';
import { Extension } from '../helpers/Extension';
import { parseJSON } from 'date-fns';
import { ViewType } from '../dashboardWebView/state';
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
import { decodeBase64Image } from '../helpers/decodeBase64Image';
import { DefaultFields } from '../constants';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
import { MediaLibrary } from '../helpers/MediaLibrary';
import imageSize from 'image-size';
import { parseWinPath } from '../helpers/parseWinPath';
import { DateHelper } from '../helpers/DateHelper';
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -31,6 +31,7 @@ export class Dashboard {
private static media: MediaInfo[] = [];
private static timers: { [folder: string]: any } = {};
private static _viewData: DashboardData | undefined;
private static mediaLib: MediaLibrary;
public static get viewData(): DashboardData | undefined {
return Dashboard._viewData;
@@ -50,6 +51,8 @@ export class Dashboard {
* Open or reveal the dashboard
*/
public static async open(data?: DashboardData) {
this.mediaLib = MediaLibrary.getInstance();
Dashboard._viewData = data;
if (Dashboard.isOpen) {
@@ -94,8 +97,8 @@ export class Dashboard {
Dashboard.isDisposed = false;
Dashboard.webview.iconPath = {
dark: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter.svg'))
dark: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-dark.svg')),
light: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-light.svg'))
};
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri);
@@ -136,6 +139,12 @@ export class Dashboard {
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.updateSetting:
Dashboard.updateSetting(msg.data);
break;
@@ -151,16 +160,16 @@ export class Dashboard {
}
break;
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(EXTENSION_STATE_PAGES_VIEW, msg.data);
Extension.getInstance().setState(ExtensionState.PagesView, msg.data);
break;
case DashboardMessage.getMedia:
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder)
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
break;
case DashboardMessage.copyToClipboard:
env.clipboard.writeText(msg.data);
break;
case DashboardMessage.refreshMedia:
Dashboard.media = [];
Dashboard.resetMedia();
Dashboard.getMedia(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
@@ -172,9 +181,27 @@ export class Dashboard {
case DashboardMessage.insertPreviewImage:
Dashboard.insertImage(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
Dashboard.updateMediaMetadata(msg?.data);
break;
case DashboardMessage.createMediaFolder:
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
break;
}
});
}
/**
* Reset media array
*/
public static resetMedia() {
Dashboard.media = [];
}
public static switchFolder(folderPath: string) {
Dashboard.resetMedia();
Dashboard.getMedia(0, folderPath);
}
/**
* Insert an image into the front matter or contents
@@ -193,16 +220,33 @@ export class Dashboard {
const panel = ExplorerView.getInstance(extensionUri);
if (data?.position) {
const wsFolder = Folders.getWorkspaceFolder();
const editor = window.activeTextEditor;
const line = data.position.line;
const character = data.position.character;
if (line) {
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![](${data.image})`));
let imgPath = data.image;
const filePath = data.file;
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
const imgDir = dirname(absImgPath);
const fileDir = dirname(filePath);
if (imgDir === fileDir) {
imgPath = join('/', basename(imgPath));
// Snippets are already parsed, so update the URL of the image
if (data.snippet) {
data.snippet = data.snippet.replace(data.image, imgPath);
}
}
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`));
}
panel.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image});
panel.updateMetadata({field: data.fieldName, value: data.image });
}
}
}
@@ -219,15 +263,17 @@ export class Dashboard {
data: {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS),
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
folders: Folders.get(),
initialized: await Template.isInitialized(),
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<ViewType | undefined>(EXTENSION_STATE_PAGES_VIEW),
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView),
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
contentFolders: Folders.get().map(f => f.path),
} as Settings
});
}
@@ -243,50 +289,76 @@ export class Dashboard {
/**
* Retrieve all media files
*/
private static async getMedia(page: number = 0, folder: string = '') {
private static async getMedia(page: number = 0, requestedFolder: string = '') {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const contentFolders = Folders.get();
const viewData = Dashboard.viewData;
let selectedFolder = requestedFolder;
if (Dashboard.media.length === 0) {
const contentFolder = Folders.get();
let allMedia: MediaInfo[] = [];
// If the static folder is not set, retreive the last opened location
if (!selectedFolder) {
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder);
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
// Support for page bundles
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
const folderPath = parse(viewData.data.filePath).dir;
selectedFolder = folderPath;
} else if (stateValue && existsSync(stateValue)) {
selectedFolder = stateValue;
}
}
}
// Go to the home folder
if (selectedFolder === HOME_PAGE_NAVIGATION_ID) {
selectedFolder = '';
}
const relSelectedFolderPath = selectedFolder ? selectedFolder.substring((parseWinPath(wsFolder?.fsPath || "")).length + 1) : '';
let allMedia: MediaInfo[] = [];
if (relSelectedFolderPath) {
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
const media = Dashboard.filterMedia(files);
allMedia = [...media];
} else {
if (staticFolder) {
const files = await workspace.findFiles(`${staticFolder || ""}/**/*`);
const folderSearch = join(staticFolder || "", '/*');
const files = await workspace.findFiles(folderSearch);
const media = Dashboard.filterMedia(files);
allMedia = [...media];
}
if (contentFolder && wsFolder) {
for (let i = 0; i < contentFolder.length; i++) {
const folder = contentFolder[i];
const relFolderPath = folder.path.substring(wsFolder.fsPath.length + 1);
const files = await workspace.findFiles(`${relFolderPath}/**/*`);
if (contentFolders && wsFolder) {
for (let i = 0; i < contentFolders.length; i++) {
const contentFolder = contentFolders[i];
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
const files = await workspace.findFiles(folderSearch);
const media = Dashboard.filterMedia(files);
allMedia = [...allMedia, ...media];
}
}
allMedia = allMedia.sort((a, b) => {
if (b.fsPath < a.fsPath) {
return -1;
}
if (b.fsPath > a.fsPath) {
return 1;
}
return 0;
});
Dashboard.media = Object.assign([], allMedia);
}
// Filter the media
allMedia = allMedia.sort((a, b) => {
if (b.fsPath < a.fsPath) {
return -1;
}
if (b.fsPath > a.fsPath) {
return 1;
}
return 0;
});
Dashboard.media = Object.assign([], allMedia);
let files: MediaInfo[] = Dashboard.media;
if (folder) {
files = files.filter(f => f.fsPath.includes(folder));
}
// Retrieve the total after filtering and before the slicing happens
const total = files.length;
@@ -295,32 +367,53 @@ export class Dashboard {
files = files.slice(page * 16, ((page + 1) * 16));
files = files.map((file) => {
try {
const metadata = Dashboard.mediaLib.get(file.fsPath);
return {
...file,
stats: statSync(file.fsPath)
stats: statSync(file.fsPath),
dimensions: imageSize(file.fsPath),
...metadata
};
} catch (e) {
return {...file, stats: undefined};
}
}).filter(f => f.stats !== undefined);
});
files = files.filter(f => f.stats !== undefined);
const folders = [...new Set(Dashboard.media.map((file) => {
let relFolderPath = wsFolder ? file.fsPath.substring(wsFolder.fsPath.length + 1) : file.fsPath;
if (staticFolder && relFolderPath.startsWith(staticFolder)) {
relFolderPath = relFolderPath.substring(staticFolder.length);
// Retrieve all the folders
let allContentFolders: string[] = [];
let allFolders: string[] = [];
if (selectedFolder) {
if (existsSync(selectedFolder)) {
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
}
if (relFolderPath?.startsWith('/')) {
relFolderPath = relFolderPath.substring(1);
} else {
for (const contentFolder of contentFolders) {
const contentPath = contentFolder.path;
if (contentPath && existsSync(contentPath)) {
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
allContentFolders = [...allContentFolders, ...subFolders];
}
}
return dirname(relFolderPath);
}))];
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
if (staticPath && existsSync(staticPath)) {
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
}
}
// Store the last opened folder
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder);
Dashboard.postWebviewMessage({
command: DashboardCommand.media,
data: {
media: files,
total: total,
folders
folders: [...allContentFolders, ...allFolders],
selectedFolder
} as MediaPaths
});
}
@@ -333,7 +426,7 @@ export class Dashboard {
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
@@ -354,7 +447,7 @@ export class Dashboard {
fmFilePath: file.filePath,
fmFileName: file.fileName,
fmDraft: article?.data.draft ? "Draft" : "Published",
fmYear: article?.data[dateField] ? parseJSON(article?.data[dateField]).getFullYear() : null,
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
@@ -362,23 +455,38 @@ export class Dashboard {
draft: article?.data.draft,
description: article?.data[descriptionField] || "",
};
if (article?.data.preview && wsFolder) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", article?.data.preview);
const contentFolderPath = join(dirname(file.filePath), article?.data.preview);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
const contentType = ArticleHelper.getContentType(article.data);
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
if (article?.data[previewField] && wsFolder) {
let fieldValue = article?.data[previewField];
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
if (previewUri) {
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
page.preview = preview?.toString() || "";
} else {
page.preview = "";
// Revalidate as the array could have been empty
if (fieldValue) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
const contentFolderPath = join(dirname(file.filePath), fieldValue);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
page[previewField] = preview?.toString() || "";
} else {
page[previewField] = "";
}
}
}
@@ -419,9 +527,13 @@ export class Dashboard {
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
if (fileName && contents) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "", folder || "");
let absFolderPath = join(wsPath, staticFolder || "");
if (folder) {
absFolderPath = folder;
}
if (!existsSync(absFolderPath)) {
absFolderPath = join(wsPath, folder || "");
@@ -475,6 +587,18 @@ export class Dashboard {
}
}
/**
* Update the metadata of the selected file
*/
private static async updateMediaMetadata({ file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; }) {
Dashboard.mediaLib.set(file, metadata);
// Check if filename needs to be updated
Dashboard.mediaLib.updateFilename(file, filename);
Dashboard.getMedia(page || 0, folder || "");
}
/**
* Post data to the dashboard
* @param msg
@@ -498,19 +622,21 @@ export class Dashboard {
const nonce = WebviewHelper.getNonce();
const version = Extension.getInstance().getVersion();
const ext = Extension.getInstance();
const version = ext.getVersion();
const isBeta = ext.isBetaVersion();
return `
<!DOCTYPE html>
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Matter Dashboard</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
<div id="app" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`dashboard-${version.installedVersion}`}" alt="Daily usage" />

View File

@@ -1,43 +1,82 @@
import { SETTINGS_CONTENT_PAGE_FOLDERS } from './../constants/settings';
import { Questions } from './../helpers/Questions';
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER } from './../constants';
import { commands, Uri, workspace, window } from "vscode";
import { basename, join } from "path";
import { ContentFolder, FileInfo, FolderInfo } from "../models";
import uniqBy = require("lodash.uniqby");
import { Template } from "./Template";
import { Notifications } from "../helpers/Notifications";
import { CONTEXT } from "../constants/context";
import { Settings } from "../helpers";
import { existsSync, mkdirSync } from 'fs';
import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
import { parseWinPath } from '../helpers/parseWinPath';
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
export class Folders {
/**
* Add a media folder
* @returns
*/
public static async addMediaFolder(data?: {selectedFolder?: string}) {
let wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
let startPath = "";
if (data?.selectedFolder) {
startPath = data.selectedFolder.replace(parseWinPath(wsFolder?.fsPath || ""), "");
} else if (staticFolder) {
startPath = `/${staticFolder}`;
}
if (startPath && !startPath.endsWith("/")) {
startPath += "/";
}
const folderName = await window.showInputBox({
prompt: `Which name would you like to give to your folder (use "/" to create multi-level folders)?`,
value: startPath,
ignoreFocusOut: true,
placeHolder: `${format(new Date(), `yyyy/MM`)}`
});
if (!folderName) {
Notifications.warning(`No folder name was specified.`);
return;
}
const folders = folderName.split("/").filter(f => f);
let parentFolders: string[] = [];
for (const folder of folders) {
const folderPath = join(parseWinPath(wsFolder?.fsPath || ""), parentFolders.join("/"), folder);
parentFolders.push(folder);
if (!existsSync(folderPath)) {
mkdirSync(folderPath);
}
}
if (Dashboard.isOpen) {
Dashboard.switchFolder(folderName);
}
}
/**
* Create content in a registered folder
* @returns
*/
public static async create() {
const folders = Folders.get();
if (!folders || folders.length === 0) {
Notifications.warning(`There are no known content locations defined in this project.`);
return;
}
let selectedFolder: string | undefined;
if (folders.length > 1) {
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
placeHolder: `Select where you want to create your content`
});
} else {
selectedFolder = folders[0].title;
}
const selectedFolder = await Questions.SelectContentFolder();
if (!selectedFolder) {
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
return;
}
const folders = Folders.get();
const location = folders.find(f => f.title === selectedFolder);
if (location) {
const folderPath = Folders.getFolderPath(Uri.file(location.path));
@@ -115,9 +154,33 @@ export class Folders {
*/
public static getWorkspaceFolder(): Uri | undefined {
const folders = workspace.workspaceFolders;
if (folders && folders.length > 0) {
if (folders && folders.length === 1) {
return folders[0].uri;
} else if (folders && folders.length > 1) {
let projectFolder = undefined;
for (const folder of folders) {
if (!projectFolder && existsSync(join(folder.uri.fsPath, Settings.globalFile))) {
projectFolder = folder.uri;
}
}
if (!projectFolder) {
window.showWorkspaceFolderPick({
placeHolder: `Please select the main workspace folder for Front Matter to use.`
}).then(selectedFolder => {
if (selectedFolder) {
Settings.createGlobalFile(selectedFolder.uri);
// Full reload to make sure the whole extension is reloaded correctly
commands.executeCommand(`workbench.action.reloadWindow`);
}
});
}
return projectFolder;
}
return undefined;
}
@@ -127,7 +190,6 @@ export class Folders {
public static getProjectFolderName(): string {
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
// const projectFolder = wsFolder?.fsPath.split('\\').join('/').split('/').pop();
return basename(wsFolder.fsPath);
}
return "";
@@ -212,7 +274,11 @@ export class Folders {
*/
private static async update(folders: ContentFolder[]) {
const wsFolder = Folders.getWorkspaceFolder();
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folders.map(folder => ({ title: folder.title, path: Folders.relWsFolder(folder, wsFolder) })));
let folderDetails = folders.map(folder => ({
title: folder.title,
path: Folders.relWsFolder(folder, wsFolder)
}));
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
}
/**
@@ -223,7 +289,7 @@ export class Folders {
*/
private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, wsFolder?.fsPath || "");
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
}
@@ -236,7 +302,7 @@ export class Folders {
*/
private static relWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(wsFolder?.fsPath || "", WORKSPACE_PLACEHOLDER);
let absPath = folder.path.replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
return absPath;
}

View File

@@ -1,11 +1,10 @@
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME } from './../constants/settings';
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT } from './../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
import { commands, env, Uri, ViewColumn, window } from "vscode";
import { Settings } from '../helpers';
import { PreviewSettings } from '../models';
import { format } from 'date-fns';
import { CONTEXT } from '../constants/context';
export class Preview {

View File

@@ -1,5 +1,4 @@
import { workspace, Uri } from "vscode";
import { CONFIG_KEY, SETTING_TEMPLATES_FOLDER } from "../constants";
import { join } from "path";
import * as fs from "fs";
import { Notifications } from "../helpers/Notifications";

View File

@@ -1,4 +1,4 @@
import { SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants/settings';
import { SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
import * as vscode from 'vscode';
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
import { ExplorerView } from '../explorerView/ExplorerView';

View File

@@ -1,15 +1,16 @@
import { Questions } from './../helpers/Questions';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { format } from 'date-fns';
import sanitize from '../helpers/Sanitize';
import { ArticleHelper, Settings } from '../helpers';
import { Article } from '.';
import { Notifications } from '../helpers/Notifications';
import { CONTEXT } from '../constants/context';
import { CONTEXT } from '../constants';
import { Project } from './Project';
import { Folders } from './Folders';
import { ContentType } from '../helpers/ContentType';
import { ContentType as IContentType } from '../models';
export class Template {
@@ -67,7 +68,7 @@ export class Template {
["yes", "no"],
{
canPickMany: false,
placeHolder: `Do you want to keep the article its contents for the template?`,
placeHolder: `Do you want to keep the contents for the template?`,
}
);
@@ -94,7 +95,7 @@ export class Template {
*/
public static async create(folderPath: string) {
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const contentTypes = ContentType.getAll();
if (!folderPath) {
Notifications.warning(`Incorrect project folder path retrieved.`);
@@ -113,39 +114,33 @@ export class Template {
}
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
placeHolder: `Select the article template to use`
placeHolder: `Select the content template to use`
});
if (!selectedTemplate) {
Notifications.warning(`No template selected.`);
return;
}
const titleValue = await vscode.window.showInputBox({
prompt: `What would you like to use as a title for the new article?`,
placeHolder: `Article title`
});
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
Notifications.warning(`You did not specify an article title.`);
return;
}
// Start the template read
const template = templates.find(t => t.fsPath.endsWith(selectedTemplate));
if (!template) {
Notifications.warning(`Article template could not be found.`);
Notifications.warning(`Content template could not be found.`);
return;
}
const fileExt = path.parse(selectedTemplate).ext;
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
let newFileName = `${sanitizedName}${fileExt}`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath);
let contentType: IContentType | undefined;
if (templateData && templateData.data && templateData.data.type) {
contentType = contentTypes?.find(t => t.name === templateData.data.type);
}
const newFilePath = path.join(folderPath, newFileName);
if (fs.existsSync(newFilePath)) {
Notifications.warning(`File already exists, please remove it before creating a new one with the same title.`);
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
if (!newFilePath) {
return;
}
@@ -165,7 +160,7 @@ export class Template {
fmData.title = titleValue;
}
if (typeof fmData.slug !== "undefined") {
fmData.slug = sanitizedName;
fmData.slug = ArticleHelper.sanitize(titleValue);
}
frontMatter = Article.updateDate(frontMatter);
@@ -180,7 +175,7 @@ export class Template {
vscode.window.showTextDocument(txtDoc);
}
Notifications.info(`Your new article has been created.`);
Notifications.info(`Your new content has been created.`);
}
/**

View File

@@ -4,6 +4,7 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default';
export const DEFAULT_CONTENT_TYPE: ContentType = {
"name": "default",
"pageBundle": false,
"fields": [
{
"title": "Title",

View File

@@ -3,9 +3,6 @@ const extensionName = "frontMatter";
export const EXTENSION_ID = 'eliostruyf.vscode-front-matter';
export const EXTENSION_BETA_ID = 'eliostruyf.vscode-front-matter-beta';
export const EXTENSION_STATE_VERSION = 'frontMatter:Version';
export const EXTENSION_STATE_PAGES_VIEW = 'frontMatter:Pages:ViewType';
export const getCommandName = (command: string) => {
return `${extensionName}.${command}`;
};
@@ -25,10 +22,13 @@ export const COMMAND_NAME = {
registerFolder: getCommandName("registerFolder"),
unregisterFolder: getCommandName("unregisterFolder"),
createContent: getCommandName("createContent"),
createByContentType: getCommandName("createByContentType"),
createByTemplate: getCommandName("createByTemplate"),
createTemplate: getCommandName("createTemplate"),
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
promote: getCommandName("promoteSettings"),
insertImage: getCommandName("insertImage"),
createFolder: getCommandName("createFolder"),
};

View File

@@ -0,0 +1,8 @@
export const ExtensionState = {
PagesView: `frontMatter:Pages:ViewType`,
SelectedFolder: `frontMatter:SelectedFolder`,
Version: `frontMatter:Version`,
SettingPromoted: `frontMatter:Settings:Promoted`,
MoveTemplatesFolder: `frontMatter:Templates:Move`,
};

View File

@@ -1,4 +1,6 @@
export const GITHUB_LINK = "https://github.com/estruyf/vscode-front-matter";
export const ISSUE_LINK = "https://github.com/estruyf/vscode-front-matter/issues";
export const SPONSOR_LINK = "https://github.com/sponsors/estruyf";
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";
export const SENTRY_LINK = "https://1ac45704bbe74264a7b4674bdc2abf48@o1022172.ingest.sentry.io/5988293";

View File

@@ -0,0 +1,8 @@
export const LocalStore = {
rootFolder: ".frontmatter",
contentFolder: "content",
templatesFolder: "templates",
mediaDatabaseFile: "mediaDb.json"
}

View File

@@ -0,0 +1 @@
export const HOME_PAGE_NAVIGATION_ID = "FrontMatter:RootFolder";

View File

@@ -1,5 +1,6 @@
export const CONTEXT = {
canInit: "frontMatterCanInit",
canOpenPreview: "frontMatterCanOpenPreview",
canOpenDashboard: "frontMatterCanOpenDashboard"
canOpenDashboard: "frontMatterCanOpenDashboard",
isEnabled: "frontMatter:enabled"
};

View File

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

View File

@@ -36,7 +36,7 @@ export const SETTING_CUSTOM_SCRIPTS = "custom.scripts";
export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
export const SETTINGS_CONTENT_STATIC_FOLDERS = "content.publicFolder";
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";

View File

@@ -3,5 +3,5 @@ export enum DashboardCommand {
pages = "pages",
settings = "settings",
media = "media",
viewData = "viewData",
viewData = "viewData"
}

View File

@@ -4,6 +4,8 @@ export enum DashboardMessage {
openFile = 'openFile',
getTheme = 'getTheme',
createContent = 'createContent',
createByContentType = 'createByContentType',
createByTemplate = 'createByTemplate',
updateSetting = 'updateSetting',
initializeProject = 'initializeProject',
reload = 'reload',
@@ -14,4 +16,6 @@ export enum DashboardMessage {
uploadMedia = 'uploadMedia',
deleteMedia = 'deleteMedia',
insertPreviewImage = 'insertPreviewImage',
updateMediaMetadata = 'updateMediaMetadata',
createMediaFolder = 'createMediaFolder'
}

View File

@@ -0,0 +1,52 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { MenuItem, MenuItems } from './Menu';
export interface IChoiceButtonProps {
title: string;
choices: {
title: string;
disabled?: boolean;
onClick: () => void;
}[];
disabled?: boolean;
onClick: () => void;
}
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
return (
<span className="relative z-50 inline-flex shadow-sm rounded-md">
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
onClick={onClick}
disabled={disabled}
>
{title}
</button>
<Menu as="span" className="-ml-px relative block">
<Menu.Button
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
disabled={disabled}>
<span className="sr-only">Open options</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<MenuItems widthClass={`w-56`}>
<div className="py-1">
{choices.map((choice) => (
<MenuItem
key={choice.title}
title={choice.title}
value={null}
onClick={choice.onClick}
disabled={choice.disabled} />
))}
</div>
</MenuItems>
</Menu>
</span>
);
};

View File

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

View File

@@ -5,7 +5,7 @@ import useDarkMode from '../../hooks/useDarkMode';
import usePages from '../hooks/usePages';
import { WelcomeScreen } from './WelcomeScreen';
import { useRecoilValue } from 'recoil';
import { DashboardViewSelector, ViewDataAtom } from '../state';
import { DashboardViewSelector } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';

View File

@@ -1,5 +1,6 @@
import { format, parseJSON } from 'date-fns';
import { format } from 'date-fns';
import * as React from 'react';
import { DateHelper } from '../../helpers/DateHelper';
export interface IDateFieldProps {
value: Date | string;
@@ -10,9 +11,9 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: Rea
React.useEffect(() => {
try {
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
setDateValue(dateString);
const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value) : value;
const dateString = parsedValue ? format(parsedValue, 'yyyy-MM-dd') : parsedValue;
setDateValue(dateString || "");
} catch (e) {
// Date is invalid
}

View File

@@ -0,0 +1,99 @@
import { CollectionIcon } from '@heroicons/react/outline';
import { basename, join } from 'path';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
export interface IBreadcrumbProps {}
export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: React.PropsWithChildren<IBreadcrumbProps>) => {
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const settings = useRecoilValue(SettingsAtom);
const [ folders, setFolders ] = React.useState<string[]>([]);
if (!settings?.wsFolder) {
return null;
}
React.useEffect(() => {
const { wsFolder, staticFolder, contentFolders } = settings;
const isValid = (folderPath: string) => {
if (staticFolder) {
const staticPath = parseWinPath(join(wsFolder, staticFolder)) as string;
const relPath = folderPath.replace(staticPath, '') as string;
if (relPath.length > 1 && folderPath.startsWith(staticPath)) {
return true;
} else if (relPath.length === 0) {
return false;
}
}
for (let i = 0; i < contentFolders.length; i++) {
const contentFolder = parseWinPath(contentFolders[i]) as string;
const relContentPath = folderPath.replace(contentFolder, '');
return relContentPath.length > 1 && folderPath.startsWith(contentFolder);
}
return false;
};
if (!selectedFolder) {
setFolders([]);
} else {
const relPath = parseWinPath(selectedFolder.replace(parseWinPath(settings.wsFolder) as string, '')) as string;
const folderParts = relPath.split('/').filter(f => f);
const allFolders: string[] = [];
let previousFolder = parseWinPath(settings.wsFolder) as string;
for (const part of folderParts) {
const folder = join(previousFolder, part);
if (isValid(folder)) {
allFolders.push(folder);
}
previousFolder = folder;
}
setFolders(allFolders);
}
}, [selectedFolder]);
return (
<nav className="bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex py-2" aria-label="Breadcrumb">
<ol role="list" className="w-full mx-auto flex space-x-4 px-5">
<li className="flex">
<div className="flex items-center">
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
<span className="sr-only">Home</span>
</button>
</div>
</li>
{folders.map((folder) => (
<li key={folder} className="flex">
<div className="flex items-center">
<svg
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<button
onClick={() => setSelectedFolder(folder)}
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
>
{basename(folder)}
</button>
</div>
</li>
))}
</ol>
</nav>
);
};

View File

@@ -1,4 +1,4 @@
import { Menu, Transition } from '@headlessui/react';
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { FolderAtom } from '../../state';

View File

@@ -6,7 +6,6 @@ import { Folders } from './Folders';
import { Settings } from '../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Startup } from '../Startup';
import { Button } from '../Button';
import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
@@ -17,6 +16,8 @@ import { ClearFilters } from './ClearFilters';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { PhotographIcon } from '@heroicons/react/outline';
import { Pagination } from '../Media/Pagination';
import { ChoiceButton } from '../ChoiceButton';
import { Breadcrumb } from './Breadcrumb';
export interface IHeaderProps {
settings: Settings | null;
@@ -37,6 +38,14 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
Messenger.send(DashboardMessage.createContent);
};
const createByContentType = () => {
Messenger.send(DashboardMessage.createByContentType);
};
const createByTemplate = () => {
Messenger.send(DashboardMessage.createByTemplate);
};
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
@@ -60,7 +69,19 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
<div className={`flex items-center space-x-4`}>
<Startup settings={settings} />
<Button onClick={createContent} disabled={!settings?.initialized}>Create content</Button>
<ChoiceButton
title={`Create content`}
choices={[{
title: `Create by content type`,
onClick: createByContentType,
disabled: !settings?.initialized
}, {
title: `Create by template`,
onClick: createByTemplate,
disabled: !settings?.initialized
}]}
onClick={createContent}
disabled={!settings?.initialized} />
</div>
</div>
@@ -93,7 +114,10 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
{
view === "media" && (
<Pagination />
<>
<Pagination />
<Breadcrumb />
</>
)
}
</div>

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import { FolderAddIcon } from '@heroicons/react/outline';
import { useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { SelectedMediaFolderAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IFolderCreationProps {}
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
const onFolderCreation = () => {
Messenger.send(DashboardMessage.createMediaFolder, {
selectedFolder
});
};
return (
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
title={`Create new folder`}
onClick={onFolderCreation}>
<FolderAddIcon className={`mr-2 h-6 w-6`} />
<span className={``}>Create new folder</span>
</button>
);
};

View File

@@ -0,0 +1,29 @@
import { FolderIcon } from '@heroicons/react/solid';
import { basename } from 'path';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { SelectedMediaFolderAtom } from '../../state';
export interface IFolderItemProps {
folder: string;
wsFolder?: string;
staticFolder?: string;
}
export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder, wsFolder, staticFolder }: React.PropsWithChildren<IFolderItemProps>) => {
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
return (
<li className={`group relative bg-gray-200 dark:bg-vulcan-300 hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
<button className={`w-full flex flex-col items-center`} onClick={() => setSelectedFolder(folder)}>
<FolderIcon className={`h-auto w-1/2`} />
<p className="text-sm font-bold pointer-events-none flex items-center">
{basename(relFolderPath)}
</p>
</button>
</li>
);
};

View File

@@ -1,77 +0,0 @@
import { Menu } from '@headlessui/react';
import { XIcon } from '@heroicons/react/outline';
import Downshift from 'downshift';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaFoldersSelector, SelectedMediaFolderAtom } from '../../state';
export interface IFolderSelectionProps {}
export const FolderSelection: React.FunctionComponent<IFolderSelectionProps> = (props: React.PropsWithChildren<IFolderSelectionProps>) => {
const folders = useRecoilValue(MediaFoldersSelector);
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ focus, setFocus ] = React.useState(false);
let allFolders: string[] = Object.assign([], folders);
allFolders = allFolders.sort((a: string, b: string) => {
if (a.toLowerCase() < b.toLowerCase()) return -1;
if (a.toLowerCase() > b.toLowerCase()) return 1;
return 0;
});
return (
<div>
<Downshift
isOpen={focus}
selectedItem={selectedFolder}
onOuterClick={() => setFocus(false)}
onSelect={(selFolder) => {
setSelectedFolder(selFolder);
setFocus(false);
}}>
{
({
getInputProps,
getItemProps,
getMenuProps,
isOpen,
inputValue,
getRootProps
}) => (
<div className={`relative flex items-center`}>
<label className={`text-sm text-gray-500 dark:text-whisper-900`}>Filter by: </label>
<div
className={`inline-flex items-center`}
{...getRootProps({} as any, {suppressRefError: true})}
>
<input disabled={!!selectedFolder} onFocus={() => setFocus(true)} className={`ml-2 py-1 px-2 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`} {...getInputProps()} />
{
selectedFolder && (
<button title={`Clear`} onClick={() => setSelectedFolder(null)}><XIcon className={`ml-2 h-6 w-6 text-red-500 hover:text-red-800`} /></button>
)
}
</div>
<div className={`${focus ? `block` : `hidden`} top-8 absolute right-0 z-10 mt-2 w-min 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`} {...getMenuProps()}>
{isOpen
? allFolders
.filter((item: string) => !inputValue || item.includes(inputValue))
.map((item, index) => (
<div
className="cursor-pointer text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100"
{...getItemProps({ key: item, index, item })}
>
{item}
</div>
))
: null}
</div>
</div>
)
}
</Downshift>
</div>
);
};

View File

@@ -1,27 +1,31 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PencilIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { LightboxAtom, PageSelector, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { Alert } from '../Modals/Alert';
import { Metadata } from '../Modals/Metadata';
export interface IItemProps {
media: MediaInfo;
}
export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWithChildren<IItemProps>) => {
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ , setLightbox ] = useRecoilState(LightboxAtom);
const [ showAlert, setShowAlert ] = React.useState(false);
const [ showForm, setShowForm ] = React.useState(false);
const [ caption, setCaption ] = React.useState(media.caption);
const [ alt, setAlt ] = React.useState(media.alt);
const [ filename, setFilename ] = React.useState<string | null>(null);
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const parseWinPath = (path: string | undefined) => {
return path?.split(`\\`).join(`/`);
}
const page = useRecoilValue(PageSelector);
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
@@ -48,6 +52,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return relPath;
};
const getFileName = () => {
return basename(parseWinPath(media.fsPath) || "");
};
const copyToClipboard = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || "");
@@ -59,18 +67,27 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
position: viewData?.data?.position || null
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
alt: alt || "",
caption: caption || ""
});
};
const insertSnippet = () => {
const relPath = getRelPath();
let snippet = settings?.mediaSnippet.join("\n");
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
snippet = snippet?.replace("{alt}", alt || "");
snippet = snippet?.replace("{caption}", caption || "");
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
position: viewData?.data?.position || null,
snippet: settings?.mediaSnippet.join("\n").replace("{mediaUrl}", parseWinPath(relPath) || "")
snippet
});
};
@@ -86,20 +103,75 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
};
const calculateSize = () => {
let sizeDetails = [];
if (media?.dimensions) {
if (media.dimensions.width && media.dimensions.height) {
sizeDetails.push(`${media.dimensions.width}x${media.dimensions.height}`);
}
}
if (media?.stats?.size) {
const size = media.stats.size / (1024*1024);
if (size > 1) {
return `${size.toFixed(2)} MB`;
sizeDetails.push(`${size.toFixed(2)} MB`);
} else {
return `${(size * 1024).toFixed(2)} KB`;
sizeDetails.push(`${(size * 1024).toFixed(2)} KB`);
}
}
return sizeDetails.join(" — ");
};
const openLightbox = () => {
setLightbox(media.vsPath || "");
};
const updateMetadata = () => {
setShowForm(true);
};
const submitMetadata = () => {
Messenger.send(DashboardMessage.updateMediaMetadata, {
file: media.fsPath,
filename,
caption,
alt,
folder: selectedFolder,
page
});
setShowForm(false);
// Reset the values
setAlt(media.alt);
setCaption(media.caption);
setFilename(getFileName());
};
useEffect(() => {
if (media.alt !== alt) {
setAlt(media.alt);
}
}, [media.alt]);
useEffect(() => {
if (media.caption !== caption) {
setCaption(media.caption);
}
}, [media.caption]);
useEffect(() => {
const name = basename(parseWinPath(media.fsPath) || "");
if (name !== filename) {
setFilename(getFileName());
}
}, [media.fsPath]);
const fileInfo = filename ? basename(filename).split('.') : null;
const extension = fileInfo?.pop();
const name = fileInfo?.join('.');
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
@@ -111,17 +183,24 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />
</div>
</button>
<div className={`relative py-4 pl-4 pr-10`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-2`}>
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<button title={`Edit metadata`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={updateMetadata}>
<PencilIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Edit metadata</span>
</button>
{
viewData?.data?.filePath ? (
<>
<button
title={`Insert into your article`}
title={`Insert into your content`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={insertToArticle}>
<CheckCircleIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Insert into your article</span>
<span className={`sr-only`}>Insert into your content</span>
</button>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
@@ -156,19 +235,95 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center">
{basename(parseWinPath(media.fsPath) || "")}
</p>
<p className="mt-2 text-sm dark:text-whisper-900 font-medium pointer-events-none flex items-center">
<b className={`mr-2`}>Folder:</b> {getFolder()}
{
media.caption && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-2`}>Caption:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.caption}</span>
</p>
)
}
{
media.alt && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-2`}>Alt:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.alt}</span>
</p>
)
}
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-2`}>Folder:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{getFolder()}</span>
</p>
{
media?.stats?.size && (
<p className="mt-2 text-sm dark:text-whisper-900 font-medium pointer-events-none flex items-center">
<b className={`mr-1`}>Size:</b> {calculateSize()}
(media?.stats?.size || media?.dimensions) && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-1`}>Size:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{calculateSize()}</span>
</p>
)
}
</div>
</li>
{
showForm && (
<Metadata
title={`Set metadata for: ${basename(parseWinPath(media.fsPath) || "")}`}
description={`Please specify the metadata you want to set for the file.`}
okBtnText={`Save`}
cancelBtnText={`Cancel`}
dismiss={() => setShowForm(false)}
trigger={submitMetadata}
isSaveDisabled={!filename}>
<div className="flex flex-col space-y-2">
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Filename
</label>
<div className="relative mt-1">
<input
className="py-1 px-2 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 w-full"
value={name}
onChange={(e) => setFilename(`${e.target.value}.${extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">
.{extension}
</span>
</div>
</div>
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Caption
</label>
<div className="mt-1">
<textarea
rows={3}
className="py-1 px-2 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 w-full"
value={caption || ''}
onChange={(e) => setCaption(e.target.value)}
/>
</div>
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Alt tag value
</label>
<div className="mt-1">
<input
className="py-1 px-2 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 w-full"
value={alt || ''}
onChange={(e) => setAlt(e.target.value)}
/>
</div>
</div>
</div>
</Metadata>
)
}
{
showAlert && (
<Alert

View File

@@ -5,7 +5,7 @@ import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
import { DashboardCommand } from '../../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { Header } from '../Header';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
@@ -15,6 +15,8 @@ import { List } from './List';
import { useDropzone } from 'react-dropzone'
import { useCallback } from 'react';
import { DashboardMessage } from '../../DashboardMessage';
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
import { FolderItem } from './FolderItem';
export interface IMediaProps {}
@@ -22,10 +24,10 @@ export const LIMIT = 16;
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom);
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
const viewData = useRecoilValue(ViewDataSelector);
@@ -51,12 +53,14 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
accept: 'image/*'
});
const messageListener = (message: MessageEvent<EventData<MediaPaths>>) => {
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.media) {
const data: MediaPaths = message.data.data as MediaPaths;
setLoading(false);
setMedia(message.data.data.media);
setTotal(message.data.data.total);
setFolders(message.data.data.folders);
setMedia(data.media);
setTotal(data.total);
setFolders(data.folders);
setSelectedFolder(data.selectedFolder);
}
};
@@ -95,6 +99,32 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
)
}
{
(media.length === 0 && folders.length === 0 && !loading) && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
<p className={`text-xl font-medium`}>No media files to show. You can drag &amp; drop new files.</p>
</div>
</div>
)
}
{
folders && folders.length > 0 && (
<div className={`mb-8`}>
<List>
{
folders && folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
</div>
)
}
<List>
{
media.map((file) => (

View File

@@ -3,8 +3,8 @@ import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { LoadingAtom, MediaTotalSelector, SelectedMediaFolderSelector } from '../../state';
import { FolderSelection } from './FolderSelection';
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector } from '../../state';
import { FolderCreation } from './FolderCreation';
import { LIMIT } from './Media';
import { PaginationButton } from './PaginationButton';
@@ -14,7 +14,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const totalMedia = useRecoilValue(MediaTotalSelector);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const [ page, setPage ] = React.useState<number>(0);
const [ page, setPage ] = useRecoilState(PageAtom);
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
@@ -82,7 +82,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.
</p>
</div>
<FolderSelection />
<FolderCreation />
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
<PaginationButton

View File

@@ -4,16 +4,18 @@ import * as React from 'react';
export interface IMenuItemProps {
title: string;
value: any;
isCurrent: boolean;
isCurrent?: boolean;
disabled?: boolean;
onClick: (value: any) => void;
}
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, disabled, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
return (
<Menu.Item>
<button
disabled={disabled}
onClick={() => onClick(value)}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100`}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium 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}
</button>

View File

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

View File

@@ -0,0 +1,92 @@
import { Dialog, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment, useRef } from 'react';
export interface IMetadataProps {
title: string;
description: string;
okBtnText: string;
cancelBtnText: string;
isSaveDisabled: boolean;
dismiss: () => void;
trigger: () => void;
}
export const Metadata: React.FunctionComponent<IMetadataProps> = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren<IMetadataProps>) => {
const cancelButtonRef = useRef(null);
return (
<Transition.Root show={true} as={Fragment}>
<Dialog className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef} onClose={() => dismiss()}>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white dark:bg-vulcan-500 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 border-whisper-900">
<div>
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-vulcan-300 dark:text-whisper-900">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-vulcan-500 dark:text-whisper-500">
{description}
</p>
</div>
<div className="mt-4">
{children}
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-teal-600 text-base font-medium text-white hover:bg-teal-700 dark:hover:bg-teal-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30"
onClick={() => trigger()}
disabled={isSaveDisabled}
>
{okBtnText}
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-200 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm"
onClick={() => dismiss()}
ref={cancelButtonRef}
>
{cancelBtnText}
</button>
</div>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@@ -1,6 +1,6 @@
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { REVIEW_LINK, SPONSOR_LINK } from '../../constants/Links';
import { REVIEW_LINK, SPONSOR_LINK } from '../../constants';
import { VersionInfo } from '../../models';
export interface ISponsorMsgProps {

View File

@@ -1,6 +1,6 @@
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants/Links';
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { FrontMatterIcon } from '../../panelWebView/components/Icons/FrontMatterIcon';
import { GitHubIcon } from '../../panelWebView/components/Icons/GitHubIcon';

View File

@@ -2,15 +2,27 @@ import * as React from "react";
import { render } from "react-dom";
import { RecoilRoot } from "recoil";
import { Dashboard } from "./components/Dashboard";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { SENTRY_LINK } from "../constants";
import './styles.css';
const elm = document.querySelector("#app");
const welcome = elm?.getAttribute("data-showWelcome");
const version = elm?.getAttribute("data-version");
const environment = elm?.getAttribute("data-environment");
Sentry.init({
dsn: SENTRY_LINK,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0, // No performance tracing required
release: version || "",
environment: environment || ""
});
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
setState: (data: T) => void;
postMessage: (msg: unknown) => void;
};
const elm = document.querySelector("#app");
const welcome = elm?.getAttribute("data-showWelcome");
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);

View File

@@ -6,7 +6,7 @@ export interface Page {
fmFileName: string;
fmModified: number;
fmDraft: "Draft" | "Published",
fmYear: number | null;
fmYear: number | null | undefined;
title: string;
slug: string;

View File

@@ -1,6 +1,7 @@
import { VersionInfo } from '../../models/VersionInfo';
import { ViewType } from '../state';
import { ContentFolder } from '../../models/ContentFolder';
import { ContentType } from '../../models';
export interface Settings {
beta: boolean;
@@ -14,4 +15,6 @@ export interface Settings {
versionInfo: VersionInfo;
pageViewType: ViewType | undefined;
mediaSnippet: string[];
contentTypes: ContentType[];
contentFolders: string[];
}

View File

@@ -0,0 +1,9 @@
import { selector } from 'recoil';
import { PageAtom } from '..';
export const PageSelector = selector({
key: 'PageSelector',
get: ({get}) => {
return get(PageAtom);
}
});

View File

@@ -5,6 +5,7 @@ export * from './GroupingSelector';
export * from './LoadingSelector';
export * from './MediaFoldersSelector';
export * from './MediaTotalSelector';
export * from './PageSelector';
export * from './SearchSelector';
export * from './SelectedMediaFolderSelector';
export * from './SettingsSelector';

View File

@@ -1,6 +1,6 @@
import { DashboardData } from '../models/DashboardData';
import { Template } from '../commands/Template';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
import * as os from 'os';
import { PanelSettings, CustomScript } from '../models/PanelSettings';
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
@@ -21,9 +21,8 @@ import { Preview } from '../commands/Preview';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { WebviewHelper } from '@estruyf/vscode';
import { Extension } from '../helpers/Extension';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
import { Dashboard } from '../commands/Dashboard';
import { ImageHelper } from '../helpers/ImageHelper';
const FILE_LIMIT = 10;
@@ -217,7 +216,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const wsFolder = Folders.getWorkspaceFolder();
const filePath = window.activeTextEditor?.document.uri.fsPath;
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const contentTypes = Settings.get<string>(SETTING_TAXONOMY_CONTENT_TYPES);
const articleDetails = this.getArticleDetails();
@@ -245,23 +243,28 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const contentType = ArticleHelper.getContentType(updatedMetadata);
if (contentType) {
const imageFields = contentType.fields.filter((field) => field.type === "image");
for (const field of imageFields) {
if (updatedMetadata[field.name]) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", updatedMetadata[field.name]);
const contentFolderPath = filePath ? join(dirname(filePath), updatedMetadata[field.name]) : null;
const imageData = ImageHelper.allRelToAbs(field, updatedMetadata[field.name])
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (contentFolderPath && existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (imageData) {
if (field.multiple && imageData instanceof Array) {
const preview = imageData.map(preview => preview && preview.absPath ? ({
...preview,
webviewUrl: this.panel?.webview.asWebviewUri(preview.absPath).toString()
}) : null);
if (previewUri) {
const preview = this.panel?.webview.asWebviewUri(previewUri);
updatedMetadata[field.name]= preview?.toString() || "";
updatedMetadata[field.name] = preview || [];
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
updatedMetadata[field.name] = {
...imageData,
webviewUrl: preview ? preview.toString() : null
};
}
} else {
updatedMetadata[field.name] = "";
updatedMetadata[field.name] = field.multiple ? [] : "";
}
}
}
@@ -295,7 +298,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
/**
* Update the metadata of the article
*/
public async updateMetadata({field, value}: { field: string, value: string }) {
public async updateMetadata({field, value }: { field: string, value: any, fieldData?: { multiple: boolean, value: string[] } }) {
if (!field) {
return;
}
@@ -312,14 +315,33 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const contentType = ArticleHelper.getContentType(article.data);
const dateFields = contentType.fields.filter((field) => field.type === "datetime");
const imageFields = contentType.fields.filter((field) => field.type === "image" && field.multiple);
for (const dateField of dateFields) {
if ((field === dateField.name) && value) {
article.data[field] = Article.formatDate(new Date(value));
} else {
} else if (!imageFields.find(f => f.name === field)) {
// Only override the field data if it is not an multiselect image field
article.data[field] = value;
}
}
for (const imageField of imageFields) {
if (field === imageField.name) {
// If value is an array, it means it comes from the explorer view itself (deletion)
if (Array.isArray(value)) {
article.data[field] = value || [];
} else { // Otherwise it is coming from the media dashboard (addition)
let fieldValue = article.data[field];
if (fieldValue && !Array.isArray(fieldValue)) {
fieldValue = [fieldValue];
}
const crntData = Object.assign([], fieldValue);
const allRelPaths = [...(crntData || []), value];
article.data[field] = [...new Set(allRelPaths)].filter(f => f);
}
}
}
ArticleHelper.update(editor, article);
this.pushMetadata(article.data);
@@ -611,13 +633,15 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const nonce = WebviewHelper.getNonce();
const version = Extension.getInstance().getVersion();
const ext = Extension.getInstance();
const version = ext.getVersion();
const isBeta = ext.isBetaVersion();
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleResetUri}" rel="stylesheet">
<link href="${styleVSCodeUri}" rel="stylesheet">
@@ -626,7 +650,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
<title>Front Matter</title>
</head>
<body>
<div id="app"></div>
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" ></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`panel-${version.installedVersion}`}" alt="Daily usage" />

View File

@@ -1,3 +1,4 @@
import { ContentType } from './helpers/ContentType';
import { Dashboard } from './commands/Dashboard';
import * as vscode from 'vscode';
import { Article, Settings, StatusListener } from './commands';
@@ -5,7 +6,7 @@ import { Folders } from './commands/Folders';
import { Preview } from './commands/Preview';
import { Project } from './commands/Project';
import { Template } from './commands/Template';
import { COMMAND_NAME } from './constants/Extension';
import { COMMAND_NAME } from './constants';
import { TaxonomyType } from './models';
import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider';
import { TagType } from './panelWebView/TagType';
@@ -13,6 +14,7 @@ import { ExplorerView } from './explorerView/ExplorerView';
import { Extension } from './helpers/Extension';
import { DashboardData } from './models/DashboardData';
import { Settings as SettingsHelper } from './helpers';
import { Content } from './commands/Content';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
@@ -32,6 +34,8 @@ export async function activate(context: vscode.ExtensionContext) {
SettingsHelper.init();
extension.migrateSettings();
SettingsHelper.checkToPromote();
collection = vscode.languages.createDiagnosticCollection('frontMatter');
@@ -104,7 +108,11 @@ export async function activate(context: vscode.ExtensionContext) {
const unregisterFolder = vscode.commands.registerCommand(COMMAND_NAME.unregisterFolder, Folders.unregister);
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Folders.create);
const createFolder = vscode.commands.registerCommand(COMMAND_NAME.createFolder, Folders.addMediaFolder);
const createByContentType = vscode.commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent);
const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create);
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create);
// Initialize command
Template.init();
@@ -184,8 +192,11 @@ export async function activate(context: vscode.ExtensionContext) {
registerFolder,
unregisterFolder,
createContent,
createByContentType,
createByTemplate,
projectInit,
collapseAll
collapseAll,
createFolder
);
}

View File

@@ -1,15 +1,19 @@
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
import { ContentType } from './../models/PanelSettings';
import * as vscode from 'vscode';
import * as matter from "gray-matter";
import * as fs from "fs";
import { DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES } from '../constants';
import { DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants';
import { DumpOptions } from 'js-yaml';
import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine';
import { Settings } from '.';
import { parse } from 'date-fns';
import { format, parse } from 'date-fns';
import { Notifications } from './Notifications';
import { Article } from '../commands';
import { basename, join } from 'path';
import { EditorHelper } from '@estruyf/vscode';
import sanitize from '../helpers/Sanitize';
import { existsSync, mkdirSync } from 'fs';
import { ContentType } from '../models';
export class ArticleHelper {
@@ -20,7 +24,7 @@ export class ArticleHelper {
*/
public static getFrontMatter(editor: vscode.TextEditor) {
const fileContents = editor.document.getText();
return ArticleHelper.parseFile(fileContents);
return ArticleHelper.parseFile(fileContents, editor.document.fileName);
}
/**
@@ -29,7 +33,7 @@ export class ArticleHelper {
*/
public static getFrontMatterByPath(filePath: string) {
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
return ArticleHelper.parseFile(file);
return ArticleHelper.parseFile(file, filePath);
}
/**
@@ -162,12 +166,63 @@ export class ArticleHelper {
return metadata;
}
/**
* Sanitize the value
* @param value
* @returns
*/
public static sanitize(value: string): string {
return sanitize(value.toLowerCase().replace(/ /g, "-"));
}
/**
* Create the file or folder for the new content
* @param contentType
* @param folderPath
* @param titleValue
* @returns The new file path
*/
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string): string | undefined {
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
// Name of the file or folder to create
const sanitizedName = ArticleHelper.sanitize(titleValue);
let newFilePath: string | undefined;
// Create a folder with the `index.md` file
if (contentType?.pageBundle) {
const newFolder = join(folderPath, sanitizedName);
if (existsSync(newFolder)) {
Notifications.error(`A page bundle with the name ${sanitizedName} already exists in ${folderPath}`);
return;
} else {
mkdirSync(newFolder);
newFilePath = join(newFolder, `index.md`);
}
} else {
let newFileName = `${sanitizedName}.md`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
}
newFilePath = join(folderPath, newFileName);
if (existsSync(newFilePath)) {
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
return;
}
}
return newFilePath;
}
/**
* Parse a markdown file and its front matter
* @param fileContents
* @returns
*/
private static parseFile(fileContents: string): matter.GrayMatterFile<string> | null {
private static parseFile(fileContents: string, fileName: string): matter.GrayMatterFile<string> | null {
try {
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
@@ -192,7 +247,22 @@ export class ArticleHelper {
}
}
} catch (error: any) {
Notifications.error(`There seems to be an issue parsing your Front Matter. ERROR: ${error.message || error}`);
const items = [{
title: "Check file",
action: async () => {
console.log(fileName);
await EditorHelper.showFile(fileName)
}
}];
Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => {
if (result?.title) {
const item = items.find(i => i.title === result.title);
if (item) {
item.action();
}
}
});
}
return null;
}

View File

@@ -0,0 +1,91 @@
import { ArticleHelper, Settings } from ".";
import { SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from "../constants";
import { ContentType as IContentType } from '../models';
import { Uri, workspace, window } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { format } from "date-fns";
import { join } from "path";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
export class ContentType {
/**
* Create content based on content types
* @returns
*/
public static async createContent() {
const selectedContentType = await Questions.SelectContentType();
if (!selectedContentType) {
return;
}
const selectedFolder = await Questions.SelectContentFolder();
if (!selectedFolder) {
return;
}
const contentTypes = ContentType.getAll();
const folders = Folders.get();
const location = folders.find(f => f.title === selectedFolder);
if (contentTypes && location) {
const folderPath = Folders.getFolderPath(Uri.file(location.path));
const contentType = contentTypes.find(ct => ct.name === selectedContentType);
if (folderPath && contentType) {
ContentType.create(contentType, folderPath);
}
}
}
/**
* Retrieve all content types
* @returns
*/
public static getAll() {
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
}
private static async create(contentType: IContentType, folderPath: string) {
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
return;
}
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
if (!newFilePath) {
return;
}
let data: any = {};
for (const field of contentType.fields) {
if (field.name === "title") {
data[field.name] = titleValue;
} else {
data[field.name] = null;
}
}
data = ArticleHelper.updateDates(Object.assign({}, data));
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
data['type'] = contentType.name;
}
const content = ArticleHelper.stringifyFrontMatter(``, data);
writeFileSync(newFilePath, content, { encoding: "utf8" });
const txtDoc = await workspace.openTextDocument(Uri.parse(newFilePath));
if (txtDoc) {
window.showTextDocument(txtDoc);
}
Notifications.info(`Your new content has been created.`);
}
}

64
src/helpers/DateHelper.ts Normal file
View File

@@ -0,0 +1,64 @@
import { parse, parseISO, parseJSON } from "date-fns";
export class DateHelper {
public static tryParse(date: any, format?: string): Date | null {
if (!date) {
return null;
}
if (date instanceof Date) {
return date;
}
if (typeof date === 'string') {
const jsonParsed = DateHelper.tryParseJson(date);
if (DateHelper.isValid(jsonParsed)) {
return jsonParsed;
}
const isoParsed = DateHelper.tryParseIso(date);
if (DateHelper.isValid(isoParsed)) {
return isoParsed;
}
if (format) {
const formatParsed = DateHelper.tryFormatParse(date, format);
if (DateHelper.isValid(formatParsed)) {
return formatParsed;
}
}
}
return null;
}
public static isValid(date: any): boolean {
return !isNaN(date.getTime());
}
public static tryFormatParse(date: string, format: string): Date | null {
try {
return parse(date, format, new Date());
} catch (err) {
return null;
}
}
public static tryParseJson(date: string): Date | null {
try {
return parseJSON(date);
} catch (err) {
return null;
}
}
public static tryParseIso(date: string): Date | null {
try {
return parseISO(date);
} catch (err) {
return null;
}
}
}

View File

@@ -1,11 +1,11 @@
import { basename } from "path";
import { existsSync, renameSync } from "fs";
import { basename, join } from "path";
import { extensions, Uri, ExtensionContext, window, workspace, commands } from "vscode";
import { Folders, WORKSPACE_PLACEHOLDER } from "../commands/Folders";
import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { EXTENSION_BETA_ID, EXTENSION_ID, EXTENSION_STATE_VERSION } from "../constants/Extension";
import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, DEFAULT_CONTENT_TYPE_NAME, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, DefaultFields, LocalStore, SETTING_TEMPLATES_FOLDER } from "../constants";
import { ContentType } from "../models";
import { Notifications } from "./Notifications";
import { parseWinPath } from "./parseWinPath";
import { Settings } from "./SettingsHelper";
@@ -32,7 +32,7 @@ export class Extension {
public getVersion(): { usedVersion: string | undefined, installedVersion: string } {
const frontMatter = extensions.getExtension(this.isBetaVersion() ? EXTENSION_BETA_ID : EXTENSION_ID)!;
let installedVersion = frontMatter.packageJSON.version;
const usedVersion = this.ctx.globalState.get<string>(EXTENSION_STATE_VERSION);
const usedVersion = this.ctx.globalState.get<string>(ExtensionState.Version);
if (this.isBetaVersion()) {
installedVersion = `${installedVersion}-beta`;
@@ -78,7 +78,7 @@ export class Extension {
* Set the current version information for the extension
*/
public setVersion(installedVersion: string): void {
this.ctx.globalState.update(EXTENSION_STATE_VERSION, installedVersion);
this.ctx.globalState.update(ExtensionState.Version, installedVersion);
}
/**
@@ -92,18 +92,35 @@ export class Extension {
* Migrate old settings to new settings
*/
public async migrateSettings(): Promise<void> {
const versionInfo = this.getVersion();
if (versionInfo.usedVersion === undefined) {
return;
}
if (!versionInfo.usedVersion) {
return;
}
// Split semantic version
const version = versionInfo.usedVersion.split('.');
const major = parseInt(version[0]);
const minor = parseInt(version[1]);
const patch = parseInt(version[2]);
// Migration to version 3.1.0
const folders = Settings.get<any>(SETTINGS_CONTENT_FOLDERS);
if (folders && folders.length > 0) {
const workspace = Folders.getWorkspaceFolder();
const projectFolder = basename(workspace?.fsPath || "");
if (major < 3 || (major === 3 && minor < 1)) {
const folders = Settings.get<any>(SETTINGS_CONTENT_FOLDERS);
if (folders && folders.length > 0) {
const workspace = Folders.getWorkspaceFolder();
const projectFolder = basename(workspace?.fsPath || "");
const paths = folders.map((folder: any) => ({
title: folder.title,
path: `${WORKSPACE_PLACEHOLDER}${folder.fsPath.split(projectFolder).slice(1).join('')}`.split('\\').join('/')
}));
const paths = folders.map((folder: any) => ({
title: folder.title,
path: `${WORKSPACE_PLACEHOLDER}${folder.fsPath.split(projectFolder).slice(1).join('')}`.split('\\').join('/')
}));
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, paths);
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, paths);
}
}
// Create team settings
@@ -112,56 +129,110 @@ export class Extension {
}
// Migration to version 4.0.0
const dateField = Settings.get<string>(SETTING_DATE_FIELD);
const lastModField = Settings.get<string>(SETTING_MODIFIED_FIELD);
const description = Settings.get<string>(SETTING_SEO_DESCRIPTION_FIELD);
const contentTypes = Settings.get<ContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
if (major < 4) {
const dateField = Settings.get<string>(SETTING_DATE_FIELD);
const lastModField = Settings.get<string>(SETTING_MODIFIED_FIELD);
const description = Settings.get<string>(SETTING_SEO_DESCRIPTION_FIELD);
const contentTypes = Settings.get<ContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
if (contentTypes) {
let needsUpdate = false;
let defaultContentType = contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
if (contentTypes) {
let needsUpdate = false;
let defaultContentType = contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
if (defaultContentType) {
if (dateField && dateField !== "date") {
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== "date");
defaultContentType.fields.push({
name: dateField,
type: "datetime"
});
needsUpdate = true;
// Check if fields need to be changed for the default content type
if (defaultContentType) {
if (dateField && dateField !== DefaultFields.PublishingDate) {
const newDateField = defaultContentType.fields.find(f => f.name === dateField);
if (!newDateField) {
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.PublishingDate);
defaultContentType.fields.push({
title: dateField,
name: dateField,
type: "datetime"
});
needsUpdate = true;
}
}
if (lastModField && lastModField !== DefaultFields.LastModified) {
const newModField = defaultContentType.fields.find(f => f.name === lastModField);
if (!newModField) {
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.LastModified);
defaultContentType.fields.push({
title: lastModField,
name: lastModField,
type: "datetime"
});
needsUpdate = true;
}
}
if (description && description !== DefaultFields.Description) {
const newDescField = defaultContentType.fields.find(f => f.name === description);
if (!newDescField) {
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.Description);
defaultContentType.fields.push({
title: description,
name: description,
type: "string"
});
needsUpdate = true;
}
}
if (needsUpdate) {
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
}
}
if (lastModField && lastModField !== "lastmod") {
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== "lastmod");
defaultContentType.fields.push({
name: lastModField,
type: "datetime"
});
needsUpdate = true;
}
if (description && description !== "description") {
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== "lastmod");
defaultContentType.fields.push({
name: description,
type: "string"
});
needsUpdate = true;
}
if (needsUpdate) {
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes);
}
}
// Migration to version 5
if (major <= 5) {
const isMoved = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.MoveTemplatesFolder);
if (!isMoved) {
const wsFolder= Folders.getWorkspaceFolder();
if (wsFolder) {
const templateFolder = join(parseWinPath(wsFolder.fsPath), `.templates`);
if (existsSync(templateFolder)) {
window.showInformationMessage(`Would you like to move your ".templates" folder to the new ".frontmatter" folder?`, 'Yes', 'No').then(async (result) => {
if (result === "Yes") {
const newFolderPath = join(parseWinPath(wsFolder.fsPath), LocalStore.rootFolder, LocalStore.templatesFolder);
renameSync(templateFolder, newFolderPath);
commands.executeCommand(`workbench.action.reloadWindow`);
Settings.update(SETTING_TEMPLATES_FOLDER, undefined, true);
Settings.update(SETTING_TEMPLATES_FOLDER, undefined);
} else if (result === "No") {
Settings.update(SETTING_TEMPLATES_FOLDER, `.templates`, true);
}
if (result === "No" || result === "Yes") {
Extension.getInstance().setState(ExtensionState.MoveTemplatesFolder, true);
}
});
}
}
}
}
}
public async setState(propKey: string, propValue: string): Promise<void> {
await this.ctx.globalState.update(propKey, propValue);
public async setState<T>(propKey: string, propValue: T, type: "workspace" | "global" = "global"): Promise<void> {
if (type === "global") {
await this.ctx.globalState.update(propKey, propValue);
} else {
await this.ctx.workspaceState.update(propKey, propValue);
}
}
public async getState<T>(propKey: string): Promise<T | undefined> {
return await this.ctx.globalState.get(propKey);
public async getState<T>(propKey: string, type: "workspace" | "global" = "global"): Promise<T | undefined> {
if (type === "global") {
return await this.ctx.globalState.get(propKey);
} else {
return await this.ctx.workspaceState.get(propKey);
}
}
public isBetaVersion() {

View File

@@ -1,5 +1,4 @@
import * as vscode from 'vscode';
import { EXTENSION_NAME } from '../constants';
import { Notifications } from './Notifications';
export class FilesHelper {

View File

@@ -0,0 +1,81 @@
import { Uri, window } from 'vscode';
import { dirname, join } from "path";
import { Field } from '../models';
import { existsSync } from 'fs';
import { Folders } from '../commands/Folders';
import { Settings } from './SettingsHelper';
import { SETTINGS_CONTENT_STATIC_FOLDER } from '../constants';
import { parseWinPath } from './parseWinPath';
export class ImageHelper {
/**
* Parse all images to use absolute paths
* @param field
* @param value
* @returns
*/
public static allRelToAbs(field: Field, value: string | string[] | undefined) {
const filePath = window.activeTextEditor?.document.uri.fsPath;
if (!filePath) {
return;
}
let previewUri = null;
if (field.multiple) {
if (Array.isArray(value)) {
previewUri = value.map(v => ({
original: v,
absPath: ImageHelper.relToAbs(filePath, v)
}));
}
} else {
if (typeof value === "string") {
return {
original: value,
absPath: ImageHelper.relToAbs(filePath, value)
};
}
}
return previewUri;
}
/**
* Parse relative path to absolute path
* @param filePath
* @param value
* @returns
*/
public static relToAbs(filePath: string, value: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
if (existsSync(staticPath)) {
return Uri.file(staticPath);
} else if (contentFolderPath && existsSync(contentFolderPath)) {
return Uri.file(contentFolderPath);
}
}
/**
* Parse absolute path to relative path
* @param imgValue
* @returns
*/
public static absToRel(imgValue: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
let relPath = imgValue || "";
if (imgValue) {
relPath = imgValue.split(parseWinPath(wsFolder?.fsPath || "")).pop() || "";
relPath = imgValue.split(staticFolder || "").pop() || "";
}
return relPath;
}
}

106
src/helpers/MediaLibrary.ts Normal file
View File

@@ -0,0 +1,106 @@
import { Dashboard } from '../commands/Dashboard';
import { workspace } from 'vscode';
import { JsonDB } from 'node-json-db/dist/JsonDB';
import { basename, dirname, join, parse } from 'path';
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders';
import { existsSync, renameSync } from 'fs';
import { Notifications } from './Notifications';
import { parseWinPath } from './parseWinPath';
import { LocalStore } from '../constants';
interface MediaRecord {
description: string;
alt: string;
}
export class MediaLibrary {
private db: JsonDB | undefined;
private static instance: MediaLibrary;
private constructor() {
const wsFolder = Folders.getWorkspaceFolder();
if (!wsFolder) {
return;
}
this.db = new JsonDB(join(parseWinPath(wsFolder?.fsPath || ""), LocalStore.rootFolder, LocalStore.contentFolder, LocalStore.mediaDatabaseFile), true, false, '/');
workspace.onDidRenameFiles(e => {
e.files.forEach(f => {
// Check if file is an image
if (f.oldUri.path.endsWith('.jpeg') ||
f.oldUri.path.endsWith('.jpg') ||
f.oldUri.path.endsWith('.png') ||
f.oldUri.path.endsWith('.gif')) {
this.rename(f.oldUri.fsPath, f.newUri.fsPath);
Dashboard.resetMedia();
}
});
});
}
public static getInstance(): MediaLibrary {
if (!MediaLibrary.instance) {
MediaLibrary.instance = new MediaLibrary();
}
return MediaLibrary.instance;
}
public get(id: string): MediaRecord | undefined {
try {
const fileId = this.parsePath(id);
if (this.db?.exists(fileId)) {
return this.db.getData(fileId);
}
return undefined;
} catch {
return undefined;
}
}
public set(id: string, metadata: any): void {
const fileId = this.parsePath(id);
this.db?.push(fileId, metadata, true);
}
public rename(oldId: string, newId: string): void {
const fileId = this.parsePath(oldId);
const newFileId = this.parsePath(newId);
const data = this.db?.getData(fileId);
if (data) {
this.db?.delete(fileId);
this.db?.push(newFileId, data, true);
}
}
public updateFilename(filePath: string, filename: string) {
const name = basename(filePath);
if (name !== filename && filename) {
try {
const oldFileInfo = parse(filePath);
const newFileInfo = parse(filename);
const newPath = join(dirname(filePath), `${newFileInfo.name}${oldFileInfo.ext}`);
if (existsSync(newPath)) {
Notifications.warning(`The name "${filename}" already exists at the file location.`);
} else {
renameSync(filePath, newPath);
this.rename(filePath, newPath);
Dashboard.resetMedia();
}
} catch(err) {
Notifications.error(`Something went wrong updating "${name}" to "${filename}".`);
}
}
}
private parsePath(path: string) {
const wsFolder = Folders.getWorkspaceFolder();
const isWindows = process.platform === 'win32';
let absPath = path.replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
return absPath.toLowerCase();
}
}

View File

@@ -4,15 +4,15 @@ import { EXTENSION_NAME } from "../constants";
export class Notifications {
public static info(message: string) {
window.showInformationMessage(`${EXTENSION_NAME}: ${message}`);
public static info(message: string, items?: any): Thenable<string> {
return window.showInformationMessage(`${EXTENSION_NAME}: ${message}`, items);
}
public static warning(message: string) {
window.showWarningMessage(`${EXTENSION_NAME}: ${message}`);
public static warning(message: string, items?: any): Thenable<string> {
return window.showWarningMessage(`${EXTENSION_NAME}: ${message}`, items);
}
public static error(message: string) {
window.showErrorMessage(`${EXTENSION_NAME}: ${message}`);
public static error(message: string, items?: any): Thenable<string> {
return window.showErrorMessage(`${EXTENSION_NAME}: ${message}`, items);
}
}

94
src/helpers/Questions.ts Normal file
View File

@@ -0,0 +1,94 @@
import { window } from 'vscode';
import { Folders } from '../commands/Folders';
import { ContentType } from './ContentType';
import { Notifications } from './Notifications';
export class Questions {
/**
* Yes/No question
* @param placeholder
* @returns
*/
public static async yesOrNo(placeholder: string) {
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: false });
return answer === "yes";
}
/**
* Specify the name of the content to create
* @param showWarning
* @returns
*/
public static async ContentTitle(showWarning: boolean = true): Promise<string | undefined> {
const title = await window.showInputBox({
prompt: `What would you like to use as a title for the content to create?`,
placeHolder: `Content title`
});
if (!title && showWarning) {
Notifications.warning(`You did not specify a title for your content.`);
return;
}
return title;
}
/**
* Select the folder for your content creation
* @param showWarning
* @returns
*/
public static async SelectContentFolder(showWarning: boolean = true): Promise<string | undefined> {
const folders = Folders.get();
let selectedFolder: string | undefined;
if (folders.length > 1) {
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
placeHolder: `Select where you want to create your content`
});
} else {
selectedFolder = folders[0].title;
}
if (!selectedFolder && showWarning) {
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
return;
}
return selectedFolder;
}
/**
* Select the content type to create new content
* @param showWarning
* @returns
*/
public static async SelectContentType(showWarning: boolean = true): Promise<string | undefined> {
const contentTypes = ContentType.getAll();
if (!contentTypes || contentTypes.length === 0) {
Notifications.warning("No content types found. Please create a content type first.");
return;
}
if (contentTypes.length === 1) {
return contentTypes[0].name;
}
const options = contentTypes.map(contentType => ({
label: contentType.name
}));
const selectedOption = await window.showQuickPick(options, {
placeHolder: `Select the content type to create your new content`,
canPickMany: false
});
if (!selectedOption && showWarning) {
Notifications.warning("No content type was selected.");
return;
}
return selectedOption?.label;
}
}

View File

@@ -1,16 +1,16 @@
import { Notifications } from './Notifications';
import { workspace } from 'vscode';
import { commands, Uri, workspace, window } from 'vscode';
import * as vscode from 'vscode';
import { TaxonomyType } from '../models';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY } from '../constants';
import { ContentType, TaxonomyType } from '../models';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, SETTINGS_CONTENT_STATIC_FOLDER, ExtensionState } from '../constants';
import { Folders } from '../commands/Folders';
import { join, basename } from 'path';
import { existsSync, readFileSync, watch, writeFileSync } from 'fs';
import { Extension } from './Extension';
export class Settings {
public static globalFile = "frontmatter.json";
private static config: vscode.WorkspaceConfiguration;
private static globalFile = "frontmatter.json";
private static globalConfig: any;
public static init() {
@@ -18,6 +18,7 @@ export class Settings {
if (fmConfig && existsSync(fmConfig)) {
const localConfig = readFileSync(fmConfig, 'utf8');
Settings.globalConfig = JSON.parse(localConfig);
commands.executeCommand('setContext', CONTEXT.isEnabled, true);
} else {
Settings.globalConfig = undefined;
}
@@ -33,6 +34,26 @@ export class Settings {
});
}
/**
* Check if the setting is present in the workspace and ask to promote them to the global settings
*/
public static async checkToPromote() {
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.SettingPromoted);
if (!isPromoted) {
if (Settings.hasSettings()) {
window.showInformationMessage(`You have local settings. Would you like to promote them to the global settings ("frontmatter.json")?`, 'Yes', 'No').then(async (result) => {
if (result === "Yes") {
Settings.promote();
}
if (result === "No" || result === "Yes") {
Extension.getInstance().setState(ExtensionState.SettingPromoted, true);
}
});
}
}
}
/**
* Check for config changes on global and local settings
* @param callback
@@ -115,6 +136,12 @@ export class Settings {
Settings.globalConfig = JSON.parse(localConfig);
Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value;
writeFileSync(fmConfig, JSON.stringify(Settings.globalConfig, null, 2), 'utf8');
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
if (workspaceSettingValue) {
await Settings.update(name, undefined);
}
return;
}
} else {
@@ -131,7 +158,10 @@ export class Settings {
*/
public static createTeamSettings() {
const wsFolder = Folders.getWorkspaceFolder();
this.createGlobalFile(wsFolder);
}
public static createGlobalFile(wsFolder: Uri | undefined | null) {
const initialConfig = {
"$schema": `https://${Extension.getInstance().isBetaVersion() ? `beta.` : ``}frontmatter.codes/frontmatter.schema.json`
};
@@ -195,6 +225,16 @@ export class Settings {
Notifications.info(`All settings promoted to team level.`);
}
/**
* Check if the setting is present in the workspace
* @param name
* @returns
*/
public static hasWorkspaceSettings<T>(name: string): T | undefined {
const setting = Settings.config.inspect<T>(name);
return (setting && typeof setting.workspaceValue !== "undefined") ? setting.workspaceValue : undefined;
}
/**
* Check if there are any Front Matter settings in the workspace
* @returns
@@ -219,7 +259,6 @@ export class Settings {
return hasSetting;
}
/**
* Check if its the project config
* @param filePath

View File

@@ -1,5 +1,4 @@
import { stopWords } from '../constants/stopwords-en';
import { charMap } from '../constants/charMap';
import { stopWords, charMap } from '../constants';
export class SlugHelper {

View File

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

View File

@@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
import { Settings } from '../dashboardWebView/models';
import { ContentType, PanelSettings } from '../models';
export default function useContentType(settings: PanelSettings | Settings | undefined | null, metadata: any) {
const [contentType, setContentType] = useState<ContentType>(DEFAULT_CONTENT_TYPE);
useEffect(() => {
if (settings) {
const contentTypeName = metadata.type as string || DEFAULT_CONTENT_TYPE_NAME;
let ct = settings.contentTypes.find(ct => ct.name === contentTypeName);
if (!ct) {
ct = settings.contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
}
if (!ct || !ct.fields) {
ct = DEFAULT_CONTENT_TYPE;
}
setContentType(ct || DEFAULT_CONTENT_TYPE)
}
}, [settings?.contentTypes, metadata?.data]);
return contentType;
}

5
src/models/Choice.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface Choice {
id: string;
title: string;
}

View File

@@ -1,13 +1,18 @@
import { Stats } from "fs";
import { ISizeCalculationResult } from "image-size/dist/types/interface";
export interface MediaPaths {
media: MediaInfo[];
total: number;
folders: string[];
selectedFolder: string;
}
export interface MediaInfo {
fsPath: string;
vsPath: string | undefined;
stats: Stats | undefined;
dimensions: ISizeCalculationResult | undefined;
caption?: string | undefined;
alt?: string | undefined;
}

View File

@@ -1,4 +1,5 @@
import { FileType } from "vscode";
import { Choice } from "./Choice";
import { DashboardData } from "./DashboardData";
export interface PanelSettings {
@@ -21,13 +22,19 @@ export interface PanelSettings {
export interface ContentType {
name: string;
fields: Field[];
pageBundle?: boolean;
}
export interface Field {
title?: string;
name: string;
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories";
choices?: string[];
choices?: string[] | Choice[];
single?: boolean;
multiple?: boolean;
isPreviewImage?: boolean;
hidden?: boolean;
}
export interface DateInfo {

View File

@@ -5,8 +5,6 @@ import { MessageHelper } from '../../helpers/MessageHelper';
import { Collapsible } from './Collapsible';
import { GlobalSettings } from './GlobalSettings';
import { OtherActions } from './OtherActions';
import { FileList } from './FileList';
import { VsLabel } from './VscodeComponents';
import { FolderAndFiles } from './FolderAndFiles';
import { SponsorMsg } from './SponsorMsg';

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
import * as Sentry from "@sentry/react";
import { VsLabel } from '../VscodeComponents';
export interface IFieldBoundaryProps {
fieldName: string;
}
export interface IFieldBoundaryState {
hasError: boolean;
}
export default class FieldBoundary extends React.Component<IFieldBoundaryProps, IFieldBoundaryState> {
constructor(props: IFieldBoundaryProps) {
super(props);
this.state = { hasError: false };
}
public static getDerivedStateFromError(error: any) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
public componentDidCatch(error: any, errorInfo: any) {
Sentry.captureMessage(`Field boundary: ${error?.message || error}`);
}
public render(): React.ReactElement<IFieldBoundaryProps> {
if (this.state.hasError) {
return (
<div className={`metadata_field`}>
<VsLabel>
<div className={`metadata_field__label`}>
<span style={{ lineHeight: "16px"}}>{this.props.fieldName}</span>
</div>
</VsLabel>
<div className={`metadata_field__error`}>
<span>Error loading field</span>
<button onClick={() => this.setState({ hasError: false })}>Retry</button>
</div>
</div>
);
}
return this.props.children as any;
}
}

View File

@@ -0,0 +1,20 @@
import { XIcon } from '@heroicons/react/outline';
import * as React from 'react';
export interface IChoiceButtonProps {
title: string;
value: string;
onClick: (value: string) => void;
}
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({title, value, onClick}: React.PropsWithChildren<IChoiceButtonProps>) => {
return (
<button
title={`Remove ${title}`}
className="metadata_field__choice__button"
onClick={() => onClick(value)}>
{title}
<XIcon className={`metadata_field__choice__button_icon`} />
</button>
);
};

View File

@@ -1,24 +1,78 @@
import { CheckIcon } from '@heroicons/react/outline';
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/outline';
import Downshift from 'downshift';
import * as React from 'react';
import { useEffect } from 'react';
import { Choice } from '../../../models/Choice';
import { VsLabel } from '../VscodeComponents';
import { ChoiceButton } from './ChoiceButton';
export interface IChoiceFieldProps {
label: string;
selected: string;
choices: string[];
onChange: (value: string) => void;
selected: string | string[];
choices: string[] | Choice[];
multiSelect?: boolean;
onChange: (value: string | string[]) => void;
}
export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label, selected, choices, onChange}: React.PropsWithChildren<IChoiceFieldProps>) => {
const [ crntSelected, setCrntSelected ] = React.useState<string | null>(selected);
export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label, selected, choices, multiSelect, onChange}: React.PropsWithChildren<IChoiceFieldProps>) => {
const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>(selected);
const dsRef = React.useRef<Downshift<string> | null>(null);
const onValueChange = (txtValue: string) => {
setCrntSelected(txtValue);
onChange(txtValue);
if (multiSelect) {
const newValue = [...(crntSelected || []) as string[], txtValue];
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected(txtValue);
onChange(txtValue);
}
};
const containsSelected = crntSelected && choices.indexOf(crntSelected) !== -1;
const removeSelected = (txtValue: string) => {
if (multiSelect) {
const newValue = [...(crntSelected || [])].filter(v => v !== txtValue);
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected("");
onChange("");
}
};
const getValue = (value: string | Choice, type: "id" | "title") => {
if (typeof value === 'string' || typeof value === 'number') {
return `${value}`;
}
return `${value[type]}`;
};
const getChoiceValue = (value: string) => {
const choice = (choices as Array<string | Choice>).find((c: string | Choice) => getValue(c, 'id') === value);
if (choice) {
return getValue(choice, 'title');
}
return "";
};
useEffect(() => {
if (crntSelected !== selected) {
setCrntSelected(selected);
}
}, [selected]);
const availableChoices = !multiSelect ? choices : (choices as Array<string | Choice>).filter((choice: string | Choice) => {
const value = typeof choice === 'string' || typeof choice === 'number' ? choice : choice.id;
if (typeof crntSelected === 'string') {
return crntSelected !== `${value}`;
} else if (crntSelected instanceof Array) {
return crntSelected.indexOf(`${value}`) === -1;
}
return true;
});
return (
<div className={`metadata_field`}>
<VsLabel>
@@ -26,17 +80,48 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label,
<CheckIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>
<select
value={crntSelected || ""}
placeholder={`Select from your ${label}`}
className={`metadata_field__choice`}
onChange={(e) => onValueChange(e.currentTarget.value)}>
{ !containsSelected && <option value='' disabled hidden></option> }
{choices.map((choice, index) => (
<option key={index} value={choice}>{choice}</option>
))}
</select>
<Downshift
ref={dsRef}
onChange={(selected) => onValueChange(selected || "")}
itemToString={item => (item ? item : '')}>
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
<div {...getRootProps(undefined, {suppressRefError: true})} className={`metadata_field__choice`}>
<button
{...getToggleButtonProps({
className: `metadata_field__choice__toggle`,
disabled: availableChoices.length === 0
})}>
<span>{`Select your ${label} value`}</span>
<ChevronDownIcon className="icon" />
</button>
<ul className={`metadata_field__choice_list ${isOpen ? "open" : "closed" }`} {...getMenuProps()}>
{
isOpen ? availableChoices.map((choice, index) => (
<li {...getItemProps({
key: getValue(choice, 'id'),
index,
item: getValue(choice, 'id'),
})}>
{ getValue(choice, 'title') || <span className={`metadata_field__choice_list__item`}>Clear value</span> }
</li>
)) : null
}
</ul>
</div>
)}
</Downshift>
{
crntSelected instanceof Array ? crntSelected.map((value: string) => (
<ChoiceButton key={value} value={value} title={getChoiceValue(value)} onClick={removeSelected} />
)) : (
crntSelected && (
<ChoiceButton key={crntSelected} value={crntSelected} title={getChoiceValue(crntSelected)} onClick={removeSelected} />
)
)
}
</div>
);
};

View File

@@ -3,6 +3,7 @@ import { VsLabel } from '../VscodeComponents';
import { ClockIcon } from '@heroicons/react/outline';
import DatePicker from 'react-datepicker';
import { forwardRef } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
export interface IDateTimeFieldProps {
label: string;
@@ -22,7 +23,7 @@ const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ value, onClick }
});
export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({label, date, format, onChange}: React.PropsWithChildren<IDateTimeFieldProps>) => {
const [ dateValue, setDateValue ] = React.useState<Date | null>(date);
const [ dateValue, setDateValue ] = React.useState<Date | null>(null);
const onDateChange = (date: Date) => {
setDateValue(date);
@@ -30,7 +31,10 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
};
React.useEffect(() => {
if (dateValue?.toISOString() !== date?.toISOString()) {
const crntValue = DateHelper.tryParse(date, format);
const stateValue = DateHelper.tryParse(dateValue, format);
if (crntValue?.toISOString() !== stateValue?.toISOString()) {
setDateValue(date);
}
}, [ date ]);
@@ -45,7 +49,7 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
<div className={`metadata_field__datetime`}>
<DatePicker
selected={dateValue as Date}
selected={dateValue as Date || new Date()}
onChange={onDateChange}
timeInputLabel="Time:"
dateFormat={format || "MM/dd/yyyy HH:mm"}

View File

@@ -1,5 +1,6 @@
import { CalculatorIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useEffect } from 'react';
import { VsLabel } from '../VscodeComponents';
export interface INumberFieldProps {
@@ -21,6 +22,12 @@ export const NumberField: React.FunctionComponent<INumberFieldProps> = ({label,
onChange(newValue);
};
useEffect(() => {
if (nrValue !== value) {
setNrValue(value);
}
}, [value]);
return (
<div className={`metadata_field`}>
<VsLabel>

View File

@@ -0,0 +1,17 @@
import * as React from 'react';
import { PreviewImageValue } from './PreviewImageField';
export interface IPreviewImageProps {
value: PreviewImageValue;
onRemove: (value: string) => void;
}
export const PreviewImage: React.FunctionComponent<IPreviewImageProps> = ({ value, onRemove }: React.PropsWithChildren<IPreviewImageProps>) => {
return (
<div className={`metadata_field__preview_image__preview`}>
<img src={value.webviewUrl} />
<button type="button" onClick={() => onRemove(value.original)} className={`metadata_field__preview_image__remove`}>Remove image</button>
</div>
);
};

View File

@@ -3,24 +3,38 @@ import * as React from 'react';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
import { PreviewImage } from './PreviewImage';
export interface PreviewImageValue {
original: string;
webviewUrl: string;
}
export interface IPreviewImageFieldProps {
label: string;
fieldName: string;
value: string | null;
value: PreviewImageValue | PreviewImageValue[] | null;
filePath: string | null;
onChange: (value: string | null) => void;
multiple?: boolean;
onChange: (value: string | string[] | null) => void;
}
export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps> = ({label, fieldName, onChange, value, filePath}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps> = ({label, fieldName, onChange, value, filePath, multiple}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
const selectImage = () => {
MessageHelper.sendMessage(CommandToCode.selectImage, {
filePath,
fieldName
filePath: filePath,
fieldName,
value,
multiple
});
};
const onImageRemove = (imageToRemove: string) => {
const newValue = value && Array.isArray(value) ? value.filter(image => image.original !== imageToRemove).map(i => i.original) : null;
onChange(newValue);
}
return (
<div className={`metadata_field`}>
<VsLabel>
@@ -29,16 +43,27 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
</div>
</VsLabel>
<div className={`metadata_field__preview_image`}>
<div className={`metadata_field__preview_image ${multiple && value && (value as PreviewImageValue[]).length > 0 ? `metadata_field__multiple_images` : ''}`}>
{
value ? (
<div>
<img src={value} />
(!value || multiple) && (
<button className={`metadata_field__preview_image__button`} title={`Add your ${label?.toLowerCase() || "image"}`} type="button" onClick={selectImage}>
<PhotographIcon />
<span className="mt-2 block text-sm font-medium text-gray-900">Add your {label?.toLowerCase() || "image"}</span>
</button>
)
}
<button onClick={() => onChange(null)} className={`metadata_field__preview_image__remove`}>Remove image</button>
</div>
) : (
<button onClick={selectImage}>Select image</button>
{
value && !Array.isArray(value) && (
<PreviewImage value={value} onRemove={() => onChange(null)} />
)
}
{
multiple && value && Array.isArray(value) && (
value.map(image => (
<PreviewImage key={image.original} value={image} onRemove={onImageRemove} />
))
)
}
</div>

View File

@@ -5,12 +5,13 @@ import { VsLabel } from '../VscodeComponents';
export interface ITextFieldProps {
label: string;
value: string | null;
singleLine: boolean | undefined;
limit: number | undefined;
rows?: number;
onChange: (txtValue: string) => void;
}
export const TextField: React.FunctionComponent<ITextFieldProps> = ({limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
const [ text, setText ] = React.useState<string | null>(value);
const onTextChange = (txtValue: string) => {
@@ -37,13 +38,28 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({limit, labe
</div>
</VsLabel>
<textarea
className={`metadata_field__textarea`}
rows={rows || 2}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)} style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
{
singleLine ? (
<input
className={`metadata_field__input`}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
) : (
<textarea
className={`metadata_field__textarea`}
rows={rows || 2}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
)
}
{
limit && limit > 0 && (text || "").length > limit && (

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { FileIcon } from './Icons/FileIcon';
import { MarkdownIcon } from './Icons/MarkdownIcon';
export interface IFileItemProps {
name: string;
path: string;
}
export const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }: React.PropsWithChildren<IFileItemProps>) => {
const openFile = () => {
MessageHelper.sendMessage(CommandToCode.openInEditor, path);
};
return (
<li className={`file_list__items__item`}
onClick={openFile}>
{
(name.endsWith('.md') || name.endsWith('.mdx')) ? (
<MarkdownIcon />
) : (
<FileIcon />
)
}
<span>{name}</span>
</li>
);
};

View File

@@ -1,9 +1,6 @@
import * as React from 'react';
import { FileInfo } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { FileIcon } from './Icons/FileIcon';
import { MarkdownIcon } from './Icons/MarkdownIcon';
import { FileItem } from './FileItem';
import { VsLabel } from './VscodeComponents';
export interface IFileListProps {
@@ -13,10 +10,6 @@ export interface IFileListProps {
}
export const FileList: React.FunctionComponent<IFileListProps> = ({files, folderName, totalFiles}: React.PropsWithChildren<IFileListProps>) => {
const openFile = (filePath: string) => {
MessageHelper.sendMessage(CommandToCode.openInEditor, filePath);
};
if (!files || files.length === 0) {
return null;
@@ -28,20 +21,8 @@ export const FileList: React.FunctionComponent<IFileListProps> = ({files, folder
<ul className="file_list__items">
{
files.map(file => (
<li key={file.fileName}
className={`file_list__items__item`}
onClick={() => openFile(file.filePath)}>
{
(file.fileName.endsWith('.md') || file.fileName.endsWith('.mdx')) ? (
<MarkdownIcon />
) : (
<FileIcon />
)
}
<span>{file.fileName}</span>
</li>
(files && files.length > 0) && files.map(file => (
<FileItem key={file.filePath} name={file.fileName} path={file.filePath} />
))
}
</ul>

View File

@@ -48,11 +48,11 @@ export const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({s
<Collapsible id={`${isBase ? "base_" : ""}settings`} className={`base__actions`} title="Global settings">
<div className={`base__action`}>
<VsLabel>Modified date</VsLabel>
<VsCheckbox label="Auto-update modified date" checked={modifiedDateUpdate} onClick={onDateCheck} />
<VsCheckbox checked={modifiedDateUpdate} onClick={onDateCheck}>Auto-update modified date</VsCheckbox>
</div>
<div className={`base__action`}>
<VsLabel>Front Matter highlight</VsLabel>
<VsCheckbox label="Highlight Front Matter" checked={fmHighlighting} onClick={onHighlightCheck} />
<VsCheckbox checked={fmHighlighting} onClick={onHighlightCheck}>Highlight Front Matter</VsCheckbox>
</div>
<div className={`base__action`}>
<VsLabel>Local preview</VsLabel>

View File

@@ -8,15 +8,16 @@ import { Toggle } from './Fields/Toggle';
import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
import { TagIcon } from './Icons/TagIcon';
import { TagPicker } from './TagPicker';
import { parseJSON } from 'date-fns';
import { DateTimeField } from './Fields/DateTimeField';
import { TextField } from './Fields/TextField';
import "react-datepicker/dist/react-datepicker.css";
import { PreviewImageField } from './Fields/PreviewImageField';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from '../../constants/ContentType';
import { PreviewImageField, PreviewImageValue } from './Fields/PreviewImageField';
import { ListUnorderedIcon } from './Icons/ListUnorderedIcon';
import { NumberField } from './Fields/NumberField';
import { ChoiceField } from './Fields/ChoiceField';
import useContentType from '../../hooks/useContentType';
import { DateHelper } from '../../helpers/DateHelper';
import FieldBoundary from './ErrorBoundary/FieldBoundary';
export interface IMetadataProps {
settings: PanelSettings | undefined;
@@ -26,6 +27,7 @@ export interface IMetadataProps {
}
export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren<IMetadataProps>) => {
const contentType = useContentType(settings, metadata);
const sendUpdate = (field: string | undefined, value: any) => {
if (!field) {
@@ -38,52 +40,46 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
});
};
const getDate = (date: string | Date) => {
if (typeof date === 'string') {
return parseJSON(date);
}
return date;
const getDate = (date: string | Date): Date | null => {
const parsedDate = DateHelper.tryParse(date, settings?.date?.format);
return parsedDate || date as Date | null;
}
if (!settings) {
return null;
}
const contentTypeName = metadata.type as string || DEFAULT_CONTENT_TYPE_NAME;
let contentType = settings.contentTypes.find(ct => ct.name === contentTypeName);
if (!contentType) {
contentType = settings.contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
}
if (!contentType || !contentType.fields) {
contentType = DEFAULT_CONTENT_TYPE;
}
const renderFields = (ctFields: Field[]) => {
if (!ctFields) {
return;
}
return ctFields.map(field => {
if (field.hidden) {
return null;
}
if (field.type === 'datetime') {
const dateValue = metadata[field.name] ? getDate(metadata[field.name] as string) : null;
return (
<DateTimeField
key={field.name}
label={field.title || field.name}
date={dateValue}
format={settings?.date?.format}
onChange={(date => sendUpdate(field.name, date))} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<DateTimeField
label={field.title || field.name}
date={dateValue}
format={settings?.date?.format}
onChange={(date => sendUpdate(field.name, date))} />
</FieldBoundary>
);
} else if (field.type === 'boolean') {
return (
<Toggle
key={field.name}
label={field.title || field.name}
checked={!!metadata[field.name] as any}
onChanged={(checked) => sendUpdate(field.name, checked)} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<Toggle
key={field.name}
label={field.title || field.name}
checked={!!metadata[field.name] as any}
onChanged={(checked) => sendUpdate(field.name, checked)} />
</FieldBoundary>
);
} else if (field.type === 'string') {
const textValue = metadata[field.name];
@@ -96,13 +92,15 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
}
return (
<TextField
key={field.name}
label={field.title || field.name}
limit={limit}
rows={3}
onChange={(value) => sendUpdate(field.name, value)}
value={textValue as string || null} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<TextField
label={field.title || field.name}
singleLine={field.single}
limit={limit}
rows={3}
onChange={(value) => sendUpdate(field.name, value)}
value={textValue as string || null} />
</FieldBoundary>
);
} else if (field.type === 'number') {
const fieldValue = metadata[field.name];
@@ -112,60 +110,70 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
}
return (
<NumberField
key={field.name}
label={field.title || field.name}
onChange={(value) => sendUpdate(field.name, value)}
value={nrValue} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<NumberField
key={field.name}
label={field.title || field.name}
onChange={(value) => sendUpdate(field.name, value)}
value={nrValue} />
</FieldBoundary>
);
} else if (field.type === 'image') {
return (
<PreviewImageField
key={field.name}
label={field.title || field.name}
fieldName={field.name}
filePath={metadata.filePath as string}
value={metadata[field.name] as string}
onChange={(value => sendUpdate(field.name, value))} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<PreviewImageField
label={field.title || field.name}
fieldName={field.name}
filePath={metadata.filePath as string}
value={metadata[field.name] as PreviewImageValue | PreviewImageValue[] | null}
multiple={field.multiple}
onChange={(value => sendUpdate(field.name, value))} />
</FieldBoundary>
);
} else if (field.type === 'choice') {
const choices = field.choices || [];
const choiceValue = metadata[field.name];
return (
<ChoiceField
key={field.name}
label={field.title || field.name}
selected={choiceValue as string}
choices={choices}
onChange={(value => sendUpdate(field.name, value))} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<ChoiceField
label={field.title || field.name}
selected={choiceValue as string}
choices={choices}
multiSelect={field.multiple}
onChange={(value => sendUpdate(field.name, value))} />
</FieldBoundary>
);
} else if (field.type === 'tags') {
return (
<TagPicker
key={field.name}
type={TagType.tags}
label={field.title || field.name}
icon={<TagIcon />}
crntSelected={metadata[field.name] as string[] || []}
options={settings?.tags || []}
freeform={settings.freeform}
focussed={focusElm === TagType.tags}
unsetFocus={unsetFocus} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<TagPicker
type={TagType.tags}
label={field.title || field.name}
icon={<TagIcon />}
crntSelected={metadata[field.name] as string[] || []}
options={settings?.tags || []}
freeform={settings.freeform}
focussed={focusElm === TagType.tags}
unsetFocus={unsetFocus} />
</FieldBoundary>
);
} else if (field.type === 'categories') {
return (
<TagPicker
key={field.name}
type={TagType.categories}
label={field.title || field.name}
icon={<ListUnorderedIcon />}
crntSelected={metadata.categories as string[] || []}
options={settings.categories}
freeform={settings.freeform}
focussed={focusElm === TagType.categories}
unsetFocus={unsetFocus} />
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<TagPicker
type={TagType.categories}
label={field.title || field.name}
icon={<ListUnorderedIcon />}
crntSelected={metadata.categories as string[] || []}
options={settings.categories}
freeform={settings.freeform}
focussed={focusElm === TagType.categories}
unsetFocus={unsetFocus} />
</FieldBoundary>
);
} else {
return null;
}
});
};
@@ -178,14 +186,17 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
}
{
<TagPicker type={TagType.keywords}
icon={<SymbolKeywordIcon />}
crntSelected={metadata.keywords as string[] || []}
options={[]}
freeform={true}
focussed={focusElm === TagType.keywords}
unsetFocus={unsetFocus}
disableConfigurable />
<FieldBoundary fieldName={`Keywords`}>
<TagPicker
type={TagType.keywords}
icon={<SymbolKeywordIcon />}
crntSelected={metadata.keywords as string[] || []}
options={[]}
freeform={true}
focussed={focusElm === TagType.keywords}
unsetFocus={unsetFocus}
disableConfigurable />
</FieldBoundary>
}
</Collapsible>
);

View File

@@ -13,6 +13,22 @@ export interface ISeoKeywordsProps {
export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
const validateKeywords = () => {
if (!keywords) {
return [];
}
if (typeof keywords === 'string') {
return [keywords];
}
if (Array.isArray(keywords)) {
return keywords;
}
return [];
}
if (!keywords || keywords.length === 0) {
return null;
}
@@ -31,7 +47,7 @@ export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keyword
</VsTableHeader>
<VsTableBody slot="body">
{
keywords.map((keyword, index) => {
validateKeywords().map((keyword, index) => {
return (
<SeoKeywordInfo key={index} keyword={keyword} {...data} />
);

View File

@@ -83,7 +83,7 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
if (selectedItem) {
let value = selectedItem || "";
const item = options.find(o => o.toLowerCase() === selectedItem.toLowerCase());
const item = options.find(o => o?.toLowerCase() === selectedItem?.toLowerCase());
if (item) {
value = item;
}
@@ -187,7 +187,7 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
</Downshift>
<Tags
values={(selected || []).sort((a: string, b: string) => a.toLowerCase() < b.toLowerCase() ? -1 : 1 )}
values={(selected || []).sort((a: string, b: string) => a?.toLowerCase() < b?.toLowerCase() ? -1 : 1 )}
onRemove={onRemove}
onCreate={onCreate}
options={options}

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