mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c48db957 | ||
|
|
d7658852b0 | ||
|
|
0a0efba37b | ||
|
|
a16c0c6355 | ||
|
|
8dcbe67152 | ||
|
|
2c20621071 | ||
|
|
48c4c0b8e4 | ||
|
|
2900777ffb | ||
|
|
0ccd428852 | ||
|
|
368ade6b44 | ||
|
|
5f6b6e3b4a | ||
|
|
43554a4303 | ||
|
|
2b0007c21a | ||
|
|
5ab0bdaa69 | ||
|
|
1c74df0266 | ||
|
|
469a1aaaf8 | ||
|
|
c5523f7aaf | ||
|
|
61b46bc5ac | ||
|
|
d7a0f71552 | ||
|
|
7d6d60039e | ||
|
|
42cc53cefc | ||
|
|
e68daa8ac2 | ||
|
|
333cc1f9df | ||
|
|
00bb8c6385 | ||
|
|
1deb969c20 | ||
|
|
928afceca7 | ||
|
|
179b31f67c | ||
|
|
7d4fe9ca0f | ||
|
|
3e33383eb1 | ||
|
|
66324fd292 | ||
|
|
8b1fbcabaa | ||
|
|
90519488c1 | ||
|
|
1012e10ddc | ||
|
|
2dd129d9bd | ||
|
|
6af5458082 | ||
|
|
9744cf0117 | ||
|
|
01921c799c | ||
|
|
b1674b4b84 | ||
|
|
9f7f803e25 | ||
|
|
b83c565e29 | ||
|
|
dee30923ff | ||
|
|
9a91be8025 | ||
|
|
46a9d6e602 |
1
.frontmatter/content/mediaDb.json
Normal file
1
.frontmatter/content/mediaDb.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [estruyf]
|
||||
open_collective: frontmatter
|
||||
custom: ["https://www.buymeacoffee.com/zMeFRy9"]
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,5 +1,32 @@
|
||||
# Change Log
|
||||
|
||||
## [6.0.0] - 2022-01-xx - [Release Notes](https://beta.frontmatter.codes/updates/v6.0.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#193](https://github.com/estruyf/vscode-front-matter/issues/193): Support added for editing data files.
|
||||
- [#197](https://github.com/estruyf/vscode-front-matter/issues/197): Support for multi-dimensional content type fields on content creation and editing.
|
||||
- [#225](https://github.com/estruyf/vscode-front-matter/issues/225): Placeholder support for front matter field values (template and content type).
|
||||
- [#226](https://github.com/estruyf/vscode-front-matter/issues/226): Ability to specify the local server start command and trigger it from the UI.
|
||||
- [#227](https://github.com/estruyf/vscode-front-matter/issues/227): Specify the file types to support with the new `frontMatter.content.supportedFileTypes` setting.
|
||||
- [#228](https://github.com/estruyf/vscode-front-matter/issues/228): Show bulk button actions in panel and dashboard view.
|
||||
- [#231](https://github.com/estruyf/vscode-front-matter/issues/231): Once you authenticate via GitHub as a supporter, the support links will be hidden from the UI.
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Added default field value for content type fields
|
||||
- HMR support for panel webview development
|
||||
- Added reveal media file action
|
||||
- [#187](https://github.com/estruyf/vscode-front-matter/issues/187): Svelte support with the [#227](https://github.com/estruyf/vscode-front-matter/issues/227) features has been added.
|
||||
- [#198](https://github.com/estruyf/vscode-front-matter/issues/198): Additional media sort options (alt, caption, and size).
|
||||
- [#230](https://github.com/estruyf/vscode-front-matter/issues/230): JSON front matter support added.
|
||||
- [#233](https://github.com/estruyf/vscode-front-matter/issues/233): Partial update when a page is updated.
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#234](https://github.com/estruyf/vscode-front-matter/issues/234): Fix for multi-word keywords
|
||||
- [#235](https://github.com/estruyf/vscode-front-matter/issues/235): Fix for reselecting the previously removed value from a choice field
|
||||
|
||||
## [5.10.0] - 2022-01-10
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
@@ -28,30 +28,50 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
## What is Front Matter?
|
||||
## ❓ What is Front Matter?
|
||||
|
||||
Front Matter BETA is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice. Jump right into editing and creating content with Front Matter and be able to preview it straight in VS Code.
|
||||
|
||||
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
|
||||
The extension supports various static-site generators and frameworks like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
|
||||
Our main extension features are:
|
||||
A couple of our extension highlights that hopefully get you interested in giving Front Matter a try:
|
||||
|
||||
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
|
||||
- Site preview within Visual Studio Code
|
||||
- Content, data, and media management
|
||||
- Search, filter, sort, etc. all your content
|
||||
- Create new content
|
||||
- Supporting tools to edit content and media
|
||||
- Preview your site/content straight in Visual Studio Code
|
||||
- SEO checks for title, description, and keywords
|
||||
- Support for custom actions/scripts
|
||||
- and many more
|
||||
- Extensibility
|
||||
- As we know, we cannot support all use cases. We provide a way to extend the functionality of the extension to your needs
|
||||
- and many more features ...
|
||||
|
||||
> Missing something? Let us know by opening an issue on the [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
|
||||
<img src="./assets/v6.0.0/content-preview.png" alt="Site preview" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 6**
|
||||
|
||||
In this version, we introduced the new data files/folders dashboard. You can find more information about the release in our [v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v6.0.0/data-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> Data files/folders are pieces of content that do not belong to any markdown content, but live on their own. Most of the time, these data files are used to store additional information about your project/blog/website that will be used to render the content.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v5.9.0/media-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
@@ -70,7 +90,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
## ⚙️ Installation
|
||||
|
||||
You can get the extension via:
|
||||
|
||||
@@ -80,23 +100,54 @@ You can get the extension via:
|
||||
|
||||
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
### Beta version
|
||||
### 🧪 Beta version
|
||||
|
||||
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
|
||||
|
||||
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
- Uninstall the main Front Matter version
|
||||
- Install the beta version
|
||||
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
|
||||
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
|
||||
|
||||
## Documentation
|
||||
## 📖 Documentation
|
||||
|
||||
<h2 align="center">
|
||||
<a href="https://beta.frontmatter.codes" title="Documentation @ beta.frontmatter.codes">
|
||||
Check out the extension documentation at beta.frontmatter.codes
|
||||
</a>
|
||||
</h2>
|
||||
All documentation can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
Documentation repository: [GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
|
||||
## 💪 Contributing
|
||||
|
||||
Pull requests are welcome. Please open an issue first to discuss what you would like to change, or which problem you would like to fix. This makes it easier for us to follow-up and plan for future releases.
|
||||
|
||||
You can always help us improve the extension in varous ways like:
|
||||
|
||||
- Testing out the extension and providing feedback
|
||||
- Reporting issues and bugs
|
||||
- Suggesting new features
|
||||
- Fixing an issue
|
||||
- Updating documentation
|
||||
- UI improvements
|
||||
- Tutorials
|
||||
- etc.
|
||||
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following projects:
|
||||
|
||||
- [Extension](https://github.com/estruyf/vscode-front-matter)
|
||||
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
- [Sample Projects](https://github.com/FrontMatter/project-samples)
|
||||
|
||||
## 👀 Show the work you are using Front Matter
|
||||
|
||||
Are you using Front Matter and are you interested in showing for which websites you use it? You can show your work by opening a [showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
|
||||
|
||||
You can open showcase issues for the following things:
|
||||
|
||||
- Show the website for which you use Front Matter;
|
||||
- Share an article/video/webcast/... that explains how you use Front Matter;
|
||||
- Got something else to share? Open an issue and we can see where it fits on our website.
|
||||
|
||||
## 👉 Contributors 🤘
|
||||
|
||||
@@ -109,6 +160,9 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
## 🖤 Backers & Sponsors 👇 🤘
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/apowell656" title="Andre Powell">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
|
||||
</a>
|
||||
<a href="https://github.com/timschps" title="Tim Schaeps">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
|
||||
</a>
|
||||
@@ -128,6 +182,9 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
98
README.md
98
README.md
@@ -26,30 +26,50 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
## What is Front Matter?
|
||||
## ❓ What is Front Matter?
|
||||
|
||||
Front Matter is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice. Jump right into editing and creating content with Front Matter and be able to preview it straight in VS Code.
|
||||
|
||||
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
|
||||
The extension supports various static-site generators and frameworks like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
|
||||
Our main extension features are:
|
||||
A couple of our extension highlights that hopefully get you interested in giving Front Matter a try:
|
||||
|
||||
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
|
||||
- Site preview within Visual Studio Code
|
||||
- Content, data, and media management
|
||||
- Search, filter, sort, etc. all your content
|
||||
- Create new content
|
||||
- Supporting tools to edit content and media
|
||||
- Preview your site/content straight in Visual Studio Code
|
||||
- SEO checks for title, description, and keywords
|
||||
- Support for custom actions/scripts
|
||||
- and many more
|
||||
- Extensibility
|
||||
- As we know, we cannot support all use cases. We provide a way to extend the functionality of the extension to your needs
|
||||
- and many more features ...
|
||||
|
||||
> Missing something? Let us know by opening an issue on the [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
|
||||
<img src="./assets/v6.0.0/content-preview.png" alt="Site preview" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 6**
|
||||
|
||||
In this version, we introduced the new data files/folders dashboard. You can find more information about the release in our [v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v6.0.0/data-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> Data files/folders are pieces of content that do not belong to any markdown content, but live on their own. Most of the time, these data files are used to store additional information about your project/blog/website that will be used to render the content.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v5.9.0/media-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
@@ -68,7 +88,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
## ⚙️ Installation
|
||||
|
||||
You can get the extension via:
|
||||
|
||||
@@ -78,23 +98,54 @@ You can get the extension via:
|
||||
|
||||
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
### Beta version
|
||||
### 🧪 Beta version
|
||||
|
||||
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
|
||||
|
||||
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
- Uninstall the main Front Matter version
|
||||
- Install the beta version
|
||||
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
|
||||
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
|
||||
|
||||
## Documentation
|
||||
## 📖 Documentation
|
||||
|
||||
<h2 align="center">
|
||||
<a href="https://frontmatter.codes" title="Documentation @ frontmatter.codes">
|
||||
Check out the extension documentation at frontmatter.codes
|
||||
</a>
|
||||
</h2>
|
||||
All documentation can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
Documentation repository: [GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
|
||||
## 💪 Contributing
|
||||
|
||||
Pull requests are welcome. Please open an issue first to discuss what you would like to change, or which problem you would like to fix. This makes it easier for us to follow-up and plan for future releases.
|
||||
|
||||
You can always help us improve the extension in varous ways like:
|
||||
|
||||
- Testing out the extension and providing feedback
|
||||
- Reporting issues and bugs
|
||||
- Suggesting new features
|
||||
- Fixing an issue
|
||||
- Updating documentation
|
||||
- UI improvements
|
||||
- Tutorials
|
||||
- etc.
|
||||
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following projects:
|
||||
|
||||
- [Extension](https://github.com/estruyf/vscode-front-matter)
|
||||
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
- [Sample Projects](https://github.com/FrontMatter/project-samples)
|
||||
|
||||
## 👀 Show the work you are using Front Matter
|
||||
|
||||
Are you using Front Matter and are you interested in showing for which websites you use it? You can show your work by opening a [showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
|
||||
|
||||
You can open showcase issues for the following things:
|
||||
|
||||
- Show the website for which you use Front Matter;
|
||||
- Share an article/video/webcast/... that explains how you use Front Matter;
|
||||
- Got something else to share? Open an issue and we can see where it fits on our website.
|
||||
|
||||
## 👉 Contributors 🤘
|
||||
|
||||
@@ -107,6 +158,9 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
## 🖤 Backers & Sponsors 👇 🤘
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/apowell656" title="Andre Powell">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
|
||||
</a>
|
||||
<a href="https://github.com/timschps" title="Tim Schaeps">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
|
||||
</a>
|
||||
@@ -126,6 +180,10 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
@@ -446,12 +446,33 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px dashed rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__box {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 2px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px dashed rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: .5rem;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__label.metadata_field__label_parent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metadata_field__label svg {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
@@ -638,6 +659,14 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
|
||||
BIN
assets/v5.9.0/media-dashboard.png
Normal file
BIN
assets/v5.9.0/media-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
BIN
assets/v6.0.0/content-preview.png
Normal file
BIN
assets/v6.0.0/content-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/v6.0.0/data-dashboard.png
Normal file
BIN
assets/v6.0.0/data-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
2684
package-lock.json
generated
2684
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
592
package.json
592
package.json
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter",
|
||||
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "5.10.0",
|
||||
"version": "6.0.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -100,9 +100,16 @@
|
||||
"frontMatter.content.defaultFileType": {
|
||||
"type": "string",
|
||||
"default": "md",
|
||||
"enum": [
|
||||
"md",
|
||||
"mdx"
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"md",
|
||||
"mdx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"markdownDescription": "Specify the default file type for the content to create. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultfiletype)",
|
||||
"scope": "Content"
|
||||
@@ -198,6 +205,30 @@
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.placeholders": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "This array of placeholders defines the placeholders that you can use in your content types and templates for automatically populating your content its front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.placeholders)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the placeholder, in your content type or template, use it as follows: {{placeholder}}"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The placeholder its value"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.publicFolder": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
@@ -250,6 +281,19 @@
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.supportedFileTypes": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
"md",
|
||||
"mdx",
|
||||
"markdown"
|
||||
],
|
||||
"markdownDescription": "Specify the file types that you want to use in Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.supportedfiletypes)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.wysiwyg": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -329,11 +373,181 @@
|
||||
"markdownDescription": "Specify if you want to open the dashboard when you start VS Code. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.openonstart)",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.data.files": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Specify the data files you want to use for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.data.files)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Your unique ID you want to use for your data file."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title you want to give to your data file."
|
||||
},
|
||||
"labelField": {
|
||||
"type": "string",
|
||||
"description": "The field you want to use as label for your data entries."
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to load. Only JSON or YAML files are supported."
|
||||
},
|
||||
"fileType": {
|
||||
"type": "string",
|
||||
"default": "json",
|
||||
"enum": [
|
||||
"json",
|
||||
"yaml"
|
||||
],
|
||||
"description": "Defines how you want to parse the file. JSON is the default."
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "The JSON schema for your data which will be used to render the data form.",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"description": "If you are using data types, you can specify your type ID."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"title",
|
||||
"file"
|
||||
],
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": "Data"
|
||||
},
|
||||
"frontMatter.data.folders": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Specify the data files you want to use for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.data.files)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Your unique ID you want to use for your data folder."
|
||||
},
|
||||
"labelField": {
|
||||
"type": "string",
|
||||
"description": "The field you want to use as label for your data entries."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the folder to load files."
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "The JSON schema for your data which will be used to render the data form.",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"description": "If you are using data types, you can specify your type ID."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"path"
|
||||
],
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": "Data"
|
||||
},
|
||||
"frontMatter.data.types": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Specify the data types. These types can be used in for your data files. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.data.types)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Your unique ID you want to use for your data type."
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "The JSON schema for your data which will be used to render the data form.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"schema"
|
||||
]
|
||||
},
|
||||
"scope": "Data"
|
||||
},
|
||||
"frontMatter.framework.id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the ID of your static site generator or framework you are using for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.id)"
|
||||
},
|
||||
"frontMatter.framework.startCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"default": null,
|
||||
"markdownDescription": "Specify the command you want to use to start your static site generator or framework. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.startcommand)"
|
||||
},
|
||||
"frontMatter.global.notifications": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"info",
|
||||
"warning",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"default": [
|
||||
"info",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.media.defaultSorting": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
@@ -417,6 +631,7 @@
|
||||
"description": "Specifies the type of content you want to create."
|
||||
},
|
||||
"fields": {
|
||||
"$id": "#contenttypefield",
|
||||
"type": "array",
|
||||
"description": "Define the fields of the content type",
|
||||
"items": {
|
||||
@@ -435,7 +650,8 @@
|
||||
"taxonomy",
|
||||
"tags",
|
||||
"categories",
|
||||
"draft"
|
||||
"draft",
|
||||
"fields"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -447,6 +663,10 @@
|
||||
"type": "string",
|
||||
"description": "Title to show in the UI"
|
||||
},
|
||||
"default": {
|
||||
"type": "string",
|
||||
"description": "Default value"
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "Define your choices",
|
||||
@@ -494,6 +714,9 @@
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The ID of your taxonomy field"
|
||||
},
|
||||
"fields": {
|
||||
"$ref": "#contenttypefield"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -529,6 +752,20 @@
|
||||
"choices"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "fields"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -639,12 +876,14 @@
|
||||
"default": "YAML",
|
||||
"enum": [
|
||||
"YAML",
|
||||
"TOML"
|
||||
"TOML",
|
||||
"JSON"
|
||||
],
|
||||
"markdownDescription": "Specify the type of Front Matter to use. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.frontmattertype)",
|
||||
"enumDescriptions": [
|
||||
"Specifies you want to use YAML markup for the front matter (default)",
|
||||
"Specifies you want to use TOML markup for the front matter"
|
||||
"Specifies you want to use TOML markup for the front matter",
|
||||
"Specifies you want to use JSON markup for the front matter"
|
||||
],
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
@@ -728,148 +967,13 @@
|
||||
"default": "yyyy-MM-dd",
|
||||
"markdownDescription": "Specify the prefix you want to add for your new article filenames. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.prefix)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.global.notifications": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"info",
|
||||
"warning",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"default": [
|
||||
"info",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
|
||||
"scope": "Templates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"title": "Collapse sections",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/close-light.svg",
|
||||
"dark": "assets/icons/close-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"title": "Create a template from current file",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createCategory",
|
||||
"title": "Create category",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTag",
|
||||
"title": "Create tag",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.exportTaxonomy",
|
||||
"title": "Export all tags & categories to your settings",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
"title": "Front Matter: New article from template"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.registerFolder",
|
||||
"title": "Front Matter: Register folder"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.unregisterFolder",
|
||||
"title": "Front Matter: Unregister folder"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"title": "Generate slug based on content title",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.init",
|
||||
"title": "Initialize project",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertCategories",
|
||||
"title": "Insert categories",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"title": "Insert image into your content",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/media-dark.svg",
|
||||
"light": "/assets/icons/media-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
"title": "Insert tags",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createContent",
|
||||
"title": "Create new content from defined content type or template",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"title": "Open dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.media",
|
||||
"title": "Open media dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.close",
|
||||
"title": "Close dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-teal.svg",
|
||||
"light": "/assets/icons/frontmatter-small-teal.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.preview",
|
||||
"title": "Preview content",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.promoteSettings",
|
||||
"title": "Promote settings from local to team level",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.remap",
|
||||
"title": "Remap or remove tag/category in all articles",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.setLastModifiedDate",
|
||||
"title": "Set lastmod date",
|
||||
"command": "frontMatter.authenticate",
|
||||
"title": "Authenticate",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
@@ -882,21 +986,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.italic",
|
||||
"title": "Italic",
|
||||
"command": "frontMatter.dashboard.close",
|
||||
"title": "Close dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/italic-light.svg",
|
||||
"dark": "assets/icons/italic-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.strikethrough",
|
||||
"title": "Strikethrough",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/strikethrough-light.svg",
|
||||
"dark": "assets/icons/strikethrough-dark.svg"
|
||||
"dark": "/assets/icons/frontmatter-small-teal.svg",
|
||||
"light": "/assets/icons/frontmatter-small-teal.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -926,6 +1021,62 @@
|
||||
"dark": "assets/icons/blockquote-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"title": "Collapse sections",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/close-light.svg",
|
||||
"dark": "assets/icons/close-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"title": "Create a template from current file",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createCategory",
|
||||
"title": "Create category",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createContent",
|
||||
"title": "Create new content from defined content type or template",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTag",
|
||||
"title": "Create tag",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.diagnostics",
|
||||
"title": "Diagnostic logging",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.exportTaxonomy",
|
||||
"title": "Export all tags & categories to your settings",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
"title": "Front Matter: New article from template"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.registerFolder",
|
||||
"title": "Front Matter: Register folder"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.unregisterFolder",
|
||||
"title": "Front Matter: Unregister folder"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"title": "Generate slug based on content title",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.heading",
|
||||
"title": "Heading",
|
||||
@@ -936,12 +1087,63 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.unorderedlist",
|
||||
"title": "Unordered list",
|
||||
"command": "frontMatter.init",
|
||||
"title": "Initialize project",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertCategories",
|
||||
"title": "Insert categories",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"title": "Insert image into your content",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/unordered-list-light.svg",
|
||||
"dark": "assets/icons/unordered-list-dark.svg"
|
||||
"dark": "/assets/icons/media-dark.svg",
|
||||
"light": "/assets/icons/media-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
"title": "Insert tags",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.italic",
|
||||
"title": "Italic",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/italic-light.svg",
|
||||
"dark": "assets/icons/italic-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"title": "Open dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.data",
|
||||
"title": "Open data dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.media",
|
||||
"title": "Open media dashboard",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -953,11 +1155,6 @@
|
||||
"dark": "assets/icons/ordered-list-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.tasklist",
|
||||
"title": "Task list",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"title": "Other markup options",
|
||||
@@ -968,9 +1165,47 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.diagnostics",
|
||||
"title": "Diagnostic logging",
|
||||
"command": "frontMatter.preview",
|
||||
"title": "Preview content",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.promoteSettings",
|
||||
"title": "Promote settings from local to team level",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.remap",
|
||||
"title": "Remap or remove tag/category in all articles",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.setLastModifiedDate",
|
||||
"title": "Set lastmod date",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.strikethrough",
|
||||
"title": "Strikethrough",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/strikethrough-light.svg",
|
||||
"dark": "assets/icons/strikethrough-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.tasklist",
|
||||
"title": "Task list",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.unorderedlist",
|
||||
"title": "Unordered list",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/unordered-list-light.svg",
|
||||
"dark": "assets/icons/unordered-list-dark.svg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -1164,10 +1399,13 @@
|
||||
"build:ext": "npm run clean && npm-run-all --parallel dev:build:*",
|
||||
"watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js",
|
||||
"watch:dashboard": "webpack serve --mode development --config ./webpack/dashboard.config.js",
|
||||
"watch:panel": "webpack serve --mode development --config ./webpack/panel.config.js",
|
||||
"dev:build:ext": "webpack --mode development --config ./webpack/extension.config.js",
|
||||
"dev:build:dashboard": "webpack --mode development --config ./webpack/dashboard.config.js",
|
||||
"dev:build:panel": "webpack --mode development --config ./webpack/panel.config.js",
|
||||
"prod:ext": "webpack --mode production --config ./webpack/extension.config.js",
|
||||
"prod:dashboard": "webpack --mode production --config ./webpack/dashboard.config.js",
|
||||
"prod:panel": "webpack --mode production --config ./webpack/panel.config.js",
|
||||
"test-compile": "tsc -p ./",
|
||||
"clean": "rimraf dist",
|
||||
"start:site": "cd ./docs && npm run dev"
|
||||
@@ -1178,6 +1416,7 @@
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"@tailwindcss/forms": "^0.3.3",
|
||||
@@ -1186,6 +1425,7 @@
|
||||
"@types/lodash.uniqby": "4.7.6",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/node": "10.17.48",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-datepicker": "^4.1.7",
|
||||
"@types/react-dom": "17.0.0",
|
||||
@@ -1193,6 +1433,8 @@
|
||||
"@vscode/codicons": "0.0.20",
|
||||
"@vscode/webview-ui-toolkit": "^0.8.1",
|
||||
"@webpack-cli/serve": "^1.6.0",
|
||||
"ajv": "^8.8.2",
|
||||
"array-move": "^4.0.0",
|
||||
"autoprefixer": "^10.3.2",
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.23.0",
|
||||
@@ -1211,22 +1453,32 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-loader": "4.3.0",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"react": "17.0.1",
|
||||
"react-datepicker": "4.2.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-dropzone": "^11.3.4",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-toastify": "^8.1.0",
|
||||
"recoil": "^0.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"style-loader": "2.0.0",
|
||||
"tailwindcss": "^2.2.7",
|
||||
"ts-loader": "8.0.3",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "4.0.2",
|
||||
"typescript": "^4.5.4",
|
||||
"uniforms": "^3.7.0",
|
||||
"uniforms-antd": "^3.7.0",
|
||||
"uniforms-bridge-json-schema": "^3.7.0",
|
||||
"uniforms-unstyled": "^3.7.0",
|
||||
"url-join-ts": "^1.0.5",
|
||||
"wc-react": "github:estruyf/wc-react",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ const tailwindcss = require('tailwindcss');
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-nested'),
|
||||
tailwindcss('./tailwind.config.js'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isValidFile } from './../helpers/isValidFile';
|
||||
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from './../constants';
|
||||
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTINGS_CONTENT_PLACEHOLDERS } from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { TaxonomyType } from "../models";
|
||||
import { Field, TaxonomyType } from "../models";
|
||||
import { format } from "date-fns";
|
||||
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
|
||||
import matter = require('gray-matter');
|
||||
@@ -180,11 +180,34 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const articleTitle: string = article.data["title"];
|
||||
let slug = SlugHelper.createSlug(articleTitle);
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const titleField = "title";
|
||||
const articleTitle: string = article.data[titleField];
|
||||
|
||||
const slug = SlugHelper.createSlug(articleTitle);
|
||||
if (slug) {
|
||||
slug = `${prefix}${slug}${suffix}`;
|
||||
article.data["slug"] = slug;
|
||||
let slugFieldValue = `${prefix}${slug}${suffix}`;
|
||||
article.data["slug"] = slugFieldValue;
|
||||
|
||||
if (contentType) {
|
||||
// Update the fields containing the slug placeholder
|
||||
let fieldsToUpdate: Field[] = contentType.fields.filter(f => f.default === "{{slug}}");
|
||||
for (const field of fieldsToUpdate) {
|
||||
article.data[field.name] = slug;
|
||||
}
|
||||
|
||||
// Update the fields containing a custom placeholder that depends on slug
|
||||
const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS);
|
||||
const customPlaceholders = placeholders?.filter(p => p.value.includes("{{slug}}"));
|
||||
for (const customPlaceholder of (customPlaceholders || [])) {
|
||||
const customPlaceholderFields = contentType.fields.filter(f => f.default === `{{${customPlaceholder.id}}}`);
|
||||
for (const pField of customPlaceholderFields) {
|
||||
article.data[pField.name] = customPlaceholder.value;
|
||||
article.data[pField.name] = ArticleHelper.processKnownPlaceholders(article.data[pField.name], articleTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArticleHelper.update(editor, article);
|
||||
|
||||
// Check if the file name should be updated by the slug
|
||||
|
||||
74
src/commands/Backers.ts
Normal file
74
src/commands/Backers.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { CONTEXT } from '../constants';
|
||||
import { Extension } from '../helpers';
|
||||
import { Credentials } from "../services/Credentials";
|
||||
import fetch from "node-fetch";
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { Dashboard } from './Dashboard';
|
||||
|
||||
export class Backers {
|
||||
private static creds: Credentials | null = null;
|
||||
|
||||
public static async init(context: ExtensionContext) {
|
||||
Backers.creds = new Credentials();
|
||||
await Backers.creds.initialize(context, Backers.tryUsernameCheck);
|
||||
|
||||
Backers.tryUsernameCheck();
|
||||
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('frontMatter.authenticate', async () => {
|
||||
Backers.tryUsernameCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public static async tryUsernameCheck() {
|
||||
try {
|
||||
const username = await Backers.getUsername();
|
||||
Backers.validate(username || "");
|
||||
} catch (e) {
|
||||
Backers.validate("");
|
||||
}
|
||||
}
|
||||
|
||||
public static async getUsername() {
|
||||
const octokit = await Backers.creds?.getOctokit();
|
||||
const user = await octokit?.users.getAuthenticated();
|
||||
|
||||
if (user?.data?.login) {
|
||||
return user?.data?.login;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public static async validate(username: string) {
|
||||
const ext = Extension.getInstance();
|
||||
|
||||
if (!username) {
|
||||
ext.setState(CONTEXT.backer, undefined, 'global');
|
||||
}
|
||||
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
const response = await fetch(`https://${isBeta ? `beta.` : ``}frontmatter.codes/api/backers?backer=${username}`);
|
||||
|
||||
if (response.ok) {
|
||||
const prevData = await ext.getState<boolean>(CONTEXT.backer, 'global');
|
||||
await ext.setState(CONTEXT.backer, true, 'global');
|
||||
|
||||
if (!prevData) {
|
||||
const explorerView = ExplorerView.getInstance();
|
||||
if (explorerView.visible) {
|
||||
explorerView.getSettings();
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.reload();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ext.setState(CONTEXT.backer, false, 'global');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { MediaLibrary } from '../helpers/MediaLibrary';
|
||||
import { DashboardListener, MediaListener, SettingsListener } from '../listeners';
|
||||
import { DataListener } from '../listeners/DataListener';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
@@ -144,6 +145,7 @@ export class Dashboard {
|
||||
MediaListener.process(msg);
|
||||
PagesListener.process(msg);
|
||||
SettingsListener.process(msg);
|
||||
DataListener.process(msg);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,14 +177,15 @@ export class Dashboard {
|
||||
*/
|
||||
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
|
||||
const dashboardFile = "dashboardWebView.js";
|
||||
const localServerUrl = "http://localhost:9000";
|
||||
const localPort = `9000`;
|
||||
const localServerUrl = `localhost:${localPort}`;
|
||||
|
||||
let scriptUri = "";
|
||||
const isProd = Extension.getInstance().isProductionMode;
|
||||
if (isProd) {
|
||||
scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', dashboardFile)).toString();
|
||||
} else {
|
||||
scriptUri = `${localServerUrl}/${dashboardFile}`;
|
||||
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
|
||||
}
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
@@ -194,10 +197,10 @@ export class Dashboard {
|
||||
const csp = [
|
||||
`default-src 'none';`,
|
||||
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
|
||||
`script-src ${isProd ? `'nonce-${nonce}'` : "http://localhost:9000 http://0.0.0.0:9000"}`,
|
||||
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`} 'unsafe-eval'`,
|
||||
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
|
||||
`font-src ${webView.cspSource}`,
|
||||
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : "ws://localhost:9000 ws://0.0.0.0:9000 http://localhost:9000 http://0.0.0.0:9000"}`
|
||||
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`
|
||||
];
|
||||
|
||||
return `
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER } from './../constants';
|
||||
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_CONTENT_SUPPORTED_FILETYPES } from './../constants';
|
||||
import { commands, Uri, workspace, window } from "vscode";
|
||||
import { basename, join } from "path";
|
||||
import { ContentFolder, FileInfo, FolderInfo } from "../models";
|
||||
@@ -13,6 +13,7 @@ import { Dashboard } from './Dashboard';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { MediaHelpers } from '../helpers/MediaHelpers';
|
||||
import { MediaListener, PagesListener } from '../listeners';
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
@@ -202,6 +203,7 @@ export class Folders {
|
||||
* Get the registered folders information
|
||||
*/
|
||||
public static async getInfo(limit?: number): Promise<FolderInfo[] | null> {
|
||||
const supportedFiles = Settings.get<string[]>(SETTINGS_CONTENT_SUPPORTED_FILETYPES);
|
||||
const folders = Folders.get();
|
||||
if (folders && folders.length > 0) {
|
||||
let folderInfo: FolderInfo[] = [];
|
||||
@@ -214,10 +216,15 @@ export class Folders {
|
||||
if (projectStart) {
|
||||
projectStart = projectStart.replace(/\\/g, '/');
|
||||
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
|
||||
const markdownFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.markdown'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
|
||||
let files = [...mdFiles, ...markdownFiles, ...mdxFiles];
|
||||
|
||||
let files: Uri[] = [];
|
||||
|
||||
for (const fileType of (supportedFiles || DEFAULT_FILE_TYPES)) {
|
||||
const filePath = join(projectStart, folder.excludeSubdir ? '/' : '**', `*${fileType.startsWith('.') ? '' : '.'}${fileType}`);
|
||||
const foundFiles = await workspace.findFiles(filePath, '**/node_modules/**');
|
||||
files = [...files, ...foundFiles];
|
||||
}
|
||||
|
||||
if (files) {
|
||||
let fileStats: FileInfo[] = [];
|
||||
|
||||
@@ -274,6 +281,19 @@ export class Folders {
|
||||
path: Folders.absWsFolder(folder, wsFolder)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the absolute file path
|
||||
* @param filePath
|
||||
* @returns
|
||||
*/
|
||||
public static getAbsFilePath(filePath: string): string {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = filePath.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
|
||||
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the folder settings
|
||||
@@ -318,4 +338,8 @@ export class Folders {
|
||||
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function SETTINGS_CONTENT_SUPPORTED_FILES<T>(SETTINGS_CONTENT_SUPPORTED_FILES: any) {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { SETTINGS_CONTENT_DEFAULT_FILETYPE } from "../constants";
|
||||
export class Project {
|
||||
|
||||
private static content = `---
|
||||
title: "{{name}}"
|
||||
slug: "/{{kebabCase name}}/"
|
||||
title:
|
||||
slug:
|
||||
description:
|
||||
author:
|
||||
date: 2019-08-22T15:20:28.000Z
|
||||
|
||||
@@ -159,13 +159,7 @@ export class Template {
|
||||
}
|
||||
|
||||
if (frontMatter.data) {
|
||||
const fmData = frontMatter.data;
|
||||
if (typeof fmData.title !== "undefined") {
|
||||
fmData.title = titleValue;
|
||||
}
|
||||
if (typeof fmData.slug !== "undefined") {
|
||||
fmData.slug = ArticleHelper.sanitize(titleValue);
|
||||
}
|
||||
frontMatter.data = ArticleHelper.updatePlaceholders(frontMatter.data, titleValue);
|
||||
|
||||
frontMatter = Article.updateDate(frontMatter);
|
||||
|
||||
|
||||
3
src/constants/DefaultFileTypes.ts
Normal file
3
src/constants/DefaultFileTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
export const DEFAULT_FILE_TYPES = [".md", ".markdown", ".mdx"];
|
||||
@@ -29,6 +29,7 @@ export const COMMAND_NAME = {
|
||||
preview: getCommandName("preview"),
|
||||
dashboard: getCommandName("dashboard"),
|
||||
dashboardMedia: getCommandName("dashboard.media"),
|
||||
dashboardData: getCommandName("dashboard.data"),
|
||||
dashboardClose: getCommandName("dashboard.close"),
|
||||
promote: getCommandName("promoteSettings"),
|
||||
insertImage: getCommandName("insertImage"),
|
||||
|
||||
@@ -2,20 +2,32 @@ export const FrameworkDetectors = [
|
||||
{
|
||||
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
|
||||
"requiredFiles": ["gatsby-config.js"],
|
||||
"requiredDependencies": ["gatsby"]
|
||||
"requiredDependencies": ["gatsby"],
|
||||
"commands": {
|
||||
"start": "npx gatsby develop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
|
||||
"requiredFiles": ["config.toml", "config.yaml", "config.yml"]
|
||||
"requiredFiles": ["config.toml", "config.yaml", "config.yml"],
|
||||
"commands": {
|
||||
"start": "hugo server -D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
|
||||
"requiredFiles": ["next.config.js"],
|
||||
"requiredDependencies": ["next"]
|
||||
"requiredDependencies": ["next"],
|
||||
"commands": {
|
||||
"start": "npx next dev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
|
||||
"requiredFiles": ["nuxt.config.js"],
|
||||
"requiredDependencies": ["nuxt"]
|
||||
"requiredDependencies": ["nuxt"],
|
||||
"commands": {
|
||||
"start": "npx nuxt"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -5,4 +5,5 @@ export const CONTEXT = {
|
||||
isEnabled: "frontMatter:enabled",
|
||||
isDashboardOpen: "frontMatter:dashboard:open",
|
||||
wysiwyg: "frontMatter:markdown:wysiwyg",
|
||||
backer: "frontMatter:backers:supporter",
|
||||
};
|
||||
@@ -46,16 +46,23 @@ export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
|
||||
export const SETTINGS_CONTENT_SORTING = "content.sorting";
|
||||
export const SETTINGS_CONTENT_WYSIWYG = "content.wysiwyg";
|
||||
export const SETTINGS_CONTENT_PLACEHOLDERS = "content.placeholders";
|
||||
|
||||
export const SETTINGS_CONTENT_SORTING_DEFAULT = "content.defaultSorting";
|
||||
export const SETTINGS_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
|
||||
|
||||
export const SETTINGS_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
|
||||
export const SETTINGS_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
|
||||
|
||||
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
export const SETTINGS_DATA_FILES = "data.files";
|
||||
export const SETTINGS_DATA_FOLDERS = "data.folders";
|
||||
export const SETTINGS_DATA_TYPES = "data.types";
|
||||
|
||||
export const SETTINGS_FRAMEWORK_ID = "framework.id";
|
||||
export const SETTINGS_FRAMEWORK_START = "framework.startCommand";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@ export enum DashboardCommand {
|
||||
settings = "settings",
|
||||
media = "media",
|
||||
viewData = "viewData",
|
||||
mediaUpdate = "mediaUpdate"
|
||||
mediaUpdate = "mediaUpdate",
|
||||
dataFileEntries = "dataFileEntries"
|
||||
}
|
||||
@@ -15,10 +15,13 @@ export enum DashboardMessage {
|
||||
refreshMedia = 'refreshMedia',
|
||||
uploadMedia = 'uploadMedia',
|
||||
deleteMedia = 'deleteMedia',
|
||||
revealMedia = 'revealMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
setFramework = 'setFramework',
|
||||
setState = 'setState',
|
||||
runCustomScript = 'runCustomScript',
|
||||
getDataEntries = 'getDataEntries',
|
||||
putDataEntries = 'putDataEntries',
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IButtonProps {
|
||||
secondary?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, disabled, children}: React.PropsWithChildren<IButtonProps>) => {
|
||||
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, className, disabled, secondary, children}: React.PropsWithChildren<IButtonProps>) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
|
||||
className={`${className || ""} inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 focus:outline-none disabled:bg-gray-500 ${secondary ? `bg-red-300 hover:bg-red-400` : `bg-teal-600 hover:bg-teal-700`}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MenuItem, MenuItems } from './Menu';
|
||||
export interface IChoiceButtonProps {
|
||||
title: string;
|
||||
choices: {
|
||||
icon?: JSX.Element;
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
@@ -36,10 +37,19 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
|
||||
|
||||
<MenuItems widthClass={`w-56`}>
|
||||
<div className="py-1">
|
||||
{choices.map((choice) => (
|
||||
{choices.map((choice, idx) => (
|
||||
<MenuItem
|
||||
key={choice.title}
|
||||
title={choice.title}
|
||||
key={idx}
|
||||
title={(
|
||||
choice.icon ? (
|
||||
<div className="flex items-center">
|
||||
{choice.icon}
|
||||
<span>{choice.title}</span>
|
||||
</div>
|
||||
) : (
|
||||
choice.title
|
||||
)
|
||||
)}
|
||||
value={null}
|
||||
onClick={choice.onClick}
|
||||
disabled={choice.disabled} />
|
||||
|
||||
@@ -30,7 +30,7 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loadin
|
||||
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { DashboardViewSelector } from '../state';
|
||||
import { Contents } from './Contents/Contents';
|
||||
import { Media } from './Media/Media';
|
||||
import { NavigationType } from '../models';
|
||||
import { DataView } from './DataView';
|
||||
|
||||
export interface IDashboardProps {
|
||||
showWelcome: boolean;
|
||||
@@ -30,15 +31,25 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (view === NavigationType.Media) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Media />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === NavigationType.Data) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<DataView />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
{
|
||||
view === NavigationType.Media ? (
|
||||
<Media />
|
||||
) : (
|
||||
<Contents pages={pages} loading={loading} />
|
||||
)
|
||||
}
|
||||
<Contents pages={pages} loading={loading} />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
69
src/dashboardWebView/components/DataView/DataForm.tsx
Normal file
69
src/dashboardWebView/components/DataView/DataForm.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import Ajv from 'ajv';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';
|
||||
import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { DataFormControls } from './DataFormControls';
|
||||
|
||||
export interface IDataFormProps {
|
||||
schema: any;
|
||||
model: any;
|
||||
onSubmit: (model: any) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export const DataForm: React.FunctionComponent<IDataFormProps> = ({ schema, model, onSubmit, onClear }: React.PropsWithChildren<IDataFormProps>) => {
|
||||
const [ bridge, setBridge ] = useState<JSONSchemaBridge | null>(null);
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, useDefaults: true });
|
||||
|
||||
const jsonValidator = (schema: object) => {
|
||||
const validator = ajv.compile(schema);
|
||||
|
||||
return (crntModel: object) => {
|
||||
validator(crntModel);
|
||||
return validator.errors?.length ? { details: validator.errors } : null;
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const schemaValidator = jsonValidator(schema);
|
||||
const bridge = new JSONSchemaBridge(schema, schemaValidator);
|
||||
setBridge(bridge);
|
||||
}, [schema]);
|
||||
|
||||
if (!bridge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className='autoform'>
|
||||
{
|
||||
model ? (
|
||||
<h2 className='text-gray-500 dark:text-whisper-900'>Modify the data</h2>
|
||||
) : (
|
||||
<h2 className='text-gray-500 dark:text-whisper-900'>Add new data</h2>
|
||||
)
|
||||
}
|
||||
|
||||
<AutoForm
|
||||
schema={bridge}
|
||||
model={model || {}}
|
||||
onSubmit={onSubmit}
|
||||
ref={form => form?.reset()}>
|
||||
<div className={`fields`}>
|
||||
<AutoFields />
|
||||
</div>
|
||||
|
||||
<div className={`errors`}>
|
||||
<ErrorsField />
|
||||
</div>
|
||||
|
||||
<DataFormControls model={model} onClear={onClear} />
|
||||
</AutoForm>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useForm } from 'uniforms';
|
||||
import { SubmitField } from 'uniforms-unstyled';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface IDataFormControlsProps {
|
||||
model: any | null;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export const DataFormControls: React.FunctionComponent<IDataFormControlsProps> = ({ model, onClear }: React.PropsWithChildren<IDataFormControlsProps>) => {
|
||||
const { formRef } = useForm();
|
||||
|
||||
return (
|
||||
<div className='text-right border-t border-gray-200 dark:border-vulcan-300'>
|
||||
<SubmitField value={model ? `Update` : `Add`} />
|
||||
<Button className='ml-4' secondary onClick={() => {
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
formRef.reset();
|
||||
}}>Cancel</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
230
src/dashboardWebView/components/DataView/DataView.tsx
Normal file
230
src/dashboardWebView/components/DataView/DataView.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import * as React from 'react';
|
||||
import { Header } from '../Header';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { DataForm } from './DataForm';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DataFile } from '../../../models/DataFile';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
import { EventData } from '@estruyf/vscode';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { Button } from '../Button';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { EmptyView } from './EmptyView';
|
||||
import { Container } from './SortableContainer';
|
||||
import { SortableItem } from './SortableItem';
|
||||
import { ChevronRightIcon } from '@heroicons/react/outline';
|
||||
import { ToastContainer, toast, Slide } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { DataType } from '../../../models/DataType';
|
||||
|
||||
export interface IDataViewProps {}
|
||||
|
||||
export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.PropsWithChildren<IDataViewProps>) => {
|
||||
const [ selectedData, setSelectedData ] = useState<DataFile | null>(null);
|
||||
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
|
||||
const [ dataEntries, setDataEntries ] = useState<any[] | null>(null);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const setSchema = (dataFile: DataFile) => {
|
||||
setSelectedData(dataFile);
|
||||
setSelectedIndex(null);
|
||||
setDataEntries(null);
|
||||
|
||||
Messenger.send(DashboardMessage.getDataEntries, { ...dataFile });
|
||||
};
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
||||
if (message.data.command === DashboardCommand.dataFileEntries) {
|
||||
setDataEntries(message.data.data);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = useCallback((index: number) => {
|
||||
const dataClone: any[] = Object.assign([], dataEntries);
|
||||
|
||||
if (!selectedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataClone.splice(index, 1);
|
||||
updateData(dataClone);
|
||||
}, [selectedData, dataEntries]);
|
||||
|
||||
|
||||
const onSubmit = useCallback((data: any) => {
|
||||
const dataClone: any[] = Object.assign([], dataEntries);
|
||||
if (selectedIndex !== null && selectedIndex !== undefined) {
|
||||
dataClone[selectedIndex] = data;
|
||||
} else {
|
||||
dataClone.push(data);
|
||||
}
|
||||
updateData(dataClone);
|
||||
}, [selectedData, dataEntries, selectedIndex]);
|
||||
|
||||
|
||||
const onSortEnd = useCallback(({ oldIndex, newIndex }: any) => {
|
||||
if (!dataEntries || dataEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectedIndex !== null && selectedIndex !== undefined) {
|
||||
setSelectedIndex(newIndex);
|
||||
}
|
||||
|
||||
const newEntries = arrayMoveImmutable(dataEntries, oldIndex, newIndex);
|
||||
updateData(newEntries);
|
||||
}, [selectedData, dataEntries, selectedIndex]);
|
||||
|
||||
const updateData = useCallback((data: any) => {
|
||||
if (!selectedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
Messenger.send(DashboardMessage.putDataEntries, {
|
||||
file: selectedData.file,
|
||||
fileType: selectedData.fileType,
|
||||
entries: data
|
||||
});
|
||||
|
||||
// Show toast message
|
||||
toast.success("Updated your data entries", {
|
||||
position: "top-right",
|
||||
autoClose: 2000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: false,
|
||||
transition: Slide
|
||||
});
|
||||
}, [selectedData]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
|
||||
const dataFiles = (settings?.dataFiles || []).map((dataFile: DataFile) => {
|
||||
if (!dataFile.schema && !dataFile.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clonedFile = Object.assign({}, dataFile);
|
||||
|
||||
if (clonedFile.type) {
|
||||
const dataType = settings?.dataTypes?.find((dataType: DataType) => dataType.id === clonedFile.type);
|
||||
if (!dataType) {
|
||||
return null;
|
||||
}
|
||||
clonedFile.schema = Object.assign({}, dataType.schema);
|
||||
}
|
||||
|
||||
return clonedFile;
|
||||
}).filter(d => d !== null) as DataFile[];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-auto inset-y-0">
|
||||
<Header settings={settings} />
|
||||
|
||||
<div className="relative w-full flex-grow mx-auto overflow-hidden">
|
||||
|
||||
<div className={`flex w-64 flex-col absolute inset-y-0`}>
|
||||
|
||||
<aside className={`flex flex-col flex-grow overflow-y-auto border-r border-gray-200 dark:border-vulcan-300 py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Select your data type</h2>
|
||||
|
||||
<nav className={`flex-1 py-4 -mx-4 `}>
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
(dataFiles && dataFiles.length > 0) && (
|
||||
dataFiles.map((dataFile) => (
|
||||
<button
|
||||
key={dataFile.id}
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
onClick={() => setSchema(dataFile)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>{dataFile.title}</span>
|
||||
</button>
|
||||
)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<section className={`pl-64 flex min-w-0 h-full`}>
|
||||
{
|
||||
selectedData ? (
|
||||
<>
|
||||
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData.title.toLowerCase()} data items</h2>
|
||||
|
||||
<div className='py-4'>
|
||||
{
|
||||
(dataEntries && dataEntries.length > 0) ? (
|
||||
<>
|
||||
<Container onSortEnd={onSortEnd} useDragHandle>
|
||||
{
|
||||
(dataEntries || []).map((dataEntry, idx) => (
|
||||
<SortableItem
|
||||
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
|
||||
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
|
||||
index={idx}
|
||||
crntIndex={idx}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
|
||||
onDeleteItem={deleteItem}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Container>
|
||||
<Button
|
||||
className='mt-4'
|
||||
onClick={() => setSelectedIndex(null)}>
|
||||
Add a new entry
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={`flex flex-col items-center justify-center`}>
|
||||
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-2/3 py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Create or modify your {selectedData.title.toLowerCase()} data</h2>
|
||||
{
|
||||
selectedData ? (
|
||||
<DataForm
|
||||
schema={selectedData?.schema}
|
||||
model={(dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null}
|
||||
onSubmit={onSubmit}
|
||||
onClear={() => setSelectedIndex(null)} />
|
||||
) : (
|
||||
<p>Select a data type to get started</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyView />
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
13
src/dashboardWebView/components/DataView/EmptyView.tsx
Normal file
13
src/dashboardWebView/components/DataView/EmptyView.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IEmptyViewProps {}
|
||||
|
||||
export const EmptyView: React.FunctionComponent<IEmptyViewProps> = (props: React.PropsWithChildren<IEmptyViewProps>) => {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center w-full'>
|
||||
<ExclamationCircleIcon className={`w-1/12 text-gray-500 dark:text-whisper-900 opacity-90`} />
|
||||
<h2 className={`text-xl text-gray-500 dark:text-whisper-900`}>Select your date type first</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { SortableContainer } from 'react-sortable-hoc';
|
||||
|
||||
export interface ISortableContainerProps {}
|
||||
|
||||
export const Container = SortableContainer(({ children }: React.PropsWithChildren<ISortableContainerProps>) => ( <ul className={`-mx-4 divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>{children}</ul> ));
|
||||
68
src/dashboardWebView/components/DataView/SortableItem.tsx
Normal file
68
src/dashboardWebView/components/DataView/SortableItem.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { PencilIcon, SelectorIcon, TrashIcon, XIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { SortableHandle, SortableElement } from 'react-sortable-hoc';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
|
||||
export interface ISortableItemProps {
|
||||
value: string;
|
||||
index: number;
|
||||
crntIndex: number;
|
||||
selectedIndex: number | null;
|
||||
onSelectedIndexChange: (index: number) => void;
|
||||
onDeleteItem: (index: number) => void;
|
||||
}
|
||||
|
||||
const DragHandle = SortableHandle(() => <SelectorIcon className={`w-6 h-6 cursor-move`} />);
|
||||
|
||||
export const SortableItem = SortableElement(({ value, selectedIndex, crntIndex, onSelectedIndexChange, onDeleteItem }: ISortableItemProps) => {
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
|
||||
const deleteItemConfirm = () => {
|
||||
setShowAlert(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<li data-test={`${selectedIndex}-${crntIndex}`} className={`py-2 px-2 w-full flex justify-between content-center hover:bg-gray-200 dark:hover:bg-vulcan-400 ${selectedIndex === crntIndex ? `bg-gray-300 dark:bg-vulcan-300` : ``}`}>
|
||||
<div className='flex items-center'>
|
||||
<DragHandle />
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
|
||||
<div className={`space-x-2 flex items-center`}>
|
||||
<button
|
||||
type='button'
|
||||
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
|
||||
title={`Edit "${value}"`}
|
||||
onClick={() => onSelectedIndexChange(crntIndex)}>
|
||||
<PencilIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
|
||||
title={`Delete "${value}"`}
|
||||
onClick={() => deleteItemConfirm()}>
|
||||
<TrashIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
showAlert && (
|
||||
<Alert
|
||||
title={`Delete data entry`}
|
||||
description={`Are you sure you want to delete the data entry?`}
|
||||
okBtnText={`Delete`}
|
||||
cancelBtnText={`Cancel`}
|
||||
dismiss={() => setShowAlert(false)}
|
||||
trigger={() => {
|
||||
setShowAlert(false);
|
||||
onDeleteItem(crntIndex);
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
});
|
||||
1
src/dashboardWebView/components/DataView/index.ts
Normal file
1
src/dashboardWebView/components/DataView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DataView';
|
||||
@@ -13,11 +13,12 @@ import { useRecoilState, useResetRecoilState } from 'recoil';
|
||||
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ClearFilters } from './ClearFilters';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import {PhotographIcon} from '@heroicons/react/outline';
|
||||
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
|
||||
import { ChoiceButton } from '../ChoiceButton';
|
||||
import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
|
||||
import { Tabs } from './Tabs';
|
||||
import { CustomScript } from '../../../models';
|
||||
import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline';
|
||||
|
||||
export interface IHeaderProps {
|
||||
settings: Settings | null;
|
||||
@@ -52,22 +53,25 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
resetSorting();
|
||||
}
|
||||
|
||||
const runBulkScript = (script: CustomScript) => {
|
||||
Messenger.send(DashboardMessage.runCustomScript, { script });
|
||||
};
|
||||
|
||||
const customActions: any[] = (settings?.scripts || []).filter(s => s.bulk && (s.type === "content" || !s.type)).map((s, idx) => ({
|
||||
title: (
|
||||
<div key={idx} className="flex items-center">
|
||||
<LightningBoltIcon className="w-4 h-4 mr-2" />
|
||||
<span>{s.title}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: () => runBulkScript(s)
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
|
||||
|
||||
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300 h-12">
|
||||
<ul className="flex items-center justify-start h-full -mb-px" data-tabs-toggle="#myTabContent" role="tablist">
|
||||
<li className="mr-2" role="presentation">
|
||||
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Contents ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="profile" aria-selected="false" onClick={() => updateView(NavigationType.Contents)}>
|
||||
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
|
||||
</button>
|
||||
</li>
|
||||
<li className="mr-2" role="presentation">
|
||||
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Media ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="dashboard" aria-selected="true" onClick={() => updateView(NavigationType.Media)}>
|
||||
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300">
|
||||
<Tabs onNavigate={updateView} />
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -81,15 +85,28 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
|
||||
<ChoiceButton
|
||||
title={`Create content`}
|
||||
choices={[{
|
||||
title: `Create by content type`,
|
||||
onClick: createByContentType,
|
||||
disabled: !settings?.initialized
|
||||
}, {
|
||||
title: `Create by template`,
|
||||
onClick: createByTemplate,
|
||||
disabled: !settings?.initialized
|
||||
}]}
|
||||
choices={[
|
||||
{
|
||||
title: (
|
||||
<div className='flex items-center'>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<span>Create by content type</span>
|
||||
</div>
|
||||
),
|
||||
onClick: createByContentType,
|
||||
disabled: !settings?.initialized
|
||||
}, {
|
||||
title: (
|
||||
<div className='flex items-center'>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<span>Create by template</span>
|
||||
</div>
|
||||
),
|
||||
onClick: createByTemplate,
|
||||
disabled: !settings?.initialized
|
||||
},
|
||||
...customActions
|
||||
]}
|
||||
onClick={createContent}
|
||||
disabled={!settings?.initialized} />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NavigationType } from '../../models';
|
||||
import { SortingOption } from '../../models/SortingOption';
|
||||
import { SearchSelector, SettingsSelector, SortingAtom } from '../../state';
|
||||
import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
import { Sorting as SortingHelpers } from '../../../helpers/Sorting';
|
||||
|
||||
export interface ISortingProps {
|
||||
disableCustomSorting?: boolean;
|
||||
@@ -23,6 +24,15 @@ export const sortOptions: SortingOption[] = [
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
const mediaSortOptions: SortingOption[] = [
|
||||
{ name: "Size (asc)", id: SortOption.SizeAsc, order: SortOrder.asc, type: SortType.number },
|
||||
{ name: "Size (desc)", id: SortOption.SizeDesc, order: SortOrder.desc, type: SortType.number },
|
||||
{ name: "Caption (asc)", id: SortOption.CaptionAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "Caption (desc)", id: SortOption.CaptionDesc, order: SortOrder.desc, type: SortType.string },
|
||||
{ name: "Alt (asc)", id: SortOption.AltAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "Alt (desc)", id: SortOption.AltDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSorting, view}: React.PropsWithChildren<ISortingProps>) => {
|
||||
const [ crntSorting, setCrntSorting ] = useRecoilState(SortingAtom);
|
||||
const searchValue = useRecoilValue(SearchSelector);
|
||||
@@ -38,6 +48,13 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
};
|
||||
|
||||
let allOptions = [...sortOptions];
|
||||
|
||||
if (view === NavigationType.Media) {
|
||||
allOptions = [...allOptions, ...mediaSortOptions];
|
||||
}
|
||||
|
||||
allOptions = allOptions.sort(SortingHelpers.alphabetically("name"))
|
||||
|
||||
if (settings?.customSorting && !disableCustomSorting) {
|
||||
allOptions = [...allOptions, ...settings.customSorting.map((s) => ({
|
||||
title: s.title || s.name,
|
||||
@@ -72,7 +89,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
|
||||
|
||||
<MenuItems>
|
||||
<MenuItems widthClass="w-48">
|
||||
{allOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
|
||||
25
src/dashboardWebView/components/Header/Tab.tsx
Normal file
25
src/dashboardWebView/components/Header/Tab.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { NavigationType } from '../../models';
|
||||
import { DashboardViewAtom } from '../../state';
|
||||
|
||||
export interface ITabProps {
|
||||
navigationType: NavigationType;
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
|
||||
const view = useRecoilValue(DashboardViewAtom);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === navigationType ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="profile"
|
||||
aria-selected="false"
|
||||
onClick={() => onNavigate(navigationType)}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
45
src/dashboardWebView/components/Header/Tabs.tsx
Normal file
45
src/dashboardWebView/components/Header/Tabs.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DatabaseIcon, PhotographIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { NavigationType } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { Tab } from './Tab';
|
||||
|
||||
export interface ITabsProps {
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.PropsWithChildren<ITabsProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
return (
|
||||
<ul className="flex items-center justify-start h-full" data-tabs-toggle="#myTabContent" role="tablist">
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Contents}
|
||||
onNavigate={onNavigate}>
|
||||
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
|
||||
</Tab>
|
||||
</li>
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Media}
|
||||
onNavigate={onNavigate}>
|
||||
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
|
||||
</Tab>
|
||||
</li>
|
||||
{
|
||||
(settings?.dataFiles && settings.dataFiles.length > 0) && (
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Data}
|
||||
onNavigate={onNavigate}>
|
||||
<DatabaseIcon className={`h-6 w-auto mr-2`} /><span>Data</span>
|
||||
</Tab>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {FolderAddIcon} from '@heroicons/react/outline';
|
||||
import {FolderAddIcon, LightningBoltIcon} from '@heroicons/react/outline';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SelectedMediaFolderAtom, SettingsSelector } from '../../state';
|
||||
@@ -31,6 +31,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
|
||||
title={`Create new folder`}
|
||||
choices={scripts.map(s => ({
|
||||
title: s.title,
|
||||
icon: <LightningBoltIcon className="w-4 h-4 mr-2" />,
|
||||
onClick: () => runCustomScript(s)
|
||||
}))}
|
||||
onClick={onFolderCreation}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { ClipboardIcon, CodeIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { basename, dirname } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
@@ -77,6 +77,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
image: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
parents: viewData?.data?.parents,
|
||||
multiple: viewData?.data?.multiple,
|
||||
value: viewData?.data?.value,
|
||||
position: viewData?.data?.position || null,
|
||||
@@ -107,6 +108,13 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
setShowAlert(true);
|
||||
};
|
||||
|
||||
const revealMedia = () => {
|
||||
Messenger.send(DashboardMessage.revealMedia, {
|
||||
file: media.fsPath,
|
||||
folder: selectedFolder
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
Messenger.send(DashboardMessage.deleteMedia, {
|
||||
file: media.fsPath,
|
||||
@@ -220,7 +228,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<QuickAction
|
||||
title='Insert image with markdown markup'
|
||||
title={(viewData.data.metadataInsert && viewData.data.fieldName) ? `Insert image for your "${viewData.data.fieldName}" field` : `Insert image with markdown markup`}
|
||||
onClick={insertToArticle}>
|
||||
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
@@ -242,15 +250,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
onClick={copyToClipboard}>
|
||||
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title='Delete media file'
|
||||
onClick={deleteMedia}>
|
||||
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<QuickAction
|
||||
title='Delete media file'
|
||||
onClick={deleteMedia}>
|
||||
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
|
||||
<Menu as="div" className="relative z-10 inline-block text-left h-5">
|
||||
@@ -286,13 +294,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
onClick={copyToClipboard} />
|
||||
|
||||
{ customScriptActions() }
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={deleteMedia} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={`Reveal media`}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
|
||||
<Lightbox />
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMenuItemProps {
|
||||
title: string;
|
||||
title: JSX.Element | string;
|
||||
value?: any;
|
||||
isCurrent?: boolean;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -6,18 +6,28 @@ import { VersionInfo } from '../../models';
|
||||
export interface ISponsorMsgProps {
|
||||
beta: boolean | undefined;
|
||||
version: VersionInfo | undefined;
|
||||
isBacker: boolean | undefined;
|
||||
}
|
||||
|
||||
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isBacker, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
|
||||
return (
|
||||
<p className={`w-full max-w-7xl mx-auto px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center justify-between`}>
|
||||
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Support Front Matter">
|
||||
<span>Support</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
|
||||
</a>
|
||||
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
|
||||
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={REVIEW_LINK} title="Review Front Matter">
|
||||
<StarIcon className={`h-5 w-5 group-hover:fill-current`} /> <span>Review</span>
|
||||
</a>
|
||||
<p className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
|
||||
{
|
||||
isBacker ? (
|
||||
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
|
||||
) : (
|
||||
<>
|
||||
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Support Front Matter">
|
||||
<span>Support</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
|
||||
</a>
|
||||
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
|
||||
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={REVIEW_LINK} title="Review Front Matter">
|
||||
<StarIcon className={`h-5 w-5 group-hover:fill-current`} /> <span>Review</span>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -2,5 +2,11 @@ export enum SortOption {
|
||||
LastModifiedAsc = "LastModifiedAsc",
|
||||
LastModifiedDesc = "LastModifiedDesc",
|
||||
FileNameAsc = "FileNameAsc",
|
||||
FileNameDesc = "FileNameDesc"
|
||||
FileNameDesc = "FileNameDesc",
|
||||
SizeAsc = "SizeAsc",
|
||||
SizeDesc = "SizeDesc",
|
||||
CaptionAsc = "CaptionAsc",
|
||||
CaptionDesc = "CaptionDesc",
|
||||
AltAsc = "AltAsc",
|
||||
AltDesc = "AltDesc",
|
||||
}
|
||||
@@ -26,6 +26,8 @@ export default function useMessages() {
|
||||
setView(NavigationType.Media);
|
||||
} else if (message.data.data?.type === NavigationType.Contents) {
|
||||
setView(NavigationType.Contents);
|
||||
} else if (message.data.data?.type === NavigationType.Data) {
|
||||
setView(NavigationType.Data);
|
||||
}
|
||||
break;
|
||||
case DashboardCommand.settings:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NavigationType {
|
||||
Contents = "contents",
|
||||
Media = "media"
|
||||
Media = "media",
|
||||
Data = "data",
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { DataType } from './../../models/DataType';
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType, CustomScript, DraftField, Framework, SortingSetting } from '../../models';
|
||||
import { SortingOption } from './SortingOption';
|
||||
import { DashboardViewType } from '.';
|
||||
import { DataFile } from '../../models/DataFile';
|
||||
|
||||
export interface Settings {
|
||||
beta: boolean;
|
||||
@@ -24,6 +26,9 @@ export interface Settings {
|
||||
customSorting: SortingSetting[] | undefined;
|
||||
dashboardState: DashboardState;
|
||||
scripts: CustomScript[];
|
||||
dataFiles: DataFile[] | undefined;
|
||||
dataTypes: DataType[] | undefined;
|
||||
isBacker: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
|
||||
@@ -29,4 +29,270 @@
|
||||
top: -1px;
|
||||
left: 2px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.autoform {
|
||||
@apply py-4;
|
||||
|
||||
h2 {
|
||||
@apply text-sm mb-2;
|
||||
}
|
||||
|
||||
form {
|
||||
label {
|
||||
@apply block;
|
||||
@apply text-gray-500;
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply w-full text-vulcan-500 px-2 py-1;
|
||||
|
||||
&::placeholder {
|
||||
@apply text-gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form-item-has-error .ant-form-item-control-input {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.ant-form-item-has-error {
|
||||
label {
|
||||
&::after {
|
||||
content: ' *';
|
||||
@apply text-red-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form-item-has-error input {
|
||||
@apply border border-red-400;
|
||||
}
|
||||
|
||||
.ant-form-item-has-error .ant-form-item-children-icon {
|
||||
@apply text-red-400 absolute right-1 top-1;
|
||||
|
||||
svg {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
}
|
||||
|
||||
.errors {
|
||||
> div {
|
||||
@apply border border-red-400 !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-disc pl-6 pr-4 py-4 bg-opacity-50 text-vulcan-500;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply capitalize text-gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
@apply w-auto mt-4 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white bg-teal-600 cursor-pointer;
|
||||
|
||||
&:hover {
|
||||
@apply bg-teal-700;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply bg-gray-500 opacity-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fields {}
|
||||
|
||||
.ant-list.ant-list-bordered {
|
||||
@apply border border-gray-300;
|
||||
}
|
||||
|
||||
.ant-btn-dashed {
|
||||
@apply border border-gray-300 border-dashed flex items-center justify-center py-1 mt-2;
|
||||
|
||||
&:hover {
|
||||
@apply text-teal-900 border-teal-900;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input:hover, .ant-input-focused, .ant-input:focus {
|
||||
@apply border-teal-600;
|
||||
}
|
||||
|
||||
.ant-btn-ghost:focus, .ant-btn-ghost:hover {
|
||||
@apply text-teal-600 border-teal-700;
|
||||
}
|
||||
|
||||
.ant-switch-checked {
|
||||
@apply bg-teal-500;
|
||||
}
|
||||
|
||||
.ant-switch {
|
||||
@apply bg-gray-400;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5715;
|
||||
list-style: none;
|
||||
font-feature-settings: "tnum";
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
min-width: 44px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
border-radius: 100px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
user-select: none;
|
||||
|
||||
&.ant-switch-checked {
|
||||
@apply bg-teal-500;
|
||||
|
||||
.ant-switch-handle {
|
||||
left: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-switch-handle {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
transition: all .2s ease-in-out;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 9px;
|
||||
box-shadow: 0 2px 4px #00230b33;
|
||||
transition: all .2s ease-in-out;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.ant-switch-inner {
|
||||
display: block;
|
||||
margin: 0 7px 0 25px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
transition: margin .2s;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
@apply border;
|
||||
|
||||
box-sizing: border-box;
|
||||
font-variant: tabular-nums;
|
||||
list-style: none;
|
||||
font-feature-settings: "tnum";
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
color: #000000d9;
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
transition: all .3s;
|
||||
display: inline-block;
|
||||
width: 90px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&.ant-input-number-focused {
|
||||
@apply border-teal-600;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-number-handler {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
overflow: hidden;
|
||||
color: #00000073;
|
||||
font-weight: 700;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
transition: all .1s linear;
|
||||
}
|
||||
|
||||
.ant-input-number-handler-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 22px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
transition: opacity .24s linear .1s;
|
||||
}
|
||||
|
||||
.ant-input-number-input {
|
||||
@apply px-2 py-1;
|
||||
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
transition: all .3s linear;
|
||||
appearance: textfield !important;
|
||||
}
|
||||
|
||||
.ant-input-number:hover .ant-input-number-handler-wrap, .ant-input-number-focused .ant-input-number-handler-wrap {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.vscode-dark .autoform {
|
||||
form {
|
||||
label {
|
||||
@apply text-whisper-900;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
@apply text-vulcan-500
|
||||
}
|
||||
|
||||
.errors {
|
||||
li {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-list.ant-list-bordered {
|
||||
@apply border-vulcan-100;
|
||||
}
|
||||
|
||||
.ant-btn-dashed {
|
||||
@apply border-vulcan-50;
|
||||
|
||||
&:hover {
|
||||
@apply text-teal-400 border-teal-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { Template } from '../commands/Template';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CUSTOM } from '../constants';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CUSTOM, CONTEXT, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START } from '../constants';
|
||||
import * as os from 'os';
|
||||
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv, ThemeIcon } from "vscode";
|
||||
import { ArticleHelper, Settings } from "../helpers";
|
||||
import { Command } from "../panelWebView/Command";
|
||||
import { CommandToCode } from '../panelWebView/CommandToCode';
|
||||
import { Article } from '../commands';
|
||||
import { TagType } from '../panelWebView/TagType';
|
||||
import { CustomTaxonomyData, DraftField, ScriptType, TaxonomyType } from '../models';
|
||||
import { CustomTaxonomyData, DraftField, Field, ScriptType, TaxonomyType } from '../models';
|
||||
import { exec } from 'child_process';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { Content } from 'mdast';
|
||||
@@ -101,13 +101,13 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
Article.toggleDraft();
|
||||
break;
|
||||
case CommandToCode.updateTags:
|
||||
this.updateTags(TagType.tags, msg.data || []);
|
||||
this.updateTags(TagType.tags, msg.data?.values || [], msg.data?.parents || []);
|
||||
break;
|
||||
case CommandToCode.updateCategories:
|
||||
this.updateTags(TagType.categories, msg.data || []);
|
||||
this.updateTags(TagType.categories, msg.data?.values || [], msg.data?.parents || []);
|
||||
break;
|
||||
case CommandToCode.updateKeywords:
|
||||
this.updateTags(TagType.keywords, msg.data || []);
|
||||
this.updateTags(TagType.keywords, msg.data?.values || [], msg.data?.parents || []);
|
||||
break;
|
||||
case CommandToCode.updateCustomTaxonomy:
|
||||
this.updateCustomTaxonomy(msg.data);
|
||||
@@ -161,13 +161,13 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
await commands.executeCommand(COMMAND_NAME.createTemplate);
|
||||
break;
|
||||
case CommandToCode.updateModifiedUpdating:
|
||||
this.updateModifiedUpdating(msg.data || false);
|
||||
this.updateSetting(SETTING_AUTO_UPDATE_DATE, msg.data || false);
|
||||
break;
|
||||
case CommandToCode.toggleWritingSettings:
|
||||
this.toggleWritingSettings();
|
||||
break;
|
||||
case CommandToCode.updateFmHighlight:
|
||||
this.updateFmHighlight((msg.data !== null && msg.data !== undefined) ? msg.data : false);
|
||||
this.updateSetting(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false);
|
||||
break;
|
||||
case CommandToCode.toggleCenterMode:
|
||||
await commands.executeCommand(`workbench.action.toggleCenteredLayout`);
|
||||
@@ -179,7 +179,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
await commands.executeCommand(COMMAND_NAME.dashboard);
|
||||
break;
|
||||
case CommandToCode.updatePreviewUrl:
|
||||
this.updatePreviewUrl(msg.data || "");
|
||||
this.updateSetting(SETTING_PREVIEW_HOST, msg.data || "");
|
||||
break;
|
||||
case CommandToCode.openInEditor:
|
||||
openFileInEditor(msg.data);
|
||||
@@ -194,6 +194,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
} as DashboardData);
|
||||
this.getMediaSelection();
|
||||
break;
|
||||
case CommandToCode.frameworkCommand:
|
||||
this.openTerminalWithCommand(msg.data.command);
|
||||
break;
|
||||
case CommandToCode.updateStartCommand:
|
||||
await this.updateSetting(SETTINGS_FRAMEWORK_START, msg.data || "");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -249,32 +255,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
// Get the current content type
|
||||
const contentType = ArticleHelper.getContentType(updatedMetadata);
|
||||
if (contentType) {
|
||||
const imageFields = contentType.fields.filter((field) => field.type === "image");
|
||||
|
||||
for (const field of imageFields) {
|
||||
if (updatedMetadata[field.name]) {
|
||||
const imageData = ImageHelper.allRelToAbs(field, updatedMetadata[field.name])
|
||||
|
||||
if (imageData) {
|
||||
if (field.multiple && imageData instanceof Array) {
|
||||
const preview = imageData.map(preview => preview && preview.absPath ? ({
|
||||
...preview,
|
||||
webviewUrl: this.panel?.webview.asWebviewUri(preview.absPath).toString()
|
||||
}) : null);
|
||||
|
||||
updatedMetadata[field.name] = preview || [];
|
||||
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
|
||||
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
|
||||
updatedMetadata[field.name] = {
|
||||
...imageData,
|
||||
webviewUrl: preview ? preview.toString() : null
|
||||
};
|
||||
}
|
||||
} else {
|
||||
updatedMetadata[field.name] = field.multiple ? [] : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
this.processImageFields(updatedMetadata, contentType.fields)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +295,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
/**
|
||||
* Update the metadata of the article
|
||||
*/
|
||||
public async updateMetadata({field, value }: { field: string, value: any, fieldData?: { multiple: boolean, value: string[] } }) {
|
||||
public async updateMetadata({field, parents, value }: { field: string, value: any, parents?: string[], fieldData?: { multiple: boolean, value: string[] } }) {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
@@ -333,12 +314,18 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
const dateFields = contentType.fields.filter((field) => field.type === "datetime");
|
||||
const imageFields = contentType.fields.filter((field) => field.type === "image" && field.multiple);
|
||||
|
||||
// Support multi-level fields
|
||||
let parentObj = article.data;
|
||||
for (const parent of parents || []) {
|
||||
parentObj = parentObj[parent];
|
||||
}
|
||||
|
||||
for (const dateField of dateFields) {
|
||||
if ((field === dateField.name) && value) {
|
||||
article.data[field] = Article.formatDate(new Date(value));
|
||||
parentObj[field] = Article.formatDate(new Date(value));
|
||||
} else if (!imageFields.find(f => f.name === field)) {
|
||||
// Only override the field data if it is not an multiselect image field
|
||||
article.data[field] = value;
|
||||
parentObj[field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,15 +333,15 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
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 || [];
|
||||
parentObj[field] = value || [];
|
||||
} else { // Otherwise it is coming from the media dashboard (addition)
|
||||
let fieldValue = article.data[field];
|
||||
let fieldValue = parentObj[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);
|
||||
parentObj[field] = [...new Set(allRelPaths)].filter(f => f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +350,29 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
this.pushMetadata(article.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a terminal and run the passed command
|
||||
* @param command
|
||||
*/
|
||||
private openTerminalWithCommand(command: string) {
|
||||
if (command) {
|
||||
let terminal = window.activeTerminal;
|
||||
|
||||
if (!terminal || (terminal && terminal.state.isInteractedWith === true)) {
|
||||
terminal = window.createTerminal({
|
||||
name: `Starting local server`,
|
||||
iconPath: new ThemeIcon('server-environment'),
|
||||
message: `Starting local server`,
|
||||
});
|
||||
}
|
||||
|
||||
if (terminal) {
|
||||
terminal.sendText(command);
|
||||
terminal.show(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a custom script
|
||||
* @param msg
|
||||
@@ -423,7 +433,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
|
||||
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
dashboardViewData: Dashboard.viewData,
|
||||
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD)
|
||||
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
|
||||
isBacker: await Extension.getInstance().getState<boolean | undefined>(CONTEXT.backer, 'global'),
|
||||
framework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
commands: {
|
||||
start: Settings.get<string>(SETTINGS_FRAMEWORK_START)
|
||||
}
|
||||
} as PanelSettings
|
||||
});
|
||||
}
|
||||
@@ -438,6 +453,58 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the image fields in the content type
|
||||
* @param updatedMetadata
|
||||
* @param fields
|
||||
* @param parents
|
||||
*/
|
||||
private processImageFields(updatedMetadata: any, fields: Field[], parents: string[] = []) {
|
||||
const imageFields = fields.filter((field) => field.type === "image");
|
||||
|
||||
// Support multi-level fields
|
||||
let parentObj = updatedMetadata;
|
||||
for (const parent of parents || []) {
|
||||
parentObj = parentObj[parent];
|
||||
}
|
||||
|
||||
// Process image fields
|
||||
if (parentObj) {
|
||||
for (const field of imageFields) {
|
||||
if (parentObj[field.name]) {
|
||||
const imageData = ImageHelper.allRelToAbs(field, parentObj[field.name])
|
||||
|
||||
if (imageData) {
|
||||
if (field.multiple && imageData instanceof Array) {
|
||||
const preview = imageData.map(preview => preview && preview.absPath ? ({
|
||||
...preview,
|
||||
webviewUrl: this.panel?.webview.asWebviewUri(preview.absPath).toString()
|
||||
}) : null);
|
||||
|
||||
parentObj[field.name] = preview || [];
|
||||
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
|
||||
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
|
||||
parentObj[field.name] = {
|
||||
...imageData,
|
||||
webviewUrl: preview ? preview.toString() : null
|
||||
};
|
||||
}
|
||||
} else {
|
||||
parentObj[field.name] = field.multiple ? [] : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are sub-fields to process
|
||||
const subFields = fields.filter((field) => field.type === "fields");
|
||||
if (subFields?.length > 0) {
|
||||
for (const field of subFields) {
|
||||
this.processImageFields(updatedMetadata, field.fields || [], [...parents, field.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file its front matter
|
||||
*/
|
||||
@@ -458,7 +525,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* @param tagType
|
||||
* @param values
|
||||
*/
|
||||
private updateTags(tagType: TagType, values: string[]) {
|
||||
private updateTags(tagType: TagType, values: string[], parents: string[]) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return "";
|
||||
@@ -466,7 +533,14 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article && article.data) {
|
||||
article.data[tagType.toLowerCase()] = values || [];
|
||||
|
||||
// Support multi-level fields
|
||||
let parentObj = article.data;
|
||||
for (const parent of parents || []) {
|
||||
parentObj = parentObj[parent];
|
||||
}
|
||||
|
||||
parentObj[tagType.toLowerCase()] = values || [];
|
||||
ArticleHelper.update(editor, article);
|
||||
this.pushMetadata(article!.data);
|
||||
}
|
||||
@@ -488,7 +562,14 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article && article.data) {
|
||||
article.data[data.name] = data.options || [];
|
||||
|
||||
// Support multi-level fields
|
||||
let parentObj = article.data;
|
||||
for (const parent of data.parents || []) {
|
||||
parentObj = parentObj[parent];
|
||||
}
|
||||
|
||||
parentObj[data.name] = data.options || [];
|
||||
ArticleHelper.update(editor, article);
|
||||
this.pushMetadata(article!.data);
|
||||
}
|
||||
@@ -656,26 +737,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the preview URL
|
||||
* Updates a setting and refreshes the retrieved settings
|
||||
* @param setting
|
||||
* @param value
|
||||
*/
|
||||
private async updatePreviewUrl(previewUrl: string) {
|
||||
await Settings.update(SETTING_PREVIEW_HOST, previewUrl);
|
||||
this.getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the Front Matter highlighting
|
||||
*/
|
||||
private async updateFmHighlight(autoUpdate: boolean) {
|
||||
await Settings.update(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, autoUpdate);
|
||||
this.getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the modified auto-update setting
|
||||
*/
|
||||
private async updateModifiedUpdating(autoUpdate: boolean) {
|
||||
await Settings.update(SETTING_AUTO_UPDATE_DATE, autoUpdate);
|
||||
private async updateSetting(setting: string, value: any) {
|
||||
await Settings.update(setting, value);
|
||||
this.getSettings();
|
||||
}
|
||||
|
||||
@@ -692,35 +759,56 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* @param webView
|
||||
*/
|
||||
private getWebviewContent(webView: Webview): string {
|
||||
const ext = Extension.getInstance();
|
||||
const dashboardFile = "panelWebView.js";
|
||||
const localPort = `9001`;
|
||||
const localServerUrl = `localhost:${localPort}`;
|
||||
const extensionPath = ext.extensionPath;
|
||||
|
||||
const styleVSCodeUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'vscode.css'));
|
||||
const styleResetUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'reset.css'));
|
||||
const stylesUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'styles.css'));
|
||||
const scriptUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'dist', 'panelWebView.js'));
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const ext = Extension.getInstance();
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
let scriptUri = "";
|
||||
const isProd = Extension.getInstance().isProductionMode;
|
||||
if (isProd) {
|
||||
scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', dashboardFile)).toString();
|
||||
} else {
|
||||
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
|
||||
}
|
||||
|
||||
const csp = [
|
||||
`default-src 'none';`,
|
||||
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
|
||||
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
|
||||
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
|
||||
`font-src ${webView.cspSource}`,
|
||||
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`
|
||||
];
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="${styleResetUri}" rel="stylesheet">
|
||||
<link href="${styleVSCodeUri}" rel="stylesheet">
|
||||
<link href="${stylesUri}" rel="stylesheet">
|
||||
|
||||
<title>Front Matter</title>
|
||||
<title>Front Matter Panel</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" ></div>
|
||||
<div id="app" data-isProd="${isProd}" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" ></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`panel-${version.installedVersion}`}" alt="Daily usage" />
|
||||
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
<script ${isProd ? `nonce="${nonce}"` : ""} src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
@@ -19,6 +19,7 @@ import ContentProvider from './providers/ContentProvider';
|
||||
import { Wysiwyg } from './commands/Wysiwyg';
|
||||
import { Diagnostics } from './commands/Diagnostics';
|
||||
import { PagesListener } from './listeners';
|
||||
import { Backers } from './commands/Backers';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
let statusDebouncer: { (fnc: any, time: number): void; };
|
||||
@@ -31,6 +32,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const { subscriptions, extensionUri, extensionPath } = context;
|
||||
|
||||
const extension = Extension.getInstance(context);
|
||||
Backers.init(context);
|
||||
|
||||
if (!extension.checkIfExtensionCanRun()) {
|
||||
return undefined;
|
||||
@@ -61,6 +63,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
Dashboard.open({ type: "media" });
|
||||
}));
|
||||
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardData, (data?: DashboardData) => {
|
||||
Dashboard.open({ type: "data" });
|
||||
}));
|
||||
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
|
||||
Dashboard.close();
|
||||
}));
|
||||
|
||||
@@ -3,20 +3,21 @@ import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/
|
||||
import * as vscode from 'vscode';
|
||||
import * as matter from "gray-matter";
|
||||
import * as fs from "fs";
|
||||
import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants';
|
||||
import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTINGS_CONTENT_PLACEHOLDERS, SETTINGS_CONTENT_SUPPORTED_FILETYPES, 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 { Extension, Settings } from '.';
|
||||
import { Extension, Logger, Settings, SlugHelper } from '.';
|
||||
import { format, parse } from 'date-fns';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Article } from '../commands';
|
||||
import { basename, join } from 'path';
|
||||
import { join } from 'path';
|
||||
import { EditorHelper } from '@estruyf/vscode';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { ContentType } from '../models';
|
||||
import { DateHelper } from './DateHelper';
|
||||
import { Diagnostic, DiagnosticSeverity, Position, window, Range } from 'vscode';
|
||||
import { DiagnosticSeverity, Position, window, Range } from 'vscode';
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
|
||||
export class ArticleHelper {
|
||||
private static notifiedFiles: string[] = [];
|
||||
@@ -145,7 +146,8 @@ export class ArticleHelper {
|
||||
*/
|
||||
public static isMarkdownFile(document: vscode.TextDocument | undefined | null = null) {
|
||||
const supportedLanguages = ["markdown", "mdx"];
|
||||
const supportedFileExtensions = [".md", ".mdx"];
|
||||
const fileTypes = Settings.get<string[]>(SETTINGS_CONTENT_SUPPORTED_FILETYPES);
|
||||
const supportedFileExtensions = fileTypes ? fileTypes.map(f => f.startsWith(`.`) ? f : `.${f}`) : DEFAULT_FILE_TYPES;
|
||||
const languageId = document?.languageId?.toLowerCase();
|
||||
const isSupportedLanguage = languageId && supportedLanguages.includes(languageId);
|
||||
document ??= vscode.window.activeTextEditor?.document;
|
||||
@@ -274,6 +276,83 @@ export class ArticleHelper {
|
||||
return newFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update placeholder values in the front matter content
|
||||
* @param data
|
||||
* @param title
|
||||
* @returns
|
||||
*/
|
||||
public static updatePlaceholders(data: any, title: string) {
|
||||
const fmData = Object.assign({}, data);
|
||||
|
||||
for (const fieldName of Object.keys(fmData)) {
|
||||
const fieldValue = fmData[fieldName];
|
||||
|
||||
if (fieldName === "title" && (fieldValue === null || fieldValue === "")) {
|
||||
fmData[fieldName] = title;
|
||||
}
|
||||
|
||||
if (fieldName === "slug" && (fieldValue === null || fieldValue === "")) {
|
||||
fmData[fieldName] = SlugHelper.createSlug(title);
|
||||
}
|
||||
|
||||
fmData[fieldName] = this.processKnownPlaceholders(fmData[fieldName], title);
|
||||
fmData[fieldName] = this.processCustomPlaceholders(fmData[fieldName], title);
|
||||
}
|
||||
|
||||
return fmData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the known placeholders
|
||||
* @param value
|
||||
* @param title
|
||||
* @returns
|
||||
*/
|
||||
public static processKnownPlaceholders(value: string, title: string) {
|
||||
if (value && typeof value === "string") {
|
||||
if (value.includes("{{title}}")) {
|
||||
const regex = new RegExp("{{title}}", "g");
|
||||
value = value.replace(regex, title);
|
||||
}
|
||||
|
||||
if (value.includes("{{slug}}")) {
|
||||
const regex = new RegExp("{{slug}}", "g");
|
||||
value = value.replace(regex, SlugHelper.createSlug(title) || "");
|
||||
}
|
||||
|
||||
if (value.includes("{{now}}")) {
|
||||
const regex = new RegExp("{{now}}", "g");
|
||||
value = value.replace(regex, Article.formatDate(new Date()));
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the custom placeholders
|
||||
* @param value
|
||||
* @param title
|
||||
* @returns
|
||||
*/
|
||||
public static processCustomPlaceholders(value: string, title: string) {
|
||||
if (value && typeof value === "string") {
|
||||
const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS);
|
||||
if (placeholders && placeholders.length > 0) {
|
||||
for (const placeholder of placeholders) {
|
||||
if (value.includes(`{{${placeholder.id}}}`)) {
|
||||
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
|
||||
const updatedValue = this.processKnownPlaceholders(placeholder.value, title);
|
||||
value = value.replace(regex, updatedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a markdown file and its front matter
|
||||
* @param fileContents
|
||||
@@ -316,6 +395,8 @@ export class ArticleHelper {
|
||||
await EditorHelper.showFile(fileName)
|
||||
}
|
||||
}];
|
||||
|
||||
Logger.error(error.message);
|
||||
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor?.document.uri) {
|
||||
@@ -329,7 +410,6 @@ export class ArticleHelper {
|
||||
fmRange = MarkdownFoldingProvider.getFrontMatterRange(editor.document);
|
||||
}
|
||||
|
||||
|
||||
if (fmRange) {
|
||||
Extension.getInstance().diagnosticCollection.set(editor.document.uri, [{
|
||||
severity: DiagnosticSeverity.Error,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PagesListener } from './../listeners/PagesListener';
|
||||
import { ArticleHelper, Settings } from ".";
|
||||
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { ContentType as IContentType, DraftField } from '../models';
|
||||
import { ContentType as IContentType, DraftField, Field } from '../models';
|
||||
import { Uri, workspace, window, commands } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Questions } from "./Questions";
|
||||
@@ -109,15 +109,7 @@ export class ContentType {
|
||||
return;
|
||||
}
|
||||
|
||||
let data: any = {};
|
||||
|
||||
for (const field of contentType.fields) {
|
||||
if (field.name === "title") {
|
||||
data[field.name] = titleValue;
|
||||
} else {
|
||||
data[field.name] = null;
|
||||
}
|
||||
}
|
||||
let data: any = this.processFields(contentType, titleValue, {});
|
||||
|
||||
data = ArticleHelper.updateDates(Object.assign({}, data));
|
||||
|
||||
@@ -136,4 +128,34 @@ export class ContentType {
|
||||
// Trigger a refresh for the dashboard
|
||||
PagesListener.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all content type fields
|
||||
* @param contentType
|
||||
* @param data
|
||||
*/
|
||||
private static processFields(obj: IContentType | Field, titleValue: string, data: any) {
|
||||
|
||||
if (obj.fields) {
|
||||
for (const field of obj.fields) {
|
||||
if (field.name === "title") {
|
||||
if (field.default) {
|
||||
data[field.name] = ArticleHelper.processKnownPlaceholders(field.default, titleValue);
|
||||
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
|
||||
} else {
|
||||
data[field.name] = titleValue;
|
||||
}
|
||||
} else {
|
||||
if (field.type === "fields") {
|
||||
data[field.name] = this.processFields(field, titleValue, {});
|
||||
} else {
|
||||
data[field.name] = field.default ? ArticleHelper.processKnownPlaceholders(field.default, titleValue) : "";
|
||||
data[field.name] = field.default ? ArticleHelper.processCustomPlaceholders(data[field.name], titleValue) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { basename, join } from "path";
|
||||
import { workspace } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Template } from "../commands/Template";
|
||||
import { ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { CONTEXT, ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DATA_FILES, SETTINGS_DATA_FOLDERS, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
|
||||
import { CustomScript, DraftField, ScriptType, SortingSetting, TaxonomyType } from "../models";
|
||||
import { DataFile } from "../models/DataFile";
|
||||
import { DataFolder } from "../models/DataFolder";
|
||||
import { DataType } from "../models/DataType";
|
||||
import { Extension } from "./Extension";
|
||||
import { FrameworkDetector } from "./FrameworkDetector";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
@@ -33,7 +38,7 @@ export class DashboardSettings {
|
||||
contentFolders: Folders.get(),
|
||||
crntFramework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type && s.type !== ScriptType.Content),
|
||||
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []),
|
||||
dashboardState: {
|
||||
contents: {
|
||||
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
|
||||
@@ -44,7 +49,58 @@ export class DashboardSettings {
|
||||
defaultSorting: Settings.get<string>(SETTINGS_MEDIA_SORTING_DEFAULT),
|
||||
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace")
|
||||
}
|
||||
}
|
||||
},
|
||||
dataFiles: await this.getDataFiles(),
|
||||
dataTypes: Settings.get<DataType[]>(SETTINGS_DATA_TYPES),
|
||||
isBacker: await ext.getState<boolean | undefined>(CONTEXT.backer, 'global')
|
||||
} as ISettings
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the data files
|
||||
* @returns
|
||||
*/
|
||||
private static async getDataFiles(): Promise<DataFile[]> {
|
||||
const wsPath = Folders.getWorkspaceFolder()?.fsPath;
|
||||
const files = Settings.get<DataFile[]>(SETTINGS_DATA_FILES);
|
||||
const folders = Settings.get<DataFolder[]>(SETTINGS_DATA_FOLDERS);
|
||||
|
||||
let clonedFiles = Object.assign([], files);
|
||||
if (folders) {
|
||||
for (let folder of folders) {
|
||||
if (!folder.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const folderPath = Folders.getAbsFilePath(folder.path);
|
||||
if (!folderPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dataFolderPath = join(folderPath.replace((wsPath || ''), ''));
|
||||
if (dataFolderPath.startsWith('/')) {
|
||||
dataFolderPath = dataFolderPath.substring(1);
|
||||
}
|
||||
|
||||
const dataJsonFiles = await workspace.findFiles(join(dataFolderPath, '*.json'));
|
||||
const dataYmlFiles = await workspace.findFiles(join(dataFolderPath, '*.yml'));
|
||||
const dataYamlFiles = await workspace.findFiles(join(dataFolderPath, '*.yaml'));
|
||||
|
||||
const dataFiles = [...dataJsonFiles, ...dataYmlFiles, ...dataYamlFiles];
|
||||
for (let dataFile of dataFiles) {
|
||||
clonedFiles.push({
|
||||
id: basename(dataFile.fsPath),
|
||||
title: basename(dataFile.fsPath),
|
||||
file: dataFile.fsPath,
|
||||
fileType: dataFile.fsPath.endsWith('.json') ? 'json' : 'yaml',
|
||||
labelField: folder.labelField,
|
||||
schema: folder.schema,
|
||||
type: folder.type
|
||||
} as DataFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clonedFiles;
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,12 @@ export class Logger {
|
||||
|
||||
Logger.channel?.appendLine(`["${type}" - ${format(new Date(), "HH:MM:ss")}] ${message}`);
|
||||
}
|
||||
|
||||
public static warning(message: string): void {
|
||||
Logger.info(message, "WARNING");
|
||||
}
|
||||
|
||||
public static error(message: string): void {
|
||||
Logger.info(message, "ERROR");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { commands, Uri, workspace, window, Position } from "vscode";
|
||||
import imageSize from "image-size";
|
||||
import { EditorHelper } from "@estruyf/vscode";
|
||||
import { ExplorerView } from "../explorerView/ExplorerView";
|
||||
import { SortOption } from "../dashboardWebView/constants/SortOption";
|
||||
|
||||
|
||||
export class MediaHelpers {
|
||||
@@ -91,20 +92,7 @@ export class MediaHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
if (crntSort?.type === SortType.string) {
|
||||
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
|
||||
} else if (crntSort?.type === SortType.date) {
|
||||
allMedia = allMedia.sort(Sorting.dateWithFallback("mtime", "fsPath"));
|
||||
} else {
|
||||
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
|
||||
}
|
||||
|
||||
if (crntSort?.order === SortOrder.desc) {
|
||||
allMedia = allMedia.reverse();
|
||||
}
|
||||
|
||||
MediaHelpers.media = Object.assign([], allMedia);
|
||||
|
||||
let files: MediaInfo[] = MediaHelpers.media;
|
||||
|
||||
// Retrieve the total after filtering and before the slicing happens
|
||||
@@ -126,6 +114,27 @@ export class MediaHelpers {
|
||||
});
|
||||
files = files.filter(f => f.mtime !== undefined);
|
||||
|
||||
// Sort the files
|
||||
if (crntSort?.type === SortType.string) {
|
||||
if (crntSort.id === SortOption.AltAsc || crntSort.id === SortOption.AltDesc) {
|
||||
files = files.sort(Sorting.alphabetically("alt"));
|
||||
} else if (crntSort.id === SortOption.CaptionAsc || crntSort.id === SortOption.CaptionDesc) {
|
||||
files = files.sort(Sorting.alphabetically("caption"));
|
||||
} else {
|
||||
files = files.sort(Sorting.alphabetically("fsPath"));
|
||||
}
|
||||
} else if (crntSort?.type === SortType.number && (crntSort?.id === SortOption.SizeAsc || crntSort?.id === SortOption.SizeDesc)) {
|
||||
files = files.sort(Sorting.numerically("size"));
|
||||
} else if (crntSort?.type === SortType.date) {
|
||||
files = files.sort(Sorting.dateWithFallback("mtime", "fsPath"));
|
||||
} else {
|
||||
files = files.sort(Sorting.alphabetically("fsPath"));
|
||||
}
|
||||
|
||||
if (crntSort?.order === SortOrder.desc) {
|
||||
files = files.reverse();
|
||||
}
|
||||
|
||||
// Retrieve all the folders
|
||||
let allContentFolders: string[] = [];
|
||||
let allFolders: string[] = [];
|
||||
@@ -295,7 +304,7 @@ export class MediaHelpers {
|
||||
panel.getMediaSelection();
|
||||
} else {
|
||||
panel.getMediaSelection();
|
||||
panel.updateMetadata({field: data.fieldName, value: data.image });
|
||||
panel.updateMetadata({field: data.fieldName, value: data.image, parents: data.parents });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class SlugHelper {
|
||||
}
|
||||
|
||||
// Remove punctuation from input string, and split it into words.
|
||||
let cleanTitle = this.removePunctuation(articleTitle);
|
||||
let cleanTitle = this.removePunctuation(articleTitle).trim();
|
||||
if (cleanTitle) {
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
|
||||
@@ -20,6 +20,17 @@ export class Sorting {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort field value numerically
|
||||
* @param property
|
||||
* @returns
|
||||
*/
|
||||
public static numerically = (property: string) => {
|
||||
return (a: any, b: any) => {
|
||||
return a[property] - b[property];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by date
|
||||
* @param property
|
||||
|
||||
@@ -11,7 +11,7 @@ export const getFormatOpts = (format: string): { language: string, delimiters: s
|
||||
const formats: { [prop: string]: { language: string, delimiters: string | [string, string] | undefined }} = {
|
||||
yaml: { language: 'yaml', delimiters: '---' },
|
||||
toml: { language: 'toml', delimiters: '+++' },
|
||||
json: { language: 'json', delimiters: ['{', '}'] },
|
||||
json: { language: 'json', delimiters: '---' },
|
||||
};
|
||||
|
||||
return formats[format];
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { DEFAULT_FILE_TYPES } from './../constants/DefaultFileTypes';
|
||||
import { Settings } from ".";
|
||||
import { SETTINGS_CONTENT_SUPPORTED_FILETYPES } from "../constants";
|
||||
import { extname } from 'path';
|
||||
|
||||
|
||||
export const isValidFile = (fileName: string) => {
|
||||
return fileName.endsWith(`.md`) ||
|
||||
fileName.endsWith(`.markdown`) ||
|
||||
fileName.endsWith(`.mdx`);
|
||||
let supportedFiles = Settings.get<string[]>(SETTINGS_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES;
|
||||
supportedFiles = supportedFiles.map(f => f.startsWith(`.`) ? f : `.${f}`);
|
||||
|
||||
// Get the extension of the file path
|
||||
const extension = extname(fileName);
|
||||
|
||||
return supportedFiles.includes(extension);
|
||||
}
|
||||
94
src/listeners/DataListener.ts
Normal file
94
src/listeners/DataListener.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { DataFile } from './../models/DataFile';
|
||||
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { Logger, Notifications } from '../helpers';
|
||||
import { commands } from 'vscode';
|
||||
|
||||
|
||||
export class DataListener extends BaseListener {
|
||||
|
||||
public static process(msg: { command: DashboardMessage, data: any }) {
|
||||
super.process(msg);
|
||||
|
||||
switch(msg.command) {
|
||||
case (DashboardMessage.getDataEntries):
|
||||
if (!(msg?.data as DataFile).file) {
|
||||
this.sendMsg(DashboardCommand.dataFileEntries, []);
|
||||
}
|
||||
|
||||
this.processDataFile(msg?.data);
|
||||
break;
|
||||
case (DashboardMessage.putDataEntries):
|
||||
this.processDataUpdate(msg?.data);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static processDataUpdate(msgData: any) {
|
||||
const { file, fileType, entries } = msgData as { file: string, fileType: string, entries: any[] };
|
||||
|
||||
const absPath = Folders.getAbsFilePath(file);
|
||||
if (!existsSync(absPath)) {
|
||||
const dirPath = dirname(absPath);
|
||||
if (!existsSync(dirPath)) {
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (fileType === 'yaml') {
|
||||
const yamlData = yaml.safeDump(entries);
|
||||
writeFileSync(absPath, yamlData, 'utf8');
|
||||
} else {
|
||||
writeFileSync(absPath, JSON.stringify(entries, null, 2));
|
||||
}
|
||||
|
||||
this.processDataFile(msgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the file data
|
||||
* @param msgData
|
||||
*/
|
||||
private static async processDataFile(msgData: DataFile) {
|
||||
try {
|
||||
const { file } = msgData;
|
||||
const dataFile = this.getDataFile(file);
|
||||
|
||||
if (msgData.fileType === "yaml") {
|
||||
const entries = yaml.safeLoad(dataFile || "");
|
||||
this.sendMsg(DashboardCommand.dataFileEntries, entries);
|
||||
} else {
|
||||
const jsonData = dataFile ? JSON.parse(dataFile) : [];
|
||||
this.sendMsg(DashboardCommand.dataFileEntries, jsonData);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger.error((ex as Error).message);
|
||||
const btnClick = await Notifications.error(`Something went wrong while processing the data file. Check your file and output log for more information.`, 'Open output');
|
||||
|
||||
if (btnClick && btnClick === 'Open output') {
|
||||
commands.executeCommand(`workbench.panel.output.focus`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file data
|
||||
* @param file
|
||||
* @returns
|
||||
*/
|
||||
private static getDataFile(file: string) {
|
||||
const absPath = Folders.getAbsFilePath(file);
|
||||
if (existsSync(absPath)) {
|
||||
return readFileSync(absPath, 'utf8');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { SortingOption } from '../dashboardWebView/models';
|
||||
import { commands } from 'vscode';
|
||||
import { commands, env, Uri } from 'vscode';
|
||||
import { COMMAND_NAME } from '../constants';
|
||||
import * as os from 'os';
|
||||
|
||||
|
||||
export class MediaListener extends BaseListener {
|
||||
@@ -28,6 +29,9 @@ export class MediaListener extends BaseListener {
|
||||
case DashboardMessage.deleteMedia:
|
||||
this.delete(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.revealMedia:
|
||||
this.openFileInFinder(msg?.data?.file);
|
||||
break;
|
||||
case DashboardMessage.insertPreviewImage:
|
||||
MediaHelpers.insertMediaToMarkdown(msg?.data);
|
||||
break;
|
||||
@@ -51,6 +55,16 @@ export class MediaListener extends BaseListener {
|
||||
this.sendMsg(DashboardCommand.media, files);
|
||||
}
|
||||
|
||||
private static openFileInFinder(file: string) {
|
||||
if (file) {
|
||||
if (os.type() === "Linux" && env.remoteName?.toLowerCase() === "wsl") {
|
||||
commands.executeCommand('remote-wsl.revealInExplorer', Uri.parse(file));
|
||||
} else {
|
||||
commands.executeCommand('revealFileInOS', Uri.parse(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the file and send a message after multiple uploads
|
||||
* @param data
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isValidFile } from './../helpers/isValidFile';
|
||||
import { existsSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { basename, dirname, join } from "path";
|
||||
import { commands, FileSystemWatcher, RelativePattern, Uri, workspace } from "vscode";
|
||||
import { Dashboard } from "../commands/Dashboard";
|
||||
import { Folders } from "../commands/Folders";
|
||||
@@ -8,7 +8,7 @@ import { COMMAND_NAME, DefaultFields, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DA
|
||||
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
|
||||
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
|
||||
import { Page } from "../dashboardWebView/models";
|
||||
import { ArticleHelper, Settings } from "../helpers";
|
||||
import { ArticleHelper, Logger, Settings } from "../helpers";
|
||||
import { ContentType } from "../helpers/ContentType";
|
||||
import { DateHelper } from "../helpers/DateHelper";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
@@ -17,6 +17,7 @@ import { BaseListener } from "./BaseListener";
|
||||
|
||||
export class PagesListener extends BaseListener {
|
||||
private static watchers: { [path: string]: FileSystemWatcher } = {};
|
||||
private static lastPages: Page[] = [];
|
||||
|
||||
/**
|
||||
* Start watching the folders in the current workspace for content changes
|
||||
@@ -39,9 +40,10 @@ export class PagesListener extends BaseListener {
|
||||
// Recreate all the watchers
|
||||
for (const folder of folders) {
|
||||
const folderUri = Uri.parse(folder.path);
|
||||
let watcher = workspace.createFileSystemWatcher(new RelativePattern(folderUri, "*"));
|
||||
watcher.onDidCreate(async (uri: Uri) => this.getPagesData);
|
||||
watcher.onDidDelete(async (uri: Uri) => this.getPagesData);
|
||||
let watcher = workspace.createFileSystemWatcher(new RelativePattern(folderUri, "*"), false, false, false);
|
||||
watcher.onDidCreate(async (uri: Uri) => this.watcherExec(uri));
|
||||
watcher.onDidChange(async (uri: Uri) => this.watcherExec(uri));
|
||||
watcher.onDidDelete(async (uri: Uri) => this.watcherExec(uri));
|
||||
this.watchers[folderUri.fsPath] = watcher;
|
||||
}
|
||||
}
|
||||
@@ -69,16 +71,29 @@ export class PagesListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
private static async watcherExec(file: Uri) {
|
||||
if (Dashboard.isOpen) {
|
||||
Logger.info(`File watcher execution for: ${file.fsPath}`)
|
||||
|
||||
const pageIdx = this.lastPages.findIndex(p => p.fmFilePath === file.fsPath);
|
||||
if (pageIdx !== -1) {
|
||||
const stats = await workspace.fs.stat(file);
|
||||
const crntPage = this.lastPages[pageIdx];
|
||||
const updatedPage = this.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder);
|
||||
if (updatedPage) {
|
||||
this.lastPages[pageIdx] = updatedPage;
|
||||
this.sendMsg(DashboardCommand.pages, this.lastPages);
|
||||
}
|
||||
} else {
|
||||
this.getPagesData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the markdown pages
|
||||
*/
|
||||
private static async getPagesData() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
|
||||
const dateField = Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Page[] = [];
|
||||
|
||||
@@ -87,62 +102,12 @@ export class PagesListener extends BaseListener {
|
||||
for (const file of folder.lastModified) {
|
||||
if (isValidFile(file.fileName)) {
|
||||
try {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title);
|
||||
|
||||
if (article?.data.title) {
|
||||
const page: Page = {
|
||||
...article.data,
|
||||
// FrontMatter properties
|
||||
fmFolder: folder.title,
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
fmDraft: ContentType.getDraftStatus(article?.data),
|
||||
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
date: article?.data[dateField] || "",
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.getWebview()?.asWebviewUri(previewUri);
|
||||
page[previewField] = preview?.toString() || "";
|
||||
} else {
|
||||
page[previewField] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (page) {
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
|
||||
}
|
||||
@@ -151,10 +116,85 @@ export class PagesListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPages = pages;
|
||||
this.sendMsg(DashboardCommand.pages, pages);
|
||||
}
|
||||
|
||||
public static refresh() {
|
||||
this.getPagesData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the page content
|
||||
* @param filePath
|
||||
* @param fileMtime
|
||||
* @param fileName
|
||||
* @param folderTitle
|
||||
* @returns
|
||||
*/
|
||||
private static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined {
|
||||
const article = ArticleHelper.getFrontMatterByPath(filePath);
|
||||
|
||||
if (article?.data.title) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
|
||||
const dateField = Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const page: Page = {
|
||||
...article.data,
|
||||
// FrontMatter properties
|
||||
fmFolder: folderTitle,
|
||||
fmModified: fileMtime,
|
||||
fmFilePath: filePath,
|
||||
fmFileName: fileName,
|
||||
fmDraft: ContentType.getDraftStatus(article?.data),
|
||||
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
date: article?.data[dateField] || "",
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Revalidate as the array could have been empty
|
||||
if (fieldValue) {
|
||||
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
|
||||
const contentFolderPath = join(dirname(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.getWebview()?.asWebviewUri(previewUri);
|
||||
page[previewField] = preview?.toString() || "";
|
||||
} else {
|
||||
page[previewField] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,5 @@ export interface CustomTaxonomyData {
|
||||
name: string | undefined;
|
||||
options?: string[] | undefined;
|
||||
option?: string | undefined;
|
||||
parents?: string[];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface DashboardData {
|
||||
type: "contents" | "media";
|
||||
type: "contents" | "media" | "data";
|
||||
data?: any;
|
||||
}
|
||||
9
src/models/DataFile.ts
Normal file
9
src/models/DataFile.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface DataFile {
|
||||
id: string;
|
||||
title: string;
|
||||
file: string;
|
||||
fileType: "json" | "yaml";
|
||||
labelField: string;
|
||||
schema?: any;
|
||||
type?: string;
|
||||
}
|
||||
9
src/models/DataFolder.ts
Normal file
9
src/models/DataFolder.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
export interface DataFolder {
|
||||
id: string;
|
||||
path: string;
|
||||
labelField: string;
|
||||
schema?: any;
|
||||
type?: string;
|
||||
}
|
||||
4
src/models/DataType.ts
Normal file
4
src/models/DataType.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface DataType {
|
||||
id: string;
|
||||
schema: any;
|
||||
}
|
||||
@@ -20,13 +20,20 @@ export interface PanelSettings {
|
||||
contentTypes: ContentType[];
|
||||
dashboardViewData: DashboardData | undefined;
|
||||
draftField: DraftField;
|
||||
isBacker: boolean | undefined;
|
||||
framework: string | undefined;
|
||||
commands: FrameworkCommands;
|
||||
}
|
||||
|
||||
export interface FrameworkCommands {
|
||||
start: string | undefined;
|
||||
}
|
||||
|
||||
export interface ContentType {
|
||||
name: string;
|
||||
fields: Field[];
|
||||
|
||||
fileType?: "md" | "mdx";
|
||||
fileType?: "md" | "mdx" | string;
|
||||
previewPath?: string | null;
|
||||
pageBundle?: boolean;
|
||||
}
|
||||
@@ -34,13 +41,15 @@ export interface ContentType {
|
||||
export interface Field {
|
||||
title?: string;
|
||||
name: string;
|
||||
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy";
|
||||
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields";
|
||||
choices?: string[] | Choice[];
|
||||
single?: boolean;
|
||||
multiple?: boolean;
|
||||
isPreviewImage?: boolean;
|
||||
hidden?: boolean;
|
||||
taxonomyId?: string;
|
||||
default?: string;
|
||||
fields?: Field[];
|
||||
}
|
||||
|
||||
export interface DateInfo {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum SortType {
|
||||
string = 'string',
|
||||
number = 'number',
|
||||
date = 'date'
|
||||
}
|
||||
@@ -28,4 +28,6 @@ export enum CommandToCode {
|
||||
selectImage = "select-image",
|
||||
updateCustomTaxonomy = "updateCustomTaxonomy",
|
||||
addToCustomTaxonomy = "addToCustomTaxonomy",
|
||||
frameworkCommand = "framework-command",
|
||||
updateStartCommand = "update-start-command",
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React
|
||||
<OtherActions settings={settings} isFile={true} />
|
||||
</div>
|
||||
|
||||
<SponsorMsg />
|
||||
<SponsorMsg isBacker={settings?.isBacker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,14 +4,14 @@ import { Collapsible } from './Collapsible';
|
||||
import { CustomScript } from './CustomScript';
|
||||
import { Preview } from './Preview';
|
||||
import { SlugAction } from './SlugAction';
|
||||
import { StartServerButton } from './StartServerButton';
|
||||
|
||||
export interface IActionsProps {
|
||||
metadata: any;
|
||||
settings: PanelSettings;
|
||||
}
|
||||
|
||||
const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithChildren<IActionsProps>) => {
|
||||
const { metadata, settings } = props;
|
||||
const Actions: React.FunctionComponent<IActionsProps> = ({ metadata, settings }: React.PropsWithChildren<IActionsProps>) => {
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0 || !settings) {
|
||||
return null;
|
||||
@@ -21,10 +21,12 @@ const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithC
|
||||
<Collapsible id={`actions`} title="Actions">
|
||||
<div className={`article__actions`}>
|
||||
|
||||
{ metadata && metadata.title && <SlugAction value={metadata.title} crntValue={metadata.slug} slugOpts={settings.slug} /> }
|
||||
{ metadata && metadata.title && <SlugAction /> }
|
||||
|
||||
{ settings?.preview?.host && <Preview slug={metadata.slug} /> }
|
||||
|
||||
<StartServerButton settings={settings} />
|
||||
|
||||
{
|
||||
(settings && settings.scripts && settings.scripts.length > 0) && (
|
||||
settings.scripts.map((value, idx) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { FolderInfo, PanelSettings } from '../../models';
|
||||
import { CustomScript, FolderInfo, PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { Collapsible } from './Collapsible';
|
||||
@@ -7,6 +7,7 @@ import { GlobalSettings } from './GlobalSettings';
|
||||
import { OtherActions } from './OtherActions';
|
||||
import { FolderAndFiles } from './FolderAndFiles';
|
||||
import { SponsorMsg } from './SponsorMsg';
|
||||
import { StartServerButton } from './StartServerButton';
|
||||
|
||||
export interface IBaseViewProps {
|
||||
settings: PanelSettings | undefined;
|
||||
@@ -31,6 +32,12 @@ const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndF
|
||||
MessageHelper.sendMessage(CommandToCode.openPreview);
|
||||
};
|
||||
|
||||
const runBulkScript = (script: CustomScript) => {
|
||||
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title: script.title, script });
|
||||
};
|
||||
|
||||
const customActions: any[] = (settings?.scripts || []).filter(s => s.bulk && (s.type === "content" || !s.type));
|
||||
|
||||
return (
|
||||
<div className="frontmatter">
|
||||
<div className={`ext_actions`}>
|
||||
@@ -39,9 +46,15 @@ const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndF
|
||||
<Collapsible id={`base_actions`} title="Actions">
|
||||
<div className={`base__actions`}>
|
||||
<button onClick={openDashboard}>Open dashboard</button>
|
||||
<StartServerButton settings={settings} />
|
||||
<button onClick={initProject} disabled={settings?.isInitialized}>Initialize project</button>
|
||||
<button onClick={createContent} disabled={!settings?.isInitialized}>Create new content</button>
|
||||
<button onClick={openPreview} disabled={!settings?.preview?.host}>Open site preview</button>
|
||||
{
|
||||
customActions.map((script) => (
|
||||
<button key={script.title} onClick={() => runBulkScript(script)}>{ script.title }</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
@@ -50,7 +63,7 @@ const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndF
|
||||
<OtherActions settings={settings} isFile={false} isBase />
|
||||
</div>
|
||||
|
||||
<SponsorMsg />
|
||||
<SponsorMsg isBacker={settings?.isBacker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label,
|
||||
|
||||
<Downshift
|
||||
ref={dsRef}
|
||||
onChange={(selected) => onValueChange(selected || "")}
|
||||
onSelect={(selected) => onValueChange(selected || "")}
|
||||
itemToString={item => (item ? item : '')}>
|
||||
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
|
||||
<div {...getRootProps(undefined, {suppressRefError: true})} className={`metadata_field__choice`}>
|
||||
|
||||
@@ -15,18 +15,21 @@ export interface IPreviewImageFieldProps {
|
||||
fieldName: string;
|
||||
value: PreviewImageValue | PreviewImageValue[] | null;
|
||||
filePath: string | null;
|
||||
parents?: string[];
|
||||
multiple?: boolean;
|
||||
onChange: (value: string | string[] | null) => void;
|
||||
}
|
||||
|
||||
export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps> = ({label, fieldName, onChange, value, filePath, multiple}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
|
||||
export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps> = ({label, fieldName, onChange, value, filePath, multiple, parents}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
|
||||
|
||||
const selectImage = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.selectImage, {
|
||||
filePath: filePath,
|
||||
fieldName,
|
||||
value,
|
||||
multiple
|
||||
multiple,
|
||||
metadataInsert: true,
|
||||
parents
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { isValidFile } from '../../helpers/isValidFile';
|
||||
import { DEFAULT_FILE_TYPES } from '../../constants/DefaultFileTypes';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { FileIcon } from './Icons/FileIcon';
|
||||
@@ -16,11 +16,14 @@ const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }: React
|
||||
MessageHelper.sendMessage(CommandToCode.openInEditor, path);
|
||||
};
|
||||
|
||||
// File extension
|
||||
const fileExtension = `.${name.split('.').pop()}`;
|
||||
|
||||
return (
|
||||
<li className={`file_list__items__item`}
|
||||
onClick={openFile}>
|
||||
{
|
||||
(isValidFile(name)) ? (
|
||||
(DEFAULT_FILE_TYPES.includes(fileExtension)) ? (
|
||||
<MarkdownIcon />
|
||||
) : (
|
||||
<FileIcon />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { VsCheckbox, VsLabel } from './VscodeComponents';
|
||||
import useStartCommand from '../hooks/useStartCommand';
|
||||
|
||||
export interface IGlobalSettingsProps {
|
||||
settings: PanelSettings | undefined;
|
||||
@@ -14,7 +15,11 @@ export interface IGlobalSettingsProps {
|
||||
const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings, isBase}: React.PropsWithChildren<IGlobalSettingsProps>) => {
|
||||
const { modifiedDateUpdate, fmHighlighting } = settings || {};
|
||||
const [ previewUrl, setPreviewUrl ] = React.useState<string>("");
|
||||
const [ startCommandValue, setStartCommandValue ] = React.useState<string | null>(null);
|
||||
const [ isDirty, setIsDirty ] = React.useState<boolean>(false);
|
||||
const { startCommand } = useStartCommand(settings);
|
||||
|
||||
const debounceStartCommand = useDebounce(startCommandValue, 1000);
|
||||
const debouncePreviewUrl = useDebounce(previewUrl, 1000);
|
||||
|
||||
const onDateCheck = () => {
|
||||
@@ -30,12 +35,21 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings
|
||||
setPreviewUrl(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const updateStartCommand = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsDirty(true);
|
||||
setStartCommandValue(e.currentTarget.value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings?.preview.host) {
|
||||
setPreviewUrl(settings.preview.host);
|
||||
}
|
||||
}, [settings?.preview.host]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setStartCommandValue(startCommand);
|
||||
}, [startCommand]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDirty) {
|
||||
setIsDirty(false);
|
||||
@@ -43,6 +57,13 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings
|
||||
}
|
||||
}, [debouncePreviewUrl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDirty) {
|
||||
setIsDirty(false);
|
||||
MessageHelper.sendMessage(CommandToCode.updateStartCommand, debounceStartCommand);
|
||||
}
|
||||
}, [debounceStartCommand]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapsible id={`${isBase ? "base_" : ""}settings`} className={`base__actions`} title="Global settings">
|
||||
@@ -62,6 +83,14 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings
|
||||
value={previewUrl}
|
||||
onChange={previewChange} />
|
||||
</div>
|
||||
<div className={`base__action`}>
|
||||
<VsLabel>Local server command</VsLabel>
|
||||
<input
|
||||
type={`text`}
|
||||
placeholder="Example: hugo server -D"
|
||||
value={startCommandValue || ""}
|
||||
onChange={updateStartCommand} />
|
||||
</div>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,10 +19,14 @@ import useContentType from '../../hooks/useContentType';
|
||||
import { DateHelper } from '../../helpers/DateHelper';
|
||||
import FieldBoundary from './ErrorBoundary/FieldBoundary';
|
||||
import { DraftField } from './Fields/DraftField';
|
||||
import { VsLabel } from './VscodeComponents';
|
||||
|
||||
export interface IMetadata {
|
||||
[prop: string]: string[] | string | null | IMetadata;
|
||||
}
|
||||
export interface IMetadataProps {
|
||||
settings: PanelSettings | undefined;
|
||||
metadata: { [prop: string]: string[] | string | null };
|
||||
metadata: IMetadata;
|
||||
focusElm: TagType | null;
|
||||
unsetFocus: () => void;
|
||||
}
|
||||
@@ -30,13 +34,14 @@ export interface IMetadataProps {
|
||||
const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren<IMetadataProps>) => {
|
||||
const contentType = useContentType(settings, metadata);
|
||||
|
||||
const sendUpdate = (field: string | undefined, value: any) => {
|
||||
const sendUpdate = (field: string | undefined, value: any, parents: string[]) => {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
MessageHelper.sendMessage(CommandToCode.updateMetadata, {
|
||||
field,
|
||||
parents,
|
||||
value
|
||||
});
|
||||
};
|
||||
@@ -50,7 +55,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderFields = (ctFields: Field[]) => {
|
||||
const renderFields = (ctFields: Field[], parent: IMetadata, parentFields: string[] = []) => {
|
||||
if (!ctFields) {
|
||||
return;
|
||||
}
|
||||
@@ -61,7 +66,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
}
|
||||
|
||||
if (field.type === 'datetime') {
|
||||
const dateValue = metadata[field.name] ? getDate(metadata[field.name] as string) : null;
|
||||
const dateValue = parent[field.name] ? getDate(parent[field.name] as string) : null;
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
@@ -69,7 +74,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
label={field.title || field.name}
|
||||
date={dateValue}
|
||||
format={settings?.date?.format}
|
||||
onChange={(date => sendUpdate(field.name, date))} />
|
||||
onChange={(date => sendUpdate(field.name, date, parentFields))} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'boolean') {
|
||||
@@ -78,12 +83,12 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
<Toggle
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
checked={!!metadata[field.name] as any}
|
||||
onChanged={(checked) => sendUpdate(field.name, checked)} />
|
||||
checked={!!parent[field.name] as any}
|
||||
onChanged={(checked) => sendUpdate(field.name, checked, parentFields)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'string') {
|
||||
const textValue = metadata[field.name];
|
||||
const textValue = parent[field.name];
|
||||
|
||||
let limit = -1;
|
||||
if (field.name === 'title') {
|
||||
@@ -99,12 +104,12 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
singleLine={field.single}
|
||||
limit={limit}
|
||||
rows={3}
|
||||
onChange={(value) => sendUpdate(field.name, value)}
|
||||
onChange={(value) => sendUpdate(field.name, value, parentFields)}
|
||||
value={textValue as string || null} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'number') {
|
||||
const fieldValue = metadata[field.name];
|
||||
const fieldValue = parent[field.name];
|
||||
let nrValue: number | null = parseInt(fieldValue as string);
|
||||
if (isNaN(nrValue)) {
|
||||
nrValue = null;
|
||||
@@ -115,7 +120,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
<NumberField
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
onChange={(value) => sendUpdate(field.name, value)}
|
||||
onChange={(value) => sendUpdate(field.name, value, parentFields)}
|
||||
value={nrValue} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
@@ -126,14 +131,15 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
label={field.title || field.name}
|
||||
fieldName={field.name}
|
||||
filePath={metadata.filePath as string}
|
||||
value={metadata[field.name] as PreviewImageValue | PreviewImageValue[] | null}
|
||||
parents={parentFields}
|
||||
value={parent[field.name] as PreviewImageValue | PreviewImageValue[] | null}
|
||||
multiple={field.multiple}
|
||||
onChange={(value => sendUpdate(field.name, value))} />
|
||||
onChange={(value) => sendUpdate(field.name, value, parentFields)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'choice') {
|
||||
const choices = field.choices || [];
|
||||
const choiceValue = metadata[field.name];
|
||||
const choiceValue = parent[field.name];
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
@@ -142,7 +148,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
selected={choiceValue as string}
|
||||
choices={choices}
|
||||
multiSelect={field.multiple}
|
||||
onChange={(value => sendUpdate(field.name, value))} />
|
||||
onChange={(value => sendUpdate(field.name, value, parentFields))} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'tags') {
|
||||
@@ -152,16 +158,17 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
type={TagType.tags}
|
||||
label={field.title || field.name}
|
||||
icon={<TagIcon />}
|
||||
crntSelected={metadata[field.name] as string[] || []}
|
||||
crntSelected={parent[field.name] as string[] || []}
|
||||
options={settings?.tags || []}
|
||||
freeform={settings.freeform}
|
||||
focussed={focusElm === TagType.tags}
|
||||
unsetFocus={unsetFocus} />
|
||||
unsetFocus={unsetFocus}
|
||||
parents={parentFields} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'taxonomy') {
|
||||
const taxonomyData = settings.customTaxonomy.find(ct => ct.id === field.taxonomyId);
|
||||
const selectedValues = metadata[field.name] || [];
|
||||
const selectedValues = parent[field.name] || [];
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
@@ -175,7 +182,8 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
focussed={focusElm === TagType.custom}
|
||||
unsetFocus={unsetFocus}
|
||||
fieldName={field.name}
|
||||
taxonomyId={field.taxonomyId} />
|
||||
taxonomyId={field.taxonomyId}
|
||||
parents={parentFields} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'categories') {
|
||||
@@ -185,16 +193,17 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
type={TagType.categories}
|
||||
label={field.title || field.name}
|
||||
icon={<ListUnorderedIcon />}
|
||||
crntSelected={metadata.categories as string[] || []}
|
||||
crntSelected={parent.categories as string[] || []}
|
||||
options={settings.categories}
|
||||
freeform={settings.freeform}
|
||||
focussed={focusElm === TagType.categories}
|
||||
unsetFocus={unsetFocus} />
|
||||
unsetFocus={unsetFocus}
|
||||
parents={parentFields} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'draft') {
|
||||
const draftField = settings?.draftField;
|
||||
const value = metadata[field.name];
|
||||
const value = parent[field.name];
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
@@ -203,9 +212,28 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
type={draftField.type}
|
||||
choices={draftField.choices || []}
|
||||
value={value as boolean | string | null | undefined}
|
||||
onChanged={(value: boolean | string) => sendUpdate(field.name, value)} />
|
||||
onChanged={(value: boolean | string) => sendUpdate(field.name, value, parentFields)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'fields') {
|
||||
if (field.fields && parent && parent[field.name]) {
|
||||
const subMetadata = parent[field.name] as IMetadata;
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<div className={`metadata_field__box`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label metadata_field__label_parent`}>
|
||||
<span style={{ lineHeight: "16px"}}>{field.title || field.name}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
{ renderFields(field.fields, subMetadata, [...parentFields, field.name]) }
|
||||
</div>
|
||||
</FieldBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -214,9 +242,9 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
|
||||
return (
|
||||
<Collapsible id={`tags`} title="Metadata" className={`inherit z-20`}>
|
||||
|
||||
|
||||
{
|
||||
renderFields(contentType?.fields)
|
||||
renderFields(contentType?.fields, metadata)
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -33,12 +33,22 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword,
|
||||
}
|
||||
};
|
||||
|
||||
const validateKeywords = (heading: string, keyword: string) => {
|
||||
const keywords = keyword.toLowerCase().split(' ');
|
||||
|
||||
if (keywords.length > 1) {
|
||||
return heading.toLowerCase().includes(keyword.toLowerCase());
|
||||
} else {
|
||||
return heading.toLowerCase().split(' ').findIndex(word => word.toLowerCase() === keyword.toLowerCase()) !== -1;
|
||||
}
|
||||
};
|
||||
|
||||
const checkHeadings = () => {
|
||||
if (!headings || headings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exists = headings.filter(heading => heading.split(' ').findIndex(word => word.toLowerCase() === keyword.toLowerCase()) !== -1);
|
||||
|
||||
const exists = headings.filter(heading => validateKeywords(heading, keyword));
|
||||
return <ValidInfo label={`Used in heading(s)`} isValid={exists.length > 0} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { SlugHelper } from '../../helpers/SlugHelper';
|
||||
import { Slug } from '../../models/PanelSettings';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export interface ISlugActionProps {
|
||||
value: string;
|
||||
crntValue: string;
|
||||
slugOpts: Slug;
|
||||
}
|
||||
export interface ISlugActionProps {}
|
||||
|
||||
const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
const { value, crntValue, slugOpts } = props;
|
||||
|
||||
let slug = SlugHelper.createSlug(value);
|
||||
slug = `${slugOpts.prefix}${slug}${slugOpts.suffix}`;
|
||||
const SlugAction: React.FunctionComponent<ISlugActionProps> = ({}: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
|
||||
const optimize = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.updateSlug);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionButton onClick={optimize} disabled={crntValue === slug} title={`Optimize slug`} />
|
||||
<ActionButton onClick={optimize} title={`Optimize slug`} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@ import * as React from 'react';
|
||||
import { SPONSOR_LINK } from '../../constants/Links';
|
||||
import { HeartIcon } from './Icons/HeartIcon';
|
||||
|
||||
export interface ISponsorMsgProps {}
|
||||
export interface ISponsorMsgProps {
|
||||
isBacker: boolean | undefined;
|
||||
}
|
||||
|
||||
const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({ isBacker }: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
if (isBacker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
return (
|
||||
<p className={`sponsor`}>
|
||||
<a href={SPONSOR_LINK} title="Support Front Matter">
|
||||
|
||||
22
src/panelWebView/components/StartServerButton.tsx
Normal file
22
src/panelWebView/components/StartServerButton.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { FrameworkDetectors } from '../../constants/FrameworkDetectors';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import useStartCommand from '../hooks/useStartCommand';
|
||||
|
||||
export interface IStartServerButtonProps {
|
||||
settings: PanelSettings | undefined;
|
||||
}
|
||||
|
||||
export const StartServerButton: React.FunctionComponent<IStartServerButtonProps> = ({settings}: React.PropsWithChildren<IStartServerButtonProps>) => {
|
||||
const { startCommand } = useStartCommand(settings);
|
||||
|
||||
const startLocalServer = (command: string) => {
|
||||
MessageHelper.sendMessage(CommandToCode.frameworkCommand, { command });
|
||||
};
|
||||
|
||||
return (
|
||||
startCommand ? <button onClick={() => startLocalServer(startCommand)}>Start server</button> : null
|
||||
);
|
||||
};
|
||||
@@ -12,19 +12,21 @@ import { CustomTaxonomyData } from '../../models';
|
||||
export interface ITagPickerProps {
|
||||
type: TagType;
|
||||
icon: JSX.Element;
|
||||
label?: string;
|
||||
crntSelected: string[];
|
||||
options: string[];
|
||||
freeform: boolean;
|
||||
focussed: boolean;
|
||||
unsetFocus: () => void;
|
||||
|
||||
parents?: string[];
|
||||
label?: string;
|
||||
disableConfigurable?: boolean;
|
||||
fieldName?: string;
|
||||
taxonomyId?: string;
|
||||
}
|
||||
|
||||
const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const { label, icon, type, crntSelected, options, freeform, focussed, unsetFocus, disableConfigurable, fieldName, taxonomyId } = props;
|
||||
const { label, icon, type, crntSelected, options, freeform, focussed, unsetFocus, disableConfigurable, fieldName, taxonomyId, parents } = props;
|
||||
const [ selected, setSelected ] = React.useState<string[]>([]);
|
||||
const [ inputValue, setInputValue ] = React.useState<string>("");
|
||||
const prevSelected = usePrevious(crntSelected);
|
||||
@@ -65,16 +67,26 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
|
||||
*/
|
||||
const sendUpdate = (values: string[]) => {
|
||||
if (type === TagType.tags) {
|
||||
MessageHelper.sendMessage(CommandToCode.updateTags, values);
|
||||
MessageHelper.sendMessage(CommandToCode.updateTags, {
|
||||
values,
|
||||
parents
|
||||
});
|
||||
} else if (type === TagType.categories) {
|
||||
MessageHelper.sendMessage(CommandToCode.updateCategories, values);
|
||||
MessageHelper.sendMessage(CommandToCode.updateCategories, {
|
||||
values,
|
||||
parents
|
||||
});
|
||||
} else if (type === TagType.keywords) {
|
||||
MessageHelper.sendMessage(CommandToCode.updateKeywords, values);
|
||||
MessageHelper.sendMessage(CommandToCode.updateKeywords, {
|
||||
values,
|
||||
parents
|
||||
});
|
||||
} else if (type === TagType.custom) {
|
||||
MessageHelper.sendMessage(CommandToCode.updateCustomTaxonomy, {
|
||||
id: taxonomyId,
|
||||
name: fieldName,
|
||||
options: values
|
||||
options: values,
|
||||
parents
|
||||
} as CustomTaxonomyData);
|
||||
}
|
||||
};
|
||||
|
||||
28
src/panelWebView/hooks/useStartCommand.tsx
Normal file
28
src/panelWebView/hooks/useStartCommand.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FrameworkDetectors } from '../../constants/FrameworkDetectors';
|
||||
import { PanelSettings } from '../../models';
|
||||
|
||||
export default function useStartCommand(settings?: PanelSettings) {
|
||||
const [startCommand, setStartCommand] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.commands?.start) {
|
||||
setStartCommand(settings?.commands?.start);
|
||||
return;
|
||||
}
|
||||
|
||||
let command: string = '';
|
||||
if (settings?.framework) {
|
||||
const framework = FrameworkDetectors.find(f => f.framework.name === settings.framework);
|
||||
if (framework?.commands?.start) {
|
||||
command = framework.commands.start;
|
||||
}
|
||||
}
|
||||
|
||||
setStartCommand(command);
|
||||
}, [settings?.framework, settings?.commands?.start]);
|
||||
|
||||
return {
|
||||
startCommand
|
||||
};
|
||||
}
|
||||
@@ -18,23 +18,32 @@ import '@bendera/vscode-webview-elements/dist/vscode-checkbox.js';
|
||||
|
||||
// import '@vscode/webview-ui-toolkit/dist/esm/checkbox';
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
const version = elm?.getAttribute("data-version");
|
||||
const environment = elm?.getAttribute("data-environment");
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || "",
|
||||
environment: environment || "",
|
||||
ignoreErrors: ['ResizeObserver loop limit exceeded']
|
||||
});
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
getState: () => T;
|
||||
setState: (data: T) => void;
|
||||
postMessage: (msg: unknown) => void;
|
||||
};
|
||||
|
||||
render(<ViewPanel />, elm);
|
||||
const elm = document.querySelector("#app");
|
||||
|
||||
if (elm) {
|
||||
const version = elm?.getAttribute("data-version");
|
||||
const environment = elm?.getAttribute("data-environment");
|
||||
const isProd = elm?.getAttribute("data-isProd");
|
||||
|
||||
if (isProd === "true") {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || "",
|
||||
environment: environment || "",
|
||||
ignoreErrors: ['ResizeObserver loop limit exceeded']
|
||||
});
|
||||
}
|
||||
|
||||
render(<ViewPanel />, elm);
|
||||
}
|
||||
|
||||
// Webpack HMR
|
||||
if ((module as any).hot) (module as any).hot.accept();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TextEditorDecorationType } from 'vscode';
|
||||
import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, FoldingRangeProvider, Range, TextDocument, window, Position } from 'vscode';
|
||||
import { SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT } from '../constants';
|
||||
import { SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_FRONTMATTER_TYPE } from '../constants';
|
||||
import { Settings } from '../helpers';
|
||||
import { FrontMatterDecorationProvider } from './FrontMatterDecorationProvider';
|
||||
|
||||
@@ -50,6 +50,15 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider {
|
||||
* @returns
|
||||
*/
|
||||
public static getFrontMatterRange(document?: TextDocument) {
|
||||
const language = Settings.get(SETTING_FRONTMATTER_TYPE) as string || "YAML";
|
||||
|
||||
let lineStart = "---";
|
||||
let lineEnd = "---";
|
||||
if (language === "TOML") {
|
||||
lineStart = "+++";
|
||||
lineEnd = lineStart;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
const lines = document.getText().split('\n');
|
||||
|
||||
@@ -59,7 +68,7 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider {
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith('---')) {
|
||||
if (line.startsWith(lineStart) || line.startsWith(lineEnd)) {
|
||||
if (i === 0 && start === null) {
|
||||
start = i;
|
||||
} else if (start !== null && end === null) {
|
||||
|
||||
69
src/services/Credentials.ts
Normal file
69
src/services/Credentials.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as Octokit from '@octokit/rest';
|
||||
import { authentication, ExtensionContext } from 'vscode';
|
||||
|
||||
const GITHUB_AUTH_PROVIDER_ID = 'github';
|
||||
// The GitHub Authentication Provider accepts the scopes described here:
|
||||
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
|
||||
const SCOPES = ['user:email'];
|
||||
|
||||
export class Credentials {
|
||||
private octokit: Octokit.Octokit | undefined;
|
||||
|
||||
async initialize(context: ExtensionContext, callback: () => void): Promise<void> {
|
||||
this.registerListeners(context, callback);
|
||||
this.setOctokit();
|
||||
}
|
||||
|
||||
private async setOctokit() {
|
||||
/**
|
||||
* By passing the `createIfNone` flag, a numbered badge will show up on the accounts activity bar icon.
|
||||
* An entry for the sample extension will be added under the menu to sign in. This allows quietly
|
||||
* prompting the user to sign in.
|
||||
* */
|
||||
const session = await authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone: false });
|
||||
|
||||
if (session) {
|
||||
this.octokit = new Octokit.Octokit({
|
||||
auth: session.accessToken
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.octokit = undefined;
|
||||
}
|
||||
|
||||
registerListeners(context: ExtensionContext, callback: () => void): void {
|
||||
/**
|
||||
* Sessions are changed when a user logs in or logs out.
|
||||
*/
|
||||
context.subscriptions.push(
|
||||
authentication.onDidChangeSessions(async e => {
|
||||
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
|
||||
await this.setOctokit();
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getOctokit(): Promise<Octokit.Octokit> {
|
||||
if (this.octokit) {
|
||||
return this.octokit;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the `createIfNone` flag is passed, a modal dialog will be shown asking the user to sign in.
|
||||
* Note that this can throw if the user clicks cancel.
|
||||
*/
|
||||
const session = await authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone: true });
|
||||
this.octokit = new Octokit.Octokit({
|
||||
auth: session.accessToken
|
||||
});
|
||||
|
||||
return this.octokit;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
@@ -38,13 +38,7 @@ const config = [
|
||||
maxEntrypointSize: 400000,
|
||||
maxAssetSize: 400000
|
||||
},
|
||||
plugins: [
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: "dashboard.html",
|
||||
openAnalyzer: false
|
||||
})
|
||||
],
|
||||
plugins: [],
|
||||
devServer: {
|
||||
compress: true,
|
||||
port: 9000,
|
||||
@@ -60,6 +54,14 @@ const config = [
|
||||
module.exports = (env, argv) => {
|
||||
for (const configItem of config) {
|
||||
configItem.mode = argv.mode;
|
||||
|
||||
if (argv.mode === 'production') {
|
||||
configItem.plugins.push(new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: "dashboard.html",
|
||||
openAnalyzer: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -56,78 +56,21 @@ const config = [
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: "extension.html",
|
||||
openAnalyzer: false
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'panelWebView',
|
||||
target: 'web',
|
||||
entry: './src/panelWebView/index.tsx',
|
||||
output: {
|
||||
filename: 'panelWebView.js',
|
||||
path: path.resolve(__dirname, '../dist')
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: [{
|
||||
loader: 'ts-loader'
|
||||
}]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.m?js/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
maxEntrypointSize: 400000,
|
||||
maxAssetSize: 400000
|
||||
},
|
||||
// optimization: {
|
||||
// splitChunks: {
|
||||
// cacheGroups: {
|
||||
// vendors: {
|
||||
// test: /node_modules/,
|
||||
// chunks: 'initial',
|
||||
// filename: 'vendors.[contenthash].js',
|
||||
// priority: 1,
|
||||
// maxInitialRequests: 2, // create only one vendor file
|
||||
// minChunks: 1,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
plugins: [
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: "viewpanel.html",
|
||||
openAnalyzer: false
|
||||
})
|
||||
]
|
||||
plugins: []
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
for (const configItem of config) {
|
||||
configItem.mode = argv.mode;
|
||||
|
||||
if (argv.mode === 'production') {
|
||||
configItem.plugins.push(new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: "extension.html",
|
||||
openAnalyzer: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
74
webpack/panel.config.js
Normal file
74
webpack/panel.config.js
Normal file
@@ -0,0 +1,74 @@
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const config = [{
|
||||
name: 'panel',
|
||||
target: 'web',
|
||||
entry: './src/panelWebView/index.tsx',
|
||||
output: {
|
||||
filename: 'panelWebView.js',
|
||||
path: path.resolve(__dirname, '../dist')
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx'],
|
||||
fallback: {
|
||||
"path": require.resolve("path-browserify")
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: [{
|
||||
loader: 'ts-loader'
|
||||
}]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.m?js/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
maxEntrypointSize: 400000,
|
||||
maxAssetSize: 400000
|
||||
},
|
||||
plugins: [],
|
||||
devServer: {
|
||||
compress: true,
|
||||
port: 9001,
|
||||
hot: true,
|
||||
allowedHosts: "all",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
for (const configItem of config) {
|
||||
configItem.mode = argv.mode;
|
||||
|
||||
if (argv.mode === 'production') {
|
||||
configItem.plugins.push(new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: "viewpanel.html",
|
||||
openAnalyzer: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
Reference in New Issue
Block a user