Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c48db957 | ||
|
|
d7658852b0 | ||
|
|
0a0efba37b | ||
|
|
a16c0c6355 | ||
|
|
8dcbe67152 | ||
|
|
2c20621071 | ||
|
|
48c4c0b8e4 | ||
|
|
2900777ffb | ||
|
|
0ccd428852 | ||
|
|
368ade6b44 | ||
|
|
5f6b6e3b4a | ||
|
|
43554a4303 | ||
|
|
2b0007c21a | ||
|
|
5ab0bdaa69 | ||
|
|
1c74df0266 | ||
|
|
469a1aaaf8 | ||
|
|
c5523f7aaf | ||
|
|
61b46bc5ac | ||
|
|
d7a0f71552 | ||
|
|
7d6d60039e | ||
|
|
42cc53cefc | ||
|
|
e68daa8ac2 | ||
|
|
333cc1f9df | ||
|
|
00bb8c6385 | ||
|
|
1deb969c20 | ||
|
|
928afceca7 | ||
|
|
179b31f67c | ||
|
|
7d4fe9ca0f | ||
|
|
3e33383eb1 | ||
|
|
66324fd292 | ||
|
|
8b1fbcabaa | ||
|
|
90519488c1 | ||
|
|
1012e10ddc | ||
|
|
2dd129d9bd | ||
|
|
6af5458082 | ||
|
|
9744cf0117 | ||
|
|
01921c799c | ||
|
|
b1674b4b84 | ||
|
|
9f7f803e25 | ||
|
|
b83c565e29 | ||
|
|
dee30923ff | ||
|
|
9a91be8025 | ||
|
|
46a9d6e602 | ||
|
|
511fd48081 | ||
|
|
0039fc1555 | ||
|
|
98044187cd | ||
|
|
a6dcc1ea79 | ||
|
|
32a686227e | ||
|
|
faa74132e5 | ||
|
|
3a847f7e42 | ||
|
|
66c978891e | ||
|
|
2f31230e07 | ||
|
|
c4225c0011 | ||
|
|
4edc7a0280 | ||
|
|
a60fe5204b | ||
|
|
bb980b4afe | ||
|
|
504658d87a | ||
|
|
feff69d969 | ||
|
|
a34d77242a | ||
|
|
1b24c1277d | ||
|
|
e6750205be | ||
|
|
1da8bf3f8b | ||
|
|
90c60b6a40 | ||
|
|
ee79f89c7f | ||
|
|
0668d48fd5 | ||
|
|
d046f73d16 | ||
|
|
f144d713d1 | ||
|
|
d31c403bdc | ||
|
|
35a0327387 | ||
|
|
9b39649bde | ||
|
|
3d857463f0 | ||
|
|
0428642a2f | ||
|
|
5182a9ae1a | ||
|
|
ab3686b3b5 | ||
|
|
c5b7b7845d | ||
|
|
2f13c335ed | ||
|
|
f219ac721f | ||
|
|
0149885289 | ||
|
|
cb80a10de2 | ||
|
|
092eb0fd2a | ||
|
|
24f79d9d3f | ||
|
|
d667b19716 | ||
|
|
4d38a0881a | ||
|
|
1478d0bd53 | ||
|
|
00a9e59bc2 | ||
|
|
2b2843234e | ||
|
|
71449aa5cc | ||
|
|
87f293a3f6 | ||
|
|
6da3ddb5dd | ||
|
|
87e80ccfe9 | ||
|
|
af9865d91b | ||
|
|
e576b6e8a4 | ||
|
|
c9f4c8f94e | ||
|
|
6ef63da973 | ||
|
|
2d85b0a554 | ||
|
|
41cbcc4c46 | ||
|
|
6c94880497 | ||
|
|
1d75c6b6b8 | ||
|
|
338bc022f9 | ||
|
|
2451730311 | ||
|
|
22cdaabe28 | ||
|
|
5a69240178 | ||
|
|
b81ef077f6 | ||
|
|
cb72b53653 | ||
|
|
050a513b48 | ||
|
|
481d1f56e4 | ||
|
|
229238703a | ||
|
|
074de212ce | ||
|
|
98e4490318 | ||
|
|
c79dc3c41f | ||
|
|
b45abe88ac | ||
|
|
2bac596e3d | ||
|
|
15a6ea8d8d | ||
|
|
72d6f8263a | ||
|
|
7549b3d989 | ||
|
|
ca628b37de | ||
|
|
db120b5c45 | ||
|
|
2973f5d27b | ||
|
|
075c7ad350 | ||
|
|
23625ed8fa | ||
|
|
9bd06909c8 | ||
|
|
1b45c2af13 | ||
|
|
b4bbf6c6fb | ||
|
|
a5b8d9668d | ||
|
|
490210da00 | ||
|
|
6878d440b1 | ||
|
|
81fa0a7d0f | ||
|
|
b7d4e547a1 | ||
|
|
2b2e256b8f | ||
|
|
7a7b31c2ae | ||
|
|
dea5eb1053 | ||
|
|
df3144ea73 | ||
|
|
a170cc3ad9 | ||
|
|
35b3813022 | ||
|
|
f0472fe89b | ||
|
|
7bdb2fa025 | ||
|
|
5ba7ad449a | ||
|
|
145441d55e | ||
|
|
62ff3419c1 | ||
|
|
34aee134d6 | ||
|
|
5899120d87 | ||
|
|
816a2fefe7 | ||
|
|
1f64e59917 | ||
|
|
5e54334fb9 | ||
|
|
d354af306f | ||
|
|
045cece0ce | ||
|
|
506012200f | ||
|
|
4ad2f0d495 | ||
|
|
b00e3cfd4b | ||
|
|
bb05489872 | ||
|
|
ede4d417bd | ||
|
|
928072fa27 | ||
|
|
6a313fcc8a | ||
|
|
725f7f4915 | ||
|
|
b9cb0ea16d | ||
|
|
61b80795a4 | ||
|
|
8daaa23774 | ||
|
|
8a0d308ceb | ||
|
|
e6a7a9aae7 | ||
|
|
f3943bd846 |
1
.frontmatter/content/mediaDb.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
.github/FUNDING.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [estruyf]
|
||||
open_collective: frontmatter
|
||||
custom: ["https://www.buymeacoffee.com/zMeFRy9"]
|
||||
|
||||
4
.github/workflows/release-beta.yml
vendored
@@ -11,9 +11,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install the dependencies
|
||||
|
||||
4
.github/workflows/release.yml
vendored
@@ -12,9 +12,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install the dependencies
|
||||
|
||||
@@ -17,4 +17,9 @@ postcss.config.js
|
||||
.templates
|
||||
.github
|
||||
scripts
|
||||
.all-contributorsrc
|
||||
.all-contributorsrc
|
||||
assets/v2.*
|
||||
assets/v3.*
|
||||
assets/v4.*
|
||||
assets/sponsors
|
||||
dist/*.html
|
||||
100
CHANGELOG.md
@@ -1,5 +1,105 @@
|
||||
# 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
|
||||
|
||||
- [#218](https://github.com/estruyf/vscode-front-matter/issues/218): Add support for creating `mdx` files from templates and content types. This introduced a new setting: `frontMatter.content.defaultFileType`.
|
||||
- [#220](https://github.com/estruyf/vscode-front-matter/issues/220): Add support DateTime updates in `mdx` files when the `mdx extension` is not installed.
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#221](https://github.com/estruyf/vscode-front-matter/issues/221): Automatic DateTime switch from on text change to on save to prevent multiple updates.
|
||||
|
||||
## [5.9.0] - 2022-01-01 - 🎇🎆
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Fixing the spinner which overlaps the global navigation bar
|
||||
- Quick actions added for media files (edit, delete, insert markdown, insert snippet)
|
||||
- [#199](https://github.com/estruyf/vscode-front-matter/issues/199): Search media files in the currently selected folder
|
||||
- [#211](https://github.com/estruyf/vscode-front-matter/issues/211): Replace text selection on media inserts
|
||||
- [#212](https://github.com/estruyf/vscode-front-matter/issues/212): Create folder watchers for content folders. When new content gets created, the dashboard updates.
|
||||
- [#213](https://github.com/estruyf/vscode-front-matter/issues/213): New media folder overview design
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#210](https://github.com/estruyf/vscode-front-matter/issues/210): Fix for adding media files with uppercase file extensions
|
||||
- [#214](https://github.com/estruyf/vscode-front-matter/issues/214): Fix for opening markdown file after creating it for the specified content type
|
||||
|
||||
## [5.8.0] - 2021-12-21 - 🎄
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Refactoring of the WebView logic to new message handlers
|
||||
- Optimized the `getMedia` call from the webview
|
||||
- Keep the dashboard its context when switching tabs
|
||||
- [#205](https://github.com/estruyf/vscode-front-matter/issues/205): Define a logging level setting
|
||||
- [#206](https://github.com/estruyf/vscode-front-matter/issues/206): Add front matter issues to the diagnostic tab
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#207](https://github.com/estruyf/vscode-front-matter/issues/207): Fix the quick picks for content creation
|
||||
- [#208](https://github.com/estruyf/vscode-front-matter/issues/208): Fix for the collapse sections action so that it is not available everywhere, but only on the Front Matter panel
|
||||
|
||||
## [5.7.0] - 2021-12-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.7.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#188](https://github.com/estruyf/vscode-front-matter/issues/188): Support for `.markdown` files added to the dashboard
|
||||
- [#190](https://github.com/estruyf/vscode-front-matter/issues/190): Diagnostic output for the extension
|
||||
- [#194](https://github.com/estruyf/vscode-front-matter/issues/194): WYSIWYG controls added for markdown files + configuration to enable/disable the functionality
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#191](https://github.com/estruyf/vscode-front-matter/issues/191): Fix beta settings page
|
||||
- [#200](https://github.com/estruyf/vscode-front-matter/issues/200): Fix last modified date sorting for media files
|
||||
- [#201](https://github.com/estruyf/vscode-front-matter/issues/201): Fix overflow issue with the media filename
|
||||
- [#202](https://github.com/estruyf/vscode-front-matter/issues/202): Fix checkbox label color for light themes
|
||||
|
||||
## [5.6.0] - 2021-11-23
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Updated camera icon from VS Code to media icon
|
||||
- Updated the media card actions to show it within a menu. This will give a better experience with custom scripts.
|
||||
- [#97](https://github.com/estruyf/vscode-front-matter/issues/97): Custom Script support for media files and folders
|
||||
- [#178](https://github.com/estruyf/vscode-front-matter/issues/178): Sorting added to the media dashboard
|
||||
- [#179](https://github.com/estruyf/vscode-front-matter/issues/179): Updated the `open dashboard` icon to make it easier to spot it
|
||||
- [#180](https://github.com/estruyf/vscode-front-matter/issues/180): Added `{filename}` as placeholder for media snippets
|
||||
- [#181](https://github.com/estruyf/vscode-front-matter/issues/181): Support for custom taxonomy fields added
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#183](https://github.com/estruyf/vscode-front-matter/issues/183): Fix type error on the `frontMatter.content.sorting` setting
|
||||
|
||||
## [5.5.0] - 2021-11-15
|
||||
|
||||
As from this version onwards, the extension will be published to [open-vsx.org](https://open-vsx.org/).
|
||||
|
||||
104
README.beta.md
@@ -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 🤘
|
||||
|
||||
@@ -106,12 +157,18 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🖤 Sponsors 👇 🤘
|
||||
## 🖤 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>
|
||||
</a>
|
||||
<a href="https://github.com/zivbk1" title="Bryan Klein">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
|
||||
</a>
|
||||
<a href="https://github.com/flikteoh" title="FlikTeoh">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
|
||||
</a>
|
||||
@@ -125,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 />
|
||||
|
||||
|
||||
105
README.md
@@ -26,30 +26,50 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
## What is Front Matter?
|
||||
## ❓ What is Front Matter?
|
||||
|
||||
Front Matter is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice. Jump right into editing and creating content with Front Matter and be able to preview it straight in VS Code.
|
||||
|
||||
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
|
||||
The extension supports various static-site generators and frameworks like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
|
||||
Our main extension features are:
|
||||
A couple of our extension highlights that hopefully get you interested in giving Front Matter a try:
|
||||
|
||||
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
|
||||
- Site preview within Visual Studio Code
|
||||
- Content, data, and media management
|
||||
- Search, filter, sort, etc. all your content
|
||||
- Create new content
|
||||
- Supporting tools to edit content and media
|
||||
- Preview your site/content straight in Visual Studio Code
|
||||
- SEO checks for title, description, and keywords
|
||||
- Support for custom actions/scripts
|
||||
- and many more
|
||||
- Extensibility
|
||||
- As we know, we cannot support all use cases. We provide a way to extend the functionality of the extension to your needs
|
||||
- and many more features ...
|
||||
|
||||
> Missing something? Let us know by opening an issue on the [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
|
||||
<img src="./assets/v6.0.0/content-preview.png" alt="Site preview" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 6**
|
||||
|
||||
In this version, we introduced the new data files/folders dashboard. You can find more information about the release in our [v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v6.0.0/data-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> Data files/folders are pieces of content that do not belong to any markdown content, but live on their own. Most of the time, these data files are used to store additional information about your project/blog/website that will be used to render the content.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/v5.9.0/media-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
@@ -68,7 +88,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
## ⚙️ Installation
|
||||
|
||||
You can get the extension via:
|
||||
|
||||
@@ -78,23 +98,54 @@ You can get the extension via:
|
||||
|
||||
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
### Beta version
|
||||
### 🧪 Beta version
|
||||
|
||||
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
|
||||
|
||||
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
- Uninstall the main Front Matter version
|
||||
- Install the beta version
|
||||
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
|
||||
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
|
||||
|
||||
## Documentation
|
||||
## 📖 Documentation
|
||||
|
||||
<h2 align="center">
|
||||
<a href="https://frontmatter.codes" title="Documentation @ frontmatter.codes">
|
||||
Check out the extension documentation at frontmatter.codes
|
||||
</a>
|
||||
</h2>
|
||||
All documentation can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
Documentation repository: [GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
|
||||
## 💪 Contributing
|
||||
|
||||
Pull requests are welcome. Please open an issue first to discuss what you would like to change, or which problem you would like to fix. This makes it easier for us to follow-up and plan for future releases.
|
||||
|
||||
You can always help us improve the extension in varous ways like:
|
||||
|
||||
- Testing out the extension and providing feedback
|
||||
- Reporting issues and bugs
|
||||
- Suggesting new features
|
||||
- Fixing an issue
|
||||
- Updating documentation
|
||||
- UI improvements
|
||||
- Tutorials
|
||||
- etc.
|
||||
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following projects:
|
||||
|
||||
- [Extension](https://github.com/estruyf/vscode-front-matter)
|
||||
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
- [Sample Projects](https://github.com/FrontMatter/project-samples)
|
||||
|
||||
## 👀 Show the work you are using Front Matter
|
||||
|
||||
Are you using Front Matter and are you interested in showing for which websites you use it? You can show your work by opening a [showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
|
||||
|
||||
You can open showcase issues for the following things:
|
||||
|
||||
- Show the website for which you use Front Matter;
|
||||
- Share an article/video/webcast/... that explains how you use Front Matter;
|
||||
- Got something else to share? Open an issue and we can see where it fits on our website.
|
||||
|
||||
## 👉 Contributors 🤘
|
||||
|
||||
@@ -104,12 +155,18 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🖤 Sponsors 👇 🤘
|
||||
## 🖤 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>
|
||||
</a>
|
||||
<a href="https://github.com/zivbk1" title="Bryan Klein">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
|
||||
</a>
|
||||
<a href="https://github.com/flikteoh" title="FlikTeoh">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
|
||||
</a>
|
||||
@@ -123,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 />
|
||||
|
||||
|
||||
9
assets/icons/blockquote-dark.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 15h15" />
|
||||
<path d="M21 19h-15" />
|
||||
<path d="M15 11h6" />
|
||||
<path d="M21 7h-6" />
|
||||
<path d="M9 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
|
||||
<path d="M3 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
9
assets/icons/blockquote-light.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 15h15" />
|
||||
<path d="M21 19h-15" />
|
||||
<path d="M15 11h6" />
|
||||
<path d="M21 7h-6" />
|
||||
<path d="M9 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
|
||||
<path d="M3 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
5
assets/icons/bold-dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6z" />
|
||||
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
5
assets/icons/bold-light.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6z" />
|
||||
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
6
assets/icons/code-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<polyline points="7 8 3 12 7 16" />
|
||||
<polyline points="17 8 21 12 17 16" />
|
||||
<line x1="14" y1="4" x2="10" y2="20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
6
assets/icons/code-light.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<polyline points="7 8 3 12 7 16" />
|
||||
<polyline points="17 8 21 12 17 16" />
|
||||
<line x1="14" y1="4" x2="10" y2="20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
7
assets/icons/codeblock-dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 12h6" />
|
||||
<path d="M12 9v6" />
|
||||
<path d="M6 19a2 2 0 0 1 -2 -2v-4l-1 -1l1 -1v-4a2 2 0 0 1 2 -2" />
|
||||
<path d="M18 19a2 2 0 0 0 2 -2v-4l1 -1l-1 -1v-4a2 2 0 0 0 -2 -2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
7
assets/icons/codeblock-light.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 12h6" />
|
||||
<path d="M12 9v6" />
|
||||
<path d="M6 19a2 2 0 0 1 -2 -2v-4l-1 -1l1 -1v-4a2 2 0 0 1 2 -2" />
|
||||
<path d="M18 19a2 2 0 0 0 2 -2v-4l1 -1l-1 -1v-4a2 2 0 0 0 -2 -2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
11
assets/icons/frontmatter-small-dark.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve" width="16" height="16">
|
||||
<path fill="#C5C5C5" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#C5C5C5" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
11
assets/icons/frontmatter-small-light.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve" width="16" height="16">
|
||||
<path fill="#424242" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
11
assets/icons/frontmatter-small-teal.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve" width="16" height="16">
|
||||
<path fill="#02aeb7" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#02aeb7" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
10
assets/icons/heading-dark.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 12h10" />
|
||||
<path d="M7 4v16" />
|
||||
<path d="M17 4v16" />
|
||||
<path d="M15 20h4" />
|
||||
<path d="M15 4h4" />
|
||||
<path d="M5 20h4" />
|
||||
<path d="M5 4h4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
10
assets/icons/heading-light.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 12h10" />
|
||||
<path d="M7 4v16" />
|
||||
<path d="M17 4v16" />
|
||||
<path d="M15 20h4" />
|
||||
<path d="M15 4h4" />
|
||||
<path d="M5 20h4" />
|
||||
<path d="M5 4h4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
6
assets/icons/italic-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="11" y1="5" x2="17" y2="5" />
|
||||
<line x1="7" y1="19" x2="13" y2="19" />
|
||||
<line x1="14" y1="5" x2="10" y2="19" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
6
assets/icons/italic-light.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="11" y1="5" x2="17" y2="5" />
|
||||
<line x1="7" y1="19" x2="13" y2="19" />
|
||||
<line x1="14" y1="5" x2="10" y2="19" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
3
assets/icons/media-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#C5C5C5" width="24" height="24">
|
||||
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 269 B |
3
assets/icons/media-light.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#424242" width="24" height="24">
|
||||
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 269 B |
7
assets/icons/options-dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="8" y2="12.01" />
|
||||
<line x1="12" y1="12" x2="12" y2="12.01" />
|
||||
<line x1="16" y1="12" x2="16" y2="12.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
7
assets/icons/options-light.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="8" y2="12.01" />
|
||||
<line x1="12" y1="12" x2="12" y2="12.01" />
|
||||
<line x1="16" y1="12" x2="16" y2="12.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
8
assets/icons/ordered-list-dark.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list-numbers" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11 6h9" />
|
||||
<path d="M11 12h9" />
|
||||
<path d="M12 18h8" />
|
||||
<path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4" />
|
||||
<path d="M6 10v-6l-2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 451 B |
8
assets/icons/ordered-list-light.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list-numbers" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11 6h9" />
|
||||
<path d="M11 12h9" />
|
||||
<path d="M12 18h8" />
|
||||
<path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4" />
|
||||
<path d="M6 10v-6l-2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 451 B |
5
assets/icons/strikethrough-dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 5v9a5 5 0 0 0 10 0v-9" />
|
||||
<path d="M4 12h16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
5
assets/icons/strikethrough-light.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 5v9a5 5 0 0 0 10 0v-9" />
|
||||
<path d="M4 12h16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
9
assets/icons/unordered-list-dark.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="9" y1="6" x2="20" y2="6" />
|
||||
<line x1="9" y1="12" x2="20" y2="12" />
|
||||
<line x1="9" y1="18" x2="20" y2="18" />
|
||||
<line x1="5" y1="6" x2="5" y2="6.01" />
|
||||
<line x1="5" y1="12" x2="5" y2="12.01" />
|
||||
<line x1="5" y1="18" x2="5" y2="18.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 533 B |
9
assets/icons/unordered-list-light.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="9" y1="6" x2="20" y2="6" />
|
||||
<line x1="9" y1="12" x2="20" y2="12" />
|
||||
<line x1="9" y1="18" x2="20" y2="18" />
|
||||
<line x1="5" y1="6" x2="5" y2="6.01" />
|
||||
<line x1="5" y1="12" x2="5" y2="12.01" />
|
||||
<line x1="5" y1="18" x2="5" y2="18.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 533 B |
@@ -446,12 +446,33 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px dashed rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__box {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 2px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px dashed rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: .5rem;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__label.metadata_field__label_parent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metadata_field__label svg {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
@@ -638,6 +659,14 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
|
||||
BIN
assets/v5.9.0/media-dashboard.png
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
assets/v6.0.0/content-preview.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/v6.0.0/data-dashboard.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
23269
package-lock.json
generated
708
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"displayName": "Front Matter",
|
||||
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"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.5.0",
|
||||
"version": "6.0.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.51.0"
|
||||
"vscode": "^1.63.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
@@ -66,7 +66,7 @@
|
||||
"onCommand:frontMatter.insertImage",
|
||||
"onView:frontMatter.explorer"
|
||||
],
|
||||
"main": "./dist/extension",
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
@@ -97,9 +97,45 @@
|
||||
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.defaultFileType": {
|
||||
"type": "string",
|
||||
"default": "md",
|
||||
"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"
|
||||
},
|
||||
"frontMatter.content.defaultSorting": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"LastModifiedAsc",
|
||||
"LastModifiedDesc",
|
||||
"FileNameAsc",
|
||||
"FileNameDesc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"markdownDescription": "Specify the default sorting option for the content dashboard. You can use one of the values from the enum or define your own ID. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultSorting)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.draftField": {
|
||||
"type": "object",
|
||||
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.draftField)",
|
||||
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.draftfield)",
|
||||
"default": {
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
@@ -111,7 +147,7 @@
|
||||
"boolean",
|
||||
"choice"
|
||||
],
|
||||
"description": ""
|
||||
"description": "Type of the draft field you want to use"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
@@ -137,7 +173,7 @@
|
||||
"frontMatter.content.fmHighlight": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.fmhighlight)",
|
||||
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.fmhighlight)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.pageFolders": {
|
||||
@@ -169,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": "",
|
||||
@@ -176,12 +236,16 @@
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.sorting": {
|
||||
"type": "object",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Define the sorting options for your dashboard content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.sorting)",
|
||||
"markdownDescription": "Define the sorting options for your dashboard content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.sorting)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the sorting option. This will be used for the storing the last used sorting option or the default option."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Name of the sorting label"
|
||||
@@ -217,6 +281,25 @@
|
||||
},
|
||||
"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,
|
||||
"markdownDescription": "Specifies if you want to enable/disable the What You See, Is What You Get (WYSIWYG) markdown controls. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.wysiwyg)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.custom.scripts": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -251,6 +334,16 @@
|
||||
"outputType": {
|
||||
"type": "string",
|
||||
"description": "The type of output for the editor panel. Can be used to change it to 'markdown' for example"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"enum": [
|
||||
"content",
|
||||
"mediaFolder",
|
||||
"mediaFile"
|
||||
],
|
||||
"description": "The type for which the script will be used."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -264,7 +357,7 @@
|
||||
"frontMatter.dashboard.mediaSnippet": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.dashboard.mediaSnippet)",
|
||||
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The parts of your snippet. Use `{mediaUrl}` as placeholder where the path of the image needs to be inserted."
|
||||
@@ -280,10 +373,192 @@
|
||||
"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)"
|
||||
"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": "",
|
||||
"enum": [
|
||||
"LastModifiedAsc",
|
||||
"LastModifiedDesc",
|
||||
"FileNameAsc",
|
||||
"FileNameDesc"
|
||||
],
|
||||
"markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.media.defaultsorting)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.panel.freeform": {
|
||||
"type": "boolean",
|
||||
@@ -306,7 +581,7 @@
|
||||
"frontMatter.site.baseURL": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.site.baseURL)",
|
||||
"markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.site.baseurl)",
|
||||
"scope": "Site"
|
||||
},
|
||||
"frontMatter.taxonomy.alignFilename": {
|
||||
@@ -325,7 +600,7 @@
|
||||
},
|
||||
"frontMatter.taxonomy.commaSeparatedFields": {
|
||||
"type": "array",
|
||||
"markdownDescription": "Specify the fields names that Front Matter should treat as a comma-separated array. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.commaSeparatedFields)",
|
||||
"markdownDescription": "Specify the fields names that Front Matter should treat as a comma-separated array. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.commaSeparatedFields)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Name of the fields you want to use as comma-separated arrays."
|
||||
@@ -337,7 +612,7 @@
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"markdownDescription": "Specify the type of contents you want to use for your articles/pages/etc. Make sure the `type` is correctly set in your front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.contentTypes)",
|
||||
"markdownDescription": "Specify the type of contents you want to use for your articles/pages/etc. Make sure the `type` is correctly set in your front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.contentTypes)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Define the content types you want to use in Front Matter.",
|
||||
@@ -346,7 +621,17 @@
|
||||
"type": "string",
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
"fileType": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"enum": [
|
||||
"md",
|
||||
"mdx"
|
||||
],
|
||||
"description": "Specifies the type of content you want to create."
|
||||
},
|
||||
"fields": {
|
||||
"$id": "#contenttypefield",
|
||||
"type": "array",
|
||||
"description": "Define the fields of the content type",
|
||||
"items": {
|
||||
@@ -362,9 +647,11 @@
|
||||
"boolean",
|
||||
"image",
|
||||
"choice",
|
||||
"taxonomy",
|
||||
"tags",
|
||||
"categories",
|
||||
"draft"
|
||||
"draft",
|
||||
"fields"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -376,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",
|
||||
@@ -418,12 +709,64 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do you want to hide the field from the metadata section?"
|
||||
},
|
||||
"taxonomyId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The ID of your taxonomy field"
|
||||
},
|
||||
"fields": {
|
||||
"$ref": "#contenttypefield"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "taxonomy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"taxonomyId"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "choice"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"choices"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "fields"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -492,6 +835,32 @@
|
||||
],
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.customTaxonomy": {
|
||||
"type": "array",
|
||||
"markdownDescription": "Specify the custom taxonomy field data. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.tags)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID for your taxonomy field"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "Options from which you can pick",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"options"
|
||||
]
|
||||
},
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.dateField": {
|
||||
"type": "string",
|
||||
"default": "date",
|
||||
@@ -507,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"
|
||||
},
|
||||
@@ -600,6 +971,56 @@
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "frontMatter.authenticate",
|
||||
"title": "Authenticate",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.bold",
|
||||
"title": "Bold",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/bold-light.svg",
|
||||
"dark": "assets/icons/bold-dark.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.markup.code",
|
||||
"title": "Code",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/code-light.svg",
|
||||
"dark": "assets/icons/code-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.codeblock",
|
||||
"title": "Codeblock",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/codeblock-light.svg",
|
||||
"dark": "assets/icons/codeblock-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.blockquote",
|
||||
"title": "Codeblock",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/blockquote-light.svg",
|
||||
"dark": "assets/icons/blockquote-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"title": "Collapse sections",
|
||||
@@ -619,11 +1040,21 @@
|
||||
"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",
|
||||
@@ -646,6 +1077,15 @@
|
||||
"title": "Generate slug based on content title",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.heading",
|
||||
"title": "Heading",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/heading-light.svg",
|
||||
"dark": "assets/icons/heading-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.init",
|
||||
"title": "Initialize project",
|
||||
@@ -660,7 +1100,10 @@
|
||||
"command": "frontMatter.insertImage",
|
||||
"title": "Insert image into your content",
|
||||
"category": "Front matter",
|
||||
"icon": "$(device-camera)"
|
||||
"icon": {
|
||||
"dark": "/assets/icons/media-dark.svg",
|
||||
"light": "/assets/icons/media-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
@@ -668,15 +1111,58 @@
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createContent",
|
||||
"title": "Create new content from defined content type or template",
|
||||
"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": "$(preview)"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.orderedlist",
|
||||
"title": "Ordered list",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/ordered-list-light.svg",
|
||||
"dark": "assets/icons/ordered-list-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"title": "Other markup options",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/options-light.svg",
|
||||
"dark": "assets/icons/options-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.preview",
|
||||
@@ -697,19 +1183,102 @@
|
||||
"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": {
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "frontMatter.markup.heading",
|
||||
"group": "navigation@-132",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.bold",
|
||||
"group": "navigation@-131",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.italic",
|
||||
"group": "navigation@-130",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.strikethrough",
|
||||
"group": "navigation@-129",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.blockquote",
|
||||
"group": "navigation@-128",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"group": "navigation@-99",
|
||||
"group": "navigation@-127",
|
||||
"when": "resourceLangId == markdown"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"group": "navigation@-126",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.orderedlist",
|
||||
"group": "1_markup@1",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.unorderedlist",
|
||||
"group": "1_markup@2",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.tasklist",
|
||||
"group": "1_markup@3",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.code",
|
||||
"group": "1_markup@4",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.codeblock",
|
||||
"group": "1_markup@5",
|
||||
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"group": "navigation@-98",
|
||||
"when": "frontMatter:enabled == true"
|
||||
"when": "frontMatter:enabled == true && frontMatter:dashboard:open == false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.close",
|
||||
"group": "navigation@-98",
|
||||
"when": "frontMatter:enabled == true && frontMatter:dashboard:open == true"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
@@ -757,12 +1326,60 @@
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.close",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.bold",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.italic",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.strikethrough",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.code",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.codeblock",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.blockquote",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.heading",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.unorderedlist",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.orderedlist",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.tasklist",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"group": "navigation"
|
||||
"group": "frontmatter-explorer"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -777,9 +1394,18 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run clean && webpack --mode production",
|
||||
"build:ext": "npm run clean && webpack --mode development",
|
||||
"dev:ext": "npm run clean && webpack --mode development --watch",
|
||||
"dev:ext": "npm run clean && npm-run-all --parallel watch:*",
|
||||
"vscode:prepublish": "npm run clean && npm-run-all --parallel prod:*",
|
||||
"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"
|
||||
@@ -790,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",
|
||||
@@ -798,12 +1425,16 @@
|
||||
"@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",
|
||||
"@types/vscode": "1.51.0",
|
||||
"@types/vscode": "^1.63.0",
|
||||
"@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",
|
||||
@@ -818,25 +1449,36 @@
|
||||
"lodash.uniqby": "4.7.0",
|
||||
"mdast-util-from-markdown": "1.0.0",
|
||||
"node-json-db": "^1.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"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": "4.44.2",
|
||||
"webpack-cli": "3.3.12"
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/js": "^3.0.0-alpha.40"
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ const tailwindcss = require('tailwindcss');
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-nested'),
|
||||
tailwindcss('./tailwind.config.js'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { 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, 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');
|
||||
@@ -14,9 +15,6 @@ import { parseWinPath } from '../helpers/parseWinPath';
|
||||
|
||||
|
||||
export class Article {
|
||||
|
||||
private static prevContent = "";
|
||||
|
||||
/**
|
||||
* Insert taxonomy
|
||||
*
|
||||
@@ -118,7 +116,37 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
const updatedArticle = this.setLastModifiedDateInner(editor.document);
|
||||
|
||||
if (typeof updatedArticle === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
ArticleHelper.update(
|
||||
editor,
|
||||
updatedArticle as matter.GrayMatterFile<string>
|
||||
);
|
||||
}
|
||||
|
||||
public static async setLastModifiedDateOnSave(
|
||||
document: vscode.TextDocument
|
||||
): Promise<vscode.TextEdit[]> {
|
||||
const updatedArticle = this.setLastModifiedDateInner(document);
|
||||
|
||||
if (typeof updatedArticle === "undefined") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const update = ArticleHelper.generateUpdate(document, updatedArticle);
|
||||
|
||||
return [update];
|
||||
}
|
||||
|
||||
private static setLastModifiedDateInner(
|
||||
document: vscode.TextDocument
|
||||
): matter.GrayMatterFile<string> | undefined {
|
||||
const article = ArticleHelper.getFrontMatterFromDocument(document);
|
||||
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
@@ -127,8 +155,7 @@ export class Article {
|
||||
const dateField = Settings.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified;
|
||||
try {
|
||||
cloneArticle.data[dateField] = Article.formatDate(new Date());
|
||||
|
||||
ArticleHelper.update(editor, cloneArticle);
|
||||
return cloneArticle;
|
||||
} catch (e: any) {
|
||||
Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
|
||||
}
|
||||
@@ -153,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
|
||||
@@ -203,7 +253,7 @@ export class Article {
|
||||
|
||||
const file = parseWinPath(editor.document.fileName);
|
||||
|
||||
if (!file.endsWith(`.md`) && !file.endsWith(`.mdx`)) {
|
||||
if (!isValidFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,30 +287,17 @@ export class Article {
|
||||
|
||||
/**
|
||||
* Article auto updater
|
||||
* @param fileChanges
|
||||
* @param event
|
||||
*/
|
||||
public static async autoUpdate(fileChanges: vscode.TextDocumentChangeEvent) {
|
||||
const txtChanges = fileChanges.contentChanges.map(c => c.text);
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
public static async autoUpdate(event: vscode.TextDocumentWillSaveEvent) {
|
||||
const document = event.document;
|
||||
if (document && ArticleHelper.isMarkdownFile(document)) {
|
||||
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
|
||||
|
||||
if (txtChanges.length > 0 && editor && ArticleHelper.isMarkdownFile()) {
|
||||
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
|
||||
|
||||
if (autoUpdate) {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (article.content === Article.prevContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
Article.prevContent = article.content;
|
||||
|
||||
Article.setLastModifiedDate();
|
||||
if (autoUpdate) {
|
||||
event.waitUntil(Article.setLastModifiedDateOnSave(document));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
74
src/commands/Backers.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { CONTEXT } from '../constants';
|
||||
import { Extension } from '../helpers';
|
||||
import { Credentials } from "../services/Credentials";
|
||||
import fetch from "node-fetch";
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { Dashboard } from './Dashboard';
|
||||
|
||||
export class Backers {
|
||||
private static creds: Credentials | null = null;
|
||||
|
||||
public static async init(context: ExtensionContext) {
|
||||
Backers.creds = new Credentials();
|
||||
await Backers.creds.initialize(context, Backers.tryUsernameCheck);
|
||||
|
||||
Backers.tryUsernameCheck();
|
||||
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('frontMatter.authenticate', async () => {
|
||||
Backers.tryUsernameCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public static async tryUsernameCheck() {
|
||||
try {
|
||||
const username = await Backers.getUsername();
|
||||
Backers.validate(username || "");
|
||||
} catch (e) {
|
||||
Backers.validate("");
|
||||
}
|
||||
}
|
||||
|
||||
public static async getUsername() {
|
||||
const octokit = await Backers.creds?.getOctokit();
|
||||
const user = await octokit?.users.getAuthenticated();
|
||||
|
||||
if (user?.data?.login) {
|
||||
return user?.data?.login;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public static async validate(username: string) {
|
||||
const ext = Extension.getInstance();
|
||||
|
||||
if (!username) {
|
||||
ext.setState(CONTEXT.backer, undefined, 'global');
|
||||
}
|
||||
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
const response = await fetch(`https://${isBeta ? `beta.` : ``}frontmatter.codes/api/backers?backer=${username}`);
|
||||
|
||||
if (response.ok) {
|
||||
const prevData = await ext.getState<boolean>(CONTEXT.backer, 'global');
|
||||
await ext.setState(CONTEXT.backer, true, 'global');
|
||||
|
||||
if (!prevData) {
|
||||
const explorerView = ExplorerView.getInstance();
|
||||
if (explorerView.visible) {
|
||||
explorerView.getSettings();
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.reload();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ext.setState(CONTEXT.backer, false, 'global');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,22 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING } from '../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { basename, dirname, extname, join, parse } from "path";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
|
||||
import { Settings as SettingsHelper } from '../helpers';
|
||||
import { DraftField, Framework, SortingSetting, TaxonomyType } from '../models';
|
||||
import { Folders } from './Folders';
|
||||
import { PagesListener } from './../listeners/PagesListener';
|
||||
import { ExtensionListener } from './../listeners/ExtensionListener';
|
||||
import { SETTINGS_DASHBOARD_OPENONSTART, CONTEXT } from '../constants';
|
||||
import { join } from "path";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode";
|
||||
import { Logger, Settings as SettingsHelper } from '../helpers';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
|
||||
import { Page } from '../dashboardWebView/models/Page';
|
||||
import { openFileInEditor } from '../helpers/openFileInEditor';
|
||||
import { Template } from './Template';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { Settings } from '../dashboardWebView/models/Settings';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
import { ViewType } from '../dashboardWebView/state';
|
||||
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
|
||||
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
|
||||
import { decodeBase64Image } from '../helpers/decodeBase64Image';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { MediaLibrary } from '../helpers/MediaLibrary';
|
||||
import imageSize from 'image-size';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { FrameworkDetector } from '../helpers/FrameworkDetector';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { DashboardListener, MediaListener, SettingsListener } from '../listeners';
|
||||
import { DataListener } from '../listeners/DataListener';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
private static isDisposed: boolean = true;
|
||||
private static media: MediaInfo[] = [];
|
||||
private static timers: { [folder: string]: any } = {};
|
||||
private static _viewData: DashboardData | undefined;
|
||||
private static mediaLib: MediaLibrary;
|
||||
private static isDisposed: boolean = true;
|
||||
|
||||
public static get viewData(): DashboardData | undefined {
|
||||
return Dashboard._viewData;
|
||||
@@ -53,15 +36,17 @@ export class Dashboard {
|
||||
* Open or reveal the dashboard
|
||||
*/
|
||||
public static async open(data?: DashboardData) {
|
||||
this.mediaLib = MediaLibrary.getInstance();
|
||||
MediaLibrary.getInstance();
|
||||
|
||||
Dashboard._viewData = data;
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.reveal();
|
||||
Dashboard.reveal(!!data);
|
||||
} else {
|
||||
Dashboard.create();
|
||||
}
|
||||
|
||||
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +59,33 @@ export class Dashboard {
|
||||
/**
|
||||
* Reveal the dashboard if it is open
|
||||
*/
|
||||
public static reveal() {
|
||||
public static reveal(hasData: boolean = false) {
|
||||
if (Dashboard.webview) {
|
||||
Dashboard.webview.reveal();
|
||||
|
||||
if (hasData) {
|
||||
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard.viewData });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static close() {
|
||||
Dashboard.webview?.dispose();
|
||||
}
|
||||
|
||||
public static reload() {
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.webview?.dispose();
|
||||
|
||||
setTimeout(() => {
|
||||
Dashboard.open();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
public static resetViewData() {
|
||||
Dashboard._viewData = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the dashboard webview
|
||||
@@ -92,7 +99,8 @@ export class Dashboard {
|
||||
'FrontMatter Dashboard',
|
||||
ViewColumn.One,
|
||||
{
|
||||
enableScripts: true
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
);
|
||||
|
||||
@@ -105,549 +113,55 @@ export class Dashboard {
|
||||
|
||||
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri);
|
||||
|
||||
Dashboard.webview.onDidChangeViewState(() => {
|
||||
Dashboard.webview.onDidChangeViewState(async () => {
|
||||
if (!this.webview?.visible) {
|
||||
Dashboard._viewData = undefined;
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
panel.getMediaSelection();
|
||||
|
||||
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: null });
|
||||
}
|
||||
|
||||
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, this.webview?.visible);
|
||||
});
|
||||
|
||||
Dashboard.webview.onDidDispose(() => {
|
||||
Dashboard.webview.onDidDispose(async () => {
|
||||
Dashboard.isDisposed = true;
|
||||
Dashboard._viewData = undefined;
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
panel.getMediaSelection();
|
||||
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, false);
|
||||
});
|
||||
|
||||
SettingsHelper.onConfigChange((global?: any) => {
|
||||
Dashboard.getSettings();
|
||||
SettingsListener.getSettings();
|
||||
});
|
||||
|
||||
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
|
||||
switch(msg.command) {
|
||||
case DashboardMessage.getViewType:
|
||||
if (Dashboard._viewData) {
|
||||
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard._viewData });
|
||||
}
|
||||
break;
|
||||
case DashboardMessage.getData:
|
||||
Dashboard.getSettings();
|
||||
Dashboard.getPages();
|
||||
break;
|
||||
case DashboardMessage.openFile:
|
||||
openFileInEditor(msg.data);
|
||||
break;
|
||||
case DashboardMessage.createContent:
|
||||
await commands.executeCommand(COMMAND_NAME.createContent);
|
||||
break;
|
||||
case DashboardMessage.createByContentType:
|
||||
await commands.executeCommand(COMMAND_NAME.createByContentType);
|
||||
break;
|
||||
case DashboardMessage.createByTemplate:
|
||||
await commands.executeCommand(COMMAND_NAME.createByTemplate);
|
||||
break;
|
||||
case DashboardMessage.updateSetting:
|
||||
Dashboard.updateSetting(msg.data);
|
||||
break;
|
||||
case DashboardMessage.initializeProject:
|
||||
await commands.executeCommand(COMMAND_NAME.init, Dashboard.getSettings);
|
||||
break;
|
||||
case DashboardMessage.reload:
|
||||
if (!Dashboard.isDisposed) {
|
||||
Dashboard.webview?.dispose();
|
||||
setTimeout(() => {
|
||||
Dashboard.open();
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
case DashboardMessage.setPageViewType:
|
||||
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
|
||||
break;
|
||||
case DashboardMessage.getMedia:
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
|
||||
break;
|
||||
case DashboardMessage.copyToClipboard:
|
||||
env.clipboard.writeText(msg.data);
|
||||
break;
|
||||
case DashboardMessage.refreshMedia:
|
||||
Dashboard.resetMedia();
|
||||
Dashboard.getMedia(0, msg?.data?.folder);
|
||||
break;
|
||||
case DashboardMessage.uploadMedia:
|
||||
Dashboard.saveFile(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.deleteMedia:
|
||||
Dashboard.deleteFile(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.insertPreviewImage:
|
||||
Dashboard.insertImage(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.updateMediaMetadata:
|
||||
Dashboard.updateMediaMetadata(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
|
||||
break;
|
||||
case DashboardMessage.setFramework:
|
||||
Dashboard.setFramework(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.setState:
|
||||
if (msg?.data?.key && msg?.data?.value) {
|
||||
Extension.getInstance().setState(msg?.data?.key, msg?.data?.value, "workspace");
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset media array
|
||||
*/
|
||||
public static resetMedia() {
|
||||
Dashboard.media = [];
|
||||
}
|
||||
|
||||
public static switchFolder(folderPath: string) {
|
||||
Dashboard.resetMedia();
|
||||
Dashboard.getMedia(0, folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an image into the front matter or contents
|
||||
* @param data
|
||||
*/
|
||||
private static async insertImage(data: any) {
|
||||
if (data?.file && data?.image) {
|
||||
if (!data?.position) {
|
||||
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
|
||||
}
|
||||
|
||||
await EditorHelper.showFile(data.file);
|
||||
Dashboard._viewData = undefined;
|
||||
Logger.info(`Receiving message from webview: ${msg.command}`);
|
||||
|
||||
const extensionUri = Extension.getInstance().extensionPath;
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
|
||||
if (data?.position) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const editor = window.activeTextEditor;
|
||||
const line = data.position.line;
|
||||
const character = data.position.character;
|
||||
if (line) {
|
||||
let imgPath = data.image;
|
||||
const filePath = data.file;
|
||||
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
|
||||
|
||||
const imgDir = dirname(absImgPath);
|
||||
const fileDir = dirname(filePath);
|
||||
|
||||
if (imgDir === fileDir) {
|
||||
imgPath = join('/', basename(imgPath));
|
||||
|
||||
// Snippets are already parsed, so update the URL of the image
|
||||
if (data.snippet) {
|
||||
data.snippet = data.snippet.replace(data.image, imgPath);
|
||||
}
|
||||
}
|
||||
|
||||
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || ``));
|
||||
}
|
||||
panel.getMediaSelection();
|
||||
} else {
|
||||
panel.getMediaSelection();
|
||||
panel.updateMetadata({field: data.fieldName, value: data.image });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the settings for the dashboard
|
||||
*/
|
||||
private static async getSettings() {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = await Template.isInitialized();
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.settings,
|
||||
data: {
|
||||
beta: ext.isBetaVersion(),
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
|
||||
folders: Folders.get(),
|
||||
initialized: isInitialized,
|
||||
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
|
||||
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
|
||||
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
draftField: SettingsHelper.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
|
||||
customSorting: SettingsHelper.get<SortingSetting[]>(SETTINGS_CONTENT_SORTING),
|
||||
contentFolders: Folders.get(),
|
||||
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
dashboardState: {
|
||||
sorting: await ext.getState<ViewType | undefined>(ExtensionState.Dashboard.Sorting, "workspace")
|
||||
}
|
||||
} as Settings
|
||||
DashboardListener.process(msg);
|
||||
ExtensionListener.process(msg);
|
||||
MediaListener.process(msg);
|
||||
PagesListener.process(msg);
|
||||
SettingsListener.process(msg);
|
||||
DataListener.process(msg);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
* Return the webview
|
||||
* @returns The webview
|
||||
*/
|
||||
private static setFramework(frameworkId: string | null) {
|
||||
SettingsHelper.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
|
||||
|
||||
if (frameworkId) {
|
||||
const allFrameworks = FrameworkDetector.getAll();
|
||||
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
|
||||
if (framework) {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
} else {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting from the dashboard
|
||||
*/
|
||||
private static async updateSetting(data: { name: string, value: any }) {
|
||||
await SettingsHelper.update(data.name, data.value);
|
||||
Dashboard.getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all media files
|
||||
*/
|
||||
private static async getMedia(page: number = 0, requestedFolder: string = '') {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const contentFolders = Folders.get();
|
||||
const viewData = Dashboard.viewData;
|
||||
let selectedFolder = requestedFolder;
|
||||
|
||||
// If the static folder is not set, retreive the last opened location
|
||||
if (!selectedFolder) {
|
||||
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder, "workspace");
|
||||
|
||||
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
|
||||
// Support for page bundles
|
||||
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
|
||||
const folderPath = parse(viewData.data.filePath).dir;
|
||||
selectedFolder = folderPath;
|
||||
} else if (stateValue && existsSync(stateValue)) {
|
||||
selectedFolder = stateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go to the home folder
|
||||
if (selectedFolder === HOME_PAGE_NAVIGATION_ID) {
|
||||
selectedFolder = '';
|
||||
}
|
||||
|
||||
let relSelectedFolderPath = selectedFolder;
|
||||
const parsedPath = parseWinPath(wsFolder?.fsPath || "");
|
||||
if (selectedFolder && selectedFolder.startsWith(parsedPath)) {
|
||||
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
|
||||
}
|
||||
|
||||
if (relSelectedFolderPath.startsWith('/')) {
|
||||
relSelectedFolderPath = relSelectedFolderPath.substring(1);
|
||||
}
|
||||
|
||||
let allMedia: MediaInfo[] = [];
|
||||
|
||||
if (relSelectedFolderPath) {
|
||||
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
|
||||
const media = Dashboard.filterMedia(files);
|
||||
allMedia = [...media];
|
||||
} else {
|
||||
if (staticFolder) {
|
||||
const folderSearch = join(staticFolder || "", '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = Dashboard.filterMedia(files);
|
||||
|
||||
allMedia = [...media];
|
||||
}
|
||||
|
||||
if (contentFolders && wsFolder) {
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const contentFolder = contentFolders[i];
|
||||
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
|
||||
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = Dashboard.filterMedia(files);
|
||||
|
||||
allMedia = [...allMedia, ...media];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allMedia = allMedia.sort((a, b) => {
|
||||
if (b.fsPath < a.fsPath) {
|
||||
return -1;
|
||||
}
|
||||
if (b.fsPath > a.fsPath) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
Dashboard.media = Object.assign([], allMedia);
|
||||
|
||||
let files: MediaInfo[] = Dashboard.media;
|
||||
|
||||
// Retrieve the total after filtering and before the slicing happens
|
||||
const total = files.length;
|
||||
|
||||
// Get media set
|
||||
files = files.slice(page * 16, ((page + 1) * 16));
|
||||
files = files.map((file) => {
|
||||
try {
|
||||
const metadata = Dashboard.mediaLib.get(file.fsPath);
|
||||
|
||||
return {
|
||||
...file,
|
||||
stats: statSync(file.fsPath),
|
||||
dimensions: imageSize(file.fsPath),
|
||||
...metadata
|
||||
};
|
||||
} catch (e) {
|
||||
return {...file, stats: undefined};
|
||||
}
|
||||
});
|
||||
files = files.filter(f => f.stats !== undefined);
|
||||
|
||||
// Retrieve all the folders
|
||||
let allContentFolders: string[] = [];
|
||||
let allFolders: string[] = [];
|
||||
|
||||
if (selectedFolder) {
|
||||
if (existsSync(selectedFolder)) {
|
||||
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
|
||||
}
|
||||
} else {
|
||||
for (const contentFolder of contentFolders) {
|
||||
const contentPath = contentFolder.path;
|
||||
if (contentPath && existsSync(contentPath)) {
|
||||
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
|
||||
allContentFolders = [...allContentFolders, ...subFolders];
|
||||
}
|
||||
}
|
||||
|
||||
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
|
||||
if (staticPath && existsSync(staticPath)) {
|
||||
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last opened folder
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.media,
|
||||
data: {
|
||||
media: files,
|
||||
total: total,
|
||||
folders: [...allContentFolders, ...allFolders],
|
||||
selectedFolder
|
||||
} as MediaPaths
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the markdown pages
|
||||
*/
|
||||
private static async getPages() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
|
||||
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Page[] = [];
|
||||
|
||||
if (folderInfo) {
|
||||
for (const folder of folderInfo) {
|
||||
for (const file of folder.lastModified) {
|
||||
if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.mdx`)) {
|
||||
try {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
|
||||
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.webview?.webview.asWebviewUri(previewUri);
|
||||
page[previewField] = preview?.toString() || "";
|
||||
} else {
|
||||
page[previewField] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pages.push(page);
|
||||
}
|
||||
} catch (error: any) {
|
||||
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.pages,
|
||||
data: pages
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the media files
|
||||
*/
|
||||
private static filterMedia(files: Uri[]) {
|
||||
return files.filter(file => {
|
||||
const ext = extname(file.fsPath);
|
||||
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext);
|
||||
}).map((file) => ({
|
||||
fsPath: file.fsPath,
|
||||
vsPath: Dashboard.webview?.webview.asWebviewUri(file).toString(),
|
||||
stats: undefined
|
||||
} as MediaInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the dropped file in the current folder
|
||||
* @param fileData
|
||||
*/
|
||||
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
|
||||
if (fileName && contents) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const wsPath = wsFolder ? wsFolder.fsPath : "";
|
||||
let absFolderPath = join(wsPath, staticFolder || "");
|
||||
|
||||
if (folder) {
|
||||
absFolderPath = folder;
|
||||
}
|
||||
|
||||
if (!existsSync(absFolderPath)) {
|
||||
absFolderPath = join(wsPath, folder || "");
|
||||
}
|
||||
|
||||
if (!existsSync(absFolderPath)) {
|
||||
Notifications.error(`We couldn't find your selected folder.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const staticPath = join(absFolderPath, fileName);
|
||||
const imgData = decodeBase64Image(contents);
|
||||
|
||||
if (imgData) {
|
||||
writeFileSync(staticPath, imgData.data);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
|
||||
|
||||
const folderPath = `${folder}`;
|
||||
if (Dashboard.timers[folderPath]) {
|
||||
clearTimeout(Dashboard.timers[folderPath]);
|
||||
delete Dashboard.timers[folderPath];
|
||||
}
|
||||
|
||||
Dashboard.timers[folderPath] = setTimeout(() => {
|
||||
Dashboard.media = [];
|
||||
Dashboard.getMedia(0, folder || "");
|
||||
delete Dashboard.timers[folderPath];
|
||||
}, 500);
|
||||
} else {
|
||||
Notifications.error(`Something went wrong uploading ${fileName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the selected file
|
||||
* @param fileData
|
||||
*/
|
||||
private static async deleteFile({ file, page, folder }: { file: string; page: number; folder: string | null; }) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
unlinkSync(file);
|
||||
|
||||
Dashboard.media = [];
|
||||
Dashboard.getMedia(page || 0, folder || "");
|
||||
} catch(err) {
|
||||
Notifications.error(`Something went wrong deleting ${basename(file)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metadata of the selected file
|
||||
*/
|
||||
private static async updateMediaMetadata({ file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; }) {
|
||||
Dashboard.mediaLib.set(file, metadata);
|
||||
|
||||
// Check if filename needs to be updated
|
||||
Dashboard.mediaLib.updateFilename(file, filename);
|
||||
|
||||
Dashboard.getMedia(page || 0, folder || "");
|
||||
public static getWebview() {
|
||||
return Dashboard.webview?.webview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to the dashboard
|
||||
* @param msg
|
||||
*/
|
||||
private static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
|
||||
public static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
|
||||
if (Dashboard.isDisposed) {
|
||||
return;
|
||||
}
|
||||
@@ -662,7 +176,17 @@ export class Dashboard {
|
||||
* @param webView
|
||||
*/
|
||||
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
|
||||
const scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', 'pages.js'));
|
||||
const dashboardFile = "dashboardWebView.js";
|
||||
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 = `http://${localServerUrl}/${dashboardFile}`;
|
||||
}
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
@@ -670,21 +194,31 @@ export class Dashboard {
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
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}`} 'unsafe-eval'`,
|
||||
`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" style="width:100%;height:100%;margin:0;padding:0;">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Front Matter Dashboard</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
|
||||
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
|
||||
<div id="app" data-isProd="${isProd}" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`dashboard-${version.installedVersion}`}" alt="Daily usage" />
|
||||
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
<script ${isProd ? `nonce="${nonce}"` : ""} src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
63
src/commands/Diagnostics.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Folders } from "./Folders";
|
||||
import { ViewColumn, workspace } from "vscode";
|
||||
import ContentProvider from "../providers/ContentProvider";
|
||||
import { join } from "path";
|
||||
import { ContentFolder } from "../models";
|
||||
|
||||
|
||||
export class Diagnostics {
|
||||
|
||||
public static async show() {
|
||||
const folders = Folders.get();
|
||||
const projectName = Folders.getProjectFolderName();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
const folderData = [];
|
||||
for (const folder of folders) {
|
||||
folderData.push(await Diagnostics.processFolder(folder, projectName));
|
||||
}
|
||||
|
||||
const all = await Diagnostics.allProjectFiles();
|
||||
|
||||
const logging = `# Project name
|
||||
|
||||
${projectName}
|
||||
|
||||
# Folders
|
||||
|
||||
${folders.map(f => `- ${f.title}: "${f.path}"`).join("\n")}
|
||||
|
||||
# Workspace folder
|
||||
|
||||
${wsFolder ? wsFolder.fsPath : "No workspace folder"}
|
||||
|
||||
# Total files
|
||||
|
||||
${all}
|
||||
|
||||
# Folders to search files
|
||||
|
||||
${folderData.join("\n")}
|
||||
`;
|
||||
|
||||
ContentProvider.show(logging, `${projectName} diagnostics`, "markdown", ViewColumn.One);
|
||||
}
|
||||
|
||||
private static async allProjectFiles() {
|
||||
const allFiles = await workspace.findFiles(`**/*.*`);
|
||||
return `Total files found: ${allFiles.length}`;
|
||||
}
|
||||
|
||||
private static async processFolder(folder: ContentFolder, projectName: string) {
|
||||
let projectStart = folder.path.split(projectName).pop();
|
||||
projectStart = projectStart || "";
|
||||
projectStart = projectStart?.replace(/\\/g, '/');
|
||||
projectStart = projectStart?.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
|
||||
const markdownFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.markdown'));
|
||||
|
||||
return `- Project start length: ${projectStart.length} | Search in: "${join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.*')}" | mdFiles: ${mdFiles.length} | mdxFiles: ${mdxFiles.length} | markdownFiles: ${markdownFiles.length}`;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -11,6 +11,9 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
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]]`;
|
||||
|
||||
@@ -62,7 +65,8 @@ export class Folders {
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.switchFolder(folderName);
|
||||
MediaHelpers.resetMedia();
|
||||
MediaListener.sendMediaFiles(0, folderName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,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[] = [];
|
||||
@@ -207,19 +212,28 @@ export class Folders {
|
||||
try {
|
||||
const projectName = Folders.getProjectFolderName();
|
||||
let projectStart = folder.path.split(projectName).pop();
|
||||
|
||||
if (projectStart) {
|
||||
projectStart = projectStart.replace(/\\/g, '/');
|
||||
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
|
||||
let files = [...mdFiles, ...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[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const fileName = basename(file.fsPath);
|
||||
|
||||
const stats = await workspace.fs.stat(file);
|
||||
|
||||
fileStats.push({
|
||||
filePath: file.fsPath,
|
||||
fileName,
|
||||
@@ -267,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
|
||||
@@ -281,6 +308,9 @@ export class Folders {
|
||||
}));
|
||||
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
|
||||
|
||||
// Reinitialize the folder listeners
|
||||
PagesListener.startWatchers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,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.');
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import { Notifications } from "../helpers/Notifications";
|
||||
import { Template } from "./Template";
|
||||
import { Folders } from "./Folders";
|
||||
import { Settings } from "../helpers";
|
||||
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
|
||||
@@ -27,6 +28,7 @@ categories: []
|
||||
public static async init(sampleTemplate: boolean = true) {
|
||||
try {
|
||||
Settings.createTeamSettings();
|
||||
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
const folder = Template.getSettings();
|
||||
const templatePath = Project.templatePath();
|
||||
@@ -35,7 +37,7 @@ categories: []
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Uri.file(join(templatePath.fsPath, "article.md"));
|
||||
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
|
||||
|
||||
if (!fs.existsSync(templatePath.fsPath)) {
|
||||
await workspace.fs.createDirectory(templatePath);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Questions } from './../helpers/Questions';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
|
||||
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
|
||||
import { ArticleHelper, Settings } from '../helpers';
|
||||
import { Article } from '.';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
@@ -11,6 +11,8 @@ import { Project } from './Project';
|
||||
import { Folders } from './Folders';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
import { PagesListener } from '../listeners';
|
||||
import { extname } from 'path';
|
||||
|
||||
export class Template {
|
||||
|
||||
@@ -49,6 +51,7 @@ export class Template {
|
||||
public static async generate() {
|
||||
const folder = Template.getSettings();
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
if (folder && editor && ArticleHelper.isMarkdownFile()) {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
@@ -82,7 +85,7 @@ export class Template {
|
||||
if (templatePath) {
|
||||
let fileContents = ArticleHelper.stringifyFrontMatter(keepContents === "no" ? "" : clonedArticle.content, clonedArticle.data);
|
||||
|
||||
const templateFile = path.join(templatePath.fsPath, `${titleValue}.md`);
|
||||
const templateFile = path.join(templatePath.fsPath, `${titleValue}.${fileType}`);
|
||||
fs.writeFileSync(templateFile, fileContents, { encoding: "utf-8" });
|
||||
|
||||
Notifications.info(`Template created and is now available in your ${folder} folder.`);
|
||||
@@ -139,7 +142,8 @@ export class Template {
|
||||
contentType = contentTypes?.find(t => t.name === templateData.data.type);
|
||||
}
|
||||
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
const fileExtension = extname(template.fsPath).replace(".", "");
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
@@ -155,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);
|
||||
|
||||
@@ -176,6 +174,9 @@ export class Template {
|
||||
}
|
||||
|
||||
Notifications.info(`Your new content has been created.`);
|
||||
|
||||
// Trigger a refresh for the dashboard
|
||||
PagesListener.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
230
src/commands/Wysiwyg.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { commands, window, Selection, QuickPickItem } from "vscode";
|
||||
import { COMMAND_NAME, CONTEXT, SETTINGS_CONTENT_WYSIWYG } from "../constants";
|
||||
import { Settings } from "../helpers";
|
||||
|
||||
enum MarkupType {
|
||||
bold = 1,
|
||||
italic,
|
||||
strikethrough,
|
||||
code,
|
||||
codeblock,
|
||||
blockquote,
|
||||
heading,
|
||||
unorderedList,
|
||||
orderedList,
|
||||
taskList
|
||||
}
|
||||
|
||||
export class Wysiwyg {
|
||||
|
||||
/**
|
||||
* Registers the markup commands for the WYSIWYG controls
|
||||
* @param subscriptions
|
||||
* @returns
|
||||
*/
|
||||
public static async registerCommands(subscriptions: any) {
|
||||
|
||||
const wysiwygEnabled = Settings.get(SETTINGS_CONTENT_WYSIWYG);
|
||||
|
||||
if (!wysiwygEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await commands.executeCommand('setContext', CONTEXT.wysiwyg, true);
|
||||
|
||||
// Surrounding markup
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.bold, () => this.addMarkup(MarkupType.bold)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.italic, () => this.addMarkup(MarkupType.italic)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.strikethrough, () => this.addMarkup(MarkupType.strikethrough)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.code, () => this.addMarkup(MarkupType.code)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.codeblock, () => this.addMarkup(MarkupType.codeblock)));
|
||||
|
||||
// Prefix markup
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.heading, () => this.addMarkup(MarkupType.heading)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.blockquote, () => this.addMarkup(MarkupType.blockquote)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.unorderedlist, () => this.addMarkup(MarkupType.unorderedList)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.orderedlist, () => this.addMarkup(MarkupType.orderedList)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.taskList, () => this.addMarkup(MarkupType.taskList)));
|
||||
|
||||
// Options
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.options, async () => {
|
||||
const qpItems: QuickPickItem[] = [
|
||||
{ label: "$(list-unordered) Unordered list", detail: "Add an unordered list", alwaysShow: true, },
|
||||
{ label: "$(list-ordered) Ordered list", detail: "Add an ordered list", alwaysShow: true },
|
||||
{ label: "$(tasklist) Task list", detail: "Add a task list", alwaysShow: true },
|
||||
{ label: "$(code) Code", detail: "Add inline code snippet", alwaysShow: true },
|
||||
{ label: "$(symbol-namespace) Code block", detail: "Add a code block", alwaysShow: true },
|
||||
]
|
||||
|
||||
const option = await window.showQuickPick([ ...qpItems ], {
|
||||
placeHolder: "Which type of markup would you like to insert?",
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: false,
|
||||
});
|
||||
|
||||
if (option) {
|
||||
if (option.label === qpItems[0].label) {
|
||||
await this.addMarkup(MarkupType.unorderedList);
|
||||
} else if (option.label === qpItems[1].label) {
|
||||
await this.addMarkup(MarkupType.orderedList);
|
||||
} else if (option.label === qpItems[2].label) {
|
||||
await this.addMarkup(MarkupType.taskList);
|
||||
} else if (option.label === qpItems[3].label) {
|
||||
await this.addMarkup(MarkupType.code);
|
||||
} else if (option.label === qpItems[4].label) {
|
||||
await this.addMarkup(MarkupType.codeblock);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the markup to the content
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
private static async addMarkup(type: MarkupType) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = editor.selection;
|
||||
const hasTextSelection = !selection.isEmpty;
|
||||
|
||||
const markers = this.getMarkers(type);
|
||||
if (!markers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const crntSelection = selection.active;
|
||||
|
||||
if (hasTextSelection) {
|
||||
// Replace the selection and surround with the markup
|
||||
const selectionText = editor.document.getText(selection);
|
||||
const txt = await this.insertText(markers, type, selectionText);
|
||||
|
||||
editor.edit(builder => {
|
||||
builder.replace(selection, txt);
|
||||
});
|
||||
} else {
|
||||
const txt = await this.insertText(markers, type);
|
||||
|
||||
// Insert the markers where cursor is located.
|
||||
const markerLength = this.isMarkupWrapping(type) ? txt.length + 1 : markers.length;
|
||||
let newPosition = crntSelection.with(crntSelection.line, crntSelection.character + markerLength);
|
||||
|
||||
await editor.edit(builder => {
|
||||
builder.insert(newPosition, txt);
|
||||
});
|
||||
|
||||
if (type === MarkupType.codeblock) {
|
||||
newPosition = crntSelection.with(crntSelection.line + 1, 0);
|
||||
}
|
||||
|
||||
editor.selection = new Selection(newPosition, newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the text will be wrapped
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
private static isMarkupWrapping(type: MarkupType) {
|
||||
return (
|
||||
type === MarkupType.blockquote ||
|
||||
type === MarkupType.heading ||
|
||||
type === MarkupType.unorderedList ||
|
||||
type === MarkupType.orderedList ||
|
||||
type === MarkupType.taskList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert text at the current cursor position
|
||||
*/
|
||||
private static async insertText(marker: string | undefined, type: MarkupType, text: string | null = null) {
|
||||
const crntText = text || this.lineBreak(type);
|
||||
|
||||
if (this.isMarkupWrapping(type)) {
|
||||
if (type === MarkupType.heading) {
|
||||
const headingLvl = await window.showQuickPick([
|
||||
"Heading 1",
|
||||
"Heading 2",
|
||||
"Heading 3",
|
||||
"Heading 4",
|
||||
"Heading 5",
|
||||
"Heading 6"
|
||||
], {
|
||||
canPickMany: false,
|
||||
placeHolder: "Which heading level do you want to insert?",
|
||||
ignoreFocusOut: false
|
||||
});
|
||||
|
||||
if (headingLvl) {
|
||||
const headingNr = parseInt(headingLvl.replace("Heading ", ""));
|
||||
return `${Array(headingNr + 1).join(marker)} ${crntText}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === MarkupType.unorderedList || type === MarkupType.taskList) {
|
||||
const lines = crntText.split("\n").map(line => `${marker} ${line}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
if (type === MarkupType.orderedList) {
|
||||
const lines = crntText.split("\n").map((line, idx) => `${idx+1}. ${line}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
return `${marker} ${crntText}`;
|
||||
} else {
|
||||
return `${marker}${crntText}${marker}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if linebreak needs to be added
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
private static lineBreak(type: MarkupType) {
|
||||
if (type === MarkupType.codeblock) {
|
||||
return `\n\n`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the type of markers
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
private static getMarkers(type: MarkupType) {
|
||||
switch(type) {
|
||||
case MarkupType.bold:
|
||||
return `**`;
|
||||
case MarkupType.italic:
|
||||
return `*`;
|
||||
case MarkupType.strikethrough:
|
||||
return `~~`;
|
||||
case MarkupType.code:
|
||||
return "`";
|
||||
case MarkupType.codeblock:
|
||||
return "```";
|
||||
case MarkupType.blockquote:
|
||||
return ">";
|
||||
case MarkupType.heading:
|
||||
return "#";
|
||||
case MarkupType.unorderedList:
|
||||
return "-";
|
||||
case MarkupType.orderedList:
|
||||
return "1.";
|
||||
case MarkupType.taskList:
|
||||
return "- [ ]";
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/components/icons/CompressIcon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ICompressIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CompressIcon: React.FunctionComponent<ICompressIconProps> = ({className}: React.PropsWithChildren<ICompressIconProps>) => {
|
||||
return (
|
||||
<svg className={className || ""} aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path fill="currentColor" d="M436 192H312c-13.3 0-24-10.7-24-24V44c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v84h84c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm-276-24V44c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v84H12c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h124c13.3 0 24-10.7 24-24zm0 300V344c0-13.3-10.7-24-24-24H12c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h84v84c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm192 0v-84h84c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12H312c-13.3 0-24 10.7-24 24v124c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12z"></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -23,7 +23,7 @@ export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"title": "Article preview",
|
||||
"title": "Content preview",
|
||||
"name": "preview",
|
||||
"type": "image"
|
||||
},
|
||||
|
||||
3
src/constants/DefaultFileTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
export const DEFAULT_FILE_TYPES = [".md", ".markdown", ".mdx"];
|
||||
@@ -28,7 +28,24 @@ export const COMMAND_NAME = {
|
||||
collapseSections: getCommandName("collapseSections"),
|
||||
preview: getCommandName("preview"),
|
||||
dashboard: getCommandName("dashboard"),
|
||||
dashboardMedia: getCommandName("dashboard.media"),
|
||||
dashboardData: getCommandName("dashboard.data"),
|
||||
dashboardClose: getCommandName("dashboard.close"),
|
||||
promote: getCommandName("promoteSettings"),
|
||||
insertImage: getCommandName("insertImage"),
|
||||
createFolder: getCommandName("createFolder"),
|
||||
diagnostics: getCommandName("diagnostics"),
|
||||
|
||||
// WYSIWYG
|
||||
bold: getCommandName("markup.bold"),
|
||||
italic: getCommandName("markup.italic"),
|
||||
strikethrough: getCommandName("markup.strikethrough"),
|
||||
code: getCommandName("markup.code"),
|
||||
codeblock: getCommandName("markup.codeblock"),
|
||||
heading: getCommandName("markup.heading"),
|
||||
blockquote: getCommandName("markup.blockquote"),
|
||||
unorderedlist: getCommandName("markup.unorderedlist"),
|
||||
orderedlist: getCommandName("markup.orderedlist"),
|
||||
taskList: getCommandName("markup.tasklist"),
|
||||
options: getCommandName("markup.options"),
|
||||
};
|
||||
@@ -7,6 +7,11 @@ export const ExtensionState = {
|
||||
MoveTemplatesFolder: `frontMatter:Templates:Move`,
|
||||
|
||||
Dashboard: {
|
||||
Sorting: `frontMatter:Dashboard:Sorting`,
|
||||
Contents: {
|
||||
Sorting: `frontMatter:Dashboard:Contents:Sorting`,
|
||||
},
|
||||
Media: {
|
||||
Sorting: `frontMatter:Dashboard:Media:Sorting`,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -2,5 +2,8 @@ export const CONTEXT = {
|
||||
canInit: "frontMatterCanInit",
|
||||
canOpenPreview: "frontMatterCanOpenPreview",
|
||||
canOpenDashboard: "frontMatterCanOpenDashboard",
|
||||
isEnabled: "frontMatter:enabled"
|
||||
isEnabled: "frontMatter:enabled",
|
||||
isDashboardOpen: "frontMatter:dashboard:open",
|
||||
wysiwyg: "frontMatter:markdown:wysiwyg",
|
||||
backer: "frontMatter:backers:supporter",
|
||||
};
|
||||
@@ -2,8 +2,12 @@ export const EXTENSION_NAME = "Front Matter";
|
||||
|
||||
export const CONFIG_KEY = "frontMatter";
|
||||
|
||||
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
|
||||
|
||||
export const SETTING_TAXONOMY_TAGS = "taxonomy.tags";
|
||||
export const SETTING_TAXONOMY_CATEGORIES = "taxonomy.categories";
|
||||
export const SETTING_TAXONOMY_CUSTOM = "taxonomy.customTaxonomy";
|
||||
|
||||
export const SETTING_DATE_FORMAT = "taxonomy.dateFormat";
|
||||
export const SETTING_COMMA_SEPARATED_FIELDS = "taxonomy.commaSeparatedFields";
|
||||
export const SETTING_TAXONOMY_CONTENT_TYPES = "taxonomy.contentTypes";
|
||||
@@ -41,11 +45,24 @@ export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
|
||||
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";
|
||||
|
||||
|
||||
@@ -3,5 +3,7 @@ export enum DashboardCommand {
|
||||
pages = "pages",
|
||||
settings = "settings",
|
||||
media = "media",
|
||||
viewData = "viewData"
|
||||
viewData = "viewData",
|
||||
mediaUpdate = "mediaUpdate",
|
||||
dataFileEntries = "dataFileEntries"
|
||||
}
|
||||
@@ -15,9 +15,13 @@ export enum DashboardMessage {
|
||||
refreshMedia = 'refreshMedia',
|
||||
uploadMedia = 'uploadMedia',
|
||||
deleteMedia = 'deleteMedia',
|
||||
revealMedia = 'revealMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
setFramework = 'setFramework',
|
||||
setState = 'setState',
|
||||
runCustomScript = 'runCustomScript',
|
||||
getDataEntries = 'getDataEntries',
|
||||
putDataEntries = 'putDataEntries',
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IButtonProps {
|
||||
secondary?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, disabled, children}: React.PropsWithChildren<IButtonProps>) => {
|
||||
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, className, disabled, secondary, children}: React.PropsWithChildren<IButtonProps>) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
|
||||
className={`${className || ""} inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 focus:outline-none disabled:bg-gray-500 ${secondary ? `bg-red-300 hover:bg-red-400` : `bg-teal-600 hover:bg-teal-700`}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import {ChevronDownIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { MenuItem, MenuItems } from './Menu';
|
||||
|
||||
export interface IChoiceButtonProps {
|
||||
title: string;
|
||||
choices: {
|
||||
icon?: JSX.Element;
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
@@ -28,7 +29,7 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
|
||||
|
||||
<Menu as="span" className="-ml-px relative block">
|
||||
<Menu.Button
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
|
||||
className="h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
|
||||
disabled={disabled}>
|
||||
<span className="sr-only">Open options</span>
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
@@ -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} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Header } from '../Header';
|
||||
import { Overview } from './Overview';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
import usePages from '../../hooks/usePages';
|
||||
|
||||
export interface IContentsProps {
|
||||
pages: Page[];
|
||||
@@ -14,23 +15,22 @@ export interface IContentsProps {
|
||||
|
||||
export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loading}: React.PropsWithChildren<IContentsProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const { pageItems } = usePages(pages);
|
||||
|
||||
const pageFolders = [...new Set(pages.map(page => page.fmFolder))];
|
||||
const pageFolders = [...new Set(pageItems.map(page => page.fmFolder))];
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
<Header
|
||||
folders={pageFolders}
|
||||
totalPages={pages.length}
|
||||
settings={settings} />
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
<Header
|
||||
folders={pageFolders}
|
||||
totalPages={pageItems.length}
|
||||
settings={settings} />
|
||||
|
||||
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
|
||||
{ loading ? <Spinner /> : <Overview pages={pages} settings={settings} /> }
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
|
||||
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
|
||||
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,11 +3,12 @@ import { useRecoilValue } from 'recoil';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Page } from '../../models/Page';
|
||||
import { SettingsSelector, ViewSelector, ViewType } from '../../state';
|
||||
import { SettingsSelector, ViewSelector } from '../../state';
|
||||
import { DateField } from '../DateField';
|
||||
import { Status } from '../Status';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import useContentType from '../../../hooks/useContentType';
|
||||
import { DashboardViewType } from '../../models';
|
||||
|
||||
export interface IItemProps extends Page {}
|
||||
|
||||
@@ -22,10 +23,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
Messenger.send(DashboardMessage.openFile, fmFilePath);
|
||||
};
|
||||
|
||||
if (view === ViewType.Grid) {
|
||||
if (view === DashboardViewType.Grid) {
|
||||
return (
|
||||
<li className="relative">
|
||||
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100`}
|
||||
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50`}
|
||||
onClick={openFile}>
|
||||
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
|
||||
{
|
||||
@@ -53,7 +54,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
} else if (view === ViewType.List) {
|
||||
} else if (view === DashboardViewType.List) {
|
||||
return (
|
||||
<li className="relative">
|
||||
<button className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`} onClick={openFile}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ViewSelector, ViewType } from '../../state';
|
||||
import { DashboardViewType } from '../../models';
|
||||
import { ViewSelector } from '../../state';
|
||||
|
||||
export interface IListProps {}
|
||||
|
||||
@@ -8,15 +9,15 @@ export const List: React.FunctionComponent<IListProps> = ({children}: React.Prop
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
|
||||
let className = '';
|
||||
if (view === ViewType.Grid) {
|
||||
if (view === DashboardViewType.Grid) {
|
||||
className = `grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`;
|
||||
} else if (view === ViewType.List) {
|
||||
} else if (view === DashboardViewType.List) {
|
||||
className = `-mx-4`;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul role="list" className={className}>
|
||||
{view === ViewType.List && (
|
||||
{view === DashboardViewType.List && (
|
||||
<li className="px-5 relative uppercase text-vulcan-100 dark:text-whisper-900 py-2 border-b border-vulcan-50">
|
||||
<div className={`grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8`}>
|
||||
<div className="col-span-8">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import {ChevronRightIcon} from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { groupBy } from '../../../helpers/GroupBy';
|
||||
|
||||
@@ -2,12 +2,13 @@ import * as React from 'react';
|
||||
import { Spinner } from './Spinner';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
import useDarkMode from '../../hooks/useDarkMode';
|
||||
import usePages from '../hooks/usePages';
|
||||
import { WelcomeScreen } from './WelcomeScreen';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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;
|
||||
@@ -15,7 +16,6 @@ export interface IDashboardProps {
|
||||
|
||||
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome}: React.PropsWithChildren<IDashboardProps>) => {
|
||||
const { loading, pages, settings } = useMessages();
|
||||
const { pageItems } = usePages(pages);
|
||||
const view = useRecoilValue(DashboardViewSelector);
|
||||
useDarkMode();
|
||||
|
||||
@@ -31,9 +31,25 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (view === 'media') {
|
||||
return <Media />;
|
||||
if (view === NavigationType.Media) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Media />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return <Contents pages={pageItems} loading={loading} />;
|
||||
if (view === NavigationType.Data) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<DataView />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Contents pages={pages} loading={loading} />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
69
src/dashboardWebView/components/DataView/DataForm.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import Ajv from 'ajv';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';
|
||||
import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { DataFormControls } from './DataFormControls';
|
||||
|
||||
export interface IDataFormProps {
|
||||
schema: any;
|
||||
model: any;
|
||||
onSubmit: (model: any) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export const DataForm: React.FunctionComponent<IDataFormProps> = ({ schema, model, onSubmit, onClear }: React.PropsWithChildren<IDataFormProps>) => {
|
||||
const [ bridge, setBridge ] = useState<JSONSchemaBridge | null>(null);
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, useDefaults: true });
|
||||
|
||||
const jsonValidator = (schema: object) => {
|
||||
const validator = ajv.compile(schema);
|
||||
|
||||
return (crntModel: object) => {
|
||||
validator(crntModel);
|
||||
return validator.errors?.length ? { details: validator.errors } : null;
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const schemaValidator = jsonValidator(schema);
|
||||
const bridge = new JSONSchemaBridge(schema, schemaValidator);
|
||||
setBridge(bridge);
|
||||
}, [schema]);
|
||||
|
||||
if (!bridge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className='autoform'>
|
||||
{
|
||||
model ? (
|
||||
<h2 className='text-gray-500 dark:text-whisper-900'>Modify the data</h2>
|
||||
) : (
|
||||
<h2 className='text-gray-500 dark:text-whisper-900'>Add new data</h2>
|
||||
)
|
||||
}
|
||||
|
||||
<AutoForm
|
||||
schema={bridge}
|
||||
model={model || {}}
|
||||
onSubmit={onSubmit}
|
||||
ref={form => form?.reset()}>
|
||||
<div className={`fields`}>
|
||||
<AutoFields />
|
||||
</div>
|
||||
|
||||
<div className={`errors`}>
|
||||
<ErrorsField />
|
||||
</div>
|
||||
|
||||
<DataFormControls model={model} onClear={onClear} />
|
||||
</AutoForm>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useForm } from 'uniforms';
|
||||
import { SubmitField } from 'uniforms-unstyled';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface IDataFormControlsProps {
|
||||
model: any | null;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export const DataFormControls: React.FunctionComponent<IDataFormControlsProps> = ({ model, onClear }: React.PropsWithChildren<IDataFormControlsProps>) => {
|
||||
const { formRef } = useForm();
|
||||
|
||||
return (
|
||||
<div className='text-right border-t border-gray-200 dark:border-vulcan-300'>
|
||||
<SubmitField value={model ? `Update` : `Add`} />
|
||||
<Button className='ml-4' secondary onClick={() => {
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
formRef.reset();
|
||||
}}>Cancel</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
230
src/dashboardWebView/components/DataView/DataView.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import * as React from 'react';
|
||||
import { Header } from '../Header';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { DataForm } from './DataForm';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DataFile } from '../../../models/DataFile';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
import { EventData } from '@estruyf/vscode';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { Button } from '../Button';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { EmptyView } from './EmptyView';
|
||||
import { Container } from './SortableContainer';
|
||||
import { SortableItem } from './SortableItem';
|
||||
import { ChevronRightIcon } from '@heroicons/react/outline';
|
||||
import { ToastContainer, toast, Slide } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { DataType } from '../../../models/DataType';
|
||||
|
||||
export interface IDataViewProps {}
|
||||
|
||||
export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.PropsWithChildren<IDataViewProps>) => {
|
||||
const [ selectedData, setSelectedData ] = useState<DataFile | null>(null);
|
||||
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
|
||||
const [ dataEntries, setDataEntries ] = useState<any[] | null>(null);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const setSchema = (dataFile: DataFile) => {
|
||||
setSelectedData(dataFile);
|
||||
setSelectedIndex(null);
|
||||
setDataEntries(null);
|
||||
|
||||
Messenger.send(DashboardMessage.getDataEntries, { ...dataFile });
|
||||
};
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
||||
if (message.data.command === DashboardCommand.dataFileEntries) {
|
||||
setDataEntries(message.data.data);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = useCallback((index: number) => {
|
||||
const dataClone: any[] = Object.assign([], dataEntries);
|
||||
|
||||
if (!selectedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataClone.splice(index, 1);
|
||||
updateData(dataClone);
|
||||
}, [selectedData, dataEntries]);
|
||||
|
||||
|
||||
const onSubmit = useCallback((data: any) => {
|
||||
const dataClone: any[] = Object.assign([], dataEntries);
|
||||
if (selectedIndex !== null && selectedIndex !== undefined) {
|
||||
dataClone[selectedIndex] = data;
|
||||
} else {
|
||||
dataClone.push(data);
|
||||
}
|
||||
updateData(dataClone);
|
||||
}, [selectedData, dataEntries, selectedIndex]);
|
||||
|
||||
|
||||
const onSortEnd = useCallback(({ oldIndex, newIndex }: any) => {
|
||||
if (!dataEntries || dataEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectedIndex !== null && selectedIndex !== undefined) {
|
||||
setSelectedIndex(newIndex);
|
||||
}
|
||||
|
||||
const newEntries = arrayMoveImmutable(dataEntries, oldIndex, newIndex);
|
||||
updateData(newEntries);
|
||||
}, [selectedData, dataEntries, selectedIndex]);
|
||||
|
||||
const updateData = useCallback((data: any) => {
|
||||
if (!selectedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
Messenger.send(DashboardMessage.putDataEntries, {
|
||||
file: selectedData.file,
|
||||
fileType: selectedData.fileType,
|
||||
entries: data
|
||||
});
|
||||
|
||||
// Show toast message
|
||||
toast.success("Updated your data entries", {
|
||||
position: "top-right",
|
||||
autoClose: 2000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: false,
|
||||
transition: Slide
|
||||
});
|
||||
}, [selectedData]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
|
||||
const dataFiles = (settings?.dataFiles || []).map((dataFile: DataFile) => {
|
||||
if (!dataFile.schema && !dataFile.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clonedFile = Object.assign({}, dataFile);
|
||||
|
||||
if (clonedFile.type) {
|
||||
const dataType = settings?.dataTypes?.find((dataType: DataType) => dataType.id === clonedFile.type);
|
||||
if (!dataType) {
|
||||
return null;
|
||||
}
|
||||
clonedFile.schema = Object.assign({}, dataType.schema);
|
||||
}
|
||||
|
||||
return clonedFile;
|
||||
}).filter(d => d !== null) as DataFile[];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-auto inset-y-0">
|
||||
<Header settings={settings} />
|
||||
|
||||
<div className="relative w-full flex-grow mx-auto overflow-hidden">
|
||||
|
||||
<div className={`flex w-64 flex-col absolute inset-y-0`}>
|
||||
|
||||
<aside className={`flex flex-col flex-grow overflow-y-auto border-r border-gray-200 dark:border-vulcan-300 py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Select your data type</h2>
|
||||
|
||||
<nav className={`flex-1 py-4 -mx-4 `}>
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
(dataFiles && dataFiles.length > 0) && (
|
||||
dataFiles.map((dataFile) => (
|
||||
<button
|
||||
key={dataFile.id}
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
onClick={() => setSchema(dataFile)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>{dataFile.title}</span>
|
||||
</button>
|
||||
)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<section className={`pl-64 flex min-w-0 h-full`}>
|
||||
{
|
||||
selectedData ? (
|
||||
<>
|
||||
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData.title.toLowerCase()} data items</h2>
|
||||
|
||||
<div className='py-4'>
|
||||
{
|
||||
(dataEntries && dataEntries.length > 0) ? (
|
||||
<>
|
||||
<Container onSortEnd={onSortEnd} useDragHandle>
|
||||
{
|
||||
(dataEntries || []).map((dataEntry, idx) => (
|
||||
<SortableItem
|
||||
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
|
||||
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
|
||||
index={idx}
|
||||
crntIndex={idx}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
|
||||
onDeleteItem={deleteItem}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Container>
|
||||
<Button
|
||||
className='mt-4'
|
||||
onClick={() => setSelectedIndex(null)}>
|
||||
Add a new entry
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={`flex flex-col items-center justify-center`}>
|
||||
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-2/3 py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Create or modify your {selectedData.title.toLowerCase()} data</h2>
|
||||
{
|
||||
selectedData ? (
|
||||
<DataForm
|
||||
schema={selectedData?.schema}
|
||||
model={(dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null}
|
||||
onSubmit={onSubmit}
|
||||
onClear={() => setSelectedIndex(null)} />
|
||||
) : (
|
||||
<p>Select a data type to get started</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyView />
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
13
src/dashboardWebView/components/DataView/EmptyView.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IEmptyViewProps {}
|
||||
|
||||
export const EmptyView: React.FunctionComponent<IEmptyViewProps> = (props: React.PropsWithChildren<IEmptyViewProps>) => {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center w-full'>
|
||||
<ExclamationCircleIcon className={`w-1/12 text-gray-500 dark:text-whisper-900 opacity-90`} />
|
||||
<h2 className={`text-xl text-gray-500 dark:text-whisper-900`}>Select your date type first</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { SortableContainer } from 'react-sortable-hoc';
|
||||
|
||||
export interface ISortableContainerProps {}
|
||||
|
||||
export const Container = SortableContainer(({ children }: React.PropsWithChildren<ISortableContainerProps>) => ( <ul className={`-mx-4 divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>{children}</ul> ));
|
||||
68
src/dashboardWebView/components/DataView/SortableItem.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { PencilIcon, SelectorIcon, TrashIcon, XIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { SortableHandle, SortableElement } from 'react-sortable-hoc';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
|
||||
export interface ISortableItemProps {
|
||||
value: string;
|
||||
index: number;
|
||||
crntIndex: number;
|
||||
selectedIndex: number | null;
|
||||
onSelectedIndexChange: (index: number) => void;
|
||||
onDeleteItem: (index: number) => void;
|
||||
}
|
||||
|
||||
const DragHandle = SortableHandle(() => <SelectorIcon className={`w-6 h-6 cursor-move`} />);
|
||||
|
||||
export const SortableItem = SortableElement(({ value, selectedIndex, crntIndex, onSelectedIndexChange, onDeleteItem }: ISortableItemProps) => {
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
|
||||
const deleteItemConfirm = () => {
|
||||
setShowAlert(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<li data-test={`${selectedIndex}-${crntIndex}`} className={`py-2 px-2 w-full flex justify-between content-center hover:bg-gray-200 dark:hover:bg-vulcan-400 ${selectedIndex === crntIndex ? `bg-gray-300 dark:bg-vulcan-300` : ``}`}>
|
||||
<div className='flex items-center'>
|
||||
<DragHandle />
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
|
||||
<div className={`space-x-2 flex items-center`}>
|
||||
<button
|
||||
type='button'
|
||||
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
|
||||
title={`Edit "${value}"`}
|
||||
onClick={() => onSelectedIndexChange(crntIndex)}>
|
||||
<PencilIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
|
||||
title={`Delete "${value}"`}
|
||||
onClick={() => deleteItemConfirm()}>
|
||||
<TrashIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
showAlert && (
|
||||
<Alert
|
||||
title={`Delete data entry`}
|
||||
description={`Are you sure you want to delete the data entry?`}
|
||||
okBtnText={`Delete`}
|
||||
cancelBtnText={`Cancel`}
|
||||
dismiss={() => setShowAlert(false)}
|
||||
trigger={() => {
|
||||
setShowAlert(false);
|
||||
onDeleteItem(crntIndex);
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
});
|
||||
1
src/dashboardWebView/components/DataView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DataView';
|
||||
@@ -4,23 +4,29 @@ import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
|
||||
import { SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../../state';
|
||||
|
||||
export interface IBreadcrumbProps {}
|
||||
|
||||
export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: React.PropsWithChildren<IBreadcrumbProps>) => {
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const [ , setSearchValue ] = useRecoilState(SearchAtom);
|
||||
const [ folders, setFolders ] = React.useState<string[]>([]);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
if (!settings?.wsFolder) {
|
||||
return null;
|
||||
const updateFolder = (folder: string) => {
|
||||
setSearchValue('');
|
||||
setSelectedFolder(folder);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { wsFolder, staticFolder, contentFolders } = settings;
|
||||
|
||||
const isValid = (folderPath: string) => {
|
||||
const isValid = (folderPath: string) => {
|
||||
if (staticFolder) {
|
||||
const staticPath = parseWinPath(join(wsFolder, staticFolder)) as string;
|
||||
const relPath = folderPath.replace(staticPath, '') as string;
|
||||
@@ -31,7 +37,7 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
|
||||
return false;
|
||||
}
|
||||
}
|
||||
1
|
||||
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const folder = contentFolders[i];
|
||||
const contentFolder = parseWinPath(folder.path) as string;
|
||||
@@ -60,41 +66,40 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
|
||||
|
||||
setFolders(allFolders);
|
||||
}
|
||||
}, [selectedFolder]);
|
||||
|
||||
}, [selectedFolder, settings]);
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex py-2" aria-label="Breadcrumb">
|
||||
<ol role="list" className="w-full mx-auto flex space-x-4 px-5">
|
||||
<li className="flex">
|
||||
<ol role="list" className="flex space-x-4 px-5 flex-1">
|
||||
<li className="flex">
|
||||
<div className="flex items-center">
|
||||
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
|
||||
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Home</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{folders.map((folder) => (
|
||||
<li key={folder} className="flex">
|
||||
<div className="flex items-center">
|
||||
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
|
||||
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Home</span>
|
||||
<svg
|
||||
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => updateFolder(folder)}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
|
||||
>
|
||||
{basename(folder)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{folders.map((folder) => (
|
||||
<li key={folder} className="flex">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => setSelectedFolder(folder)}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
|
||||
>
|
||||
{basename(folder)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { XCircleIcon } from '@heroicons/react/solid';
|
||||
import {XCircleIcon} from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { FolderSelector, TagSelector, CategorySelector, SortingAtom, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { FilterIcon } from '@heroicons/react/solid';
|
||||
import {FilterIcon} from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
|
||||
|
||||
@@ -3,21 +3,22 @@ import { Sorting } from './Sorting';
|
||||
import { Searchbox } from './Searchbox';
|
||||
import { Filter } from './Filter';
|
||||
import { Folders } from './Folders';
|
||||
import { Settings } from '../../models';
|
||||
import { Settings, NavigationType } from '../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Startup } from '../Startup';
|
||||
import { Navigation } from '../Navigation';
|
||||
import { Grouping } from '.';
|
||||
import { ViewSwitch } from './ViewSwitch';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { CategoryAtom, DashboardViewAtom, TagAtom } from '../../state';
|
||||
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 { Pagination } from '../Media/Pagination';
|
||||
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
|
||||
import { ChoiceButton } from '../ChoiceButton';
|
||||
import { Breadcrumb } from './Breadcrumb';
|
||||
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;
|
||||
@@ -33,6 +34,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
|
||||
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
|
||||
const [ view, setView ] = useRecoilState(DashboardViewAtom);
|
||||
const resetSorting = useResetRecoilState(SortingAtom)
|
||||
|
||||
const createContent = () => {
|
||||
Messenger.send(DashboardMessage.createContent);
|
||||
@@ -46,22 +48,34 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
Messenger.send(DashboardMessage.createByTemplate);
|
||||
};
|
||||
|
||||
const updateView = (view: NavigationType) => {
|
||||
setView(view);
|
||||
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={`px-4 bg-gray-50 dark:bg-vulcan-50 border-b-2 border-gray-200 dark:border-vulcan-200`}>
|
||||
<div className={`flex items-center justify-start`}>
|
||||
<button className={`p-2 flex items-center ${view === "contents" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => setView("contents")}>
|
||||
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
|
||||
</button>
|
||||
<button className={`p-2 flex items-center ${view === "media" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => setView("media")}>
|
||||
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300">
|
||||
<Tabs onNavigate={updateView} />
|
||||
</div>
|
||||
|
||||
{
|
||||
view === "contents" && (
|
||||
view === NavigationType.Contents && (
|
||||
<>
|
||||
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
|
||||
<Searchbox />
|
||||
@@ -71,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>
|
||||
@@ -95,7 +122,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end space-x-4 lg:space-x-6 xl:space-x-8 bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100`}>
|
||||
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100 space-x-4 lg:space-x-6 xl:space-x-8`}>
|
||||
<ClearFilters />
|
||||
|
||||
<Folders />
|
||||
@@ -106,17 +133,18 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
|
||||
<Grouping />
|
||||
|
||||
<Sorting />
|
||||
<Sorting view={NavigationType.Contents} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
view === "media" && (
|
||||
view === NavigationType.Media && (
|
||||
<>
|
||||
<Pagination />
|
||||
<Breadcrumb />
|
||||
<MediaHeaderTop />
|
||||
|
||||
<MediaHeaderBottom />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
72
src/dashboardWebView/components/Header/Pagination.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { LIMIT } from '../../hooks/useMedia';
|
||||
import { MediaTotalSelector, PageAtom } from '../../state';
|
||||
import { PaginationButton } from './PaginationButton';
|
||||
|
||||
export interface IPaginationProps {}
|
||||
|
||||
export const Pagination: React.FunctionComponent<IPaginationProps> = (props: React.PropsWithChildren<IPaginationProps>) => {
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
|
||||
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
|
||||
|
||||
const getButtons = (): number[] => {
|
||||
const maxButtons = 5;
|
||||
const buttons: number[] = [];
|
||||
const start = page - maxButtons;
|
||||
const end = page + maxButtons;
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i >= 0 && i <= totalPages) {
|
||||
buttons.push(i);
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
title="First"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(0)
|
||||
}
|
||||
}} />
|
||||
|
||||
<PaginationButton
|
||||
title="Previous"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(page - 1)
|
||||
}
|
||||
}} />
|
||||
|
||||
{getButtons().map((button) => (
|
||||
<button
|
||||
key={button}
|
||||
disabled={button === page}
|
||||
onClick={() => {
|
||||
setPage(button)
|
||||
}
|
||||
}
|
||||
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'} max-h-8`}
|
||||
>{button + 1}</button>
|
||||
))}
|
||||
|
||||
<PaginationButton
|
||||
title="Next"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(page + 1)} />
|
||||
|
||||
<PaginationButton
|
||||
title="Last"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(totalPages)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
src/dashboardWebView/components/Header/PaginationStatus.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaTotalSelector, PageAtom, SearchAtom, SelectedMediaFolderSelector } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import { LIMIT } from '../../hooks/useMedia';
|
||||
|
||||
export interface IPaginationStatusProps {}
|
||||
|
||||
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const [ , setSearch ] = useRecoilState(SearchAtom);
|
||||
|
||||
const getTotalPage = () => {
|
||||
const mediaItems = ((page + 1) * LIMIT);
|
||||
if (totalMedia < mediaItems) {
|
||||
return totalMedia;
|
||||
}
|
||||
return mediaItems;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
setPage(0);
|
||||
setSearch('');
|
||||
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden sm:flex">
|
||||
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
|
||||
title="Refresh media"
|
||||
onClick={refresh}>
|
||||
<RefreshIcon className={`h-5 w-5`} />
|
||||
<span className="sr-only">Refresh media</span>
|
||||
</button>
|
||||
|
||||
<p className="text-sm text-gray-500 dark:text-whisper-900">
|
||||
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
|
||||
<span className="font-medium">{totalMedia}</span> results
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +1,46 @@
|
||||
import { FilterIcon, SearchIcon } from '@heroicons/react/solid';
|
||||
import {SearchIcon} from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
import { SearchAtom } from '../../state';
|
||||
|
||||
export interface ISearchboxProps {}
|
||||
export interface ISearchboxProps {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({}: React.PropsWithChildren<ISearchboxProps>) => {
|
||||
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({placeholder}: React.PropsWithChildren<ISearchboxProps>) => {
|
||||
const [ value, setValue ] = React.useState('');
|
||||
const [ , setDebounceValue ] = useRecoilState(SearchAtom);
|
||||
const [ debounceSearchValue, setDebounceValue ] = useRecoilState(SearchAtom);
|
||||
const debounceSearch = useDebounce<string>(value, 500);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!debounceSearchValue && value) {
|
||||
setValue('');
|
||||
}
|
||||
} , [debounceSearchValue]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDebounceValue(debounceSearch);
|
||||
}, [debounceSearch]);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex space-x-4 flex-1">
|
||||
<div className="min-w-0">
|
||||
<label htmlFor="search" className="sr-only">Search</label>
|
||||
<div className="relative flex justify-center">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="search"
|
||||
name="search"
|
||||
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`}
|
||||
placeholder="Search"
|
||||
placeholder={placeholder || "Search"}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -6,58 +6,96 @@ import { ExtensionState } from '../../../constants';
|
||||
import { SortOrder, SortType } from '../../../models';
|
||||
import { SortOption } from '../../constants/SortOption';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
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 {}
|
||||
export interface ISortingProps {
|
||||
disableCustomSorting?: boolean;
|
||||
view: NavigationType;
|
||||
}
|
||||
|
||||
export const sortOptions: SortingOption[] = [
|
||||
{ name: "Last modified", id: SortOption.LastModified, order: SortOrder.desc, type: SortType.string },
|
||||
{ name: "Last modified (asc)", id: SortOption.LastModifiedAsc, order: SortOrder.asc, type: SortType.date },
|
||||
{ name: "Last modified (desc)", id: SortOption.LastModifiedDesc, order: SortOrder.desc, type: SortType.date },
|
||||
{ name: "By filename (asc)", id: SortOption.FileNameAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
export const Sorting: React.FunctionComponent<ISortingProps> = ({}: React.PropsWithChildren<ISortingProps>) => {
|
||||
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);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const updateSorting = (value: SortingOption) => {
|
||||
|
||||
Messenger.send(DashboardMessage.setState, {
|
||||
key: ExtensionState.Dashboard.Sorting,
|
||||
key: `${view === NavigationType.Contents ? ExtensionState.Dashboard.Contents.Sorting : ExtensionState.Dashboard.Media.Sorting}`,
|
||||
value: value
|
||||
})
|
||||
});
|
||||
|
||||
setCrntSorting(value)
|
||||
};
|
||||
|
||||
let allOptions = [...sortOptions];
|
||||
if (settings?.customSorting) {
|
||||
|
||||
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,
|
||||
name: s.name,
|
||||
id: `${s.name}-${s.order}`,
|
||||
id: s.id || `${s.name}-${s.order}`,
|
||||
order: s.order,
|
||||
type: s.type
|
||||
}))];
|
||||
}
|
||||
|
||||
let crntSort = allOptions.find(x => x.id === crntSorting?.id) || sortOptions[0];
|
||||
let crntSortingOption = crntSorting;
|
||||
if (!crntSortingOption) {
|
||||
if (view === NavigationType.Contents) {
|
||||
crntSortingOption = settings?.dashboardState?.contents?.sorting || null;
|
||||
} else if (view === NavigationType.Media) {
|
||||
crntSortingOption = settings?.dashboardState?.media?.sorting || null;
|
||||
}
|
||||
|
||||
if (crntSortingOption === null) {
|
||||
if (view === NavigationType.Contents && settings?.dashboardState.contents.defaultSorting) {
|
||||
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
|
||||
} else if (view === NavigationType.Media && settings?.dashboardState.contents.defaultSorting) {
|
||||
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let crntSort = allOptions.find(x => x.id === crntSortingOption?.id) || sortOptions[0];
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<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}
|
||||
title={option.title || option.name}
|
||||
value={option}
|
||||
isCurrent={option.id === crntSorting?.id}
|
||||
isCurrent={option.id === crntSort.id}
|
||||
onClick={(value) => updateSorting(value)} />
|
||||
))}
|
||||
</MenuItems>
|
||||
|
||||
25
src/dashboardWebView/components/Header/Tab.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { NavigationType } from '../../models';
|
||||
import { DashboardViewAtom } from '../../state';
|
||||
|
||||
export interface ITabProps {
|
||||
navigationType: NavigationType;
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
|
||||
const view = useRecoilValue(DashboardViewAtom);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === navigationType ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="profile"
|
||||
aria-selected="false"
|
||||
onClick={() => onNavigate(navigationType)}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
45
src/dashboardWebView/components/Header/Tabs.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DatabaseIcon, PhotographIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { NavigationType } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { Tab } from './Tab';
|
||||
|
||||
export interface ITabsProps {
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.PropsWithChildren<ITabsProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
return (
|
||||
<ul className="flex items-center justify-start h-full" data-tabs-toggle="#myTabContent" role="tablist">
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Contents}
|
||||
onNavigate={onNavigate}>
|
||||
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
|
||||
</Tab>
|
||||
</li>
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Media}
|
||||
onNavigate={onNavigate}>
|
||||
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
|
||||
</Tab>
|
||||
</li>
|
||||
{
|
||||
(settings?.dataFiles && settings.dataFiles.length > 0) && (
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Data}
|
||||
onNavigate={onNavigate}>
|
||||
<DatabaseIcon className={`h-6 w-auto mr-2`} /><span>Data</span>
|
||||
</Tab>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { ViewAtom, ViewType, SettingsSelector } from '../../state';
|
||||
import { ViewGridIcon, ViewListIcon } from '@heroicons/react/solid';
|
||||
import { ViewAtom, SettingsSelector } from '../../state';
|
||||
import {ViewListIcon, ViewGridIcon} from '@heroicons/react/solid';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { DashboardViewType } from '../../models';
|
||||
|
||||
export interface IViewSwitchProps {}
|
||||
|
||||
@@ -12,7 +13,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: Rea
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const toggleView = () => {
|
||||
const newView = view === ViewType.Grid ? ViewType.List : ViewType.Grid;
|
||||
const newView = view === DashboardViewType.Grid ? DashboardViewType.List : DashboardViewType.Grid;
|
||||
setView(newView);
|
||||
Messenger.send(DashboardMessage.setPageViewType, newView);
|
||||
};
|
||||
@@ -25,11 +26,11 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: Rea
|
||||
|
||||
return (
|
||||
<div className={`flex rounded-sm bg-vulcan-50 lg:mb-1`}>
|
||||
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === ViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
|
||||
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === DashboardViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
|
||||
<ViewGridIcon className={`w-4 h-4`} />
|
||||
<span className={`sr-only`}>Change to grid</span>
|
||||
</button>
|
||||
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === ViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
|
||||
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
|
||||
<ViewListIcon className={`w-4 h-4`} />
|
||||
<span className={`sr-only`}>Change to list</span>
|
||||
</button>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
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 } from '../../state';
|
||||
import { SelectedMediaFolderAtom, SettingsSelector } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ChoiceButton } from '../ChoiceButton';
|
||||
import { CustomScript, ScriptType } from '../../../models';
|
||||
|
||||
export interface IFolderCreationProps {}
|
||||
|
||||
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const onFolderCreation = () => {
|
||||
Messenger.send(DashboardMessage.createMediaFolder, {
|
||||
@@ -16,13 +19,36 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
|
||||
});
|
||||
};
|
||||
|
||||
const runCustomScript = (script: CustomScript) => {
|
||||
Messenger.send(DashboardMessage.runCustomScript, {script, path: selectedFolder});
|
||||
};
|
||||
|
||||
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
|
||||
if (scripts.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-1 justify-end">
|
||||
<ChoiceButton
|
||||
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}
|
||||
disabled={!settings?.initialized} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
onClick={onFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create new folder</span>
|
||||
</button>
|
||||
<div className="flex flex-1 justify-end">
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
onClick={onFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create new folder</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FolderIcon } from '@heroicons/react/solid';
|
||||
import {FolderIcon} from '@heroicons/react/solid';
|
||||
import { basename } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@@ -16,11 +16,13 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder,
|
||||
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
|
||||
|
||||
return (
|
||||
<li className={`group relative bg-gray-200 dark:bg-vulcan-300 hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
|
||||
<button className={`w-full flex flex-col items-center`} onClick={() => setSelectedFolder(folder)}>
|
||||
<FolderIcon className={`h-auto w-1/2`} />
|
||||
<li className={`group relative hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
|
||||
<button className={`w-full flex flex-row items-center h-full`} onClick={() => setSelectedFolder(folder)}>
|
||||
<div>
|
||||
<FolderIcon className={`h-12 w-12 mr-4`} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-bold pointer-events-none flex items-center">
|
||||
<p className="text-sm font-bold pointer-events-none flex items-center text-left overflow-hidden break-words">
|
||||
{basename(relFolderPath)}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PencilIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { Menu } from '@headlessui/react';
|
||||
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';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { CustomScript } from '../../../helpers/CustomScript';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { ScriptType } from '../../../models';
|
||||
import { MediaInfo } from '../../../models/MediaPaths';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LightboxAtom, PageSelector, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { MenuItem, MenuItems } from '../Menu';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { Metadata } from '../Modals/Metadata';
|
||||
|
||||
import { MenuButton } from './MenuButton'
|
||||
import { QuickAction } from './QuickAction';
|
||||
|
||||
export interface IItemProps {
|
||||
media: MediaInfo;
|
||||
}
|
||||
@@ -61,12 +67,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || "");
|
||||
};
|
||||
|
||||
const runCustomScript = (script: CustomScript) => {
|
||||
Messenger.send(DashboardMessage.runCustomScript, {script, path: media.fsPath});
|
||||
};
|
||||
|
||||
const insertToArticle = () => {
|
||||
const relPath = getRelPath();
|
||||
Messenger.send(DashboardMessage.insertPreviewImage, {
|
||||
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,
|
||||
@@ -78,9 +89,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const insertSnippet = () => {
|
||||
const relPath = getRelPath();
|
||||
let snippet = settings?.mediaSnippet.join("\n");
|
||||
|
||||
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
|
||||
snippet = snippet?.replace("{alt}", alt || "");
|
||||
snippet = snippet?.replace("{caption}", caption || "");
|
||||
snippet = snippet?.replace("{filename}", basename(relPath || ""));
|
||||
|
||||
Messenger.send(DashboardMessage.insertPreviewImage, {
|
||||
image: parseWinPath(relPath) || "",
|
||||
@@ -95,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,
|
||||
@@ -111,8 +131,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}
|
||||
}
|
||||
|
||||
if (media?.stats?.size) {
|
||||
const size = media.stats.size / (1024*1024);
|
||||
if (media?.size) {
|
||||
const size = media.size / (1024*1024);
|
||||
if (size > 1) {
|
||||
sizeDetails.push(`${size.toFixed(2)} MB`);
|
||||
} else {
|
||||
@@ -149,6 +169,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
setFilename(getFileName());
|
||||
};
|
||||
|
||||
const customScriptActions = () => {
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={script.title}
|
||||
onClick={() => runCustomScript(script)} />
|
||||
))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (media.alt !== alt) {
|
||||
setAlt(media.alt);
|
||||
@@ -174,7 +203,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
|
||||
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50">
|
||||
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
|
||||
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
|
||||
<PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
|
||||
@@ -185,54 +214,103 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</button>
|
||||
<div className={`relative py-4 pl-4 pr-12`}>
|
||||
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
|
||||
<button title={`Edit metadata`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={updateMetadata}>
|
||||
<PencilIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Edit metadata</span>
|
||||
</button>
|
||||
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<button
|
||||
title={`Insert into your content`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={insertToArticle}>
|
||||
<CheckCircleIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Insert into your content</span>
|
||||
</button>
|
||||
<div className="flex items-center border border-transparent group-hover:bg-gray-50 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
|
||||
|
||||
<div className='hidden group-hover:inline-block h-5'>
|
||||
<QuickAction
|
||||
title='Edit metadata'
|
||||
onClick={updateMetadata}>
|
||||
<PencilIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<QuickAction
|
||||
title={(viewData.data.metadataInsert && viewData.data.fieldName) ? `Insert image for your "${viewData.data.fieldName}" field` : `Insert image with markdown markup`}
|
||||
onClick={insertToArticle}>
|
||||
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
<QuickAction
|
||||
title='Insert snippet'
|
||||
onClick={insertSnippet}>
|
||||
<CodeIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
)
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<QuickAction
|
||||
title='Copy media path'
|
||||
onClick={copyToClipboard}>
|
||||
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<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">
|
||||
<MenuButton title={`Menu`} />
|
||||
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={`Edit metadata`}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
<button
|
||||
title={`Insert your media snippet`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={insertSnippet}>
|
||||
<CodeIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Insert your media snippet</span>
|
||||
</button>
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={`Insert image markdown`}
|
||||
onClick={insertToArticle} />
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
<MenuItem
|
||||
title={`Insert snippet`}
|
||||
onClick={insertSnippet} />
|
||||
)
|
||||
}
|
||||
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
title={`Copy media path`}
|
||||
onClick={copyToClipboard} />
|
||||
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button title={`Copy media path`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={copyToClipboard}>
|
||||
<ClipboardCopyIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Copy media path</span>
|
||||
</button>
|
||||
<button title={`Delete media`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={deleteMedia}>
|
||||
<TrashIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Delete media</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={`Reveal media`}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center">
|
||||
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center break-all">
|
||||
{basename(parseWinPath(media.fsPath) || "")}
|
||||
</p>
|
||||
{
|
||||
@@ -256,7 +334,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{getFolder()}</span>
|
||||
</p>
|
||||
{
|
||||
(media?.stats?.size || media?.dimensions) && (
|
||||
(media?.size || media?.dimensions) && (
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
<b className={`mr-1`}>Size:</b>
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{calculateSize()}</span>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IListProps {}
|
||||
export interface IListProps {
|
||||
gap?: number;
|
||||
}
|
||||
|
||||
export const List: React.FunctionComponent<IListProps> = ({gap, children}: React.PropsWithChildren<IListProps>) => {
|
||||
const gapClass = gap !== undefined ? `gap-y-${gap}` : `gap-y-8`;
|
||||
|
||||
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
|
||||
return (
|
||||
<ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
|
||||
<ul role="list" className={`grid grid-cols-2 gap-x-4 ${gapClass} sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { UploadIcon } from '@heroicons/react/outline';
|
||||
import {UploadIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
|
||||
@@ -17,19 +17,17 @@ import { useCallback } from 'react';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { FolderItem } from './FolderItem';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
|
||||
export interface IMediaProps {}
|
||||
|
||||
export const LIMIT = 16;
|
||||
|
||||
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
|
||||
const { media } = useMedia();
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
|
||||
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
|
||||
const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom);
|
||||
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
|
||||
const folders = useRecoilValue(MediaFoldersAtom);
|
||||
const loading = useRecoilValue(LoadingAtom);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
acceptedFiles.forEach((file) => {
|
||||
@@ -52,96 +50,75 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
onDrop,
|
||||
accept: 'image/*'
|
||||
});
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
|
||||
if (message.data.command === DashboardCommand.media) {
|
||||
const data: MediaPaths = message.data.data as MediaPaths;
|
||||
setLoading(false);
|
||||
setMedia(data.media);
|
||||
setTotal(data.total);
|
||||
setFolders(data.folders);
|
||||
setSelectedFolder(data.selectedFolder);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
Messenger.listen<MediaPaths>(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, ['']);
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
<Header settings={settings} />
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
<Header settings={settings} />
|
||||
|
||||
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
|
||||
|
||||
{
|
||||
viewData?.data?.filePath && (
|
||||
<div className={`text-lg text-center mb-6`}>
|
||||
<p>Select the image you want to use for your article.</p>
|
||||
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
isDragActive && (
|
||||
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
|
||||
<UploadIcon className={`h-32`} />
|
||||
<p className={`text-xl max-w-md text-center`}>
|
||||
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(media.length === 0 && folders.length === 0 && !loading) && (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
|
||||
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & drop new files.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
folders && folders.length > 0 && (
|
||||
<div className={`mb-8`}>
|
||||
<List>
|
||||
{
|
||||
folders && folders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<List>
|
||||
{
|
||||
media.map((file) => (
|
||||
<Item key={file.fsPath} media={file} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
|
||||
|
||||
{
|
||||
loading && ( <Spinner /> )
|
||||
viewData?.data?.filePath && (
|
||||
<div className={`text-lg text-center mb-6`}>
|
||||
<p>Select the image you want to use for your article.</p>
|
||||
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
isDragActive && (
|
||||
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
|
||||
<UploadIcon className={`h-32`} />
|
||||
<p className={`text-xl max-w-md text-center`}>
|
||||
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Lightbox />
|
||||
{
|
||||
(media.length === 0 && folders.length === 0 && !loading) && (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
|
||||
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & drop new files.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
|
||||
{
|
||||
folders && folders.length > 0 && (
|
||||
<div className={`mb-8`}>
|
||||
<List gap={0}>
|
||||
{
|
||||
folders && folders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<List>
|
||||
{
|
||||
media.map((file) => (
|
||||
<Item key={file.fsPath} media={file} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{
|
||||
loading && ( <Spinner /> )
|
||||
}
|
||||
|
||||
<Lightbox />
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
src/dashboardWebView/components/Media/MediaHeaderBottom.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { NavigationType } from '../../models/NavigationType';
|
||||
import { SettingsAtom } from '../../state';
|
||||
import { Sorting } from '../Header';
|
||||
import { Breadcrumb } from '../Header/Breadcrumb';
|
||||
import { Pagination } from '../Header/Pagination';
|
||||
|
||||
export interface IMediaHeaderBottomProps {}
|
||||
|
||||
export const MediaHeaderBottom: React.FunctionComponent<IMediaHeaderBottomProps> = (props: React.PropsWithChildren<IMediaHeaderBottomProps>) => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
if (!settings?.wsFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex justify-between py-2" aria-label="Breadcrumb">
|
||||
|
||||
<Breadcrumb />
|
||||
|
||||
<Pagination />
|
||||
|
||||
<div className={`flex px-5 flex-1 justify-end`}>
|
||||
<Sorting view={NavigationType.Media} disableCustomSorting />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
81
src/dashboardWebView/components/Media/MediaHeaderTop.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { EventData } from '@estruyf/vscode';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
import { usePrevious } from '../../../panelWebView/hooks/usePrevious';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LoadingAtom, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state';
|
||||
import { Searchbox } from '../Header';
|
||||
import { PaginationStatus } from '../Header/PaginationStatus';
|
||||
import { FolderCreation } from './FolderCreation';
|
||||
|
||||
export interface IMediaHeaderTopProps {}
|
||||
|
||||
export const MediaHeaderTop: React.FunctionComponent<IMediaHeaderTopProps> = ({}: React.PropsWithChildren<IMediaHeaderTopProps>) => {
|
||||
const [ lastUpdated, setLastUpdated ] = React.useState<string | null>(null);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const crntSorting = useRecoilValue(SortingSelector);
|
||||
const [ , setLoading ] = useRecoilState(LoadingAtom);
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const debounceGetMedia = useDebounce<string | null>(lastUpdated, 200);
|
||||
const prevSelectedFolder = usePrevious<string | null>(selectedFolder);
|
||||
|
||||
const mediaUpdate = (message: MessageEvent<EventData<{ key: string, value: any }>>) => {
|
||||
if (message.data.command === DashboardCommand.mediaUpdate) {
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page,
|
||||
folder: selectedFolder || '',
|
||||
sorting: crntSorting
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) {
|
||||
setLoading(true);
|
||||
setPage(0);
|
||||
setLastUpdated(new Date().getTime().toString());
|
||||
}
|
||||
}, [selectedFolder]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLastUpdated(new Date().getTime().toString());
|
||||
}, [crntSorting]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (debounceGetMedia) {
|
||||
setLoading(true);
|
||||
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page,
|
||||
folder: selectedFolder || '',
|
||||
sorting: crntSorting
|
||||
});
|
||||
}
|
||||
}, [debounceGetMedia]);
|
||||
|
||||
React.useEffect(() => {
|
||||
Messenger.listen(mediaUpdate);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(mediaUpdate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<Searchbox placeholder={`Search in folder`} />
|
||||
|
||||
<PaginationStatus />
|
||||
|
||||
<FolderCreation />
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
20
src/dashboardWebView/components/Media/MenuButton.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import {DotsVerticalIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMenuButtonProps {
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IMenuButtonProps>) => {
|
||||
return (
|
||||
<div className={`inline-flex items-center ${disabled ? 'opacity-50' : ''}`}>
|
||||
<Menu.Button disabled={disabled} className="group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600">
|
||||
<span className="sr-only">{title}</span>
|
||||
<DotsVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector } from '../../state';
|
||||
import { FolderCreation } from './FolderCreation';
|
||||
import { LIMIT } from './Media';
|
||||
import { PaginationButton } from './PaginationButton';
|
||||
|
||||
export interface IPaginationProps {}
|
||||
|
||||
export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.PropsWithChildren<IPaginationProps>) => {
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const [ , setLoading ] = useRecoilState(LoadingAtom);
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
|
||||
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
|
||||
|
||||
const getTotalPage = () => {
|
||||
const mediaItems = ((page + 1) * LIMIT);
|
||||
if (totalMedia < mediaItems) {
|
||||
return totalMedia;
|
||||
}
|
||||
return mediaItems;
|
||||
};
|
||||
|
||||
// Write me function to retrieve buttons before and after current page
|
||||
const getButtons = (): number[] => {
|
||||
const maxButtons = 5;
|
||||
const buttons: number[] = [];
|
||||
const start = page - maxButtons;
|
||||
const end = page + maxButtons;
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i >= 0 && i <= totalPages) {
|
||||
buttons.push(i);
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
setPage(0);
|
||||
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page,
|
||||
folder: selectedFolder || ''
|
||||
});
|
||||
}, [page]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page: 0,
|
||||
folder: selectedFolder || ''
|
||||
});
|
||||
setPage(0);
|
||||
}, [selectedFolder]);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="py-4 px-5 flex items-center justify-between bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<div className="hidden sm:flex">
|
||||
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
|
||||
title="Refresh media"
|
||||
onClick={refresh}>
|
||||
<RefreshIcon className={`h-5 w-5`} />
|
||||
<span className="sr-only">Refresh media</span>
|
||||
</button>
|
||||
|
||||
<p className="text-sm text-gray-500 dark:text-whisper-900">
|
||||
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
|
||||
<span className="font-medium">{totalMedia}</span> results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FolderCreation />
|
||||
|
||||
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
title="First"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(0)
|
||||
}
|
||||
}} />
|
||||
|
||||
<PaginationButton
|
||||
title="Previous"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(page - 1)
|
||||
}
|
||||
}} />
|
||||
|
||||
{getButtons().map((button) => (
|
||||
<button
|
||||
key={button}
|
||||
disabled={button === page}
|
||||
onClick={() => {
|
||||
setPage(button)
|
||||
}
|
||||
}
|
||||
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'}`}
|
||||
>{button + 1}</button>
|
||||
))}
|
||||
|
||||
<PaginationButton
|
||||
title="Next"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(page + 1)} />
|
||||
|
||||
<PaginationButton
|
||||
title="Last"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(totalPages)} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
19
src/dashboardWebView/components/Media/QuickAction.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IQuickActionProps {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({title, onClick, children}: React.PropsWithChildren<IQuickActionProps>) => {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
className={`px-2 group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600`}>
|
||||
{children}
|
||||
<span className='sr-only'>{title}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import {ChevronDownIcon} from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMenuButtonProps {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMenuItemProps {
|
||||
title: string;
|
||||
value: any;
|
||||
title: JSX.Element | string;
|
||||
value?: any;
|
||||
isCurrent?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: (value: any) => void;
|
||||
@@ -15,7 +15,7 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value,
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={() => onClick(value)}
|
||||
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
|
||||
className={`${!isCurrent ? `font-normal` : `font-bold`} text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={`${widthClass || "w-40"} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<Menu.Items className={`${widthClass || ""} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dialog, Transition, } from '@headlessui/react';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import {ExclamationIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { Fragment, useRef } from 'react';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface ISpinnerProps {}
|
||||
|
||||
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
return (
|
||||
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
|
||||
<div className={`fixed top-12 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
|
||||
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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="Sponsor Front Matter">
|
||||
<span>Sponsor</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>
|
||||
);
|
||||
};
|
||||