Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2a0934c2 | ||
|
|
d3eb7b223c | ||
|
|
f74eec954f | ||
|
|
864c4e7aa6 | ||
|
|
5667906caf | ||
|
|
2fe7c524e7 | ||
|
|
5cc83526ad | ||
|
|
76b5e99a08 | ||
|
|
7d5505d421 | ||
|
|
d97a11f8b5 | ||
|
|
0590cec684 | ||
|
|
b4cdc4feb9 | ||
|
|
986fd95524 | ||
|
|
f51fec5fb9 | ||
|
|
8198ce2af3 | ||
|
|
defffc4c8e | ||
|
|
8f47cbfb0b | ||
|
|
0be91c17d0 | ||
|
|
fae7ab8417 | ||
|
|
df239f2cc0 | ||
|
|
70099dc97f | ||
|
|
c11be0e3ec | ||
|
|
f8b7870180 | ||
|
|
c58a5c62d9 | ||
|
|
ce92444bf2 | ||
|
|
b2709ebffd | ||
|
|
2b20cf9d24 | ||
|
|
f4a499ad0f | ||
|
|
4494b158c0 | ||
|
|
3416e55264 | ||
|
|
9b53e31cd5 | ||
|
|
f49b93b042 | ||
|
|
a005930c14 | ||
|
|
9dea1ee6ed | ||
|
|
1ea0999d17 | ||
|
|
2e6a466ba5 | ||
|
|
fbcd430dc6 | ||
|
|
2bd910db47 | ||
|
|
8158c9a483 | ||
|
|
4622fbe757 | ||
|
|
97a635c2de | ||
|
|
e8c67c75fd | ||
|
|
6151ecb4c1 | ||
|
|
1d8c192c07 | ||
|
|
014911b7a9 | ||
|
|
7b2d7b8aa5 | ||
|
|
caceed2d4c | ||
|
|
476ec6c2fd | ||
|
|
5374edfa01 | ||
|
|
6a0cac9dfb | ||
|
|
b525a6a211 | ||
|
|
c295761560 | ||
|
|
6154164b4b | ||
|
|
05ce2d3537 | ||
|
|
544f24bcba | ||
|
|
1c354ed976 | ||
|
|
9b9bf1bfbe | ||
|
|
29d5f02d10 | ||
|
|
9bc2fbc141 | ||
|
|
cac009b773 | ||
|
|
bf1639cac7 | ||
|
|
5f28e145c4 | ||
|
|
8199ab964e | ||
|
|
14c050e34b | ||
|
|
b96411d1f9 | ||
|
|
87c469a6c7 | ||
|
|
c29aef03f0 | ||
|
|
9eaf94de7a | ||
|
|
f5e7526fae | ||
|
|
579e4925c8 | ||
|
|
4a4c558d9d | ||
|
|
9bb34850e2 | ||
|
|
c6e37532bc | ||
|
|
221f962beb | ||
|
|
17566279e2 | ||
|
|
4e7488414d | ||
|
|
c206817431 | ||
|
|
995c6fff2b | ||
|
|
c216062537 | ||
|
|
80a27a1e5e | ||
|
|
b8f5e10f4b | ||
|
|
d8bffdcf6c | ||
|
|
71c0360523 | ||
|
|
3260c2b3e0 | ||
|
|
a523c4ab63 | ||
|
|
24f69ea819 | ||
|
|
12d816b761 | ||
|
|
0e13a3783f | ||
|
|
09e5f3aa7f | ||
|
|
5991db1a83 |
7
.github/ISSUE_TEMPLATE/feedback_request.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Feedback
|
||||
about: Tell more on what you think
|
||||
title: 'Feedback: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
54
CHANGELOG.md
@@ -1,5 +1,59 @@
|
||||
# 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
|
||||
- [#115](https://github.com/estruyf/vscode-front-matter/issues/115): Fix for updating added categories/tags
|
||||
- [#116](https://github.com/estruyf/vscode-front-matter/issues/116): Fix for not showing the `-1` limit on inputs
|
||||
|
||||
## [4.0.0] - 2021-09-22 - [Release Notes](https://beta.frontmatter.codes/updates/v4_0_0)
|
||||
|
||||
- [#101](https://github.com/estruyf/vscode-front-matter/issues/101): Date picker available on the metadata section
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 align="center">
|
||||
<a href="https://beta.frontmatter.codes">
|
||||
<img alt="Front Matter BETA" src="./assets/frontmatter-beta.png">
|
||||
<a href="https://frontmatter.codes">
|
||||
<img alt="Front Matter" src="https://frontmatter.codes/assets/frontmatter-social.png">
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||

|
||||
|
||||
## What is Front Matter?
|
||||
|
||||
Front Matter BETA is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
@@ -98,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>
|
||||
|
||||
|
||||
28
README.md
@@ -1,6 +1,6 @@
|
||||
<h1 align="center">
|
||||
<a href="https://frontmatter.codes">
|
||||
<img alt="Front Matter" src="./assets/frontmatter-teal-128x128.png">
|
||||
<img alt="Front Matter" src="https://frontmatter.codes/assets/frontmatter-social.png">
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||

|
||||
|
||||
## What is Front Matter?
|
||||
|
||||
Front Matter is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
@@ -96,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>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 14 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
@@ -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 |
@@ -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 |
BIN
assets/frontmatter-short-min.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
16
assets/frontmatter-short-min.svg
Normal 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 |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.7 KiB |
1
assets/frontmatter-teal-min.svg
Normal 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 |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
1
assets/icons/frontmatter-dark.svg
Normal 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 |
1
assets/icons/frontmatter-light.svg
Normal 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 |
12
assets/icons/frontmatter-short-dark.svg
Normal 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 |
12
assets/icons/frontmatter-short-light.svg
Normal 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 |
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "4.0.0",
|
||||
"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",
|
||||
|
||||
78
package.json
@@ -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.0",
|
||||
"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",
|
||||
@@ -658,4 +712,4 @@
|
||||
"dependencies": {
|
||||
"@docsearch/js": "^3.0.0-alpha.40"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 || ``));
|
||||
let imgPath = data.image;
|
||||
const filePath = data.file;
|
||||
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
|
||||
|
||||
const imgDir = dirname(absImgPath);
|
||||
const fileDir = dirname(filePath);
|
||||
|
||||
if (imgDir === fileDir) {
|
||||
imgPath = join('/', basename(imgPath));
|
||||
|
||||
// Snippets are already parsed, so update the URL of the image
|
||||
if (data.snippet) {
|
||||
data.snippet = data.snippet.replace(data.image, imgPath);
|
||||
}
|
||||
}
|
||||
|
||||
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || ``));
|
||||
}
|
||||
panel.getMediaSelection();
|
||||
} else {
|
||||
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" />
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default';
|
||||
|
||||
export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
8
src/constants/ExtensionState.ts
Normal 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`,
|
||||
};
|
||||
@@ -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";
|
||||
8
src/constants/LocalStore.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const LocalStore = {
|
||||
rootFolder: ".frontmatter",
|
||||
contentFolder: "content",
|
||||
templatesFolder: "templates",
|
||||
mediaDatabaseFile: "mediaDb.json"
|
||||
}
|
||||
1
src/constants/Navigation.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const HOME_PAGE_NAVIGATION_ID = "FrontMatter:RootFolder";
|
||||
@@ -1,5 +1,6 @@
|
||||
export const CONTEXT = {
|
||||
canInit: "frontMatterCanInit",
|
||||
canOpenPreview: "frontMatterCanOpenPreview",
|
||||
canOpenDashboard: "frontMatterCanOpenDashboard"
|
||||
canOpenDashboard: "frontMatterCanOpenDashboard",
|
||||
isEnabled: "frontMatter:enabled"
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -3,5 +3,5 @@ export enum DashboardCommand {
|
||||
pages = "pages",
|
||||
settings = "settings",
|
||||
media = "media",
|
||||
viewData = "viewData",
|
||||
viewData = "viewData"
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
52
src/dashboardWebView/components/ChoiceButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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`} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
99
src/dashboardWebView/components/Header/Breadcrumb.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
28
src/dashboardWebView/components/Media/FolderCreation.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
29
src/dashboardWebView/components/Media/FolderItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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) => (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
92
src/dashboardWebView/components/Modals/Metadata.tsx
Normal 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">
|
||||
​
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
@@ -6,7 +6,7 @@ export interface Page {
|
||||
fmFileName: string;
|
||||
fmModified: number;
|
||||
fmDraft: "Draft" | "Published",
|
||||
fmYear: number | null;
|
||||
fmYear: number | null | undefined;
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
|
||||
@@ -1,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[];
|
||||
}
|
||||
9
src/dashboardWebView/state/selectors/PageSelector.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { selector } from 'recoil';
|
||||
import { PageAtom } from '..';
|
||||
|
||||
export const PageSelector = selector({
|
||||
key: 'PageSelector',
|
||||
get: ({get}) => {
|
||||
return get(PageAtom);
|
||||
}
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
@@ -433,7 +455,9 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
this.pushMetadata(article!.data);
|
||||
if (article?.data) {
|
||||
this.pushMetadata(article!.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -609,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">
|
||||
@@ -624,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" />
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
91
src/helpers/ContentType.ts
Normal 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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { basename } from "path";
|
||||
import { extensions, Uri, ExtensionContext } from "vscode";
|
||||
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 { 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,14 +32,39 @@ 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`;
|
||||
}
|
||||
|
||||
if (usedVersion !== installedVersion) {
|
||||
Notifications.info(`Find out what is new at [v${installedVersion} release notes](https://${this.isBetaVersion() ? 'beta.' : ''}frontmatter.codes/updates)`);
|
||||
const whatIsNewTitle = `Check the changelog`;
|
||||
const githubTitle = `Give it a ⭐️`;
|
||||
|
||||
const whatIsNew = {
|
||||
title: whatIsNewTitle,
|
||||
run: () => {
|
||||
const uri = Uri.file(`${Extension.getInstance().extensionPath.fsPath}/CHANGELOG.md`);
|
||||
workspace.openTextDocument(uri).then((() => {
|
||||
commands.executeCommand("markdown.showPreview", uri)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const starGitHub = {
|
||||
title: githubTitle,
|
||||
run: () => {
|
||||
commands.executeCommand('vscode.open', Uri.parse(GITHUB_LINK));
|
||||
}
|
||||
};
|
||||
|
||||
window.showInformationMessage(`${EXTENSION_NAME} has been updated to v${installedVersion} — check out what's new!`, starGitHub, whatIsNew).then((selection => {
|
||||
if (selection?.title === whatIsNewTitle || selection?.title === githubTitle) {
|
||||
selection.run();
|
||||
}
|
||||
}));
|
||||
|
||||
this.setVersion(installedVersion);
|
||||
}
|
||||
|
||||
@@ -53,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,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
|
||||
@@ -87,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() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { EXTENSION_NAME } from '../constants';
|
||||
import { Notifications } from './Notifications';
|
||||
|
||||
export class FilesHelper {
|
||||
|
||||
81
src/helpers/ImageHelper.ts
Normal 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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, writeFileSync } from 'fs';
|
||||
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,17 +34,47 @@ 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
|
||||
*/
|
||||
public static onConfigChange(callback: (global?: any) => void) {
|
||||
const projectConfig = Settings.projectConfigPath;
|
||||
|
||||
workspace.onDidChangeConfiguration(() => {
|
||||
callback();
|
||||
});
|
||||
|
||||
// Background listener for when it is not a user interaction
|
||||
if (projectConfig && existsSync(projectConfig)) {
|
||||
watch(projectConfig, () => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
workspace.onDidSaveTextDocument(async (e) => {
|
||||
const filename = e.uri.fsPath;
|
||||
|
||||
if (Settings.checkProjectConfig(filename)) {
|
||||
const file = await workspace.openTextDocument(e.uri);
|
||||
if (file) {
|
||||
@@ -105,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 {
|
||||
@@ -121,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`
|
||||
};
|
||||
@@ -185,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
|
||||
@@ -209,7 +259,6 @@ export class Settings {
|
||||
return hasSetting;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if its the project config
|
||||
* @param filePath
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { stopWords } from '../constants/stopwords-en';
|
||||
import { charMap } from '../constants/charMap';
|
||||
import { stopWords, charMap } from '../constants';
|
||||
|
||||
export class SlugHelper {
|
||||
|
||||
|
||||
3
src/helpers/parseWinPath.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const parseWinPath = (path: string | undefined): string => {
|
||||
return path?.split(`\\`).join(`/`) || '';
|
||||
}
|
||||
27
src/hooks/useContentType.tsx
Normal 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
@@ -0,0 +1,5 @@
|
||||
|
||||
export interface Choice {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
49
src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
20
src/panelWebView/components/Fields/ChoiceButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
src/panelWebView/components/Fields/PreviewImage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
@@ -26,7 +27,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({limit, labe
|
||||
|
||||
let isValid = true;
|
||||
if (limit && limit !== -1) {
|
||||
isValid = ((text || "").length < limit);
|
||||
isValid = ((text || "").length <= limit);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -37,16 +38,31 @@ 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 && (text || "").length >= limit && (
|
||||
limit && limit > 0 && (text || "").length > limit && (
|
||||
<div className={`metadata_field__limit`}>
|
||||
Field limit reached {(text || "").length}/{limit}
|
||||
</div>
|
||||
|
||||
32
src/panelWebView/components/FileItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
|
||||
|
||||
React.useEffect(() => {
|
||||
if (prevSelected !== crntSelected) {
|
||||
setSelected(crntSelected);
|
||||
setSelected(typeof crntSelected === "string" ? [crntSelected] : crntSelected);
|
||||
}
|
||||
}, [crntSelected]);
|
||||
|
||||
@@ -186,7 +186,12 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
|
||||
}
|
||||
</Downshift>
|
||||
|
||||
<Tags values={selected.sort((a: string, b: string) => a.toLowerCase() < b.toLowerCase() ? -1 : 1 )} onRemove={onRemove} onCreate={onCreate} options={options} disableConfigurable={!!disableConfigurable} />
|
||||
<Tags
|
||||
values={(selected || []).sort((a: string, b: string) => a?.toLowerCase() < b?.toLowerCase() ? -1 : 1 )}
|
||||
onRemove={onRemove}
|
||||
onCreate={onCreate}
|
||||
options={options}
|
||||
disableConfigurable={!!disableConfigurable} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||