mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
177 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71072d9520 | ||
|
|
b64dd8f88a | ||
|
|
173c89d86f | ||
|
|
f5f558d5bc | ||
|
|
c9c38ef10b | ||
|
|
c30f401c4f | ||
|
|
9b92050af8 | ||
|
|
31a41e2a66 | ||
|
|
baa56bc246 | ||
|
|
f53e81e0cb | ||
|
|
f454266846 | ||
|
|
0ba3c22795 | ||
|
|
ff38cf361c | ||
|
|
57e93b91c5 | ||
|
|
c1161b95ed | ||
|
|
32dc63b62a | ||
|
|
0c1198c802 | ||
|
|
ed4b78cfdc | ||
|
|
65f77baf2b | ||
|
|
eabdf00d3d | ||
|
|
c084a15e08 | ||
|
|
e577ba591e | ||
|
|
b17c7f888a | ||
|
|
0ed41b7d7e | ||
|
|
2e1faaa34f | ||
|
|
63f02f4f0e | ||
|
|
489fc5ec9e | ||
|
|
4c8ecdb344 | ||
|
|
8d705ff6c5 | ||
|
|
cfe68e65e8 | ||
|
|
0e179f5fd7 | ||
|
|
6cabd6283b | ||
|
|
6135e38fce | ||
|
|
935b2230af | ||
|
|
6dcd89e9cd | ||
|
|
2775b2051f | ||
|
|
5ebb2d7370 | ||
|
|
c9488e6661 | ||
|
|
442261e655 | ||
|
|
1aa2d41c95 | ||
|
|
e5a2194c23 | ||
|
|
cf6f051ee8 | ||
|
|
bebde4de68 | ||
|
|
174c4b7734 | ||
|
|
1f7519ee60 | ||
|
|
b6482546a5 | ||
|
|
0decd84f7f | ||
|
|
a1dbda0b23 | ||
|
|
427245f211 | ||
|
|
4678189eab | ||
|
|
15d89e34cf | ||
|
|
cbb0d8f72b | ||
|
|
131150f5a6 | ||
|
|
a31bca73e7 | ||
|
|
1d5f940c94 | ||
|
|
70ea6a5a16 | ||
|
|
849af69ce2 | ||
|
|
754570a9ec | ||
|
|
f7f6f26997 | ||
|
|
946d84a7a9 | ||
|
|
781ab6ac40 | ||
|
|
df86d02e8b | ||
|
|
19e468c908 | ||
|
|
5a81ea19b8 | ||
|
|
64a38e56b9 | ||
|
|
fca0528a7e | ||
|
|
936916acf8 | ||
|
|
61e9fc0308 | ||
|
|
2356623d7a | ||
|
|
ee70acebb6 | ||
|
|
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 |
1
.frontmatter/content/mediaDb.json
Normal file
1
.frontmatter/content/mediaDb.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [estruyf]
|
||||
open_collective: frontmatter
|
||||
custom: ["https://www.buymeacoffee.com/zMeFRy9"]
|
||||
|
||||
4
.github/workflows/release-beta.yml
vendored
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
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
|
||||
|
||||
@@ -22,3 +22,4 @@ assets/v2.*
|
||||
assets/v3.*
|
||||
assets/v4.*
|
||||
assets/sponsors
|
||||
dist/*.html
|
||||
106
CHANGELOG.md
106
CHANGELOG.md
@@ -1,5 +1,111 @@
|
||||
# Change Log
|
||||
|
||||
## [6.1.1] - 2022-03-02
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#275](https://github.com/estruyf/vscode-front-matter/issues/275): Fix for rendering the panel when content contains an invalid markdown syntax tree
|
||||
|
||||
## [6.1.0] - 2022-02-28 - [Release notes](https://beta.frontmatter.codes/updates/v6.1.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#176](https://github.com/estruyf/vscode-front-matter/issues/176): New `block` field type that allows you to you to define a group of fields which can be used to create a list of data
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Updated the activity bar icon for better visibility
|
||||
- Storing the panel collapse section states
|
||||
- [#241](https://github.com/estruyf/vscode-front-matter/issues/241): Added taxonomy limit field property which allows you to limit the number of selections
|
||||
- [#242](https://github.com/estruyf/vscode-front-matter/issues/242): Keep comments at the root of the front matter
|
||||
- [#248](https://github.com/estruyf/vscode-front-matter/issues/248): Added support for front matter highlighting to all file types specified in `frontMatter.content.supportedFileTypes`
|
||||
- [#255](https://github.com/estruyf/vscode-front-matter/issues/255): Added support for default values on block fields / data creation
|
||||
- [#257](https://github.com/estruyf/vscode-front-matter/issues/257): Allow preview images to be used in multi-dimensional fields
|
||||
- [#271](https://github.com/estruyf/vscode-front-matter/issues/271): Added image size placeholders for media snippets
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- Show the data item its details when clicking on the record
|
||||
- Refactoring of the explorer view panel listeners
|
||||
- Added `{{now}}` placeholder to the publishing date for content creation
|
||||
- [#243](https://github.com/estruyf/vscode-front-matter/issues/243): Refactoring front matter parsing
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#247](https://github.com/estruyf/vscode-front-matter/issues/247): Fix the front matter highlighting in markdown documents
|
||||
- [#261](https://github.com/estruyf/vscode-front-matter/issues/261): Fix to allow that tag and category fields can be renamed
|
||||
- [#264](https://github.com/estruyf/vscode-front-matter/issues/264): Fix for Windows paths on content folder registration
|
||||
- [#268](https://github.com/estruyf/vscode-front-matter/issues/268): Fix for panel which only shows loading indicator
|
||||
|
||||
## [6.0.0] - 2022-01-25 - [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
|
||||
|
||||
@@ -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,9 +157,12 @@ 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>
|
||||
@@ -128,6 +182,9 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
100
README.md
100
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,9 +155,12 @@ 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>
|
||||
@@ -126,6 +180,10 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.inherit {
|
||||
position: inherit !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.z-10 { z-index: 10 !important; }
|
||||
@@ -143,6 +143,7 @@
|
||||
}
|
||||
|
||||
.article__tags {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -165,6 +166,10 @@
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
}
|
||||
|
||||
.article__tags__input input:disabled {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.article__tags__input.freeform {
|
||||
position: relative;
|
||||
}
|
||||
@@ -446,12 +451,33 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__box {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px 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;
|
||||
}
|
||||
@@ -613,7 +639,7 @@ input:checked + .field__toggle__slider:before {
|
||||
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 2px dashed var(--vscode-button-background);
|
||||
border: 1px dashed var(--vscode-button-background);
|
||||
padding: 1.5rem;
|
||||
filter: brightness(85%);
|
||||
}
|
||||
@@ -638,6 +664,14 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
|
||||
BIN
assets/v5.9.0/media-dashboard.png
Normal file
BIN
assets/v5.9.0/media-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
BIN
assets/v6.0.0/content-preview.png
Normal file
BIN
assets/v6.0.0/content-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/v6.0.0/data-dashboard.png
Normal file
BIN
assets/v6.0.0/data-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
23208
package-lock.json
generated
23208
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
767
package.json
767
package.json
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ const tailwindcss = require('tailwindcss');
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-nested'),
|
||||
tailwindcss('./tailwind.config.js'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
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, TelemetryEvent } 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');
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { extname, basename, parse, dirname } from 'path';
|
||||
import { COMMAND_NAME, DefaultFields } from '../constants';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { MediaListener } from '../listeners/panel';
|
||||
|
||||
|
||||
export class Article {
|
||||
|
||||
private static prevContent = "";
|
||||
|
||||
/**
|
||||
* Insert taxonomy
|
||||
*
|
||||
@@ -104,7 +103,7 @@ export class Article {
|
||||
* Update the date in the front matter
|
||||
* @param article
|
||||
*/
|
||||
public static updateDate(article: matter.GrayMatterFile<string>, forceCreate: boolean = false) {
|
||||
public static updateDate(article: ParsedFrontMatter, forceCreate: boolean = false) {
|
||||
article.data = ArticleHelper.updateDates(article.data);
|
||||
return article;
|
||||
}
|
||||
@@ -118,7 +117,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 ParsedFrontMatter
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
): ParsedFrontMatter | undefined {
|
||||
const article = ArticleHelper.getFrontMatterFromDocument(document);
|
||||
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
@@ -127,8 +156,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.`);
|
||||
}
|
||||
@@ -138,6 +166,8 @@ export class Article {
|
||||
* Generate the slug based on the article title
|
||||
*/
|
||||
public static async generateSlug() {
|
||||
Telemetry.send(TelemetryEvent.generateSlug);
|
||||
|
||||
const prefix = Settings.get(SETTING_SLUG_PREFIX) as string;
|
||||
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
|
||||
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
|
||||
@@ -153,11 +183,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 +256,7 @@ export class Article {
|
||||
|
||||
const file = parseWinPath(editor.document.fileName);
|
||||
|
||||
if (!file.endsWith(`.md`) && !file.endsWith(`.markdown`) && !file.endsWith(`.mdx`)) {
|
||||
if (!isValidFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,30 +290,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,13 +337,13 @@ export class Article {
|
||||
} as DashboardData);
|
||||
|
||||
// Let the editor panel know you are selecting an image
|
||||
ExplorerView.getInstance().getMediaSelection();
|
||||
MediaListener.getMediaSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current article
|
||||
*/
|
||||
private static getCurrent(): matter.GrayMatterFile<string> | undefined {
|
||||
private static getCurrent(): ParsedFrontMatter | undefined {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
@@ -324,7 +364,7 @@ export class Article {
|
||||
* @param field
|
||||
* @param forceCreate
|
||||
*/
|
||||
private static articleDate(article: matter.GrayMatterFile<string>, field: string, forceCreate: boolean) {
|
||||
private static articleDate(article: ParsedFrontMatter, field: string, forceCreate: boolean) {
|
||||
if (typeof article.data[field] !== "undefined" || forceCreate) {
|
||||
article.data[field] = Article.formatDate(new Date());
|
||||
}
|
||||
|
||||
75
src/commands/Backers.ts
Normal file
75
src/commands/Backers.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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';
|
||||
import { SettingsListener } from '../listeners/panel';
|
||||
|
||||
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) {
|
||||
SettingsListener.getSettings();
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.reload();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ext.setState(CONTEXT.backer, false, 'global');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,20 @@
|
||||
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, CONTEXT, SETTING_CUSTOM_SCRIPTS, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_MEDIA_SORTING_DEFAULT } 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 { CustomScript as ICustomScript, DraftField, Framework, ScriptType, SortingSetting, SortOrder, SortType, TaxonomyType } from '../models';
|
||||
import { Folders } from './Folders';
|
||||
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 { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { FrameworkDetector } from '../helpers/FrameworkDetector';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { SortingOption } from '../dashboardWebView/models';
|
||||
import { Sorting } from '../helpers/Sorting';
|
||||
import imageSize from 'image-size';
|
||||
import { CustomScript } from '../helpers/CustomScript';
|
||||
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener } from '../listeners/dashboard';
|
||||
import { MediaListener as PanelMediaListener } from '../listeners/panel'
|
||||
|
||||
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;
|
||||
@@ -56,12 +34,12 @@ 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();
|
||||
}
|
||||
@@ -79,15 +57,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
|
||||
@@ -101,7 +97,8 @@ export class Dashboard {
|
||||
'FrontMatter Dashboard',
|
||||
ViewColumn.One,
|
||||
{
|
||||
enableScripts: true
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
);
|
||||
|
||||
@@ -117,8 +114,9 @@ export class Dashboard {
|
||||
Dashboard.webview.onDidChangeViewState(async () => {
|
||||
if (!this.webview?.visible) {
|
||||
Dashboard._viewData = undefined;
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
panel.getMediaSelection();
|
||||
PanelMediaListener.getMediaSelection();
|
||||
|
||||
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: null });
|
||||
}
|
||||
|
||||
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, this.webview?.visible);
|
||||
@@ -127,105 +125,33 @@ export class Dashboard {
|
||||
Dashboard.webview.onDidDispose(async () => {
|
||||
Dashboard.isDisposed = true;
|
||||
Dashboard._viewData = undefined;
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
panel.getMediaSelection();
|
||||
PanelMediaListener.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, msg?.data?.sorting);
|
||||
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.runCustomScript:
|
||||
CustomScript.run(msg?.data?.script, msg?.data?.path);
|
||||
break;
|
||||
case DashboardMessage.setState:
|
||||
if (msg?.data?.key && msg?.data?.value) {
|
||||
Extension.getInstance().setState(msg?.data?.key, msg?.data?.value, "workspace");
|
||||
}
|
||||
break;
|
||||
}
|
||||
Logger.info(`Receiving message from webview: ${msg.command}`);
|
||||
|
||||
DashboardListener.process(msg);
|
||||
ExtensionListener.process(msg);
|
||||
MediaListener.process(msg);
|
||||
PagesListener.process(msg);
|
||||
SettingsListener.process(msg);
|
||||
DataListener.process(msg);
|
||||
TelemetryListener.process(msg);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset media array
|
||||
* Return the webview
|
||||
* @returns The webview
|
||||
*/
|
||||
public static resetMedia() {
|
||||
Dashboard.media = [];
|
||||
}
|
||||
|
||||
public static switchFolder(folderPath: string) {
|
||||
Dashboard.resetMedia();
|
||||
Dashboard.getMedia(0, folderPath);
|
||||
public static getWebview() {
|
||||
return Dashboard.webview?.webview;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,483 +168,22 @@ export class Dashboard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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,
|
||||
scripts: (SettingsHelper.get<ICustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type && s.type !== ScriptType.Content),
|
||||
dashboardState: {
|
||||
contents: {
|
||||
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
|
||||
defaultSorting: SettingsHelper.get<string>(SETTINGS_CONTENT_SORTING_DEFAULT)
|
||||
},
|
||||
media: {
|
||||
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
|
||||
defaultSorting: SettingsHelper.get<string>(SETTINGS_MEDIA_SORTING_DEFAULT)
|
||||
}
|
||||
}
|
||||
} as Settings
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
*/
|
||||
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 = '', sort: SortingOption | null = null) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const contentFolders = Folders.get();
|
||||
const viewData = Dashboard.viewData;
|
||||
let selectedFolder = requestedFolder;
|
||||
|
||||
const ext = Extension.getInstance();
|
||||
const crntSort = sort === null ? await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace") : sort;
|
||||
|
||||
// If the static folder is not set, retreive the last opened location
|
||||
if (!selectedFolder) {
|
||||
const stateValue = await ext.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 = await Dashboard.updateMediaData(Dashboard.filterMedia(files));
|
||||
|
||||
allMedia = [...media];
|
||||
} else {
|
||||
if (staticFolder) {
|
||||
const folderSearch = join(staticFolder || "", '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = await Dashboard.updateMediaData(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 = await Dashboard.updateMediaData(Dashboard.filterMedia(files));
|
||||
|
||||
allMedia = [...allMedia, ...media];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crntSort?.type === SortType.string) {
|
||||
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
|
||||
} else if (crntSort?.type === SortType.date) {
|
||||
allMedia = allMedia.sort(Sorting.dateWithFallback("mtime", "fsPath"));
|
||||
} else {
|
||||
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
|
||||
}
|
||||
|
||||
if (crntSort?.order === SortOrder.desc) {
|
||||
allMedia = allMedia.reverse();
|
||||
}
|
||||
|
||||
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,
|
||||
dimensions: imageSize(file.fsPath),
|
||||
...metadata
|
||||
};
|
||||
} catch (e) {
|
||||
return {...file};
|
||||
}
|
||||
});
|
||||
files = files.filter(f => f.mtime !== 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");
|
||||
|
||||
let sortedFolders = [...allContentFolders, ...allFolders];
|
||||
sortedFolders = sortedFolders.sort((a, b) => {
|
||||
if (a.toLowerCase() < b.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.toLowerCase() > b.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (crntSort?.order === SortOrder.desc) {
|
||||
sortedFolders = sortedFolders.reverse();
|
||||
}
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.media,
|
||||
data: {
|
||||
media: files,
|
||||
total: total,
|
||||
folders: sortedFolders,
|
||||
selectedFolder
|
||||
} as MediaPaths
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metadata of the retrieved files
|
||||
* @param files
|
||||
*/
|
||||
private static async updateMediaData(files: MediaInfo[]) {
|
||||
files = files.map((m: MediaInfo) => {
|
||||
const stats = statSync(m.fsPath);
|
||||
return Object.assign({}, m, stats);
|
||||
});
|
||||
|
||||
return Object.assign([], files);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(`.markdown`) || 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 || "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the webview HTML contents
|
||||
* @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();
|
||||
|
||||
@@ -726,21 +191,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>
|
||||
`;
|
||||
|
||||
@@ -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, TelemetryEvent } from './../constants';
|
||||
import { commands, Uri, workspace, window } from "vscode";
|
||||
import { basename, join } from "path";
|
||||
import { ContentFolder, FileInfo, FolderInfo } from "../models";
|
||||
@@ -11,6 +11,10 @@ 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/dashboard';
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
@@ -62,8 +66,11 @@ export class Folders {
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.switchFolder(folderName);
|
||||
MediaHelpers.resetMedia();
|
||||
MediaListener.sendMediaFiles(0, folderName);
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.addMediaFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +125,8 @@ export class Folders {
|
||||
await Folders.update(folders);
|
||||
|
||||
Notifications.info(`Folder registered`);
|
||||
|
||||
Telemetry.send(TelemetryEvent.registerFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +139,8 @@ export class Folders {
|
||||
let folders = Folders.get();
|
||||
folders = folders.filter(f => f.path !== folder.fsPath);
|
||||
await Folders.update(folders);
|
||||
|
||||
Telemetry.send(TelemetryEvent.unregisterFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +210,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[] = [];
|
||||
@@ -211,10 +223,15 @@ export class Folders {
|
||||
if (projectStart) {
|
||||
projectStart = projectStart.replace(/\\/g, '/');
|
||||
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
|
||||
const markdownFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.markdown'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
|
||||
let files = [...mdFiles, ...markdownFiles, ...mdxFiles];
|
||||
|
||||
let files: Uri[] = [];
|
||||
|
||||
for (const fileType of (supportedFiles || DEFAULT_FILE_TYPES)) {
|
||||
const filePath = join(projectStart, folder.excludeSubdir ? '/' : '**', `*${fileType.startsWith('.') ? '' : '.'}${fileType}`);
|
||||
const foundFiles = await workspace.findFiles(filePath, '**/node_modules/**');
|
||||
files = [...files, ...foundFiles];
|
||||
}
|
||||
|
||||
if (files) {
|
||||
let fileStats: FileInfo[] = [];
|
||||
|
||||
@@ -285,6 +302,22 @@ export class Folders {
|
||||
}));
|
||||
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
|
||||
|
||||
// Reinitialize the folder listeners
|
||||
PagesListener.startWatchers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +328,7 @@ export class Folders {
|
||||
*/
|
||||
private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
|
||||
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
|
||||
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
@@ -308,8 +341,8 @@ export class Folders {
|
||||
*/
|
||||
private static relWsFolder(folder: ContentFolder, wsFolder?: Uri) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = folder.path.replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
|
||||
let absPath = parseWinPath(folder.path).replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
|
||||
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT } from './../constants';
|
||||
import { Telemetry } from './../helpers/Telemetry';
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent } from './../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { join } from "path";
|
||||
import { commands, env, Uri, ViewColumn, window } from "vscode";
|
||||
@@ -133,6 +134,8 @@ export class Preview {
|
||||
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
Telemetry.send(TelemetryEvent.openPreview);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Telemetry } from './../helpers/Telemetry';
|
||||
import { workspace, Uri } from "vscode";
|
||||
import { join } from "path";
|
||||
import * as fs from "fs";
|
||||
@@ -5,12 +6,13 @@ import { Notifications } from "../helpers/Notifications";
|
||||
import { Template } from "./Template";
|
||||
import { Folders } from "./Folders";
|
||||
import { Settings } from "../helpers";
|
||||
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } 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 +29,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 +38,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);
|
||||
@@ -45,6 +48,8 @@ categories: []
|
||||
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
Notifications.info("Project initialized successfully.");
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.initialization)
|
||||
} catch (err: any) {
|
||||
Notifications.error(`Sorry, something went wrong - ${err?.message || err}`);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as matter from 'gray-matter';
|
||||
import * as fs from 'fs';
|
||||
import { TaxonomyType } from "../models";
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, EXTENSION_NAME } from '../constants';
|
||||
import { ArticleHelper, Settings as SettingsHelper, FilesHelper } from '../helpers';
|
||||
import { TomlEngine, getFmLanguage, getFormatOpts } from '../helpers/TomlEngine';
|
||||
import { FrontMatterParser } from '../parsers';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
|
||||
@@ -90,10 +89,6 @@ export class Settings {
|
||||
const progressNr = allMdFiles.length/100;
|
||||
progress.report({ increment: 0});
|
||||
|
||||
// Get language options
|
||||
const language = getFmLanguage();
|
||||
const langOpts = getFormatOpts(language);
|
||||
|
||||
let i = 0;
|
||||
for (const file of allMdFiles) {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
@@ -102,10 +97,7 @@ export class Settings {
|
||||
const txtData = mdFile.getText();
|
||||
if (txtData) {
|
||||
try {
|
||||
const article = matter(txtData, {
|
||||
...TomlEngine,
|
||||
...langOpts
|
||||
});
|
||||
const article = FrontMatterParser.fromFile(txtData);
|
||||
if (article && article.data) {
|
||||
const { data } = article;
|
||||
const mdTags = data["tags"];
|
||||
@@ -218,13 +210,8 @@ export class Settings {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
const mdFile = fs.readFileSync(file.path, { encoding: "utf8" });
|
||||
if (mdFile) {
|
||||
const language = getFmLanguage();
|
||||
const langOpts = getFormatOpts(language);
|
||||
try {
|
||||
const article = matter(mdFile, {
|
||||
...TomlEngine,
|
||||
...langOpts
|
||||
});
|
||||
const article = FrontMatterParser.fromFile(mdFile);
|
||||
if (article && article.data) {
|
||||
const { data } = article;
|
||||
let taxonomies: string[] = data[matterProp];
|
||||
@@ -239,9 +226,7 @@ export class Settings {
|
||||
data[matterProp] = [...new Set(taxonomies)].sort();
|
||||
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
fs.writeFileSync(file.path, matter.stringify(article.content, article.data, {
|
||||
...TomlEngine,
|
||||
...langOpts,
|
||||
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ArticleHelper, SeoHelper, Settings } from '../helpers';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { DataListener } from '../listeners/panel';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -58,7 +59,7 @@ export class StatusListener {
|
||||
|
||||
const panel = ExplorerView.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
panel.pushMetadata(article!.data);
|
||||
DataListener.pushMetadata(article!.data);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -68,7 +69,7 @@ export class StatusListener {
|
||||
} else {
|
||||
const panel = ExplorerView.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
panel.pushMetadata(null);
|
||||
DataListener.pushMetadata(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, TelemetryEvent } from '../constants';
|
||||
import { ArticleHelper, Settings } from '../helpers';
|
||||
import { Article } from '.';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
@@ -11,6 +11,9 @@ import { Project } from './Project';
|
||||
import { Folders } from './Folders';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
import { PagesListener } from '../listeners/dashboard';
|
||||
import { extname } from 'path';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
|
||||
export class Template {
|
||||
|
||||
@@ -49,6 +52,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 +86,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 +143,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 +160,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 +175,11 @@ export class Template {
|
||||
}
|
||||
|
||||
Notifications.info(`Your new content has been created.`);
|
||||
|
||||
Telemetry.send(TelemetryEvent.createContentFromTemplate);
|
||||
|
||||
// Trigger a refresh for the dashboard
|
||||
PagesListener.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
41
src/components/uniforms-frontmatter/AutoField.tsx
Normal file
41
src/components/uniforms-frontmatter/AutoField.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as invariant from 'invariant';
|
||||
import { createAutoField } from 'uniforms';
|
||||
import { PreviewImageField } from '../../panelWebView/components/Fields/PreviewImageField';
|
||||
export { AutoFieldProps } from 'uniforms';
|
||||
|
||||
import BoolField from './BoolField';
|
||||
import DateField from './DateField';
|
||||
import ListField from './ListField';
|
||||
import NestField from './NestField';
|
||||
import NumField from './NumField';
|
||||
import RadioField from './RadioField';
|
||||
import SelectField from './SelectField';
|
||||
import TextField from './TextField';
|
||||
|
||||
const AutoField = createAutoField(props => {
|
||||
|
||||
if (props.allowedValues) {
|
||||
return props.checkboxes && props.fieldType !== Array
|
||||
? RadioField
|
||||
: SelectField;
|
||||
}
|
||||
|
||||
switch (props.fieldType) {
|
||||
case Array:
|
||||
return ListField;
|
||||
case Boolean:
|
||||
return BoolField;
|
||||
case Date:
|
||||
return DateField;
|
||||
case Number:
|
||||
return NumField;
|
||||
case Object:
|
||||
return NestField;
|
||||
case String:
|
||||
return TextField;
|
||||
}
|
||||
|
||||
return invariant(false, 'Unsupported field type: %s', props.fieldType);
|
||||
});
|
||||
|
||||
export default AutoField;
|
||||
29
src/components/uniforms-frontmatter/AutoFields.tsx
Normal file
29
src/components/uniforms-frontmatter/AutoFields.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ComponentType, createElement, Fragment } from 'react';
|
||||
import { useForm } from 'uniforms';
|
||||
|
||||
import AutoField from './AutoField';
|
||||
|
||||
export type AutoFieldsProps = {
|
||||
autoField?: ComponentType<{ name: string }>;
|
||||
element?: ComponentType | string;
|
||||
fields?: string[];
|
||||
omitFields?: string[];
|
||||
};
|
||||
|
||||
export default function AutoFields({
|
||||
autoField = AutoField,
|
||||
element = Fragment,
|
||||
fields,
|
||||
omitFields = [],
|
||||
...props
|
||||
}: AutoFieldsProps) {
|
||||
const { schema } = useForm();
|
||||
|
||||
return createElement(
|
||||
element,
|
||||
props,
|
||||
(fields ?? schema.getSubfields())
|
||||
.filter(field => !omitFields.includes(field))
|
||||
.map(field => createElement(autoField, { key: field, name: field })),
|
||||
);
|
||||
}
|
||||
13
src/components/uniforms-frontmatter/AutoForm.tsx
Normal file
13
src/components/uniforms-frontmatter/AutoForm.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AutoForm } from 'uniforms';
|
||||
|
||||
import ValidatedQuickForm from './ValidatedQuickForm';
|
||||
|
||||
function Auto(parent: any) {
|
||||
class _ extends AutoForm.Auto(parent) {
|
||||
static Auto = Auto;
|
||||
}
|
||||
|
||||
return _ as unknown as AutoForm;
|
||||
}
|
||||
|
||||
export default Auto(ValidatedQuickForm);
|
||||
13
src/components/uniforms-frontmatter/BaseForm.tsx
Normal file
13
src/components/uniforms-frontmatter/BaseForm.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BaseForm } from 'uniforms';
|
||||
|
||||
function Unstyled(parent: any) {
|
||||
class _ extends parent {
|
||||
static Unstyled = Unstyled;
|
||||
|
||||
static displayName = `Unstyled${parent.displayName}`;
|
||||
}
|
||||
|
||||
return _ as unknown as typeof BaseForm;
|
||||
}
|
||||
|
||||
export default Unstyled(BaseForm);
|
||||
52
src/components/uniforms-frontmatter/BoolField.css
Normal file
52
src/components/uniforms-frontmatter/BoolField.css
Normal file
@@ -0,0 +1,52 @@
|
||||
.field__toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.field__toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.field__toggle__slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--frontmatter-toggle-secondaryBackground, var(--vscode-button-secondaryBackground));
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.field__toggle__slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .field__toggle__slider {
|
||||
background-color: var(--frontmatter-toggle-background, var(--vscode-button-background));
|
||||
}
|
||||
|
||||
input:focus + .field__toggle__slider {
|
||||
box-shadow: 0 0 1px var(--frontmatter-toggle-background, var(--vscode-button-background));
|
||||
}
|
||||
|
||||
input:checked + .field__toggle__slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
44
src/components/uniforms-frontmatter/BoolField.tsx
Normal file
44
src/components/uniforms-frontmatter/BoolField.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
import { Ref } from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
import './BoolField.css';
|
||||
import { LabelField } from './LabelField';
|
||||
|
||||
export type BoolFieldProps = HTMLFieldProps<
|
||||
boolean,
|
||||
HTMLDivElement,
|
||||
{ inputRef?: Ref<HTMLInputElement> }
|
||||
>;
|
||||
|
||||
function Bool({
|
||||
disabled,
|
||||
id,
|
||||
inputRef,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
readOnly,
|
||||
value,
|
||||
...props
|
||||
}: BoolFieldProps) {
|
||||
return (
|
||||
<div {...filterDOMProps(props)}>
|
||||
<LabelField label={label} id={id} required={props.required} />
|
||||
|
||||
<label className="field__toggle">
|
||||
<input
|
||||
checked={value || false}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
name={name}
|
||||
onChange={() => !disabled && !readOnly && onChange(!value)}
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span className="field__toggle__slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<BoolFieldProps>(Bool, { kind: 'leaf' });
|
||||
57
src/components/uniforms-frontmatter/DateField.tsx
Normal file
57
src/components/uniforms-frontmatter/DateField.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { Ref } from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
|
||||
/* istanbul ignore next */
|
||||
const DateConstructor = (typeof global === 'object' ? global : window).Date;
|
||||
const dateFormat = (value?: Date) => value?.toISOString().slice(0, -8);
|
||||
|
||||
export type DateFieldProps = HTMLFieldProps<
|
||||
Date,
|
||||
HTMLDivElement,
|
||||
{ inputRef?: Ref<HTMLInputElement>; max?: Date; min?: Date }
|
||||
>;
|
||||
|
||||
function Date({
|
||||
disabled,
|
||||
id,
|
||||
inputRef,
|
||||
label,
|
||||
max,
|
||||
min,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly,
|
||||
value,
|
||||
...props
|
||||
}: DateFieldProps) {
|
||||
return (
|
||||
<div {...filterDOMProps(props)}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
|
||||
<input
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
max={dateFormat(max)}
|
||||
min={dateFormat(min)}
|
||||
name={name}
|
||||
onChange={event => {
|
||||
const date = new DateConstructor(event.target.valueAsNumber);
|
||||
if (date.getFullYear() < 10000) {
|
||||
onChange(date);
|
||||
} else if (isNaN(event.target.valueAsNumber)) {
|
||||
onChange(undefined);
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
ref={inputRef}
|
||||
type="datetime-local"
|
||||
value={dateFormat(value) ?? ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<DateFieldProps>(Date, { kind: 'leaf' });
|
||||
19
src/components/uniforms-frontmatter/ErrorField.tsx
Normal file
19
src/components/uniforms-frontmatter/ErrorField.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { HTMLProps } from 'react';
|
||||
import { Override, connectField, filterDOMProps } from 'uniforms';
|
||||
|
||||
export type ErrorFieldProps = Override<
|
||||
Omit<HTMLProps<HTMLDivElement>, 'onChange'>,
|
||||
{ error?: any; errorMessage?: string }
|
||||
>;
|
||||
|
||||
function Error({ children, error, errorMessage, ...props }: ErrorFieldProps) {
|
||||
return !error ? null : (
|
||||
<div {...filterDOMProps(props)}>{children || errorMessage}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<ErrorFieldProps>(Error, {
|
||||
initialValue: false,
|
||||
kind: 'leaf',
|
||||
});
|
||||
18
src/components/uniforms-frontmatter/ErrorsField.css
Normal file
18
src/components/uniforms-frontmatter/ErrorsField.css
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
|
||||
.autoform-error {
|
||||
background-color: var(--frontmatter-error-background, var(--vscode-inputValidation-errorBackground));
|
||||
border: 1px solid var(--frontmatter-error-border, var(--vscode-inputValidation-errorBorder));
|
||||
border-radius: 2px;
|
||||
margin: 20px 0px;
|
||||
padding: 10px;
|
||||
color: var(--frontmatter-error-foreground, var(--vscode-editor-foreground));
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
23
src/components/uniforms-frontmatter/ErrorsField.tsx
Normal file
23
src/components/uniforms-frontmatter/ErrorsField.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { HTMLProps } from 'react';
|
||||
import { filterDOMProps, useForm } from 'uniforms';
|
||||
import './ErrorsField.css';
|
||||
|
||||
export type ErrorsFieldProps = HTMLProps<HTMLDivElement>;
|
||||
|
||||
export default function ErrorsField(props: ErrorsFieldProps) {
|
||||
const { error, schema } = useForm();
|
||||
return !error && !props.children ? null : (
|
||||
<div className='autoform-error'>
|
||||
<div {...filterDOMProps(props)}>
|
||||
{props.children}
|
||||
|
||||
<ul>
|
||||
{schema.getErrorMessages(error).map((message, index) => (
|
||||
<li key={index}>{message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/components/uniforms-frontmatter/HiddenField.tsx
Normal file
35
src/components/uniforms-frontmatter/HiddenField.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { HTMLProps, Ref, useEffect } from 'react';
|
||||
import { Override, filterDOMProps, useField } from 'uniforms';
|
||||
|
||||
export type HiddenFieldProps = Override<
|
||||
HTMLProps<HTMLInputElement>,
|
||||
{
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
name: string;
|
||||
noDOM?: boolean;
|
||||
value?: any;
|
||||
}
|
||||
>;
|
||||
|
||||
export default function HiddenField({ value, ...rawProps }: HiddenFieldProps) {
|
||||
const props = useField(rawProps.name, rawProps, { initialValue: false })[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined && value !== props.value) {
|
||||
props.onChange(value);
|
||||
}
|
||||
});
|
||||
|
||||
return props.noDOM ? null : (
|
||||
<input
|
||||
disabled={props.disabled}
|
||||
name={props.name}
|
||||
readOnly={props.readOnly}
|
||||
ref={props.inputRef}
|
||||
type="hidden"
|
||||
value={value ?? props.value ?? ''}
|
||||
{...filterDOMProps(props)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
src/components/uniforms-frontmatter/LabelField.css
Normal file
14
src/components/uniforms-frontmatter/LabelField.css
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
|
||||
.autoform__label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
line-height: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
.autoform__label__required {
|
||||
color: var(--vscode-inputValidation-errorBorder);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
20
src/components/uniforms-frontmatter/LabelField.tsx
Normal file
20
src/components/uniforms-frontmatter/LabelField.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import './LabelField.css';
|
||||
|
||||
export interface ILabelFieldProps {
|
||||
id: string;
|
||||
label: string | ReactNode;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const LabelField: React.FunctionComponent<ILabelFieldProps> = ({ label, id, required }: React.PropsWithChildren<ILabelFieldProps>) => {
|
||||
return (
|
||||
label ? (
|
||||
<label className="autoform__label" htmlFor={id}>
|
||||
{label}
|
||||
{required && <span title='Required field' className='autoform__label__required'>*</span>}
|
||||
</label>
|
||||
) : null
|
||||
);
|
||||
};
|
||||
21
src/components/uniforms-frontmatter/ListAddField.css
Normal file
21
src/components/uniforms-frontmatter/ListAddField.css
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
|
||||
.autoform__list_add_field {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
border: 1px dashed var(--frontmatter-field-border, var(--vscode-editor-foreground));
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: .5rem;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--frontmatter-field-borderActive, var(--vscode-button-background));
|
||||
color: var(--frontmatter-field-borderActive, var(--vscode-button-background));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
63
src/components/uniforms-frontmatter/ListAddField.tsx
Normal file
63
src/components/uniforms-frontmatter/ListAddField.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { PlusIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
HTMLFieldProps,
|
||||
connectField,
|
||||
filterDOMProps,
|
||||
joinName,
|
||||
useField,
|
||||
} from 'uniforms';
|
||||
import './ListAddField.css';
|
||||
|
||||
export type ListAddFieldProps = HTMLFieldProps<
|
||||
unknown,
|
||||
HTMLSpanElement,
|
||||
{ initialCount?: number }
|
||||
>;
|
||||
|
||||
function ListAdd({
|
||||
disabled,
|
||||
initialCount,
|
||||
name,
|
||||
readOnly,
|
||||
value,
|
||||
...props
|
||||
}: ListAddFieldProps) {
|
||||
const nameParts = joinName(null, name);
|
||||
const parentName = joinName(nameParts.slice(0, -1));
|
||||
const parent = useField<
|
||||
{ initialCount?: number; maxCount?: number },
|
||||
unknown[]
|
||||
>(parentName, { initialCount }, { absoluteName: true })[0];
|
||||
|
||||
const limitNotReached =
|
||||
!disabled && !(parent.maxCount! <= parent.value!.length);
|
||||
|
||||
function onAction(event: React.KeyboardEvent | React.MouseEvent) {
|
||||
if (
|
||||
limitNotReached &&
|
||||
!readOnly &&
|
||||
(!('key' in event) || event.key === 'Enter')
|
||||
) {
|
||||
parent.onChange(parent.value!.concat([Object.assign({}, value)]));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className='autoform__list_add_field'
|
||||
{...filterDOMProps(props as any)}
|
||||
onClick={onAction}
|
||||
onKeyDown={onAction}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<PlusIcon />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<ListAddFieldProps>(ListAdd, {
|
||||
initialValue: false,
|
||||
kind: 'leaf',
|
||||
});
|
||||
27
src/components/uniforms-frontmatter/ListDelField.css
Normal file
27
src/components/uniforms-frontmatter/ListDelField.css
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
|
||||
.autoform__list_del_field {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: .5rem;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 1px;
|
||||
background: var(--frontmatter-list-border, var(--vscode-editor-foreground));
|
||||
width: 100%;
|
||||
margin-right: .5rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
}
|
||||
63
src/components/uniforms-frontmatter/ListDelField.tsx
Normal file
63
src/components/uniforms-frontmatter/ListDelField.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
HTMLFieldProps,
|
||||
connectField,
|
||||
filterDOMProps,
|
||||
joinName,
|
||||
useField,
|
||||
} from 'uniforms';
|
||||
import './ListDelField.css';
|
||||
|
||||
export type ListDelFieldProps = HTMLFieldProps<unknown, HTMLSpanElement>;
|
||||
|
||||
function ListDel({ disabled, name, readOnly, ...props }: ListDelFieldProps) {
|
||||
const nameParts = joinName(null, name);
|
||||
const nameIndex = +nameParts[nameParts.length - 1];
|
||||
const parentName = joinName(nameParts.slice(0, -1));
|
||||
const parent = useField<{ minCount?: number }, unknown[]>(
|
||||
parentName,
|
||||
{},
|
||||
{ absoluteName: true },
|
||||
)[0];
|
||||
|
||||
const limitNotReached =
|
||||
!disabled && !(parent.minCount! >= parent.value!.length);
|
||||
|
||||
function onAction(
|
||||
event:
|
||||
| React.KeyboardEvent<HTMLSpanElement>
|
||||
| React.MouseEvent<HTMLSpanElement, MouseEvent>,
|
||||
) {
|
||||
if (
|
||||
limitNotReached &&
|
||||
!readOnly &&
|
||||
(!('key' in event) || event.key === 'Enter')
|
||||
) {
|
||||
const value = parent.value!.slice();
|
||||
value.splice(nameIndex, 1);
|
||||
parent.onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className='autoform__list_del_field'
|
||||
{...filterDOMProps(props)}
|
||||
onClick={onAction}
|
||||
onKeyDown={onAction}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className='line'></div>
|
||||
<TrashIcon />
|
||||
</span>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<ListDelFieldProps>(ListDel, {
|
||||
initialValue: false,
|
||||
kind: 'leaf',
|
||||
});
|
||||
8
src/components/uniforms-frontmatter/ListField.css
Normal file
8
src/components/uniforms-frontmatter/ListField.css
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
.autoform__list_field {
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1rem;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--frontmatter-list-border, rgba(255, 255, 255, 0.2));
|
||||
}
|
||||
46
src/components/uniforms-frontmatter/ListField.tsx
Normal file
46
src/components/uniforms-frontmatter/ListField.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { Children, cloneElement, isValidElement } from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
|
||||
import ListAddField from './ListAddField';
|
||||
import ListItemField from './ListItemField';
|
||||
|
||||
import './ListField.css';
|
||||
import { LabelField } from './LabelField';
|
||||
|
||||
export type ListFieldProps = HTMLFieldProps<
|
||||
unknown[],
|
||||
HTMLDivElement,
|
||||
{ initialCount?: number; itemProps?: object }
|
||||
>;
|
||||
|
||||
function List({
|
||||
children = <ListItemField name="$" />,
|
||||
initialCount,
|
||||
itemProps,
|
||||
label,
|
||||
value,
|
||||
...props
|
||||
}: ListFieldProps) {
|
||||
return (
|
||||
<div className="autoform__list_field" {...filterDOMProps(props)}>
|
||||
<LabelField label={label} id={props.id} required={props.required} />
|
||||
|
||||
{value?.map((item, itemIndex) =>
|
||||
Children.map(children, (child, childIndex) =>
|
||||
isValidElement(child)
|
||||
? cloneElement(child, {
|
||||
key: `${itemIndex}-${childIndex}`,
|
||||
name: (child.props.name || "").replace('$', '' + itemIndex),
|
||||
...itemProps,
|
||||
})
|
||||
: child,
|
||||
),
|
||||
)}
|
||||
|
||||
<ListAddField initialCount={initialCount} name="$" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<ListFieldProps>(List);
|
||||
23
src/components/uniforms-frontmatter/ListItemField.tsx
Normal file
23
src/components/uniforms-frontmatter/ListItemField.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { connectField } from 'uniforms';
|
||||
|
||||
import AutoField from './AutoField';
|
||||
import ListDelField from './ListDelField';
|
||||
|
||||
export type ListItemFieldProps = { children?: ReactNode; value?: unknown };
|
||||
|
||||
function ListItem({
|
||||
children = <AutoField label={null} name="" />,
|
||||
}: ListItemFieldProps) {
|
||||
return (
|
||||
<div>
|
||||
<ListDelField name="" />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<ListItemFieldProps>(ListItem, {
|
||||
initialValue: false,
|
||||
});
|
||||
41
src/components/uniforms-frontmatter/LongTextField.tsx
Normal file
41
src/components/uniforms-frontmatter/LongTextField.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { Ref } from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
|
||||
export type LongTextFieldProps = HTMLFieldProps<
|
||||
string,
|
||||
HTMLDivElement,
|
||||
{ inputRef?: Ref<HTMLTextAreaElement> }
|
||||
>;
|
||||
|
||||
function LongText({
|
||||
disabled,
|
||||
id,
|
||||
inputRef,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly,
|
||||
value,
|
||||
...props
|
||||
}: LongTextFieldProps) {
|
||||
return (
|
||||
<div {...filterDOMProps(props)}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
|
||||
<textarea
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
name={name}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
ref={inputRef}
|
||||
value={value ?? ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<LongTextFieldProps>(LongText, { kind: 'leaf' });
|
||||
32
src/components/uniforms-frontmatter/NestField.tsx
Normal file
32
src/components/uniforms-frontmatter/NestField.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
|
||||
import AutoField from './AutoField';
|
||||
import { LabelField } from './LabelField';
|
||||
|
||||
export type NestFieldProps = HTMLFieldProps<
|
||||
object,
|
||||
HTMLDivElement,
|
||||
{ itemProps?: object }
|
||||
>;
|
||||
|
||||
function Nest({
|
||||
children,
|
||||
fields,
|
||||
itemProps,
|
||||
label,
|
||||
...props
|
||||
}: NestFieldProps) {
|
||||
return (
|
||||
<div {...filterDOMProps(props)}>
|
||||
<LabelField label={label} id={props.id} required={props.required} />
|
||||
|
||||
{children ||
|
||||
fields.map(field => (
|
||||
<AutoField key={field} name={field} {...itemProps} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<NestFieldProps>(Nest);
|
||||
54
src/components/uniforms-frontmatter/NumField.tsx
Normal file
54
src/components/uniforms-frontmatter/NumField.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import { Ref } from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
import { LabelField } from './LabelField';
|
||||
|
||||
export type NumFieldProps = HTMLFieldProps<
|
||||
number,
|
||||
HTMLDivElement,
|
||||
{ decimal?: boolean; inputRef?: Ref<HTMLInputElement> }
|
||||
>;
|
||||
|
||||
function Num({
|
||||
decimal,
|
||||
disabled,
|
||||
id,
|
||||
inputRef,
|
||||
label,
|
||||
max,
|
||||
min,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly,
|
||||
step,
|
||||
value,
|
||||
...props
|
||||
}: NumFieldProps) {
|
||||
return (
|
||||
<div {...filterDOMProps(props)}>
|
||||
<LabelField label={label} id={id} required={props.required} />
|
||||
|
||||
<input
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
max={max}
|
||||
min={min}
|
||||
name={name}
|
||||
onChange={event => {
|
||||
const parse = decimal ? parseFloat : parseInt;
|
||||
const value = parse(event.target.value);
|
||||
onChange(isNaN(value) ? undefined : value);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
ref={inputRef}
|
||||
step={step || (decimal ? 0.01 : 1)}
|
||||
type="number"
|
||||
value={value ?? ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<NumFieldProps>(Num, { kind: 'leaf' });
|
||||
28
src/components/uniforms-frontmatter/QuickForm.tsx
Normal file
28
src/components/uniforms-frontmatter/QuickForm.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { QuickForm } from 'uniforms';
|
||||
|
||||
import AutoField from './AutoField';
|
||||
import BaseForm from './BaseForm';
|
||||
import ErrorsField from './ErrorsField';
|
||||
import SubmitField from './SubmitField';
|
||||
|
||||
function Quick(parent: any) {
|
||||
class _ extends QuickForm.Quick(parent) {
|
||||
static Quick = Quick;
|
||||
|
||||
getAutoField() {
|
||||
return AutoField;
|
||||
}
|
||||
|
||||
getErrorsField() {
|
||||
return ErrorsField;
|
||||
}
|
||||
|
||||
getSubmitField() {
|
||||
return SubmitField;
|
||||
}
|
||||
}
|
||||
|
||||
return _ as unknown as QuickForm;
|
||||
}
|
||||
|
||||
export default Quick(BaseForm);
|
||||
62
src/components/uniforms-frontmatter/RadioField.tsx
Normal file
62
src/components/uniforms-frontmatter/RadioField.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import omit = require('lodash.omit');
|
||||
import * as React from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
import { LabelField } from './LabelField';
|
||||
|
||||
const base64: typeof btoa =
|
||||
typeof btoa === 'undefined'
|
||||
? /* istanbul ignore next */ x => Buffer.from(x).toString('base64')
|
||||
: btoa;
|
||||
const escape = (x: string) => base64(encodeURIComponent(x)).replace(/=+$/, '');
|
||||
|
||||
export type RadioFieldProps = HTMLFieldProps<
|
||||
string,
|
||||
HTMLDivElement,
|
||||
{
|
||||
allowedValues?: string[];
|
||||
checkboxes?: boolean;
|
||||
transform?: (value: string) => string;
|
||||
}
|
||||
>;
|
||||
|
||||
function Radio({
|
||||
allowedValues,
|
||||
disabled,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
readOnly,
|
||||
transform,
|
||||
value,
|
||||
...props
|
||||
}: RadioFieldProps) {
|
||||
return (
|
||||
<div {...omit(filterDOMProps(props), ['checkboxes'])}>
|
||||
<LabelField label={label} id={id} required={props.required} />
|
||||
|
||||
{allowedValues?.map(item => (
|
||||
<div key={item}>
|
||||
<input
|
||||
checked={item === value}
|
||||
disabled={disabled}
|
||||
id={`${id}-${escape(item)}`}
|
||||
name={name}
|
||||
onChange={() => {
|
||||
if (!readOnly) {
|
||||
onChange(item);
|
||||
}
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
|
||||
<label htmlFor={`${id}-${escape(item)}`}>
|
||||
{transform ? transform(item) : item}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<RadioFieldProps>(Radio, { kind: 'leaf' });
|
||||
5
src/components/uniforms-frontmatter/SelectField.css
Normal file
5
src/components/uniforms-frontmatter/SelectField.css
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
.autoform__select_field {
|
||||
color: var(--frontmatter-select-foreground, var(--vscode-editor-foreground));
|
||||
}
|
||||
110
src/components/uniforms-frontmatter/SelectField.tsx
Normal file
110
src/components/uniforms-frontmatter/SelectField.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import xor = require('lodash.xor');
|
||||
import * as React from 'react';
|
||||
import { Ref } from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
import { LabelField } from './LabelField';
|
||||
import './SelectField.css';
|
||||
|
||||
const base64: typeof btoa =
|
||||
typeof btoa === 'undefined'
|
||||
? /* istanbul ignore next */ x => Buffer.from(x).toString('base64')
|
||||
: btoa;
|
||||
const escape = (x: string) => base64(encodeURIComponent(x)).replace(/=+$/, '');
|
||||
|
||||
export type SelectFieldProps = HTMLFieldProps<
|
||||
string | string[],
|
||||
HTMLDivElement,
|
||||
{
|
||||
allowedValues?: string[];
|
||||
checkboxes?: boolean;
|
||||
disableItem?: (value: string) => boolean;
|
||||
inputRef?: Ref<HTMLSelectElement>;
|
||||
transform?: (value: string) => string;
|
||||
}
|
||||
>;
|
||||
|
||||
function Select({
|
||||
allowedValues,
|
||||
checkboxes,
|
||||
disabled,
|
||||
fieldType,
|
||||
id,
|
||||
inputRef,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
disableItem,
|
||||
transform,
|
||||
value,
|
||||
...props
|
||||
}: SelectFieldProps) {
|
||||
const multiple = fieldType === Array;
|
||||
return (
|
||||
<div className='autoform__select_field' {...filterDOMProps(props)}>
|
||||
<LabelField label={label} id={id} required={required} />
|
||||
|
||||
{checkboxes ? (
|
||||
allowedValues!.map(item => (
|
||||
<div key={item}>
|
||||
<input
|
||||
checked={
|
||||
fieldType === Array ? value!.includes(item) : value === item
|
||||
}
|
||||
disabled={disableItem?.(item) ?? disabled}
|
||||
id={`${id}-${escape(item)}`}
|
||||
name={name}
|
||||
onChange={() => {
|
||||
if (!readOnly) {
|
||||
onChange(fieldType === Array ? xor([item], value) : item);
|
||||
}
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
<label htmlFor={`${id}-${escape(item)}`}>
|
||||
{transform ? transform(item) : item}
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<select
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
multiple={multiple}
|
||||
name={name}
|
||||
onChange={event => {
|
||||
if (!readOnly) {
|
||||
const item = event.target.value;
|
||||
if (multiple) {
|
||||
const clear = event.target.selectedIndex === -1;
|
||||
onChange(clear ? [] : xor([item], value));
|
||||
} else {
|
||||
onChange(item !== '' ? item : undefined);
|
||||
}
|
||||
}
|
||||
}}
|
||||
ref={inputRef}
|
||||
value={value ?? ''}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
>
|
||||
{(!!placeholder || !required || value === undefined) && !multiple && (
|
||||
<option value="" disabled={required} hidden={required}>
|
||||
{placeholder || label}
|
||||
</option>
|
||||
)}
|
||||
|
||||
{allowedValues?.map(value => (
|
||||
<option disabled={disableItem?.(value)} key={value} value={value}>
|
||||
{transform ? transform(value) : value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectField<SelectFieldProps>(Select, { kind: 'leaf' });
|
||||
29
src/components/uniforms-frontmatter/SubmitField.tsx
Normal file
29
src/components/uniforms-frontmatter/SubmitField.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { HTMLProps, Ref } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Override, filterDOMProps, useForm } from 'uniforms';
|
||||
|
||||
export type SubmitFieldProps = Override<
|
||||
HTMLProps<HTMLInputElement>,
|
||||
{ inputRef?: Ref<HTMLInputElement>; value?: string }
|
||||
>;
|
||||
|
||||
export default function SubmitField({
|
||||
disabled,
|
||||
inputRef,
|
||||
readOnly,
|
||||
value,
|
||||
...props
|
||||
}: SubmitFieldProps) {
|
||||
const { error, state } = useForm();
|
||||
|
||||
return (
|
||||
<input
|
||||
disabled={disabled === undefined ? !!(error || state.disabled) : disabled}
|
||||
readOnly={readOnly}
|
||||
ref={inputRef}
|
||||
type="submit"
|
||||
{...(value ? { value } : {})}
|
||||
{...filterDOMProps(props)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
49
src/components/uniforms-frontmatter/TextField.tsx
Normal file
49
src/components/uniforms-frontmatter/TextField.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Ref } from 'react';
|
||||
import * as React from 'react';
|
||||
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
|
||||
import { LabelField } from './LabelField';
|
||||
|
||||
export type TextFieldProps = HTMLFieldProps<
|
||||
string,
|
||||
HTMLDivElement,
|
||||
{ inputRef?: Ref<HTMLInputElement> }
|
||||
>;
|
||||
|
||||
function Text({
|
||||
autoComplete,
|
||||
disabled,
|
||||
id,
|
||||
inputRef,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly,
|
||||
type,
|
||||
value,
|
||||
...props
|
||||
}: TextFieldProps) {
|
||||
|
||||
return (
|
||||
<div {...filterDOMProps(props)}>
|
||||
<LabelField label={label} id={id} required={props.required} />
|
||||
|
||||
<input
|
||||
autoComplete={autoComplete}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
name={name}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
ref={inputRef}
|
||||
type={type}
|
||||
value={value ?? ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Text.defaultProps = { type: 'text' };
|
||||
|
||||
export default connectField<TextFieldProps>(Text, { kind: 'leaf' });
|
||||
13
src/components/uniforms-frontmatter/ValidatedForm.tsx
Normal file
13
src/components/uniforms-frontmatter/ValidatedForm.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ValidatedForm } from 'uniforms';
|
||||
|
||||
import BaseForm from './BaseForm';
|
||||
|
||||
function Validated(parent: any) {
|
||||
class _ extends ValidatedForm.Validated(parent) {
|
||||
static Validated = Validated;
|
||||
}
|
||||
|
||||
return _ as unknown as ValidatedForm;
|
||||
}
|
||||
|
||||
export default Validated(BaseForm);
|
||||
@@ -0,0 +1,5 @@
|
||||
import BaseForm from './BaseForm';
|
||||
import QuickForm from './QuickForm';
|
||||
import ValidatedForm from './ValidatedForm';
|
||||
|
||||
export default ValidatedForm.Validated(QuickForm.Quick(BaseForm));
|
||||
23
src/components/uniforms-frontmatter/index.ts
Normal file
23
src/components/uniforms-frontmatter/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export { default as AutoField, AutoFieldProps } from './AutoField';
|
||||
export { default as AutoFields, AutoFieldsProps } from './AutoFields';
|
||||
export { default as AutoForm } from './AutoForm';
|
||||
export { default as BaseForm } from './BaseForm';
|
||||
export { default as BoolField, BoolFieldProps } from './BoolField';
|
||||
export { default as DateField, DateFieldProps } from './DateField';
|
||||
export { default as ErrorField, ErrorFieldProps } from './ErrorField';
|
||||
export { default as ErrorsField, ErrorsFieldProps } from './ErrorsField';
|
||||
export { default as HiddenField, HiddenFieldProps } from './HiddenField';
|
||||
export { default as ListAddField, ListAddFieldProps } from './ListAddField';
|
||||
export { default as ListDelField, ListDelFieldProps } from './ListDelField';
|
||||
export { default as ListField, ListFieldProps } from './ListField';
|
||||
export { default as ListItemField, ListItemFieldProps } from './ListItemField';
|
||||
export { default as LongTextField, LongTextFieldProps } from './LongTextField';
|
||||
export { default as NestField, NestFieldProps } from './NestField';
|
||||
export { default as NumField, NumFieldProps } from './NumField';
|
||||
export { default as QuickForm } from './QuickForm';
|
||||
export { default as RadioField, RadioFieldProps } from './RadioField';
|
||||
export { default as SelectField, SelectFieldProps } from './SelectField';
|
||||
export { default as SubmitField, SubmitFieldProps } from './SubmitField';
|
||||
export { default as TextField, TextFieldProps } from './TextField';
|
||||
export { default as ValidatedForm } from './ValidatedForm';
|
||||
export { default as ValidatedQuickForm } from './ValidatedQuickForm';
|
||||
3
src/constants/DefaultFileTypes.ts
Normal file
3
src/constants/DefaultFileTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
export const DEFAULT_FILE_TYPES = [".md", ".markdown", ".mdx"];
|
||||
@@ -28,6 +28,8 @@ 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"),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
];
|
||||
28
src/constants/TelemetryEvent.ts
Normal file
28
src/constants/TelemetryEvent.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const TelemetryEvent = {
|
||||
activate: 'activate',
|
||||
initialization: 'initialization',
|
||||
openContentDashboard: 'openContentDashboard',
|
||||
openMediaDashboard: 'openMediaDashboard',
|
||||
openDataDashboard: 'openDataDashboard',
|
||||
closeDashboard: 'closeDashboard',
|
||||
generateSlug: 'generateSlug',
|
||||
createContentFromTemplate: 'createContentFromTemplate',
|
||||
createContentFromContentType: 'createContentFromContentType',
|
||||
registerFolder: 'registerFolder',
|
||||
unregisterFolder: 'unregisterFolder',
|
||||
addMediaFolder: 'addMediaFolder',
|
||||
promoteSettings: 'promoteSettings',
|
||||
openPreview: 'openPreview',
|
||||
uploadMedia: 'uploadMedia',
|
||||
refreshMedia: 'refreshMedia',
|
||||
deleteMedia: 'deleteMedia',
|
||||
insertMediaToContent: 'insertMediaToContent',
|
||||
updateMediaMetadata: 'updateMediaMetadata',
|
||||
openExplorerView: 'openExplorerView',
|
||||
|
||||
// Webviews
|
||||
webviewWelcomeScreen: 'webviewWelcomeScreen',
|
||||
webviewMediaView: 'webviewMediaView',
|
||||
webviewDataView: 'webviewDataView',
|
||||
webviewContentsView: 'webviewContentsView',
|
||||
};
|
||||
@@ -5,4 +5,5 @@ export const CONTEXT = {
|
||||
isEnabled: "frontMatter:enabled",
|
||||
isDashboardOpen: "frontMatter:dashboard:open",
|
||||
wysiwyg: "frontMatter:markdown:wysiwyg",
|
||||
backer: "frontMatter:backers:supporter",
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
export * from './ContentType';
|
||||
export * from './DefaultFields';
|
||||
export * from './DefaultFileTypes';
|
||||
export * from './Extension';
|
||||
export * from './ExtensionState';
|
||||
export * from './FrameworkDetectors';
|
||||
export * from './Links';
|
||||
export * from './LocalStore';
|
||||
export * from './Navigation';
|
||||
export * from './TelemetryEvent';
|
||||
export * from './charMap';
|
||||
export * from './context';
|
||||
export * from './settings';
|
||||
|
||||
@@ -2,9 +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_TAXONOMY_FIELD_GROUPS = "taxonomy.fieldGroups";
|
||||
|
||||
export const SETTING_DATE_FORMAT = "taxonomy.dateFormat";
|
||||
export const SETTING_COMMA_SEPARATED_FIELDS = "taxonomy.commaSeparatedFields";
|
||||
@@ -30,6 +33,8 @@ export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
|
||||
export const SETTING_TEMPLATES_FOLDER = "templates.folder";
|
||||
export const SETTING_TEMPLATES_PREFIX = "templates.prefix";
|
||||
|
||||
export const SETTING_TELEMETRY_DISABLE = "telemetry.disable";
|
||||
|
||||
export const SETTING_PANEL_FREEFORM = "panel.freeform";
|
||||
|
||||
export const SETTING_PREVIEW_HOST = "preview.host";
|
||||
@@ -44,14 +49,25 @@ 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_FILE_PRESERVE_CASING = "file.preserveCasing";
|
||||
|
||||
export const SETTINGS_FRAMEWORK_ID = "framework.id";
|
||||
export const SETTINGS_FRAMEWORK_START = "framework.startCommand";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@ export enum DashboardCommand {
|
||||
settings = "settings",
|
||||
media = "media",
|
||||
viewData = "viewData",
|
||||
mediaUpdate = "mediaUpdate"
|
||||
mediaUpdate = "mediaUpdate",
|
||||
dataFileEntries = "dataFileEntries"
|
||||
}
|
||||
@@ -15,10 +15,14 @@ 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',
|
||||
sendTelemetry = 'sendTelemetry',
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
|
||||
@@ -7,6 +7,10 @@ import { Overview } from './Overview';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
import usePages from '../../hooks/usePages';
|
||||
import { useEffect } from 'react';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
|
||||
export interface IContentsProps {
|
||||
pages: Page[];
|
||||
@@ -19,20 +23,24 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loadin
|
||||
|
||||
const pageFolders = [...new Set(pageItems.map(page => page.fmFolder))];
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.send(DashboardMessage.sendTelemetry, {
|
||||
event: TelemetryEvent.webviewContentsView
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
<Header
|
||||
folders={pageFolders}
|
||||
totalPages={pageItems.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={pageItems} 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,34 +3,32 @@ 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 { 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 {}
|
||||
|
||||
const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const contentType = useContentType(settings, { type });
|
||||
|
||||
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
|
||||
|
||||
const openFile = () => {
|
||||
Messenger.send(DashboardMessage.openFile, fmFilePath);
|
||||
};
|
||||
|
||||
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">
|
||||
{
|
||||
previewField && pageData[previewField] ? (
|
||||
<img src={`${pageData[previewField]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
pageData[PREVIEW_IMAGE_FIELD] ? (
|
||||
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
) : (
|
||||
<div className={`flex items-center justify-center bg-whisper-500 dark:bg-vulcan-200 dark:group-hover:bg-vulcan-100`}>
|
||||
<MarkdownIcon className={`h-32 text-vulcan-100 dark:text-whisper-100`} />
|
||||
@@ -53,7 +51,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';
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useRecoilValue } from 'recoil';
|
||||
import { DashboardViewSelector } from '../state';
|
||||
import { Contents } from './Contents/Contents';
|
||||
import { Media } from './Media/Media';
|
||||
import { ViewType } from '../models';
|
||||
import { NavigationType } from '../models';
|
||||
import { DataView } from './DataView';
|
||||
|
||||
export interface IDashboardProps {
|
||||
showWelcome: boolean;
|
||||
@@ -30,9 +31,25 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (view === ViewType.Media) {
|
||||
return <Media />;
|
||||
if (view === NavigationType.Media) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Media />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return <Contents pages={pages} 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>
|
||||
);
|
||||
};
|
||||
70
src/dashboardWebView/components/DataView/DataForm.tsx
Normal file
70
src/dashboardWebView/components/DataView/DataForm.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
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 '../../../components/uniforms-frontmatter';
|
||||
// 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: any) => 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>
|
||||
);
|
||||
};
|
||||
235
src/dashboardWebView/components/DataView/DataView.tsx
Normal file
235
src/dashboardWebView/components/DataView/DataView.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
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';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
|
||||
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);
|
||||
|
||||
Messenger.send(DashboardMessage.sendTelemetry, {
|
||||
event: TelemetryEvent.webviewDataView
|
||||
});
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
|
||||
const dataFiles = (settings?.dataFiles || []).map((dataFile: DataFile) => {
|
||||
if (!dataFile.schema && !dataFile.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clonedFile = Object.assign({}, dataFile);
|
||||
|
||||
if (clonedFile.type) {
|
||||
const dataType = settings?.dataTypes?.find((dataType: DataType) => dataType.id === clonedFile.type);
|
||||
if (!dataType) {
|
||||
return null;
|
||||
}
|
||||
clonedFile.schema = Object.assign({}, dataType.schema);
|
||||
}
|
||||
|
||||
return clonedFile;
|
||||
}).filter(d => d !== null) as DataFile[];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-auto inset-y-0">
|
||||
<Header settings={settings} />
|
||||
|
||||
<div className="relative w-full flex-grow mx-auto overflow-hidden">
|
||||
|
||||
<div className={`flex w-64 flex-col absolute inset-y-0`}>
|
||||
|
||||
<aside className={`flex flex-col flex-grow overflow-y-auto border-r border-gray-200 dark:border-vulcan-300 py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Select your data type</h2>
|
||||
|
||||
<nav className={`flex-1 py-4 -mx-4 `}>
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
(dataFiles && dataFiles.length > 0) && (
|
||||
dataFiles.map((dataFile) => (
|
||||
<button
|
||||
key={dataFile.id}
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
onClick={() => setSchema(dataFile)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>{dataFile.title}</span>
|
||||
</button>
|
||||
)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<section className={`pl-64 flex min-w-0 h-full`}>
|
||||
{
|
||||
selectedData ? (
|
||||
<>
|
||||
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData.title.toLowerCase()} data items</h2>
|
||||
|
||||
<div className='py-4'>
|
||||
{
|
||||
(dataEntries && dataEntries.length > 0) ? (
|
||||
<>
|
||||
<Container onSortEnd={onSortEnd} useDragHandle>
|
||||
{
|
||||
(dataEntries || []).map((dataEntry, idx) => (
|
||||
<SortableItem
|
||||
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
|
||||
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
|
||||
index={idx}
|
||||
crntIndex={idx}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
|
||||
onDeleteItem={deleteItem}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Container>
|
||||
<Button
|
||||
className='mt-4'
|
||||
onClick={() => setSelectedIndex(null)}>
|
||||
Add a new entry
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={`flex flex-col items-center justify-center`}>
|
||||
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-2/3 py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Create or modify your {selectedData.title.toLowerCase()} data</h2>
|
||||
{
|
||||
selectedData ? (
|
||||
<DataForm
|
||||
schema={selectedData?.schema}
|
||||
model={(dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null}
|
||||
onSubmit={onSubmit}
|
||||
onClear={() => setSelectedIndex(null)} />
|
||||
) : (
|
||||
<p>Select a data type to get started</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyView />
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
13
src/dashboardWebView/components/DataView/EmptyView.tsx
Normal file
13
src/dashboardWebView/components/DataView/EmptyView.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IEmptyViewProps {}
|
||||
|
||||
export const EmptyView: React.FunctionComponent<IEmptyViewProps> = (props: React.PropsWithChildren<IEmptyViewProps>) => {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center w-full'>
|
||||
<ExclamationCircleIcon className={`w-1/12 text-gray-500 dark:text-whisper-900 opacity-90`} />
|
||||
<h2 className={`text-xl text-gray-500 dark:text-whisper-900`}>Select your date type first</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { SortableContainer } from 'react-sortable-hoc';
|
||||
|
||||
export interface ISortableContainerProps {}
|
||||
|
||||
export const Container = SortableContainer(({ children }: React.PropsWithChildren<ISortableContainerProps>) => ( <ul className={`-mx-4 divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>{children}</ul>));
|
||||
70
src/dashboardWebView/components/DataView/SortableItem.tsx
Normal file
70
src/dashboardWebView/components/DataView/SortableItem.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
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={`sortable_item 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 w-full'
|
||||
onClick={() => onSelectedIndexChange(crntIndex)}>
|
||||
<DragHandle />
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
|
||||
<div className={`space-x-2 flex items-center`}>
|
||||
<button
|
||||
type='button'
|
||||
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
|
||||
title={`Edit "${value}"`}
|
||||
onClick={() => onSelectedIndexChange(crntIndex)}>
|
||||
<PencilIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`text-gray-500 dark:text-whisper-900 hover:text-gray-600 dark:hover:text-whisper-500`}
|
||||
title={`Delete "${value}"`}
|
||||
onClick={() => deleteItemConfirm()}>
|
||||
<TrashIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
showAlert && (
|
||||
<Alert
|
||||
title={`Delete data entry`}
|
||||
description={`Are you sure you want to delete the data entry?`}
|
||||
okBtnText={`Delete`}
|
||||
cancelBtnText={`Cancel`}
|
||||
dismiss={() => setShowAlert(false)}
|
||||
trigger={() => {
|
||||
setShowAlert(false);
|
||||
onDeleteItem(crntIndex);
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
});
|
||||
1
src/dashboardWebView/components/DataView/index.ts
Normal file
1
src/dashboardWebView/components/DataView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DataView';
|
||||
@@ -2,27 +2,31 @@ import { CollectionIcon } from '@heroicons/react/outline';
|
||||
import { basename, join } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Sorting } from '.';
|
||||
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { ViewType } from '../../models';
|
||||
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;
|
||||
@@ -33,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;
|
||||
@@ -62,45 +66,40 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
|
||||
|
||||
setFolders(allFolders);
|
||||
}
|
||||
}, [selectedFolder]);
|
||||
|
||||
}, [selectedFolder, settings]);
|
||||
|
||||
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">
|
||||
<ol role="list" className="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>
|
||||
|
||||
<div className={`flex px-5`}>
|
||||
<Sorting view={ViewType.Media} disableCustomSorting />
|
||||
</div>
|
||||
</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,7 +3,7 @@ import { Sorting } from './Sorting';
|
||||
import { Searchbox } from './Searchbox';
|
||||
import { Filter } from './Filter';
|
||||
import { Folders } from './Folders';
|
||||
import { Settings, ViewType } from '../../models';
|
||||
import { Settings, NavigationType } from '../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Startup } from '../Startup';
|
||||
import { Navigation } from '../Navigation';
|
||||
@@ -13,11 +13,12 @@ import { useRecoilState, useResetRecoilState } from 'recoil';
|
||||
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ClearFilters } from './ClearFilters';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { PhotographIcon } from '@heroicons/react/outline';
|
||||
import { 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;
|
||||
@@ -47,27 +48,34 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
Messenger.send(DashboardMessage.createByTemplate);
|
||||
};
|
||||
|
||||
const updateView = (view: ViewType) => {
|
||||
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={() => updateView(ViewType.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={() => updateView(ViewType.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 === ViewType.Contents && (
|
||||
view === NavigationType.Contents && (
|
||||
<>
|
||||
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
|
||||
<Searchbox />
|
||||
@@ -77,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>
|
||||
@@ -101,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 />
|
||||
@@ -112,17 +133,18 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
|
||||
<Grouping />
|
||||
|
||||
<Sorting view={ViewType.Contents} />
|
||||
<Sorting view={NavigationType.Contents} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
view === ViewType.Media && (
|
||||
view === NavigationType.Media && (
|
||||
<>
|
||||
<Pagination />
|
||||
<Breadcrumb />
|
||||
<MediaHeaderTop />
|
||||
|
||||
<MediaHeaderBottom />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
72
src/dashboardWebView/components/Header/Pagination.tsx
Normal file
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
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,14 +6,15 @@ import { ExtensionState } from '../../../constants';
|
||||
import { SortOrder, SortType } from '../../../models';
|
||||
import { SortOption } from '../../constants/SortOption';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { ViewType } from '../../models';
|
||||
import { NavigationType } from '../../models';
|
||||
import { SortingOption } from '../../models/SortingOption';
|
||||
import { SearchSelector, SettingsSelector, SortingAtom } from '../../state';
|
||||
import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
import { Sorting as SortingHelpers } from '../../../helpers/Sorting';
|
||||
|
||||
export interface ISortingProps {
|
||||
disableCustomSorting?: boolean;
|
||||
view: ViewType;
|
||||
view: NavigationType;
|
||||
}
|
||||
|
||||
export const sortOptions: SortingOption[] = [
|
||||
@@ -23,6 +24,15 @@ export const sortOptions: SortingOption[] = [
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
const mediaSortOptions: SortingOption[] = [
|
||||
{ name: "Size (asc)", id: SortOption.SizeAsc, order: SortOrder.asc, type: SortType.number },
|
||||
{ name: "Size (desc)", id: SortOption.SizeDesc, order: SortOrder.desc, type: SortType.number },
|
||||
{ name: "Caption (asc)", id: SortOption.CaptionAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "Caption (desc)", id: SortOption.CaptionDesc, order: SortOrder.desc, type: SortType.string },
|
||||
{ name: "Alt (asc)", id: SortOption.AltAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "Alt (desc)", id: SortOption.AltDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSorting, view}: React.PropsWithChildren<ISortingProps>) => {
|
||||
const [ crntSorting, setCrntSorting ] = useRecoilState(SortingAtom);
|
||||
const searchValue = useRecoilValue(SearchSelector);
|
||||
@@ -30,7 +40,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
|
||||
const updateSorting = (value: SortingOption) => {
|
||||
Messenger.send(DashboardMessage.setState, {
|
||||
key: `${view === ViewType.Contents ? ExtensionState.Dashboard.Contents.Sorting : ExtensionState.Dashboard.Media.Sorting}`,
|
||||
key: `${view === NavigationType.Contents ? ExtensionState.Dashboard.Contents.Sorting : ExtensionState.Dashboard.Media.Sorting}`,
|
||||
value: value
|
||||
});
|
||||
|
||||
@@ -38,6 +48,13 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
};
|
||||
|
||||
let allOptions = [...sortOptions];
|
||||
|
||||
if (view === NavigationType.Media) {
|
||||
allOptions = [...allOptions, ...mediaSortOptions];
|
||||
}
|
||||
|
||||
allOptions = allOptions.sort(SortingHelpers.alphabetically("name"))
|
||||
|
||||
if (settings?.customSorting && !disableCustomSorting) {
|
||||
allOptions = [...allOptions, ...settings.customSorting.map((s) => ({
|
||||
title: s.title || s.name,
|
||||
@@ -50,16 +67,16 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
|
||||
let crntSortingOption = crntSorting;
|
||||
if (!crntSortingOption) {
|
||||
if (view === ViewType.Contents) {
|
||||
if (view === NavigationType.Contents) {
|
||||
crntSortingOption = settings?.dashboardState?.contents?.sorting || null;
|
||||
} else if (view === ViewType.Media) {
|
||||
} else if (view === NavigationType.Media) {
|
||||
crntSortingOption = settings?.dashboardState?.media?.sorting || null;
|
||||
}
|
||||
|
||||
if (crntSortingOption === null) {
|
||||
if (view === ViewType.Contents && settings?.dashboardState.contents.defaultSorting) {
|
||||
if (view === NavigationType.Contents && settings?.dashboardState.contents.defaultSorting) {
|
||||
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
|
||||
} else if (view === ViewType.Media && settings?.dashboardState.contents.defaultSorting) {
|
||||
} else if (view === NavigationType.Media && settings?.dashboardState.contents.defaultSorting) {
|
||||
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
|
||||
}
|
||||
}
|
||||
@@ -72,13 +89,13 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
|
||||
|
||||
<MenuItems>
|
||||
<MenuItems widthClass="w-48">
|
||||
{allOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
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
25
src/dashboardWebView/components/Header/Tab.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { NavigationType } from '../../models';
|
||||
import { DashboardViewAtom } from '../../state';
|
||||
|
||||
export interface ITabProps {
|
||||
navigationType: NavigationType;
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
|
||||
const view = useRecoilValue(DashboardViewAtom);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === navigationType ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="profile"
|
||||
aria-selected="false"
|
||||
onClick={() => onNavigate(navigationType)}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
45
src/dashboardWebView/components/Header/Tabs.tsx
Normal file
45
src/dashboardWebView/components/Header/Tabs.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DatabaseIcon, PhotographIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { NavigationType } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { Tab } from './Tab';
|
||||
|
||||
export interface ITabsProps {
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.PropsWithChildren<ITabsProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
return (
|
||||
<ul className="flex items-center justify-start h-full" data-tabs-toggle="#myTabContent" role="tablist">
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Contents}
|
||||
onNavigate={onNavigate}>
|
||||
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
|
||||
</Tab>
|
||||
</li>
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Media}
|
||||
onNavigate={onNavigate}>
|
||||
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
|
||||
</Tab>
|
||||
</li>
|
||||
{
|
||||
(settings?.dataFiles && settings.dataFiles.length > 0) && (
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Data}
|
||||
onNavigate={onNavigate}>
|
||||
<DatabaseIcon className={`h-6 w-auto mr-2`} /><span>Data</span>
|
||||
</Tab>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
@@ -1,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,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { FolderAddIcon } from '@heroicons/react/outline';
|
||||
import {FolderAddIcon, LightningBoltIcon} from '@heroicons/react/outline';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SelectedMediaFolderAtom, SettingsSelector } from '../../state';
|
||||
@@ -26,24 +26,29 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
|
||||
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
|
||||
if (scripts.length > 0) {
|
||||
return (
|
||||
<ChoiceButton
|
||||
title={`Create new folder`}
|
||||
choices={scripts.map(s => ({
|
||||
title: s.title,
|
||||
onClick: () => runCustomScript(s)
|
||||
}))}
|
||||
onClick={onFolderCreation}
|
||||
disabled={!settings?.initialized} />
|
||||
<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,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { PhotographIcon } from '@heroicons/react/outline';
|
||||
import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { basename, dirname } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -76,9 +77,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
image: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
parents: viewData?.data?.parents,
|
||||
multiple: viewData?.data?.multiple,
|
||||
value: viewData?.data?.value,
|
||||
position: viewData?.data?.position || null,
|
||||
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
|
||||
alt: alt || "",
|
||||
caption: caption || ""
|
||||
});
|
||||
@@ -92,6 +95,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
snippet = snippet?.replace("{alt}", alt || "");
|
||||
snippet = snippet?.replace("{caption}", caption || "");
|
||||
snippet = snippet?.replace("{filename}", basename(relPath || ""));
|
||||
snippet = snippet?.replace("{mediaWidth}", media?.dimensions?.width?.toString() || "");
|
||||
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
|
||||
|
||||
Messenger.send(DashboardMessage.insertPreviewImage, {
|
||||
image: parseWinPath(relPath) || "",
|
||||
@@ -106,6 +111,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,
|
||||
@@ -194,7 +206,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`} />
|
||||
@@ -206,11 +218,56 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
<div className={`relative py-4 pl-4 pr-12`}>
|
||||
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<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>
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={`Edit metadata`}
|
||||
onClick={updateMetadata}
|
||||
@@ -240,13 +297,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
onClick={copyToClipboard} />
|
||||
|
||||
{ customScriptActions() }
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={deleteMedia} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={`Reveal media`}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -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,11 +1,8 @@
|
||||
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';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { LoadingAtom, MediaFoldersAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { Header } from '../Header';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
@@ -13,23 +10,22 @@ import { Item } from './Item';
|
||||
import { Lightbox } from './Lightbox';
|
||||
import { List } from './List';
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { FolderItem } from './FolderItem';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
|
||||
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) => {
|
||||
@@ -48,100 +44,85 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
});
|
||||
}, [selectedFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.send(DashboardMessage.sendTelemetry, {
|
||||
event: TelemetryEvent.webviewMediaView
|
||||
});
|
||||
}, []);
|
||||
|
||||
const {getRootProps, isDragActive} = useDropzone({
|
||||
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
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
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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { DotsVerticalIcon } from '@heroicons/react/outline';
|
||||
import {DotsVerticalIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMenuButtonProps {
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { EventData } from '@estruyf/vscode';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector, SortingSelector } 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 crntSorting = useRecoilValue(SortingSelector);
|
||||
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 });
|
||||
}
|
||||
|
||||
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(() => {
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page,
|
||||
folder: selectedFolder || '',
|
||||
sorting: crntSorting
|
||||
});
|
||||
}, [page]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page: 0,
|
||||
folder: selectedFolder || '',
|
||||
sorting: crntSorting
|
||||
});
|
||||
setPage(0);
|
||||
}, [selectedFolder]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getMedia, {
|
||||
page,
|
||||
folder: selectedFolder || '',
|
||||
sorting: crntSorting
|
||||
});
|
||||
}, [crntSorting]);
|
||||
|
||||
React.useEffect(() => {
|
||||
Messenger.listen(mediaUpdate);
|
||||
}, []);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user