Compare commits

...

43 Commits

Author SHA1 Message Date
Elio Struyf
09c48db957 Merge pull request #238 from estruyf/dev 2022-01-25 17:44:14 +01:00
Elio Struyf
d7658852b0 Reveal media file 2022-01-24 22:27:55 +01:00
Elio Struyf
0a0efba37b Updated changelog 2022-01-24 18:34:58 +01:00
Elio Struyf
a16c0c6355 #235 - Fix for reselecting the previously removed value 2022-01-24 18:34:24 +01:00
Elio Struyf
8dcbe67152 updated changelog 2022-01-22 19:11:25 +01:00
Elio Struyf
2c20621071 #234 - Fix for multi-word keywords 2022-01-22 19:10:42 +01:00
Elio Struyf
48c4c0b8e4 Updated supporters 2022-01-22 16:15:17 +01:00
Elio Struyf
2900777ffb updated changelog 2022-01-22 10:37:21 +01:00
Elio Struyf
0ccd428852 updating the docs 2022-01-22 10:35:57 +01:00
Elio Struyf
368ade6b44 Updated changelog 2022-01-21 17:03:03 +01:00
Elio Struyf
5f6b6e3b4a Fix for multi-dimensional fields + authentication improvement 2022-01-21 16:22:30 +01:00
Elio Struyf
43554a4303 #226 - Update the local server message 2022-01-21 14:00:29 +01:00
Elio Struyf
2b0007c21a Update changelog 2022-01-21 13:55:28 +01:00
Elio Struyf
5ab0bdaa69 #233 - Partial dashboard update on file change 2022-01-21 13:53:24 +01:00
Elio Struyf
1c74df0266 #225 - removed slug validation in button for dependent placeholders 2022-01-20 17:02:32 +01:00
Elio Struyf
469a1aaaf8 updated readme 2022-01-19 19:35:09 +01:00
Elio Struyf
c5523f7aaf Update overflow on data view 2022-01-19 14:08:17 +01:00
Elio Struyf
61b46bc5ac Update comments 2022-01-19 14:00:05 +01:00
Elio Struyf
d7a0f71552 #225 - Optimize slug will now process all fields with {{slug}} 2022-01-19 13:53:19 +01:00
Elio Struyf
7d6d60039e Optimized styling for data dashboard 2022-01-19 12:59:25 +01:00
Elio Struyf
42cc53cefc #226 - Allow to start the local server for the framework or SSG 2022-01-19 10:15:58 +01:00
Elio Struyf
e68daa8ac2 Styling updates 2022-01-18 18:57:06 +01:00
Elio Struyf
333cc1f9df Update FUNDING.yml 2022-01-18 13:24:12 +01:00
Elio Struyf
00bb8c6385 #227 - File type updates 2022-01-18 11:02:05 +01:00
Elio Struyf
1deb969c20 #227 - Added new supported file types setting 2022-01-17 14:48:46 +01:00
Elio Struyf
928afceca7 #231 - Backer support added 2022-01-17 14:11:04 +01:00
Elio Struyf
179b31f67c #230 - front matter json support added 2022-01-16 19:49:18 +01:00
Elio Struyf
7d4fe9ca0f Updated changelog 2022-01-15 20:35:31 +01:00
Elio Struyf
3e33383eb1 #225 - Placeholder support for front matter field values (template and content type) 2022-01-15 20:35:06 +01:00
Elio Struyf
66324fd292 #193 - support added for data folders 2022-01-15 19:31:15 +01:00
Elio Struyf
8b1fbcabaa Fix keys 2022-01-14 20:35:48 +01:00
Elio Struyf
90519488c1 #228 - Added action icon 2022-01-14 20:31:04 +01:00
Elio Struyf
1012e10ddc #228 - Add bulk actions 2022-01-14 20:23:28 +01:00
Elio Struyf
2dd129d9bd Remove unused references 2022-01-14 17:52:06 +01:00
Elio Struyf
6af5458082 #193 - YAML support for data files added 2022-01-14 15:22:51 +01:00
Elio Struyf
9744cf0117 #193 - Added data type support 2022-01-14 13:00:27 +01:00
Elio Struyf
01921c799c #193 - Support for list/array fields 2022-01-14 11:26:30 +01:00
Elio Struyf
b1674b4b84 #193 - Data file dashboard added 2022-01-13 20:18:32 +01:00
Elio Struyf
9f7f803e25 #198 - Additional media sort options 2022-01-12 14:44:45 +01:00
Elio Struyf
b83c565e29 HMR support for panel development 2022-01-12 14:09:59 +01:00
Elio Struyf
dee30923ff #197 - Update fields type + support for taxonomy and images 2022-01-12 11:25:44 +01:00
Elio Struyf
9a91be8025 #197 - Support for multi-dimensional content type fields 2022-01-11 12:23:46 +01:00
Elio Struyf
46a9d6e602 6.0.0 2022-01-11 08:55:06 +01:00
93 changed files with 5312 additions and 768 deletions

View File

@@ -0,0 +1 @@
{}

1
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,5 @@
# These are supported funding model platforms
github: [estruyf]
open_collective: frontmatter
custom: ["https://www.buymeacoffee.com/zMeFRy9"]

View File

@@ -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

View File

@@ -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 />

View File

@@ -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 />

View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

2684
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"displayName": "Front Matter",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "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"
}
}
}

View File

@@ -2,6 +2,7 @@ const tailwindcss = require('tailwindcss');
module.exports = {
plugins: [
require('postcss-nested'),
tailwindcss('./tailwind.config.js'),
require('autoprefixer'),
],

View File

@@ -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
View 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');
}
}
}

View File

@@ -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 `

View File

@@ -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.');
}

View File

@@ -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

View File

@@ -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);

View File

@@ -0,0 +1,3 @@
export const DEFAULT_FILE_TYPES = [".md", ".markdown", ".mdx"];

View File

@@ -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"),

View File

@@ -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"
}
}
];

View File

@@ -5,4 +5,5 @@ export const CONTEXT = {
isEnabled: "frontMatter:enabled",
isDashboardOpen: "frontMatter:dashboard:open",
wysiwyg: "frontMatter:markdown:wysiwyg",
backer: "frontMatter:backers:supporter",
};

View File

@@ -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";

View File

@@ -4,5 +4,6 @@ export enum DashboardCommand {
settings = "settings",
media = "media",
viewData = "viewData",
mediaUpdate = "mediaUpdate"
mediaUpdate = "mediaUpdate",
dataFileEntries = "dataFileEntries"
}

View File

@@ -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',
}

View File

@@ -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}
>

View File

@@ -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} />

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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> ));

View 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);
}} />
)
}
</>
);
});

View File

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

View File

@@ -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>

View File

@@ -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}

View 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>
);
};

View 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>
);
};

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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",
}

View File

@@ -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:

View File

@@ -1,4 +1,5 @@
export enum NavigationType {
Contents = "contents",
Media = "media"
Media = "media",
Data = "data",
}

View File

@@ -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 {

View File

@@ -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;
}
}
}

View File

@@ -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>
`;

View File

@@ -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();
}));

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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];

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -4,4 +4,5 @@ export interface CustomTaxonomyData {
name: string | undefined;
options?: string[] | undefined;
option?: string | undefined;
parents?: string[];
}

View File

@@ -1,4 +1,4 @@
export interface DashboardData {
type: "contents" | "media";
type: "contents" | "media" | "data";
data?: any;
}

9
src/models/DataFile.ts Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
export interface DataType {
id: string;
schema: any;
}

View File

@@ -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 {

View File

@@ -1,4 +1,5 @@
export enum SortType {
string = 'string',
number = 'number',
date = 'date'
}

View File

@@ -28,4 +28,6 @@ export enum CommandToCode {
selectImage = "select-image",
updateCustomTaxonomy = "updateCustomTaxonomy",
addToCustomTaxonomy = "addToCustomTaxonomy",
frameworkCommand = "framework-command",
updateStartCommand = "update-start-command",
}

View File

@@ -59,7 +59,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React
<OtherActions settings={settings} isFile={true} />
</div>
<SponsorMsg />
<SponsorMsg isBacker={settings?.isBacker} />
</div>
);
};

View File

@@ -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) => (

View File

@@ -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>
);
};

View File

@@ -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`}>

View File

@@ -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
});
};

View File

@@ -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 />

View File

@@ -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>
</>
);

View File

@@ -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)
}
{

View File

@@ -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} />;
};

View File

@@ -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`} />
);
};

View File

@@ -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">

View 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
);
};

View File

@@ -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);
}
};

View 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
};
}

View File

@@ -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();

View File

@@ -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) {

View 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;
}
}

View File

@@ -13,7 +13,8 @@
"sourceMap": true,
"rootDir": "src",
"strict": true,
"jsx": "react"
"jsx": "react",
"strictNullChecks": true
},
"exclude": [
"node_modules",

View File

@@ -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;

View File

@@ -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
View 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;
};