Compare commits

...

110 Commits

Author SHA1 Message Date
Elio Struyf
eb9984396b Merge pull request #423 from estruyf/dev 2022-09-23 12:47:54 +02:00
Elio Struyf
b7b79024e1 8.1.1 2022-09-23 12:44:18 +02:00
Elio Struyf
d17cc901ff #422 - Fix panel initialization logic 2022-09-23 09:20:59 +02:00
Elio Struyf
1fe03197e3 Merge pull request #421 from estruyf/dev 2022-09-22 13:43:18 +02:00
Elio Struyf
a1eaa5baca Updated changelog for v8.1.0 2022-09-22 11:49:15 +02:00
Elio Struyf
b83b2205c0 Fix issue in image field with required message 2022-09-21 09:55:57 +02:00
Elio Struyf
989d20c474 #417 - New hyperlink wysiwyg option 2022-09-19 10:40:15 +02:00
Elio Struyf
2cf3ff93c5 #418 - Heading and divider fields 2022-09-19 10:18:41 +02:00
Elio Struyf
67b44dce42 #369 - taxonomy fix 2022-09-19 09:52:02 +02:00
Elio Struyf
c182a67daa Remove logging 2022-09-19 09:45:02 +02:00
Elio Struyf
2494e4c6c5 #369 - Notification fix 2022-09-19 09:43:57 +02:00
Elio Struyf
efc230f81e #391 - New description property 2022-09-16 16:47:32 +02:00
Elio Struyf
e455fa764b #369 - required fields 2022-09-15 11:13:26 +02:00
Elio Struyf
c6273fa9c1 #408 - add missing view mode 2022-09-12 10:38:53 +02:00
Elio Struyf
9f37ff773e #395: Media snippet enhancements 2022-09-09 18:01:55 +02:00
Elio Struyf
9b21e15c63 #402: Custom sorting of content now supports number fields 2022-09-09 16:44:37 +02:00
Elio Struyf
fe41d9a751 Remove bold version 2022-09-09 13:32:55 +02:00
Elio Struyf
5e91a0e7af Update version 2022-09-09 13:26:39 +02:00
Elio Struyf
e00186890c Update beta script 2022-09-09 13:22:36 +02:00
Elio Struyf
b2b017efc0 #404: Ordering of snippet fields is based on their field definition 2022-09-08 21:08:46 +02:00
Elio Struyf
51b11b66ab #403: Fix for media files with spaces on importing in article content 2022-09-08 20:59:50 +02:00
Elio Struyf
2275c1b9cc #401 - Enable paging on the content dashboard 2022-09-08 11:38:45 +02:00
Elio Struyf
bf98ff9a1d Disable popper 2022-09-08 11:01:29 +02:00
Elio Struyf
23c5a7bc18 #400: Fix for draft/published grouping 2022-09-08 10:44:07 +02:00
Elio
4d05c660c8 #398: Fix Windows folder path parsing in data folder retrieval 2022-09-08 10:00:38 +02:00
Elio Struyf
83d4427c09 New titles for inputs 2022-09-07 14:50:21 +02:00
Elio Struyf
45285d3cf2 8.1.0 2022-09-07 12:29:02 +02:00
Elio Struyf
f46fdb9fb0 Preview tab title 2022-09-07 10:03:25 +02:00
Elio Struyf
3557360297 #396 - Fix for index and _index previews 2022-09-07 09:59:27 +02:00
Elio Struyf
600c225265 Added divider between action buttons and custom ones 2022-09-07 08:48:14 +02:00
Elio Struyf
7fa814ca1b Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-09-05 15:00:33 +02:00
Elio
e4f44def47 Update changelog 2022-09-05 14:58:06 +02:00
Elio
08aa73f9c3 Merge branch 'dev' of https://github.com/estruyf/vscode-front-matter into dev 2022-09-05 14:57:27 +02:00
Elio
fcb564b054 #393 - Fix Windows file path 2022-09-05 14:55:34 +02:00
Elio Struyf
ac4aea68eb #352: Schema updates 2022-09-05 14:25:59 +02:00
Elio Struyf
ad6c37f62d Audit fix for dependencies 2022-09-05 10:55:38 +02:00
Elio Struyf
bc3d5cb6b2 Added contact links for issues 2022-09-05 10:53:57 +02:00
Elio Struyf
88c8cc82c8 #394: Ordering of snippet fields is based on their field definition 2022-09-05 10:37:30 +02:00
Elio Struyf
69e0dc3343 Hide the initialize project action 2022-09-02 13:44:11 +02:00
Elio Struyf
dda9b88752 #388: Added a stop server action 2022-09-02 13:42:50 +02:00
Elio Struyf
5b712e64d7 #390: Implement another JSON parser 2022-09-02 13:26:25 +02:00
Elio Struyf
af1cc15d3d Fix for new popper menu on media items 2022-09-02 09:50:09 +02:00
Elio Struyf
76e3c08405 Fix for custom placeholders that return errors 2022-09-02 09:49:48 +02:00
Elio Struyf
ebae16f724 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-08-11 20:58:32 +02:00
Elio Struyf
911adaa5d6 #383: Add the item menu to the content list view 2022-08-11 20:56:07 +02:00
Elio Struyf
1766c19133 #385: Add a default draft field value 2022-08-11 17:52:36 +02:00
Elio Struyf
4a8bbaf82e Fix for empty tags 2022-08-11 17:43:07 +02:00
Elio Struyf
fa7b9f3ad1 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-08-10 16:09:36 +02:00
Elio Struyf
ecc9c74091 #384: Fix issue title field in sub-fields 2022-08-10 16:09:21 +02:00
Elio Struyf
282c95be29 Initialize listeners 2022-08-04 21:33:00 +02:00
Elio Struyf
9325ce3638 Keep track of all config listeners 2022-08-04 21:32:30 +02:00
Elio Struyf
a4da46ca21 #379 - New frontMatter.config.reload command 2022-08-04 21:29:21 +02:00
Elio Struyf
44f30f70d5 Update authenticate command 2022-08-04 20:58:45 +02:00
Elio Struyf
2ef39cb2ed Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-08-04 15:23:50 +02:00
Elio Struyf
c8ecc92309 #376: post script functionality 2022-08-04 15:23:41 +02:00
Elio Struyf
3ca6609ace #377 - Git sync command 2022-07-30 14:23:57 +02:00
Elio Struyf
670791fcf6 #377 - update setting name 2022-07-29 16:16:38 +02:00
Elio Struyf
30dc33a859 #377: Git sync actions added on panel and content dashboard 2022-07-29 16:13:46 +02:00
Elio Struyf
07ed95793c #378 - Last modified update only to content in content folders 2022-07-28 11:36:26 +02:00
Elio Struyf
9a9ec33f9f Sorting fix 2022-07-27 12:08:39 +02:00
Elio Struyf
89aab6c74e #352 - Added notification + progress notification 2022-07-27 11:56:51 +02:00
Elio Struyf
c0e6c79c67 qna link 2022-07-22 16:03:45 +03:00
Elio Struyf
7badfda41b #352 - Custom placeholders now support scripting 2022-07-21 18:38:53 +03:00
Elio Struyf
9445ce6d37 #374 - Auto-fold the front matter section 2022-07-21 13:15:49 +03:00
Elio Struyf
1aa8f77590 #370 - updated JSON schema 2022-07-20 15:00:17 +03:00
Elio Struyf
dab6a46d98 #372 - Update taxonomy to taxonomies 2022-07-18 17:30:19 +03:00
Elio Struyf
6b940e2f24 Merge branch 'main' into dev 2022-07-18 17:27:06 +03:00
Elio Struyf
8c0ce05133 Hide settings icon 2022-07-17 08:13:51 +02:00
Elio Struyf
937494f81c Hide open settings + new docs and settings link 2022-07-17 08:13:42 +02:00
Elio Struyf
7392d7ea0d Merge pull request #371 from estruyf/dev 2022-07-13 13:57:22 +02:00
Elio Struyf
9fcc231a7a 8.0.1 2022-07-13 13:52:14 +02:00
Elio Struyf
149703a5df Fix for tags rendering on content cards 2022-07-13 13:52:06 +02:00
Elio Struyf
463455121e Improve media cards 2022-07-13 10:04:52 +02:00
Elio Struyf
43ae9a6ba2 Merge pull request #367 from estruyf/dev 2022-07-11 16:09:40 +02:00
Elio Struyf
c24cc2165f Changelog update 2022-07-11 16:03:03 +02:00
Elio Struyf
f177a61d4f Hide template button if disabled 2022-07-11 15:09:47 +02:00
Elio Struyf
f13b9e8ea5 #364 - Check content if ends with newline 2022-07-11 15:09:18 +02:00
Elio Struyf
c04dd79778 Test updates 2022-06-30 17:39:10 +02:00
Elio Struyf
00273a8c86 #366 - Better support for block in block fields 2022-06-29 09:57:32 +02:00
Elio Struyf
231ef804dc Added sponsor info 2022-06-28 17:13:11 +02:00
Elio Struyf
44dc22c792 #365 - FIx for the spinner 2022-06-28 15:17:32 +02:00
Elio Struyf
830fc550bd Ignore keywords field 2022-06-28 14:48:10 +02:00
Elio Struyf
6c7567a15c Fix when there is no title 2022-06-28 09:17:03 +02:00
Elio Struyf
be9797cc77 Updated gitignore 2022-06-28 09:05:28 +02:00
Elio Struyf
a78d9c5906 #364 - Honour file ending rules in data files 2022-06-28 09:04:38 +02:00
Elio Struyf
8f4fe45d9e Initial e2e test setup 2022-06-28 08:53:15 +02:00
Elio Struyf
79157feed5 #353 - Add the default content type on initialization 2022-06-16 11:53:08 +02:00
Elio Struyf
09888d5657 #291 - Hierarchy field support 2022-06-16 11:41:54 +02:00
Elio Struyf
a0371167bc #356 - fix schema for fieldGroups 2022-06-15 16:52:59 +02:00
Elio Struyf
0dc2623ded #358 - FIx for relative path of the public folder 2022-06-14 13:54:56 +02:00
Elio Struyf
7d81a83672 Merge branch 'main' into dev 2022-06-13 10:28:52 +02:00
Elio
d2c5a850ef Keep panel open 2022-06-13 10:18:04 +02:00
Elio
5b334db3c9 #354 - Windows file parsing fix 2022-06-13 10:17:56 +02:00
Elio Struyf
69aa7a7648 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-06-13 09:10:51 +02:00
Elio Struyf
97e4313d93 Move content type methods 2022-06-13 09:10:40 +02:00
Elio Struyf
3f7acd7e26 Merge branch 'main' into dev 2022-06-11 20:04:31 +02:00
Elio Struyf
cf376cdda7 #291 - Taxonomy dashboard improvements + command 2022-06-10 15:50:49 +02:00
Elio Struyf
1a6acce77f #350 - New previewPath property for page folders 2022-06-10 10:52:10 +02:00
Elio Struyf
e9258e1a7f #351 - New template property for content types 2022-06-10 10:28:18 +02:00
Elio Struyf
61b461661d Update use count 2022-06-10 09:35:58 +02:00
Elio Struyf
a12a3852d2 Fixes for the table overflow 2022-06-10 09:34:35 +02:00
Elio Struyf
0c94b33606 Move taxonomy value 2022-06-09 16:22:53 +02:00
Elio Struyf
23f3fbfadf 8.0.0 2022-06-09 15:40:24 +02:00
Elio Struyf
434e87b074 Changes to the taxonomy dashboard 2022-06-09 15:40:21 +02:00
Elio Struyf
081fb7ce2e Start of the taxonomy dashboard implementation 2022-06-08 18:37:13 +02:00
Elio Struyf
bd43ba8a6d #349 - Slug field 2022-06-03 15:58:19 +02:00
Elio Struyf
bd2860e225 #307 - List field 2022-06-02 09:19:06 +02:00
Elio Struyf
daeaf0a59d 7.4.0 2022-06-01 13:48:29 +02:00
Elio Struyf
9cc7ea09d6 #345 - Improve the UI of the media dashboard 2022-06-01 13:48:24 +02:00
Elio Struyf
4b6f283bf3 #348 - breadcrumb fix 2022-06-01 13:48:12 +02:00
164 changed files with 18293 additions and 1590 deletions

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://frontmatter.codes/docs
about: See our documentation.
- name: Changelog
url: https://frontmatter.codes/updates
about: See our changelog.
- name: Front Matter website
url: https://frontmatter.codes
about: Our website.
- name: Support Front Matter
url: https://github.com/sponsors/estruyf
about: Support Front Matter development.

6
.gitignore vendored
View File

@@ -4,4 +4,8 @@ node_modules
*.vsix
.DS_Store
dist
todo.md
todo.md
e2e/storage
e2e/extensions
e2e/sample

14
.vscode/launch.json vendored
View File

@@ -29,20 +29,6 @@
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: test-compile"
}
]
}

43
.vscode/settings.json vendored
View File

@@ -10,8 +10,43 @@
"typescript.tsc.autoDetect": "off",
"eliostruyf.writingstyleguide.terms.isDisabled": true,
"eliostruyf.writingstyleguide.biasFree.isDisabled": true,
"exportall.config.folderListener": [
"/src/pagesView/state/atom",
"/src/pagesView/state/selectors"
]
"squarl.groups": [
{
"id": "dashboard",
"name": "Dashboard"
},
{
"id": "panel",
"name": "Panel"
}
],
"squarl.bookmarks": [
{
"name": "App.tsx",
"path": "src/dashboardWebView/components/App.tsx",
"description": "Start of dashboard",
"type": "file",
"groupId": "dashboard"
},
{
"name": "ViewPanel.tsx",
"path": "src/panelWebView/ViewPanel.tsx",
"description": "Start of panel",
"type": "file",
"groupId": "panel"
},
{
"name": "styles.css",
"path": "src/panelWebView/styles.css",
"description": "Panel styles",
"type": "file",
"groupId": "panel"
},
{
"name": "settings.ts",
"path": "src/constants/settings.ts",
"description": "Settings names",
"type": "file"
}
],
}

View File

@@ -26,4 +26,6 @@ dist/*.html
frontmatter.json
.frontmatter
webpack
README.beta.md
README.beta.md
e2e
storage

View File

@@ -1,5 +1,88 @@
# Change Log
## [8.1.1] - 2022-09-23
### 🐞 Fixes
- [#422](https://github.com/estruyf/vscode-front-matter/issues/422): Fix in panel initialization logic
## [8.1.0] - 2022-09-22 - [Release notes](https://beta.frontmatter.codes/updates/v8.1.0)
### ✨ New features
- [#369](https://github.com/estruyf/vscode-front-matter/issues/369): New `required` property to specify if a content-type field is required
- [#376](https://github.com/estruyf/vscode-front-matter/issues/376): Ability to run scripts after content was created
- [#377](https://github.com/estruyf/vscode-front-matter/issues/377): Git sync actions added on panel and content dashboard (pull and push your changes to remote)
- [#379](https://github.com/estruyf/vscode-front-matter/issues/377): New `frontMatter.config.reload` command to reload the configuration file + reinitialize its listeners
- [#391](https://github.com/estruyf/vscode-front-matter/issues/391): New `description` property to show a message underneath the input field
- [#401](https://github.com/estruyf/vscode-front-matter/issues/401): Content dashboard now has pagination enabled and can be disabled via the `frontMatter.dashboard.content.pagination` setting
### 🎨 Enhancements
- [#352](https://github.com/estruyf/vscode-front-matter/issues/352): Custom placeholders now support scripting
- [#370](https://github.com/estruyf/vscode-front-matter/issues/370): Define the tags and categories as reserved keywords for custom taxonomy
- [#372](https://github.com/estruyf/vscode-front-matter/issues/372): Rename Taxonomy tab to Taxonomies
- [#374](https://github.com/estruyf/vscode-front-matter/issues/374): Hide the front matter section to use the panel instead
- [#383](https://github.com/estruyf/vscode-front-matter/issues/383): Add the item menu to the content list view
- [#385](https://github.com/estruyf/vscode-front-matter/issues/385): If no default value for the draft field is defined, the field value will be set to `true`
- [#388](https://github.com/estruyf/vscode-front-matter/issues/388): New stop server action has been added to the panel
- [#390](https://github.com/estruyf/vscode-front-matter/issues/390): Implement another JSON parser in order to be able to parse the `frontmatter.json` file better
- [#394](https://github.com/estruyf/vscode-front-matter/issues/394): Ordering of snippet fields is based on their field definition
- [#395](https://github.com/estruyf/vscode-front-matter/issues/395): Added support for custom snippet fields on media snippets
- [#402](https://github.com/estruyf/vscode-front-matter/issues/402): Custom sorting of content now supports `number` fields
- [#417](https://github.com/estruyf/vscode-front-matter/issues/417): New `hyperlink` wysiwyg option
- [#418](https://github.com/estruyf/vscode-front-matter/issues/418): New `heading` and `divider` fields for your content-type definition
### ⚡️ Optimizations
- Internal post message optimizations to the webviews
- Preview tab now shows the title of the page/content if present
### 🐞 Fixes
- [#378](https://github.com/estruyf/vscode-front-matter/issues/378): Fix last modified update only to content in content folders
- [#384](https://github.com/estruyf/vscode-front-matter/issues/384): Fix issue `title` field in sub-fields
- [#393](https://github.com/estruyf/vscode-front-matter/issues/393): Fix Windows file path for retrieving the preview path
- [#396](https://github.com/estruyf/vscode-front-matter/issues/396): Fix for `index` and `_index` page previews
- [#398](https://github.com/estruyf/vscode-front-matter/issues/398): Fix Windows folder path parsing in data folder retrieval
- [#400](https://github.com/estruyf/vscode-front-matter/issues/400): Fix for draft/published content grouping
- [#403](https://github.com/estruyf/vscode-front-matter/issues/403): Fix for media files with spaces on importing in article content
- [#404](https://github.com/estruyf/vscode-front-matter/issues/404): Fix for published sorting option in media dashboard
- [#408](https://github.com/estruyf/vscode-front-matter/issues/408): Fix for missing `dashboard.taxonomy.view` view mode in the JSON schema
## [8.0.1] - 2022-07-13
### 🐞 Fixes
- Fix `PSD` media card icon image
- Fix missing clipboard icon for the media card action
- Fix in tags rendering on content cards
## [8.0.0] - 2022-07-11 - [Release notes](https://beta.frontmatter.codes/updates/v8.0.0)
### ✨ New Features
- [#291](https://github.com/estruyf/vscode-front-matter/issues/291): New taxonomy dashboard for managing tags, categories, and custom taxonomies
### 🎨 Enhancements
- Ignore the SEO `keywords` field for missing content type field
- [#307](https://github.com/estruyf/vscode-front-matter/issues/307): New `list` field which allows to create a list of items
- [#345](https://github.com/estruyf/vscode-front-matter/issues/345): Media dashboard UI improvements to visualize the content and public folders
- [#349](https://github.com/estruyf/vscode-front-matter/issues/349): New `slug` field which allows you to manage the slug of your post from the Front Matter panel
- [#350](https://github.com/estruyf/vscode-front-matter/issues/350): New `previewPath` property for the `frontMatter.content.pageFolders` setting. This allows you to specify a section prefix for all content created in that directory.
- [#351](https://github.com/estruyf/vscode-front-matter/issues/351): New `template` property for content types which allows you to combine templates and content types for content creation
- [#353](https://github.com/estruyf/vscode-front-matter/issues/353): Add the default content type on project initialization
- [#366](https://github.com/estruyf/vscode-front-matter/issues/366): Better support for using block fields in another block field
### 🐞 Fixes
- [#348](https://github.com/estruyf/vscode-front-matter/issues/348): Fix media dashboard breadcrumb when multiple page folders are in use
- [#356](https://github.com/estruyf/vscode-front-matter/issues/356): Re-introduce the `labelField` to the `frontMatter.taxonomy.fieldGroups` setting
- [#358](https://github.com/estruyf/vscode-front-matter/issues/358): Fix for relative path of the public folder
- [#364](https://github.com/estruyf/vscode-front-matter/issues/364): Honour file ending rules in data files
- [#365](https://github.com/estruyf/vscode-front-matter/issues/365): Show spinner on the initial load of the content dashboard
## [7.3.4] - 2022-06-13
### 🐞 Fixes

View File

@@ -54,6 +54,12 @@ A couple of our extension highlights that hopefully get you interested in giving
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 8**
The taxonomy dashboard got introduced on which you can manage your tags, categories, and custom taxonomy.
![Taxonomy dashboard](https://beta.frontmatter.codes/assets/marketplace/v8.1.0/taxonomy-dashboard.png)
**Version 7**
Snippets support for Front Matter has been added!

View File

@@ -52,6 +52,12 @@ A couple of our extension highlights that hopefully get you interested in giving
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 8**
The taxonomy dashboard got introduced on which you can manage your tags, categories, and custom taxonomy.
![Taxonomy dashboard](https://frontmatter.codes/assets/marketplace/v8.1.0/taxonomy-dashboard.png)
**Version 7**
Snippets support for Front Matter has been added!

View File

@@ -41,7 +41,9 @@
}
.collapsible__body,
.ext_settings {
.ext_settings,
.git_actions,
.initialize_actions {
padding: 1rem 1.25rem;
box-sizing: border-box;
}
@@ -163,36 +165,6 @@
border: 1px solid rgba(0, 0, 0, .9);
}
.article__tags__input input {
border: 1px solid var(--vscode-inputValidation-infoBorder);
}
.article__tags__input input:disabled {
border-color: transparent;
}
.article__tags__input.freeform {
position: relative;
outline: 1px solid var(--vscode-inputValidation-infoBorder);
outline-offset: -1px;
}
.article__tags__input.freeform input {
padding-right: 35px;
border: 0;
}
.article__tags__input button {
position: absolute;
bottom: 0;
top: 0;
right: 0;
width: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.article__tags ul {
color: var(--vscode-dropdown-foreground);
background-color: var(--vscode-dropdown-background);
@@ -400,162 +372,6 @@ input:checked + .field__toggle__slider:before {
}
/* Metadata */
.metadata_field {
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;
}
.metadata_field__error {
color: var(--vscode-errorForeground);
display: flex;
justify-content: space-between;
align-items: center;
}
.metadata_field__error button {
color: var(--vscode-button-secondaryForeground);
background-color: var(--vscode-button-secondaryBackground);
padding-left: 1rem;
padding-right: 1rem;
width: auto;
}
.metadata_field__error button:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.metadata_field__input, .metadata_field__input:focus,
.metadata_field__textarea, .metadata_field__textarea:focus {
outline: none;
}
.metadata_field__limit {
color: var(--vscode-inputValidation-warningBorder);
margin-top: .25rem;
}
.metadata_field__number {
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
outline: none !important;
}
.metadata_field__choice__toggle {
color: var(--vscode-input-placeholderForeground);
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
outline: none !important;
width: 100%;
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
background-color: var(--vscode-input-background);
display: flex;
align-items: center;
position: relative;
}
.metadata_field__choice__toggle:hover,
.metadata_field__choice__toggle:focus,
.metadata_field__choice__toggle:active,
.metadata_field__choice__toggle:disabled {
background-color: var(--vscode-input-background);
}
.metadata_field__choice__toggle span {
margin-right: 1rem;
}
.metadata_field__choice__toggle svg.icon {
height: 1rem;
width: 1rem;
margin-left: .25rem;
position: absolute;
right: .25rem;
}
.metadata_field__choice_list {
width: 90%;
margin: 0;
padding: 0;
z-index: 1;
position: absolute;
list-style: none;
overflow: auto;
max-height: 200px;
color: var(--vscode-dropdown-foreground);
background-color: var(--vscode-dropdown-background);
}
.metadata_field__choice_list.open {
border: 1px solid rgba(0, 0, 0, .9);
}
.metadata_field__choice_list li {
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
cursor: pointer;
}
.metadata_field__choice_list li:active {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
}
.metadata_field__choice_list li[aria-selected="true"] {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-hoverBackground);
}
.metadata_field__choice_list li[aria-disabled="true"] {
display: none;
}
.metadata_field__choice_list__item {
opacity: 0.8;
}
.metadata_field__choice__button {
margin-top: .5rem;
display: inline-flex;
align-items: center;
width: auto;
margin-right: .5rem;
}
.metadata_field__choice__button_icon {
height: 1.25rem;
width: 1.25rem;
margin-left: .5rem;
}
.metadata_field__datetime {
display: flex;
@@ -578,75 +394,6 @@ input:checked + .field__toggle__slider:before {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.metadata_field__multiple_images {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.metadata_field__preview_image img {
display: block;
margin: 0 auto;
max-height: 16rem;
}
.metadata_field__file__button,
.metadata_field__preview_image__button {
background-color: transparent;
border: 1px dashed var(--vscode-button-background);
padding: 1.5rem;
filter: brightness(85%);
}
.metadata_field__file__button:hover,
.metadata_field__preview_image__button:hover {
background-color: rgba(255, 255, 255, .1);
filter: brightness(100%);
}
.metadata_field__file__button svg,
.metadata_field__preview_image__button svg {
color: var(--vscode-foreground);
display: block;
width: 3rem;
height: 3rem;
margin: 0 auto;
}
.metadata_field__file__button span,
.metadata_field__preview_image__button span {
color: var(--vscode-foreground);
display: inline-block;
margin: 0 auto;
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;
flex-direction: column;
justify-content: flex-end;
}
.metadata_field__preview_image__remove {
background-color: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-inputValidation-errorForeground);
}
.metadata_field__preview_image__remove:hover {
background-color: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-inputValidation-errorForeground);
opacity: .9;
}
/* File list */
.file_list vscode-label {
border-bottom: 1px solid var(--vscode-foreground);

80
e2e/src/command.test.ts Normal file
View File

@@ -0,0 +1,80 @@
import { By, VSBrowser, EditorView, WebView, Workbench, Notification, StatusBar, NotificationType } from "vscode-extension-tester";
import { expect } from "chai";
import { sleep } from "./utils";
import { join } from "path";
// https://github.com/microsoft/vscode-java-dependency/blob/4256fa6adcaff5ec24dbdbb8d9a516fad21431c5/test/ui/index.ts
// https://github.com/microsoft/vscode-java-dependency/blob/4256fa6adcaff5ec24dbdbb8d9a516fad21431c5/test/ui/command.test.ts
describe("Initialization testing", function() {
this.timeout(2 * 60 * 1000 /*ms*/);
let workbench: Workbench;
let view: WebView;
before(async function() {
await VSBrowser.instance.openResources(join(__dirname, '../sample'));
await sleep(3000);
workbench = new Workbench();
await workbench.executeCommand("frontMatter.dashboard");
await sleep(3000);
await new EditorView().openEditor(`FrontMatter Dashboard`);
view = new WebView();
await view.switchToFrame();
});
it("1. Open welcome dashboard", async function() {
const element = await view.findWebElement(By.css('h1'));
const title = await element.getText();
expect(title).has.string(`Front Matter`);
});
it("2. Initialize project", async function() {
const btn = await view.findWebElement(By.css('[data-test="welcome-init"] button'));
expect(btn).to.exist;
await btn.click();
await sleep(1000);
await VSBrowser.instance.driver.wait(() => {
return notificationExists(workbench, 'Front Matter:');
}, 2000) as Notification;
const notifications = await workbench.getNotifications();
let notification!: Notification;
for (const not of notifications) {
console.log(not);
// const message = await not.get;
// console.log(message);
// if (message.includes('Front Matter:')) {
// notification = not;
// }
}
expect(await notification.getMessage()).has.string(`Project initialized successfully.`);
});
it("3. Check if project file is created", async function() {});
});
async function notificationExists(workbench: Workbench, text: string): Promise<Notification | undefined> {
const notifications = await (await (new StatusBar()).openNotificationsCenter()).getNotifications(NotificationType.Info);
console.log(`Notifications:`, notifications.length);
for (const notification of notifications) {
const message = await notification.getMessage();
console.log(message)
if (message.indexOf(text) >= 0) {
return notification;
}
}
}

33
e2e/src/runTests.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as path from 'path';
import * as semver from "semver";
import { ExTester, ReleaseQuality } from "vscode-extension-tester";
async function main(): Promise<void> {
const vsCodeVersion: semver.SemVer = new semver.SemVer(`1.66.0`);
const version = vsCodeVersion.version;
const storageFolder = path.join(__dirname, "..", "storage");
const extFolder = path.join(__dirname, "..", "extensions");
try {
const testPath = path.join(__dirname, "command.test.js");
const exTester = new ExTester(storageFolder, ReleaseQuality.Stable, extFolder);
await exTester.downloadCode(version);
await exTester.installVsix();
// await exTester.installFromMarketplace("eliostruyf.vscode-front-matter");
await exTester.downloadChromeDriver(version);
// await exTester.setupRequirements({vscodeVersion: version});
const result = await exTester.runTests(testPath, {
vscodeVersion: version
});
process.exit(result);
} catch (err) {
console.log(err);
process.exit(1);
}
}
main();

1
e2e/src/utils/index.ts Normal file
View File

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

3
e2e/src/utils/sleep.ts Normal file
View File

@@ -0,0 +1,3 @@
export async function sleep(time: number) {
await new Promise((resolve) => setTimeout(resolve, time));
}

13356
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"displayName": "Front Matter",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "7.3.4",
"version": "8.1.1",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -22,6 +22,10 @@
"href": "https://www.buymeacoffee.com/zMeFRy9"
}
],
"sponsor": {
"url": "https://github.com/sponsors/estruyf"
},
"qna": "https://github.com/estruyf/vscode-front-matter/discussions",
"engines": {
"vscode": "^1.63.0"
},
@@ -56,7 +60,7 @@
"activitybar": [
{
"id": "frontmatter-explorer",
"title": "FrontMatter",
"title": "Front Matter",
"icon": "assets/frontmatter-short-min.svg"
}
]
@@ -65,9 +69,9 @@
"frontmatter-explorer": [
{
"id": "frontMatter.explorer",
"name": "FrontMatter",
"name": "Front Matter",
"icon": "assets/frontmatter-short-min.svg",
"contextualTitle": "FrontMatter",
"contextualTitle": "Front Matter",
"type": "webview"
}
]
@@ -78,7 +82,7 @@
"frontMatter.content.autoUpdateDate": {
"type": "boolean",
"default": false,
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page (only content located in your content folder). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
"scope": "Content"
},
"frontMatter.content.defaultFileType": {
@@ -164,6 +168,17 @@
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.fmhighlight)",
"scope": "Content"
},
"frontMatter.content.hideFm": {
"type": "boolean",
"markdownDescription": "Specify if you want to hide the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.hidefm)",
"scope": "Content"
},
"frontMatter.content.hideFmMessage": {
"type": "string",
"default": "Use the editor panel to make front matter changes",
"markdownDescription": "Specify the message to display when the Front Matter is hidden. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.hidefmMessage)",
"scope": "Content"
},
"frontMatter.content.pageFolders": {
"type": "array",
"default": [],
@@ -183,6 +198,14 @@
"type": "boolean",
"default": false,
"description": "Exclude sub-directories"
},
"previewPath": {
"type": [
"null",
"string"
],
"default": null,
"description": "Defines a custom preview path for the folder."
}
},
"additionalProperties": false,
@@ -207,12 +230,18 @@
"value": {
"type": "string",
"description": "The placeholder its value"
},
"script": {
"type": "string",
"description": "The script to execute to get the value of the placeholder"
},
"command": {
"$ref": "#scriptCommand"
}
},
"additionalProperties": false,
"required": [
"id",
"value"
"id"
]
},
"scope": "Content"
@@ -300,7 +329,8 @@
"default": "string",
"enum": [
"string",
"date"
"date",
"number"
],
"description": "Type of the field value"
}
@@ -340,6 +370,10 @@
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the script."
},
"title": {
"type": "string",
"description": "Title you want to give to your script. Will be shown as the title of the button."
@@ -379,6 +413,7 @@
"description": "The type for which the script will be used."
},
"command": {
"$id": "#scriptCommand",
"type": "string",
"oneOf": [
{
@@ -396,6 +431,11 @@
],
"description": "The type of script you want to execute.",
"default": "node"
},
"hidden": {
"type": "boolean",
"description": "Hide the action from the UI",
"default": false
}
},
"additionalProperties": false,
@@ -406,6 +446,12 @@
},
"scope": "Custom scripts"
},
"frontMatter.dashboard.content.pagination": {
"type": "boolean",
"default": true,
"markdownDescription": "Specify if you want to enable/disable pagination for your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)",
"scope": "Dashboard"
},
"frontMatter.dashboard.content.cardTags": {
"type": "string",
"default": "tags",
@@ -619,6 +665,16 @@
"default": null,
"markdownDescription": "Specify the command you want to use to start your static site generator or framework. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.startcommand)"
},
"frontMatter.git.enabled": {
"type": "boolean",
"markdownDescription": "Specify if you want to use the Git actions for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.git.enabled)",
"default": false
},
"frontMatter.git.commitMessage": {
"type": "string",
"markdownDescription": "Specify the commit message you want to use for the sync. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.git.commitmessage)",
"default": "Synced by Front Matter"
},
"frontMatter.global.activeMode": {
"type": [
"string",
@@ -656,7 +712,8 @@
"panel.otherActions",
"dashboard.snippets.view",
"dashboard.snippets.manage",
"dashboard.data.view"
"dashboard.data.view",
"dashboard.taxonomy.view"
]
}
}
@@ -686,6 +743,14 @@
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
"scope": "Templates"
},
"frontMatter.global.disabledNotificaitons": {
"type": "array",
"default": [],
"markdownDescription": "This is an array with the notifications types that can be disabled for Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.disablednotifications)",
"enum": [
"requiredFieldValidation"
]
},
"frontMatter.media.defaultSorting": {
"type": "string",
"default": "",
@@ -806,7 +871,11 @@
"fields",
"json",
"block",
"dataFile"
"list",
"dataFile",
"slug",
"divider",
"heading"
],
"description": "Define the type of field"
},
@@ -818,6 +887,10 @@
"type": "string",
"description": "Title to show in the UI"
},
"description": {
"type": "string",
"description": "Description to show in the UI"
},
"default": {
"type": [
"string",
@@ -880,7 +953,20 @@
"taxonomyId": {
"type": "string",
"default": "",
"description": "The ID of your taxonomy field"
"description": "The ID of your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.",
"not": {
"anyOf": [
{
"const": ""
},
{
"const": "tags"
},
{
"const": "categories"
}
]
}
},
"fileExtensions": {
"type": "array",
@@ -943,6 +1029,16 @@
"type": "string",
"default": "",
"description": "Specify the property name that will be used to show the value for the field"
},
"editable": {
"type": "boolean",
"default": true,
"description": "Specify if the field is editable"
},
"required": {
"type": "boolean",
"default": false,
"description": "Specify if the field is required"
}
},
"additionalProperties": false,
@@ -951,6 +1047,20 @@
"name"
],
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "divider"
}
}
},
"then": {
"required": [
"type"
]
}
},
{
"if": {
"properties": {
@@ -1065,6 +1175,16 @@
],
"default": null,
"description": "Defines a custom preview path for the content type."
},
"template": {
"type": "string",
"default": "",
"description": "An optional template that can be used for creating new content."
},
"postScript": {
"type": "string",
"default": "",
"description": "An optional post script that can be used after new content creation."
}
},
"additionalProperties": false,
@@ -1128,11 +1248,24 @@
"properties": {
"id": {
"type": "string",
"description": "ID for your taxonomy field"
"description": "ID for your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.",
"not": {
"anyOf": [
{
"const": ""
},
{
"const": "tags"
},
{
"const": "categories"
}
]
}
},
"options": {
"type": "array",
"description": "Options from which you can pick",
"description": "Options from which you can pick.",
"items": {
"type": "string"
}
@@ -1168,6 +1301,10 @@
"type": "string",
"description": "The name of the field group"
},
"labelField": {
"type": "string",
"description": "The name of the field to be used as display value"
},
"fields": {
"$ref": "#contenttypefield"
}
@@ -1270,6 +1407,12 @@
"default": false,
"markdownDescription": "Specify if you want to disable the telemetry. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.telemetry.disable)"
},
"frontMatter.templates.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.enabled)",
"scope": "Templates"
},
"frontMatter.templates.folder": {
"type": "string",
"default": ".frontmatter/templates",
@@ -1281,40 +1424,39 @@
"default": "yyyy-MM-dd",
"markdownDescription": "Specify the prefix you want to add for your new article filenames. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.prefix)",
"scope": "Templates"
},
"frontMatter.templates.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.enabled)",
"scope": "Templates"
}
}
},
"commands": [
{
"command": "frontMatter.config.reload",
"title": "Reload config",
"category": "Front Matter"
},
{
"command": "frontMatter.authenticate",
"title": "Authenticate",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.contenttype.generate",
"title": "Generate content type from current file",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"title": "Add missing fields from front matter to content type",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.contenttype.setContentType",
"title": "Set the content type to use for the current file",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.markup.blockquote",
"title": "Blockquote",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/blockquote-light.svg",
"dark": "assets/icons/blockquote-dark.svg"
@@ -1323,7 +1465,7 @@
{
"command": "frontMatter.markup.bold",
"title": "Bold",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/bold-light.svg",
"dark": "assets/icons/bold-dark.svg"
@@ -1332,7 +1474,7 @@
{
"command": "frontMatter.dashboard.close",
"title": "Close dashboard",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-teal.svg",
"light": "/assets/icons/frontmatter-small-teal.svg"
@@ -1341,7 +1483,7 @@
{
"command": "frontMatter.markup.code",
"title": "Code",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/code-light.svg",
"dark": "assets/icons/code-dark.svg"
@@ -1350,16 +1492,22 @@
{
"command": "frontMatter.markup.codeblock",
"title": "Codeblock",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/codeblock-light.svg",
"dark": "assets/icons/codeblock-dark.svg"
}
},
{
"command": "frontMatter.markup.hyperlink",
"title": "Hyperlink",
"category": "Front Matter",
"icon": "$(link)"
},
{
"command": "frontMatter.collapseSections",
"title": "Collapse sections",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/close-light.svg",
"dark": "assets/icons/close-dark.svg"
@@ -1368,37 +1516,37 @@
{
"command": "frontMatter.initTemplate",
"title": "Initialize the template folder",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.createTemplate",
"title": "Create template from current file",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.createCategory",
"title": "Create category",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.createContent",
"title": "Create new content from defined content type or template",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.createTag",
"title": "Create tag",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.diagnostics",
"title": "Diagnostic logging",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.exportTaxonomy",
"title": "Export all tags & categories to your settings",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.createFromTemplate",
@@ -1415,12 +1563,12 @@
{
"command": "frontMatter.generateSlug",
"title": "Generate slug based on content title",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.markup.heading",
"title": "Heading",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/heading-light.svg",
"dark": "assets/icons/heading-dark.svg"
@@ -1429,17 +1577,17 @@
{
"command": "frontMatter.init",
"title": "Initialize project",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.insertCategories",
"title": "Insert categories",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.insertMedia",
"title": "Insert media into your content",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/media-dark.svg",
"light": "/assets/icons/media-light.svg"
@@ -1448,7 +1596,7 @@
{
"command": "frontMatter.insertSnippet",
"title": "Insert snippet into your content",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/scissors-dark.svg",
"light": "/assets/icons/scissors-light.svg"
@@ -1457,12 +1605,12 @@
{
"command": "frontMatter.insertTags",
"title": "Insert tags",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.markup.italic",
"title": "Italic",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/italic-light.svg",
"dark": "assets/icons/italic-dark.svg"
@@ -1471,7 +1619,7 @@
{
"command": "frontMatter.dashboard",
"title": "Open dashboard",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
@@ -1480,7 +1628,7 @@
{
"command": "frontMatter.dashboard.data",
"title": "Open data dashboard",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
@@ -1489,7 +1637,7 @@
{
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
@@ -1498,7 +1646,16 @@
{
"command": "frontMatter.dashboard.snippets",
"title": "Open snippets dashboard",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.taxonomy",
"title": "Open taxonomies dashboard",
"category": "Front Matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
@@ -1507,7 +1664,7 @@
{
"command": "frontMatter.markup.orderedlist",
"title": "Ordered list",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/ordered-list-light.svg",
"dark": "assets/icons/ordered-list-dark.svg"
@@ -1516,7 +1673,7 @@
{
"command": "frontMatter.markup.options",
"title": "Other markup options",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/options-light.svg",
"dark": "assets/icons/options-dark.svg"
@@ -1525,27 +1682,27 @@
{
"command": "frontMatter.preview",
"title": "Preview content",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.promoteSettings",
"title": "Promote settings from local to team level",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.remap",
"title": "Remap or remove tag/category in all articles",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.setLastModifiedDate",
"title": "Set lastmod date",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.markup.strikethrough",
"title": "Strikethrough",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/strikethrough-light.svg",
"dark": "assets/icons/strikethrough-dark.svg"
@@ -1554,22 +1711,27 @@
{
"command": "frontMatter.mode.switch",
"title": "Switch mode",
"category": "Front matter",
"category": "Front Matter",
"icon": "$(preview)"
},
{
"command": "frontMatter.markup.tasklist",
"title": "Task list",
"category": "Front matter"
"category": "Front Matter"
},
{
"command": "frontMatter.markup.unorderedlist",
"title": "Unordered list",
"category": "Front matter",
"category": "Front Matter",
"icon": {
"light": "assets/icons/unordered-list-light.svg",
"dark": "assets/icons/unordered-list-dark.svg"
}
},
{
"command": "frontMatter.git.sync",
"title": "Sync",
"category": "Front Matter"
}
],
"menus": {
@@ -1590,7 +1752,7 @@
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.strikethrough",
"command": "frontMatter.markup.hyperlink",
"group": "navigation@-130",
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
},
@@ -1688,6 +1850,10 @@
"command": "frontMatter.dashboard.snippets",
"when": "frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.git.sync",
"when": "frontMatter:git:enabled"
},
{
"command": "frontMatter.collapseSections",
"when": "false"
@@ -1819,7 +1985,7 @@
{
"command": "frontMatter.dashboard",
"group": "navigation@2",
"when": "view == frontMatter.explorer"
"when": "view == frontMatter.explorer || view == explorer"
}
]
},
@@ -1890,7 +2056,9 @@
"prod:panel": "webpack --mode production --config ./webpack/panel.config.js",
"test-compile": "tsc -p ./",
"clean": "rimraf dist",
"start:site": "cd ./docs && npm run dev"
"start:site": "cd ./docs && npm run dev",
"clean:test": "rm ./e2e/sample/frontmatter.json || exit 0 && rm -rf ./e2e/sample/.frontmatter || exit 0",
"test": "tsc -p tsconfig.e2e.json && npm run clean:test && node ./e2e/out/runTests.js"
},
"devDependencies": {
"@actions/core": "^1.8.2",
@@ -1900,9 +2068,11 @@
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@octokit/rest": "^18.12.0",
"@popperjs/core": "^2.11.6",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/chai": "^4.3.1",
"@types/glob": "7.1.3",
"@types/invariant": "^2.2.35",
"@types/js-yaml": "3.12.1",
@@ -1910,7 +2080,7 @@
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^5.2.6",
"@types/mocha": "^5.2.7",
"@types/mustache": "^4.1.2",
"@types/node": "10.17.48",
"@types/node-fetch": "^2.5.12",
@@ -1925,6 +2095,7 @@
"ajv": "^8.8.2",
"array-move": "^4.0.0",
"autoprefixer": "^10.3.2",
"chai": "^4.3.6",
"css-loader": "5.2.7",
"date-fns": "2.23.0",
"downshift": "6.0.6",
@@ -1935,12 +2106,14 @@
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
"invariant": "^2.2.4",
"jsonc-parser": "^3.2.0",
"lodash-es": "^4.17.21",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"mime-types": "^2.1.35",
"mocha": "^10.0.0",
"mustache": "^4.2.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
@@ -1953,10 +2126,13 @@
"react-dom": "17.0.1",
"react-dropzone": "^11.3.4",
"react-quill": "^2.0.0-beta.4",
"react-router-dom": "^6.3.0",
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^8.1.0",
"recoil": "^0.4.1",
"rimraf": "^3.0.2",
"semver": "^7.3.7",
"simple-git": "^3.10.0",
"style-loader": "2.0.0",
"tailwindcss": "^2.2.7",
"tailwindcss-nested-groups": "^1.2.4",
@@ -1968,6 +2144,7 @@
"uniforms-bridge-json-schema": "^3.7.0",
"uniforms-unstyled": "^3.7.0",
"url-join-ts": "^1.0.5",
"vscode-extension-tester": "^4.2.5",
"wc-react": "github:estruyf/wc-react",
"webpack": "^5.65.0",
"webpack-bundle-analyzer": "^4.5.0",

View File

@@ -15,7 +15,7 @@ packageJson.homepage = "https://beta.frontmatter.codes";
console.log(packageJson.version);
core.summary.addHeading(`Version info`).addDetails(`${packageJson.version}`);
core.summary.addHeading(`Version info`).addRaw(`Version: ${packageJson.version}`).write();
const scripts = packageJson.scripts;
for (const key in scripts) {

View File

@@ -1,8 +1,9 @@
import { Folders } from './Folders';
import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType';
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, SETTING_CONTENT_PLACEHOLDERS, TelemetryEvent } from './../constants';
import * as vscode from 'vscode';
import { Field, TaxonomyType } from "../models";
import { CustomPlaceholder, Field, TaxonomyType } from "../models";
import { format } from "date-fns";
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
import { Notifications } from '../helpers/Notifications';
@@ -168,13 +169,34 @@ export class Article {
}
/**
* Generate the slug based on the article title
* Generate the new slug
*/
public static async generateSlug() {
Telemetry.send(TelemetryEvent.generateSlug);
public static generateSlug(title: string) {
if (!title) {
return;
}
const prefix = Settings.get(SETTING_SLUG_PREFIX) as string;
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
const slug = SlugHelper.createSlug(title);
if (slug) {
return {
slug,
slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}`
};
}
return undefined;
}
/**
* Generate the slug based on the article title
*/
public static async updateSlug() {
Telemetry.send(TelemetryEvent.generateSlug);
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
const filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const editor = vscode.window.activeTextEditor;
@@ -191,22 +213,21 @@ export class Article {
const contentType = ArticleHelper.getContentType(article.data);
const titleField = "title";
const articleTitle: string = article.data[titleField];
const slugInfo = Article.generateSlug(articleTitle);
const slug = SlugHelper.createSlug(articleTitle);
if (slug) {
let slugFieldValue = `${prefix}${slug}${suffix}`;
article.data["slug"] = slugFieldValue;
if (slugInfo && slugInfo.slug && slugInfo.slugWithPrefixAndSuffix) {
article.data["slug"] = slugInfo.slugWithPrefixAndSuffix;
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;
article.data[field.name] = slugInfo.slug;
}
// Update the fields containing a custom placeholder that depends on slug
const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS);
const customPlaceholders = placeholders?.filter(p => p.value.includes("{{slug}}"));
const placeholders = Settings.get<CustomPlaceholder[]>(SETTING_CONTENT_PLACEHOLDERS);
const customPlaceholders = placeholders?.filter(p => p.value && p.value.includes("{{slug}}"));
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
for (const customPlaceholder of (customPlaceholders || [])) {
const customPlaceholderFields = contentType.fields.filter(f => f.default === `{{${customPlaceholder.id}}}`);
@@ -227,7 +248,7 @@ export class Article {
const ext = extname(editor.document.fileName);
const fileName = basename(editor.document.fileName);
let slugName = slug.startsWith("/") ? slug.substring(1) : slug;
let slugName = slugInfo.slug.startsWith("/") ? slugInfo.slug.substring(1) : slugInfo.slug;
slugName = slugName.endsWith("/") ? slugName.substring(0, slugName.length - 1) : slugName;
let newFileName = `${slugName}${ext}`;
@@ -303,6 +324,14 @@ export class Article {
if (document && ArticleHelper.isSupportedFile(document)) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
// Is article located in one of the content folders
const folders = Folders.get();
const documentPath = parseWinPath(document.fileName);
const folder = folders.find(f => documentPath.startsWith(f.path));
if (!folder) {
return;
}
if (autoUpdate) {
event.waitUntil(Article.setLastModifiedDateOnSave(document));
}
@@ -335,6 +364,7 @@ export class Article {
const contentType = article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
const position = editor.selection.active;
const selectionText = editor.document.getText(editor.selection);
await vscode.commands.executeCommand(COMMAND_NAME.dashboard, {
type: "media",
@@ -342,7 +372,8 @@ export class Article {
pageBundle: !!contentType.pageBundle,
filePath: editor.document.uri.fsPath,
fieldName: basename(editor.document.uri.fsPath),
position
position,
selection: selectionText
}
} as DashboardData);

View File

@@ -1,5 +1,5 @@
import { commands, ExtensionContext } from 'vscode';
import { CONTEXT } from '../constants';
import { COMMAND_NAME, CONTEXT } from '../constants';
import { Extension } from '../helpers';
import { Credentials } from "../services/Credentials";
import fetch from "node-fetch";
@@ -17,7 +17,7 @@ export class Backers {
Backers.tryUsernameCheck();
context.subscriptions.push(
commands.registerCommand('frontMatter.authenticate', async () => {
commands.registerCommand(COMMAND_NAME.authenticate, async () => {
Backers.tryUsernameCheck();
})
);

View File

@@ -8,6 +8,7 @@ export class Content {
const templatesEnabled = await Settings.get(SETTING_TEMPLATES_ENABLED);
if (!templatesEnabled) {
commands.executeCommand(COMMAND_NAME.createByContentType);
return;
}
const options: QuickPickItem[] = [{
@@ -19,6 +20,7 @@ export class Content {
} as QuickPickItem];
const selectedOption = await window.showQuickPick(options, {
title: "Create content",
placeHolder: `Select how you want to create your new content`,
canPickMany: false,
ignoreFocusOut: true

View File

@@ -1,4 +1,4 @@
import { SETTING_DASHBOARD_OPENONSTART, CONTEXT } from '../constants';
import { SETTING_DASHBOARD_OPENONSTART, CONTEXT, ExtensionState } from '../constants';
import { join } from "path";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode";
import { Logger, Settings as SettingsHelper } from '../helpers';
@@ -7,9 +7,9 @@ import { Extension } from '../helpers/Extension';
import { WebviewHelper } from '@estruyf/vscode';
import { DashboardData } from '../models/DashboardData';
import { MediaLibrary } from '../helpers/MediaLibrary';
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener } from '../listeners/dashboard';
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener } from '../listeners/dashboard';
import { MediaListener as PanelMediaListener } from '../listeners/panel'
import { ModeListener } from '../listeners/general';
import { GitListener, ModeListener } from '../listeners/general';
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -74,6 +74,7 @@ export class Dashboard {
public static reload() {
if (Dashboard.isOpen) {
Dashboard.webview?.dispose();
Extension.getInstance().setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace")
setTimeout(() => {
Dashboard.open();
@@ -145,6 +146,8 @@ export class Dashboard {
TelemetryListener.process(msg);
SnippetListener.process(msg);
ModeListener.process(msg);
GitListener.process(msg);
TaxonomyListener.process(msg);
});
}

View File

@@ -52,7 +52,7 @@ ${folderData.join("\n")}
let projectStart = folder.path.split(projectName).pop();
projectStart = projectStart || "";
projectStart = projectStart?.replace(/\\/g, '/');
projectStart = projectStart?.startsWith('/') ? projectStart.substr(1) : projectStart;
projectStart = projectStart?.startsWith('/') ? projectStart.substring(1) : projectStart;
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));

View File

@@ -27,7 +27,7 @@ export class Folders {
*/
public static async addMediaFolder(data?: {selectedFolder?: string}) {
let wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER);
let staticFolder = Folders.getStaticFolderRelativePath();
let startPath = "";
@@ -42,6 +42,7 @@ export class Folders {
}
const folderName = await window.showInputBox({
title: `Add media folder`,
prompt: `Which name would you like to give to your folder (use "/" to create multi-level folders)?`,
value: startPath,
ignoreFocusOut: true,
@@ -96,7 +97,7 @@ export class Folders {
/**
* Register the new folder path
* @param folder
* @param folderInfo
*/
public static async register(folderInfo: { title: string, path: Uri } | Uri) {
let folderName = folderInfo instanceof Uri ? undefined : folderInfo.title;
@@ -115,7 +116,8 @@ export class Folders {
}
if (!folderName) {
folderName = await window.showInputBox({
folderName = await window.showInputBox({
title: `Register folder`,
prompt: `Which name would you like to specify for this folder?`,
placeHolder: `Folder name`,
value: basename(folder.fsPath),
@@ -153,6 +155,25 @@ export class Folders {
}
}
/**
* Get the static folder its relative path
* @returns
*/
public static getStaticFolderRelativePath(): string | undefined {
let staticFolder = Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER);
if (staticFolder && staticFolder.includes(WORKSPACE_PLACEHOLDER)) {
staticFolder = Folders.getAbsFilePath(staticFolder);
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
const relativePath = relative(parseWinPath(wsFolder.fsPath), parseWinPath(staticFolder));
return relativePath;
}
}
return staticFolder;
}
/**
* Retrieve the folder path
* @param folder
@@ -231,7 +252,7 @@ export class Folders {
if (projectStart) {
projectStart = projectStart.replace(/\\/g, '/');
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
projectStart = projectStart.startsWith('/') ? projectStart.substring(1) : projectStart;
let files: Uri[] = [];
@@ -347,7 +368,7 @@ export class Folders {
const isWindows = process.platform === 'win32';
let absPath = filePath.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
return parseWinPath(absPath);
}
/**
@@ -360,7 +381,7 @@ export class Folders {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
return parseWinPath(absPath);
}
/**

View File

@@ -3,18 +3,19 @@ import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
import { commands, env, Uri, ViewColumn, window } from "vscode";
import { Extension, Settings } from '../helpers';
import { PreviewSettings } from '../models';
import { Extension, parseWinPath, Settings } from '../helpers';
import { ContentFolder, PreviewSettings } from '../models';
import { format } from 'date-fns';
import { DateHelper } from '../helpers/DateHelper';
import { Article } from '.';
import { urlJoin } from 'url-join-ts';
import { WebviewHelper } from '@estruyf/vscode';
import { Folders } from './Folders';
export class Preview {
/** 
/**
* Init the preview
*/
public static async init() {
@@ -37,6 +38,29 @@ export class Preview {
let slug = article?.data ? article.data.slug : "";
let pathname = settings.pathname;
// Check if there is a pathname defined on content folder level
const folders = Folders.get();
if (folders.length > 0) {
const foldersWithPath = folders.filter(folder => folder.previewPath);
const filePath = parseWinPath(editor?.document.uri.fsPath);
let selectedFolder: ContentFolder | null = null;
for (const folder of foldersWithPath) {
const folderPath = parseWinPath(folder.path);
if (filePath.startsWith(folderPath)) {
if (!selectedFolder || selectedFolder.path.length < folderPath.length) {
selectedFolder = folder;
}
}
}
if (selectedFolder) {
pathname = selectedFolder.previewPath;
}
}
// Check if there is a pathname defined on content type level
if (article?.data) {
const contentType = ArticleHelper.getContentType(article.data);
if (contentType && contentType.previewPath) {
@@ -58,10 +82,18 @@ export class Preview {
}
}
// Make sure there are no backslashes in the slug
slug = parseWinPath(slug);
// Verify if the slug doesn't end with _index or index
if (slug.endsWith('_index') || slug.endsWith('index')) {
slug = slug.substring(0, slug.endsWith('_index') ? slug.length - 6 : slug.length - 5);
}
// Create the preview webview
const webView = window.createWebviewPanel(
'frontMatterPreview',
'FrontMatter Preview',
article?.data?.title ? `Preview: ${article?.data?.title}` : 'FrontMatter Preview',
{
viewColumn: ViewColumn.Beside,
preserveFocus: true

View File

@@ -1,3 +1,4 @@
import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType';
import { Telemetry } from './../helpers/Telemetry';
import { workspace, Uri } from "vscode";
import { join } from "path";
@@ -6,7 +7,7 @@ import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { FrameworkDetector, Logger, Settings } from "../helpers";
import { SETTING_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { SettingsListener } from '../listeners/dashboard';
export class Project {
@@ -35,6 +36,9 @@ categories: []
try {
Settings.createTeamSettings();
// Add the default content type
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true);
if (sampleTemplate !== undefined) {
await Project.createSampleTemplate();
} else {

View File

@@ -1,10 +1,9 @@
import { TaxonomyHelper } from './../helpers/TaxonomyHelper';
import * as vscode from 'vscode';
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 { FrontMatterParser } from '../parsers';
import { DumpOptions } from 'js-yaml';
import { Notifications } from '../helpers/Notifications';
export class Settings {
@@ -15,7 +14,7 @@ export class Settings {
* @param type
*/
public static async create(type: TaxonomyType) {
const newOption = await vscode.window.showInputBox({
const newOption = await vscode.window.showInputBox({
prompt: `Insert the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} that you want to add to your configuration.`,
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
ignoreFocusOut: true
@@ -76,7 +75,7 @@ export class Settings {
*/
public static async export() {
// Retrieve all the Markdown files
const allMdFiles = await FilesHelper.getMdFiles();
const allMdFiles = await FilesHelper.getAllFiles();
if (!allMdFiles) {
return;
}
@@ -152,11 +151,13 @@ export class Settings {
const taxType = await vscode.window.showQuickPick([
"Tag",
"Category"
], {
], {
title: `Remap`,
placeHolder: `What do you want to remap?`,
canPickMany: false,
ignoreFocusOut: true
});
if (!taxType) {
return;
}
@@ -196,76 +197,10 @@ export class Settings {
}
}
// Retrieve all the markdown files
const allMdFiles = await FilesHelper.getMdFiles();
if (!allMdFiles) {
return;
if (newOptionValue) {
TaxonomyHelper.process("edit", type, selectedOption, newOptionValue);
} else {
TaxonomyHelper.process("delete", type, selectedOption, undefined);
}
let progressText = `${EXTENSION_NAME}: Remapping "${selectedOption}" ${type === TaxonomyType.Tag ? "tag" : "category"} to "${newOptionValue}".`;
if (!newOptionValue) {
progressText = `${EXTENSION_NAME}: Deleting "${selectedOption}" ${type === TaxonomyType.Tag ? "tag" : "category"}.`;
}
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: progressText,
cancellable: false
}, async (progress) => {
// Set the initial progress
const progressNr = allMdFiles.length/100;
progress.report({ increment: 0});
const matterProp: string = type === TaxonomyType.Tag ? "tags" : "categories";
let i = 0;
for (const file of allMdFiles) {
progress.report({ increment: (++i/progressNr) });
const mdFile = fs.readFileSync(file.path, { encoding: "utf8" });
if (mdFile) {
try {
const article = FrontMatterParser.fromFile(mdFile);
if (article && article.data) {
const { data } = article;
let taxonomies: string[] = data[matterProp];
if (taxonomies && taxonomies.length > 0) {
const idx = taxonomies.findIndex(o => o === selectedOption);
if (idx !== -1) {
if (newOptionValue) {
taxonomies[idx] = newOptionValue;
} else {
taxonomies = taxonomies.filter(o => o !== selectedOption);
}
data[matterProp] = [...new Set(taxonomies)].sort();
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
// Update the file
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any), { encoding: "utf8" });
}
}
}
} catch (e) {
// Continue with the next file
}
}
}
// Update the settings
const idx = options.findIndex(o => o === selectedOption);
if (newOptionValue) {
// Add or update the new option
if (idx !== -1) {
options[idx] = newOptionValue;
} else {
options.push(newOptionValue);
}
} else {
// Remove the selected option
options = options.filter(o => o !== selectedOption);
}
await SettingsHelper.updateTaxonomy(type, options);
Notifications.info(`${newOptionValue ? "Remapping" : "Deleation"} of the ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"} completed.`);
});
}
}

View File

@@ -1,11 +1,13 @@
import { CONTEXT, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
import { ParsedFrontMatter } from './../parsers/FrontMatterParser';
import { CONTEXT, NOTIFICATION_TYPE, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
import * as vscode from 'vscode';
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
import { ArticleHelper, Notifications, SeoHelper, Settings } from '../helpers';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DefaultFields } from '../constants';
import { ContentType } from '../helpers/ContentType';
import { DataListener } from '../listeners/panel';
import { commands } from 'vscode';
import { Field } from '../models';
export class StatusListener {
@@ -42,7 +44,7 @@ export class StatusListener {
}
}
// Check SEO for title and description length
// Check SEO and required fields
if (article && article.data) {
collection.clear();
@@ -58,6 +60,9 @@ export class StatusListener {
if (article.data[fieldName] && descLength > -1) {
SeoHelper.checkLength(editor, collection, article, fieldName, descLength);
}
// Check the required fields
StatusListener.verifyRequiredFields(editor, article, collection);
}
const panel = ExplorerView.getInstance();
@@ -80,4 +85,98 @@ export class StatusListener {
frontMatterSB.hide();
}
/**
* Verify the required fields
* @param article
* @param collection
*/
private static verifyRequiredFields(editor: vscode.TextEditor, article: ParsedFrontMatter, collection: vscode.DiagnosticCollection) {
// Check for missing fields
const emptyFields = ContentType.findEmptyRequiredFields(article);
const fieldsToReport = [];
if (emptyFields && emptyFields.length > 0) {
const text = editor.document.getText();
const markdown = ArticleHelper.stringifyFrontMatter("", article.data);
const editorSpaces = vscode.window.activeTextEditor?.options?.tabSize;
const requiredDiagnostics: vscode.Diagnostic[] = [];
for (const fields of emptyFields) {
let txtIdx = -1;
let fieldName = "";
let level = 0;
for (const field of fields) {
const totalSpaces = level * (typeof editorSpaces === "string" ? parseInt(editorSpaces) : editorSpaces || 2);
const crntIdx = StatusListener.findFieldLine(text, txtIdx, totalSpaces, field);
if (crntIdx && crntIdx > txtIdx) {
txtIdx = crntIdx;
fieldName = field.name;
}
++level;
}
if (txtIdx !== -1 && txtIdx < markdown.length) {
fieldsToReport.push(fields.map(f => f.title).join("/"));
const posStart = editor.document.positionAt(txtIdx);
const posEnd = editor.document.positionAt(txtIdx + 1 + fieldName.length);
const diagnostic: vscode.Diagnostic = {
code: '',
message: `This ${fields.map(f => f.name).join("/")} field is required to contain a value.`,
range: new vscode.Range(posStart, posEnd),
severity: vscode.DiagnosticSeverity.Error,
source: 'Front Matter'
};
requiredDiagnostics.push(diagnostic);
}
}
if (collection.has(editor.document.uri)) {
const otherDiag = collection.get(editor.document.uri) || [];
collection.set(editor.document.uri, [...otherDiag, ...requiredDiagnostics]);
} else {
collection.set(editor.document.uri, [...requiredDiagnostics]);
}
if (fieldsToReport.length > 0) {
Notifications.showIfNotDisabled(NOTIFICATION_TYPE.requiredFieldValidation, "ERROR_ONCE", `The following fields are required to contain a value: ${fieldsToReport.join(", ")}`);
}
}
}
/**
* Find the line of the field
* @param text
* @param startIdx
* @param totalSpaces
* @param field
* @returns
*/
private static findFieldLine(text: string, startIdx: number, totalSpaces: number, field: Field): number | undefined {
const crntIdx = text.indexOf(field.name, startIdx === -1 ? 0 : startIdx);
if (crntIdx > -1) {
// Find the linebreak before the current index
const txtFromStart = text.substring(0, crntIdx);
const splitLineBreaks = txtFromStart.split(/\r?\n/);
const lastLine = splitLineBreaks[splitLineBreaks.length - 1];
if (lastLine.length === totalSpaces) {
if (crntIdx > startIdx) {
return crntIdx;
}
} else {
return StatusListener.findFieldLine(text, crntIdx + field.name.length, totalSpaces, field);
}
}
return;
}
}

View File

@@ -6,9 +6,7 @@ import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEv
import { ArticleHelper, Settings } from '../helpers';
import { Article } from '.';
import { Notifications } from '../helpers/Notifications';
import { CONTEXT } from '../constants';
import { Project } from './Project';
import { Folders } from './Folders';
import { ContentType } from '../helpers/ContentType';
import { ContentType as IContentType } from '../models';
import { PagesListener } from '../listeners/dashboard';
@@ -17,39 +15,6 @@ import { Telemetry } from '../helpers/Telemetry';
export class Template {
/**
* Check if the template folder is available
*/
public static async init() {
const isInitialized = await Template.isInitialized();
await vscode.commands.executeCommand('setContext', CONTEXT.canInit, !isInitialized);
if (isInitialized) {
await vscode.commands.executeCommand('setContext', CONTEXT.initialized, true);
}
}
/**
* Check if the project is already initialized
*/
public static async isInitialized() {
const wsFolder = Folders.getWorkspaceFolder();
const folder = Template.getSettings();
if (!folder || !wsFolder) {
return false;
}
const templatePath = vscode.Uri.file(path.join(wsFolder.fsPath, folder));
try {
await vscode.workspace.fs.stat(templatePath);
return true;
} catch (e) {
return false;
}
}
/**
* Generate a template
*/
@@ -62,7 +27,8 @@ export class Template {
const article = ArticleHelper.getFrontMatter(editor);
const clonedArticle = Object.assign({}, article);
const titleValue = await vscode.window.showInputBox({
const titleValue = await vscode.window.showInputBox({
title: `Template title`,
prompt: `What name would you like to give your template?`,
placeHolder: `article`,
ignoreFocusOut: true
@@ -76,6 +42,7 @@ export class Template {
const keepContents = await vscode.window.showQuickPick(
["yes", "no"],
{
title: `Keep contents`,
canPickMany: false,
placeHolder: `Do you want to keep the contents for the template?`,
ignoreFocusOut: true
@@ -132,6 +99,7 @@ export class Template {
}
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
title: `Select a template`,
placeHolder: `Select the content template to use`,
ignoreFocusOut: true
});
@@ -175,7 +143,7 @@ export class Template {
}
if (frontMatter.data) {
frontMatter.data = ArticleHelper.updatePlaceholders(frontMatter.data, titleValue);
frontMatter.data = await ArticleHelper.updatePlaceholders(frontMatter.data, titleValue, newFilePath);
frontMatter = Article.updateDate(frontMatter);

View File

@@ -1,4 +1,4 @@
import { commands, window, Selection, QuickPickItem } from "vscode";
import { commands, window, Selection, QuickPickItem, TextEditor } from "vscode";
import { COMMAND_NAME, CONTEXT, SETTING_CONTENT_WYSIWYG } from "../constants";
import { Settings } from "../helpers";
@@ -12,7 +12,8 @@ enum MarkupType {
heading,
unorderedList,
orderedList,
taskList
taskList,
hyperlink,
}
export class Wysiwyg {
@@ -46,6 +47,9 @@ export class Wysiwyg {
subscriptions.push(commands.registerCommand(COMMAND_NAME.orderedlist, () => this.addMarkup(MarkupType.orderedList)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.taskList, () => this.addMarkup(MarkupType.taskList)));
// Other markup
subscriptions.push(commands.registerCommand(COMMAND_NAME.hyperlink, () => this.addMarkup(MarkupType.hyperlink)));
// Options
subscriptions.push(commands.registerCommand(COMMAND_NAME.options, async () => {
const qpItems: QuickPickItem[] = [
@@ -55,9 +59,11 @@ export class Wysiwyg {
{ label: "$(code) Code", detail: "Add inline code snippet", alwaysShow: true },
{ label: "$(symbol-namespace) Code block", detail: "Add a code block", alwaysShow: true },
{ label: "$(quote) Blockquote", detail: "Add a blockquote", alwaysShow: true },
{ label: "$(symbol-text) Strikethrough", detail: "Add a strikethrough", alwaysShow: true },
]
const option = await window.showQuickPick([ ...qpItems ], {
const option = await window.showQuickPick([ ...qpItems ], {
title: "WYSIWYG Options",
placeHolder: "Which type of markup would you like to insert?",
canPickMany: false,
ignoreFocusOut: true
@@ -76,6 +82,8 @@ export class Wysiwyg {
await this.addMarkup(MarkupType.codeblock);
} else if (option.label === qpItems[5].label) {
await this.addMarkup(MarkupType.blockquote);
} else if (option.label === qpItems[6].label) {
await this.addMarkup(MarkupType.strikethrough);
}
}
}));
@@ -95,6 +103,10 @@ export class Wysiwyg {
const selection = editor.selection;
const hasTextSelection = !selection.isEmpty;
if (type === MarkupType.hyperlink) {
return this.addHyperlink(editor, selection);
}
const markers = this.getMarkers(type);
if (!markers) {
return;
@@ -129,6 +141,51 @@ export class Wysiwyg {
}
}
/**
* Add a hyperlink to the content
* @returns void
*/
private static async addHyperlink(editor: TextEditor, selection: Selection) {
const hasTextSelection = !selection.isEmpty;
const linkText = hasTextSelection ? editor.document.getText(selection) : "";
const link = await window.showInputBox({
title: "WYSIWYG Hyperlink",
placeHolder: "Enter the URL",
prompt: "Enter the URL",
value: linkText,
ignoreFocusOut: true
});
const text = await window.showInputBox({
title: "WYSIWYG Text",
prompt: "Enter the text for the hyperlink",
placeHolder: "Enter the text for the hyperlink",
value: linkText,
ignoreFocusOut: true
});
if (link) {
const txt = `[${text || link}](${link})`;
if (hasTextSelection) {
editor.edit(builder => {
builder.replace(selection, txt);
});
} else {
const crntSelection = selection.active;
const markerLength = txt.length;
const newPosition = crntSelection.with(crntSelection.line, crntSelection.character + markerLength);
await editor.edit(builder => {
builder.insert(newPosition, txt);
});
editor.selection = new Selection(newPosition, newPosition);
}
}
}
/**
* Check if the text will be wrapped
* @param type
@@ -159,7 +216,8 @@ export class Wysiwyg {
"Heading 4",
"Heading 5",
"Heading 6"
], {
], {
title: "Heading Level",
canPickMany: false,
placeHolder: "Which heading level do you want to insert?",
ignoreFocusOut: true

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
export interface IMergeIconProps {
className: string;
}
export const MergeIcon: React.FunctionComponent<IMergeIconProps> = ({className}: React.PropsWithChildren<IMergeIconProps>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className={className}>
<path xmlns="http://www.w3.org/2000/svg" d="M7.586 8.00366L4 8.00366C3.44772 8.00366 3 7.55595 3 7.00366C3 6.45138 3.44772 6.00366 4 6.00366L8 6.00366C8.26509 6.00366 8.51933 6.10892 8.70685 6.2963L13.414 11H18.5845L15.2931 7.71103C14.9025 7.32065 14.9023 6.68748 15.2926 6.29681C15.683 5.90615 16.3162 5.90592 16.7068 6.2963L21.7068 11.2926C21.8945 11.4802 22 11.7346 22 11.9998C22 12.2651 21.8947 12.5195 21.7071 12.7071L16.7071 17.7071C16.3166 18.0976 15.6834 18.0976 15.2929 17.7071C14.9024 17.3166 14.9024 16.6834 15.2929 16.2929L18.5858 13H13.4142L8.70711 17.7071C8.51957 17.8947 8.26522 18 8 18H4C3.44772 18 3 17.5523 3 17C3 16.4477 3.44772 16 4 16H7.58579L11.5855 12.0003L7.586 8.00366Z" fill="currentcolor"/>
</svg>
);
};

View File

@@ -0,0 +1,5 @@
export const DefaultFieldValues = {
faultyCustomPlaceholder: "<failed to process>"
}

View File

@@ -32,6 +32,7 @@ export const COMMAND_NAME = {
dashboardMedia: getCommandName("dashboard.media"),
dashboardSnippets: getCommandName("dashboard.snippets"),
dashboardData: getCommandName("dashboard.data"),
dashboardTaxonomy: getCommandName("dashboard.taxonomy"),
dashboardClose: getCommandName("dashboard.close"),
promote: getCommandName("promoteSettings"),
createFolder: getCommandName("createFolder"),
@@ -55,10 +56,20 @@ export const COMMAND_NAME = {
unorderedlist: getCommandName("markup.unorderedlist"),
orderedlist: getCommandName("markup.orderedlist"),
taskList: getCommandName("markup.tasklist"),
hyperlink: getCommandName("markup.hyperlink"),
options: getCommandName("markup.options"),
// Content types
generateContentType: getCommandName("contenttype.generate"),
addMissingFields: getCommandName("contenttype.addMissingFields"),
setContentType: getCommandName("contenttype.setContentType"),
// Git
gitSync: getCommandName("git.sync"),
// Authenticate
authenticate: getCommandName("authenticate"),
// Config
reloadConfig: getCommandName("config.reload"),
};

View File

@@ -17,6 +17,9 @@ export const FEATURE_FLAG = {
},
data: {
view: "dashboard.data.view",
},
taxonomy: {
view: "dashboard.taxonomy.view"
}
}
};

View File

@@ -3,8 +3,11 @@
export const GeneralCommands = {
toWebview: {
setMode: "setMode",
gitSyncingStart: "gitSyncingStart",
gitSyncingEnd: "gitSyncingEnd",
},
toVSCode: {
openLink: "openLink",
gitSync: "gitSync",
}
};

View File

@@ -2,5 +2,7 @@ export const GITHUB_LINK = "https://github.com/estruyf/vscode-front-matter";
export const ISSUE_LINK = "https://github.com/estruyf/vscode-front-matter/issues";
export const SPONSOR_LINK = "https://github.com/sponsors/estruyf";
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";
export const DOCUMENTATION_LINK = "https://frontmatter.codes/docs";
export const DOCUMENTATION_SETTINGS_LINK = "https://frontmatter.codes/docs/settings";
export const SENTRY_LINK = "https://1ac45704bbe74264a7b4674bdc2abf48@o1022172.ingest.sentry.io/5988293";

View File

@@ -0,0 +1,5 @@
export const NOTIFICATION_TYPE = {
requiredFieldValidation: "requiredFieldValidation",
}

View File

@@ -10,6 +10,7 @@ export const TelemetryEvent = {
openMediaDashboard: 'openMediaDashboard',
openDataDashboard: 'openDataDashboard',
openSnippetsDashboard: 'openSnippetsDashboard',
openTaxonomyDashboard: 'openTaxonomyDashboard',
closeDashboard: 'closeDashboard',
// Other actions
@@ -21,6 +22,7 @@ export const TelemetryEvent = {
uploadMedia: 'uploadMedia',
refreshMedia: 'refreshMedia',
deleteMedia: 'deleteMedia',
insertContentSnippet: 'insertContentSnippet',
insertMediaToContent: 'insertMediaToContent',
insertFileToContent: 'insertFileToContent',
updateMediaMetadata: 'updateMediaMetadata',
@@ -41,4 +43,8 @@ export const TelemetryEvent = {
webviewDataView: 'webviewDataView',
webviewContentsView: 'webviewContentsView',
webviewSnippetsView: 'webviewSnippetsView',
webviewTaxonomyDashboard: 'webviewTaxonomyDashboard',
// Git
gitSync: 'gitSync',
};

View File

@@ -1,6 +1,4 @@
export const CONTEXT = {
canInit: "frontMatter:CanInit",
initialized: "frontMatter:Initialized",
canOpenPreview: "frontMatter:CanOpenPreview",
canOpenDashboard: "frontMatter:CanOpenDashboard",
isEnabled: "frontMatter:enabled",
@@ -13,4 +11,6 @@ export const CONTEXT = {
isSnippetsDashboardEnabled: "frontMatter:dashboard:snippets:enabled",
isDataDashboardEnabled: "frontMatter:dashboard:data:enabled",
isGitEnabled: "frontMatter:git:enabled",
};

View File

@@ -1,4 +1,5 @@
export * from './ContentType';
export * from './DefaultFieldValues';
export * from './DefaultFields';
export * from './DefaultFileTypes';
export * from './Extension';
@@ -9,6 +10,7 @@ export * from './GeneralCommands';
export * from './Links';
export * from './LocalStore';
export * from './Navigation';
export * from './NotificationType';
export * from './PreviewCommands';
export * from './TelemetryEvent';
export * from './charCode';

View File

@@ -3,6 +3,7 @@ export const EXTENSION_NAME = "Front Matter";
export const CONFIG_KEY = "frontMatter";
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
export const SETTING_GLOBAL_NOTIFICATIONS_DISABLED = "global.disabledNotifications";
export const SETTING_GLOBAL_MODES = "global.modes";
export const SETTING_GLOBAL_ACTIVE_MODE = "global.activeMode";
@@ -59,10 +60,14 @@ export const SETTING_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
export const SETTING_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
export const SETTING_CONTENT_HIDE_FRONTMATTER = "content.hideFm";
export const SETTING_CONTENT_HIDE_FRONTMATTER_MESSAGE = "content.hideFmMessage";
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
export const SETTING_DASHBOARD_CONTENT_PAGINATION = "dashboard.content.pagination";
export const SETTING_DATA_FILES = "data.files";
export const SETTING_DATA_FOLDERS = "data.folders";
@@ -75,6 +80,9 @@ export const SETTING_FRAMEWORK_START = "framework.startCommand";
export const SETTING_SITE_BASEURL = "site.baseURL";
export const SETTING_GIT_ENABLED = "git.enabled";
export const SETTING_GIT_COMMIT_MSG = "git.commitMessage";
/**
* @deprecated
*/

View File

@@ -8,4 +8,7 @@ export enum DashboardCommand {
mediaUpdate = "mediaUpdate",
dataFileEntries = "dataFileEntries",
searchReady = "searchReady",
// Taxonomy dashboard
setTaxonomyData = "setTaxonomyData",
}

View File

@@ -41,6 +41,16 @@ export enum DashboardMessage {
addSnippet = 'addSnippet',
updateSnippet = 'updateSnippet',
// Taxonomy dashboard
getTaxonomyData = 'getTaxonomyData',
editTaxonomy = "editTaxonomy",
mergeTaxonomy = "mergeTaxonomy",
deleteTaxonomy = "deleteTaxonomy",
addToTaxonomy = "addToTaxonomy",
createTaxonomy = "createTaxonomy",
importTaxonomy = "importTaxonomy",
moveTaxonomy = "moveTaxonomy",
// Other
getTheme = 'getTheme',
updateSetting = 'updateSetting',

View File

@@ -0,0 +1,90 @@
import * as React from 'react';
import { Spinner } from './Spinner';
import useMessages from '../hooks/useMessages';
import useDarkMode from '../../hooks/useDarkMode';
import { WelcomeScreen } from './WelcomeScreen';
import { useRecoilValue } from 'recoil';
import { DashboardViewSelector, ModeAtom } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { DataView } from './DataView';
import { Snippets } from './SnippetsView/Snippets';
import { FEATURE_FLAG } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { TaxonomyView } from './TaxonomyView';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { routePaths } from '..';
import { useEffect, useMemo } from 'react';
import { UnknownView } from './UnknownView';
export interface IAppProps {
showWelcome: boolean;
}
export const App: React.FunctionComponent<IAppProps> = ({showWelcome}: React.PropsWithChildren<IAppProps>) => {
const { loading, pages, settings } = useMessages();
const view = useRecoilValue(DashboardViewSelector);
const mode = useRecoilValue(ModeAtom);
const navigate = useNavigate();
useDarkMode();
const viewState: any = Messenger.getState() || {};
const isAllowed = (features: string[], flag: string) => {
if (!features ||( features.length > 0 && !features.includes(flag))) {
return false;
}
return true;
}
const allowDataView = useMemo(() => {
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.data.view)
}, [mode?.features]);
const allowTaxonomyView = useMemo(() => {
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.taxonomy.view)
}, [mode?.features]);
useEffect(() => {
if (view && routePaths[view]) {
navigate(routePaths[view]);
return;
}
navigate(routePaths[view]);
}, [view]);
if (!settings) {
return <Spinner />;
}
if (showWelcome || viewState.isWelcomeConfiguring) {
return <WelcomeScreen settings={settings} />;
}
if (!settings.initialized || settings.contentFolders?.length === 0) {
return <WelcomeScreen settings={settings} />;
}
return (
<main className={`h-full w-full`}>
<Routes>
<Route path={routePaths.welcome} element={<WelcomeScreen settings={settings} />} />
<Route path={routePaths.contents} element={<Contents pages={pages} loading={loading} />} />
<Route path={routePaths.media} element={<Media />} />
<Route path={routePaths.snippets} element={<Snippets />} />
{
allowDataView && <Route path={routePaths.data} element={<DataView />} />
}
{
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
}
<Route path={`*`} element={<UnknownView />} />
</Routes>
</main>
);
};

View File

@@ -27,36 +27,40 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
{title}
</button>
<Menu as="span" className="-ml-px relative block">
<Menu.Button
className="h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
disabled={disabled}>
<span className="sr-only">Open options</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
{
choices.length > 0 && (
<Menu as="span" className="-ml-px relative block">
<Menu.Button
className="h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
disabled={disabled}>
<span className="sr-only">Open options</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<MenuItems widthClass={`w-56`}>
<div className="py-1">
{choices.map((choice, idx) => (
<MenuItem
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} />
))}
</div>
</MenuItems>
</Menu>
<MenuItems widthClass={`w-56`} disablePopper>
<div className="py-1">
{choices.map((choice, idx) => (
<MenuItem
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} />
))}
</div>
</MenuItems>
</Menu>
)
}
</span>
);
};

View File

@@ -6,17 +6,27 @@ import { CustomScript, ScriptType } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
import { Alert } from '../Modals/Alert';
import { usePopper } from 'react-popper';
import { useState } from 'react';
export interface IContentActionsProps {
title: string;
path: string;
scripts: CustomScript[] | undefined;
listView?: boolean;
onOpen: () => void;
}
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen }: React.PropsWithChildren<IContentActionsProps>) => {
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen, listView }: React.PropsWithChildren<IContentActionsProps>) => {
const [ showDeletionAlert, setShowDeletionAlert ] = React.useState(false);
const [referenceElement, setReferenceElement] = useState<any>(null);
const [popperElement, setPopperElement] = useState<any>(null);
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
placement: listView ? 'right-start' : 'bottom-end',
strategy: 'fixed'
})
const onView = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onOpen();
@@ -40,7 +50,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
}, [path]);
const customScriptActions = React.useMemo(() => {
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk && !script.hidden).map(script => (
<MenuItem
key={script.title}
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
@@ -50,37 +60,45 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
return (
<>
<div className={`group-scope absolute top-6 right-0 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full p-2 -mt-4">
<div className={`${listView ? '' : 'group-scope absolute top-6 right-0'} flex flex-col space-y-4`}>
<div className={`flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full ${listView ? '' : 'p-2 -mt-4'}`}>
<Menu as="div" className="relative z-10 flex text-left">
<div className='hidden group-scope-hover:flex'>
<QuickAction
title={`View content`}
onClick={onView}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete content`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<Menu as="div" className={`relative flex text-left ${listView ? '' : 'z-10'}`}>
{
!listView && (
<div className='hidden group-scope-hover:flex'>
<QuickAction
title={`View content`}
onClick={onView}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction
title={`Delete content`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
)
}
<div ref={setReferenceElement} className={`flex`}>
<ActionMenuButton title={`Menu`} />
</div>
<ActionMenuButton title={`Menu`} />
<div className='menu_items__wrapper z-20' ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<MenuItems updatePopper={forceUpdate || undefined} widthClass='w-44' marginTopClass={listView ? '' : ''}>
<MenuItem
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
onClick={(value, e) => onView(e)} />
<MenuItems widthClass='w-44' marginTopClass='mt-6'>
<MenuItem
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
onClick={(value, e) => onView(e)} />
{ customScriptActions }
{ customScriptActions }
<MenuItem
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
onClick={(value, e) => onDelete(e)} />
</MenuItems>
<MenuItem
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
onClick={(value, e) => onDelete(e)} />
</MenuItems>
</div>
</Menu>
</div>
</div>

View File

@@ -30,22 +30,31 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
}
const tagField = settings.dashboardState.contents.tags;
const tagsValue = pageData[tagField] || [];
let tagsValue = [];
if (Array.isArray(tagsValue)) {
if (tagField === "tags") {
tagsValue = pageData.fmTags;
} else if (tagField === "categories") {
tagsValue = pageData.fmCategories;
} else {
tagsValue = pageData[tagField] || [];
}
if (typeof tagsValue === "string") {
return [tagsValue];
} else if (Array.isArray(tagsValue)) {
return tagsValue;
}
return [tagsValue];
return [];
}, [settings, pageData]);
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 shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 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">
<div className={`group 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 shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50`}>
<button onClick={openFile} className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200 cursor-pointer">
{
pageData[PREVIEW_IMAGE_FIELD] ? (
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
@@ -55,7 +64,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
</div>
)
}
</div>
</button>
<div className="relative p-4 w-full">
<div className={`flex justify-between items-center`}>
@@ -70,35 +79,48 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
onOpen={openFile} />
</div>
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<button onClick={openFile} className={`text-left`}><h2 className="mt-2 mb-2 font-bold">{title}</h2></button>
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p>
<button onClick={openFile} className={`text-left`}><p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p></button>
{
tags && tags.length > 0 && (
<div className="mt-2">
{
tags.map((tag, index) => (
<span
key={index}
className="inline-block mr-1 mt-1 text-[#5D561D] dark:text-[#F0ECD0] text-xs">
#{tag}
</span>
tag && (
<span
key={index}
className="inline-block mr-1 mt-1 text-[#5D561D] dark:text-[#F0ECD0] text-xs">
#{tag}
</span>
)
))
}
</div>
)
}
</div>
</button>
</div>
</li>
);
} 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}>
<div className="col-span-8 font-bold truncate">
{title}
<div 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`}>
<div className="col-span-8 font-bold truncate flex items-center space-x-4">
<button
title={`Open: ${title}`}
onClick={openFile}>
{title}
</button>
<ContentActions
title={title}
path={fmFilePath}
scripts={settings?.scripts}
onOpen={openFile}
listView />
</div>
<div className="col-span-2">
<DateField value={date} />
@@ -106,10 +128,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<div className="col-span-2">
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
</div>
</button>
</div>
</li>
);
}
return null;
};
};

View File

@@ -1,13 +1,15 @@
import { Disclosure } from '@headlessui/react';
import {ChevronRightIcon} from '@heroicons/react/solid';
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { groupBy } from '../../../helpers/GroupBy';
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
import { GroupOption } from '../../constants/GroupOption';
import { Page } from '../../models/Page';
import { Settings } from '../../models/Settings';
import { GroupingSelector } from '../../state';
import { GroupingSelector, PageAtom } from '../../state';
import { PAGE_LIMIT } from '../Header/Pagination';
import { Item } from './Item';
import { List } from './List';
@@ -18,6 +20,23 @@ export interface IOverviewProps {
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settings}: React.PropsWithChildren<IOverviewProps>) => {
const grouping = useRecoilValue(GroupingSelector);
const page = useRecoilValue(PageAtom);
const pagedPages = useMemo(() => {
if (settings?.dashboardState.contents.pagination) {
return pages.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT));
}
return pages;
}, [pages, page, settings]);
const groupName = useCallback((groupId, groupedPages) => {
if (grouping === GroupOption.Draft) {
return `${groupId} (${groupedPages[groupId].length})`;
}
return `${GroupOption[grouping]}: ${groupId} (${groupedPages[groupId].length})`;
}, [grouping])
if (!pages || !pages.length) {
return (
@@ -58,7 +77,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settin
<ChevronRightIcon
className={`w-8 h-8 mr-1 ${open ? "transform rotate-90" : ""}`}
/>
{GroupOption[grouping]}: {groupId} ({groupedPages[groupId].length})
{ groupName(groupId, groupedPages) }
</h2>
</Disclosure.Button>
@@ -80,7 +99,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settin
return (
<List>
{pages.map((page, idx) => (
{pagedPages.map((page, idx) => (
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>

View File

@@ -1,72 +0,0 @@
import * as React from 'react';
import { Spinner } from './Spinner';
import useMessages from '../hooks/useMessages';
import useDarkMode from '../../hooks/useDarkMode';
import { WelcomeScreen } from './WelcomeScreen';
import { useRecoilValue } from 'recoil';
import { DashboardViewSelector, ModeAtom } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { NavigationType } from '../models';
import { DataView } from './DataView';
import { Snippets } from './SnippetsView/Snippets';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IDashboardProps {
showWelcome: boolean;
}
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome}: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages, settings } = useMessages();
const view = useRecoilValue(DashboardViewSelector);
const mode = useRecoilValue(ModeAtom);
useDarkMode();
const viewState: any = Messenger.getState() || {};
if (!settings) {
return <Spinner />;
}
if (showWelcome || viewState.isWelcomeConfiguring) {
return <WelcomeScreen settings={settings} />;
}
if (!settings.initialized || settings.contentFolders?.length === 0) {
return <WelcomeScreen settings={settings} />;
}
if (view === NavigationType.Snippets) {
return (
<main className={`h-full w-full`}>
<Snippets />
</main>
);
}
if (view === NavigationType.Media) {
return (
<main className={`h-full w-full`}>
<Media />
</main>
);
}
if (view === NavigationType.Data) {
return (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.data.view}>
<main className={`h-full w-full`}>
<DataView />
</main>
</FeatureFlag>
);
}
return (
<main className={`h-full w-full`}>
<Contents pages={pages} loading={loading} />
</main>
);
};

View File

@@ -20,6 +20,7 @@ import { ToastContainer, toast, Slide } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { DataType } from '../../../models/DataType';
import { TelemetryEvent } from '../../../constants';
import { NavigationItem } from '../Layout';
export interface IDataViewProps {}
@@ -150,14 +151,13 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
{
(dataFiles && dataFiles.length > 0) && (
dataFiles.map((dataFile, idx) => (
<button
<NavigationItem
key={`${dataFile.id}-${idx}`}
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'}`}
isSelected={selectedData?.id === dataFile.id}
onClick={() => setSchema(dataFile)}>
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
<span>{dataFile.title}</span>
</button>
</NavigationItem>
)
))
}
@@ -172,7 +172,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
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>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData?.title?.toLowerCase() || ""} data items</h2>
<div className='py-4'>
{
@@ -245,4 +245,4 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
<ToastContainer />
</div>
);
};
};

View File

@@ -38,14 +38,18 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
}
}
let valid = false;
for (let i = 0; i < contentFolders.length; i++) {
const folder = contentFolders[i];
const contentFolder = parseWinPath(folder.path) as string;
const relContentPath = folderPath.replace(contentFolder, '');
return relContentPath.length > 1 && folderPath.startsWith(contentFolder);
if (!valid) {
valid = relContentPath.length > 1 && folderPath.startsWith(contentFolder);
}
}
return false;
return valid;
};
if (!selectedFolder) {

View File

@@ -4,6 +4,8 @@ import { useRecoilValue, useResetRecoilState } from 'recoil';
import { FolderSelector, TagSelector, CategorySelector, SortingAtom, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
import { DefaultValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
export const guardRecoilDefaultValue = (
candidate: any
@@ -34,7 +36,7 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props:
resetCategory();
};
React.useEffect(() => {
useEffect(() => {
if (folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
setShow(true);
} else {

View File

@@ -29,7 +29,7 @@ export const Filter: React.FunctionComponent<IFilterProps> = ({label, activeItem
)}
title={activeItem || DEFAULT_VALUE} />
<MenuItems>
<MenuItems disablePopper>
<MenuItem
title={DEFAULT_VALUE}
value={null}

View File

@@ -22,7 +22,7 @@ export const Folders: React.FunctionComponent<IFoldersProps> = ({}: React.PropsW
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Showing`} title={crntFolder || DEFAULT_TYPE} />
<MenuItems>
<MenuItems disablePopper>
<MenuItem
title={DEFAULT_TYPE}
value={null}

View File

@@ -7,7 +7,7 @@ import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface IGroupingProps {}
export const groupOptions = [
export const GROUP_OPTIONS = [
{ name: "None", id: GroupOption.none },
{ name: "Year", id: GroupOption.Year },
{ name: "Draft/Published", id: GroupOption.Draft },
@@ -16,15 +16,15 @@ export const groupOptions = [
export const Grouping: React.FunctionComponent<IGroupingProps> = ({}: React.PropsWithChildren<IGroupingProps>) => {
const [ group, setGroup ] = useRecoilState(GroupingAtom);
const crntGroup = groupOptions.find(x => x.id === group);
const crntGroup = GROUP_OPTIONS.find(x => x.id === group);
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Group by`} title={crntGroup?.name || ""} />
<MenuItems>
{groupOptions.map((option) => (
<MenuItems disablePopper>
{GROUP_OPTIONS.map((option) => (
<MenuItem
key={option.id}
title={option.name}

View File

@@ -9,8 +9,8 @@ import { Startup } from '../Startup';
import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
import { CategoryAtom, GroupingSelector, SortingAtom, TagAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -19,6 +19,12 @@ import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
import { Tabs } from './Tabs';
import { CustomScript } from '../../../models';
import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline';
import { useLocation, useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import { useEffect, useMemo } from 'react';
import { SyncButton } from './SyncButton';
import { PAGE_LIMIT, Pagination } from './Pagination';
import { GroupOption } from '../../constants/GroupOption';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -34,8 +40,10 @@ export interface IHeaderProps {
export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPages, folders, settings }: React.PropsWithChildren<IHeaderProps>) => {
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
const [ view, setView ] = useRecoilState(DashboardViewAtom);
const resetSorting = useResetRecoilState(SortingAtom)
const grouping = useRecoilValue(GroupingSelector);
const resetSorting = useResetRecoilState(SortingAtom);
const location = useLocation();
const navigate = useNavigate();
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
@@ -50,7 +58,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
};
const updateView = (view: NavigationType) => {
setView(view);
navigate(routePaths[view]);
resetSorting();
}
@@ -68,6 +76,59 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
onClick: () => runBulkScript(s)
}));
const choiceOptions = useMemo(() => {
const isEnabled = settings?.dashboardState?.contents?.templatesEnabled || false;
if (isEnabled) {
return [
{
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
];
}
return [];
}, [settings?.dashboardState?.contents?.templatesEnabled]);
useEffect(() => {
if (location.search) {
const searchParams = new URLSearchParams(location.search);
const taxonomy = searchParams.get("taxonomy");
const value = searchParams.get("value");
if (taxonomy && value) {
if (taxonomy === "tags") {
setCrntTag(value);
} else if (taxonomy === "categories") {
setCrntCategory(value);
}
}
return;
}
setCrntTag("");
setCrntCategory("");
}, [location.search]);
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
@@ -76,39 +137,20 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
</div>
{
view === NavigationType.Contents && (
location.pathname === routePaths.contents && (
<>
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
<Searchbox />
<div className={`flex items-center justify-end space-x-4 flex-1`}>
<Startup settings={settings} />
<SyncButton />
<ChoiceButton
title={`Create content`}
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}
choices={choiceOptions}
onClick={createContent}
disabled={!settings?.initialized} />
</div>
</div>
@@ -136,12 +178,20 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
<Sorting view={NavigationType.Contents} />
</div>
{
(settings?.dashboardState.contents.pagination) && (totalPages || 0) > PAGE_LIMIT && (!grouping || grouping === GroupOption.none) && (
<div className={`flex justify-center py-2 border-b border-gray-300 dark:border-vulcan-100`}>
<Pagination totalPages={totalPages || 0} />
</div>
)
}
</>
)
}
{
view === NavigationType.Media && (
location.pathname === routePaths.media && (
<>
<MediaHeaderTop />

View File

@@ -1,16 +1,29 @@
import * as React from 'react';
import { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { LIMIT } from '../../hooks/useMedia';
import { routePaths } from '../..';
import { MediaTotalSelector, PageAtom } from '../../state';
import { PaginationButton } from './PaginationButton';
export interface IPaginationProps {}
export interface IPaginationProps {
totalPages?: number;
}
export const Pagination: React.FunctionComponent<IPaginationProps> = (props: React.PropsWithChildren<IPaginationProps>) => {
export const PAGE_LIMIT = 16;
export const Pagination: React.FunctionComponent<IPaginationProps> = ({ totalPages }: React.PropsWithChildren<IPaginationProps>) => {
const [ page, setPage ] = useRecoilState(PageAtom);
const totalMedia = useRecoilValue(MediaTotalSelector);
const location = useLocation();
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
const totalItems: number = useMemo(() => {
if (location.pathname === routePaths.contents) {
return Math.ceil((totalPages || 0) / PAGE_LIMIT) - 1
} else {
return Math.ceil(totalMedia / PAGE_LIMIT) - 1;
}
}, [location.pathname, totalPages, totalMedia]);
const getButtons = (): number[] => {
const maxButtons = 5;
@@ -19,12 +32,16 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = (props: Rea
const end = page + maxButtons;
for (let i = start; i <= end; i++) {
if (i >= 0 && i <= totalPages) {
if (i >= 0 && i <= totalItems) {
buttons.push(i);
}
}
return buttons;
};
useEffect(() => {
setPage(0);
}, []);
return (
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
@@ -54,19 +71,19 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = (props: Rea
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`}
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 rounded-sm`}
>{button + 1}</button>
))}
<PaginationButton
title="Next"
disabled={page >= totalPages}
disabled={page >= totalItems}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalPages}
onClick={() => setPage(totalPages)} />
disabled={page >= totalItems}
onClick={() => setPage(totalItems)} />
</div>
);
};

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom } from '../../state';
import { LIMIT } from '../../hooks/useMedia';
import { PAGE_LIMIT } from './Pagination';
export interface IPaginationStatusProps {}
@@ -10,7 +10,7 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
const page = useRecoilValue(PageAtom);
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
const mediaItems = ((page + 1) * PAGE_LIMIT);
if (totalMedia < mediaItems) {
return totalMedia;
}
@@ -20,7 +20,7 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
return (
<div className="hidden sm:flex">
<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{' '}
Showing <span className="font-medium">{(page * PAGE_LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
</p>
</div>

View File

@@ -18,14 +18,17 @@ export interface ISortingProps {
}
export const sortOptions: SortingOption[] = [
{ name: "Published (asc)", id: SortOption.PublishedAsc, order: SortOrder.asc, type: SortType.date },
{ name: "Published (desc)", id: SortOption.PublishedDesc, order: SortOrder.desc, type: SortType.date },
{ name: "Last modified (asc)", id: SortOption.LastModifiedAsc, order: SortOrder.asc, type: SortType.date },
{ name: "Last modified (desc)", id: SortOption.LastModifiedDesc, order: SortOrder.desc, type: SortType.date },
{ name: "By filename (asc)", id: SortOption.FileNameAsc, order: SortOrder.asc, type: SortType.string },
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
];
const contentSortOptions: SortingOption[] = [
{ name: "Published (asc)", id: SortOption.PublishedAsc, order: SortOrder.asc, type: SortType.date },
{ name: "Published (desc)", id: SortOption.PublishedDesc, order: SortOrder.desc, type: SortType.date }
];
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 },
@@ -53,6 +56,8 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
if (view === NavigationType.Media) {
allOptions = [...allOptions, ...mediaSortOptions];
} else if (view === NavigationType.Contents) {
allOptions = [...contentSortOptions, ...allOptions];
}
allOptions = allOptions.sort(SortingHelpers.alphabetically("name"))
@@ -91,7 +96,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
<MenuItems widthClass="w-48">
<MenuItems widthClass="w-48" disablePopper>
{allOptions.map((option) => (
<MenuItem
key={option.id}

View File

@@ -0,0 +1,55 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { GeneralCommands } from '../../../constants';
import { SettingsSelector } from '../../state';
export interface ISyncButtonProps {}
export const SyncButton: React.FunctionComponent<ISyncButtonProps> = (props: React.PropsWithChildren<ISyncButtonProps>) => {
const settings = useRecoilValue(SettingsSelector);
const [ isSyncing, setIsSyncing ] = useState(false);
const pull = () => {
Messenger.send(GeneralCommands.toVSCode.gitSync);
};
const messageListener = (message: MessageEvent<EventData<any>>) => {
const { command, data } = message.data;
if (command === GeneralCommands.toWebview.gitSyncingStart) {
setIsSyncing(true);
} else if (command === GeneralCommands.toWebview.gitSyncingEnd) {
setIsSyncing(false);
}
};
useEffect(() => {
Messenger.listen(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, []);
if (!settings?.git?.actions || !settings?.git.isGitRepo) {
return null;
}
return (
<div className='git_actions'>
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
onClick={pull}
disabled={isSyncing}
>
<RefreshIcon className={`w-4 h-4 mr-2 ${isSyncing ? 'animate-reverse-spin' : ''}`} aria-hidden="true" />
<span>Sync</span>
</button>
</div>
);
};

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { NavigationType } from '../../models';
import { DashboardViewAtom } from '../../state';
export interface ITabProps {
navigationType: NavigationType;
@@ -9,11 +8,11 @@ export interface ITabProps {
}
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
const view = useRecoilValue(DashboardViewAtom);
const location = useLocation();
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"}`}
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 ${location.pathname === `/${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"

View File

@@ -1,4 +1,4 @@
import { DatabaseIcon, PhotographIcon, ScissorsIcon } from '@heroicons/react/outline';
import { DatabaseIcon, PhotographIcon, ScissorsIcon, TagIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
@@ -49,6 +49,15 @@ export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.
</Tab>
</li>
</FeatureFlag>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.taxonomy.view}>
<li className="mr-2" role="presentation">
<Tab
navigationType={NavigationType.Taxonomy}
onNavigate={onNavigate}>
<TagIcon className={`h-6 w-auto mr-2`} /><span>Taxonomies</span>
</Tab>
</li>
</FeatureFlag>
</ul>
);
};

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
export interface INavigationBarProps {
title?: string;
bottom?: JSX.Element;
}
export const NavigationBar: React.FunctionComponent<INavigationBarProps> = ({title, bottom, children}: React.PropsWithChildren<INavigationBarProps>) => {
return (
<aside className={`w-2/12 px-4 py-6 h-full flex flex-col flex-grow border-r border-gray-200 dark:border-vulcan-300`}>
{
title && <h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>{title}</h2>
}
<nav className={`flex-1 py-4 -mx-4 h-full`}>
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
<div>
{children}
</div>
</div>
</nav>
{
bottom && bottom
}
</aside>
);
};

View File

@@ -0,0 +1,17 @@
import * as React from 'react';
export interface INavigationItemProps {
isSelected?: boolean;
onClick?: () => void;
}
export const NavigationItem: React.FunctionComponent<INavigationItemProps> = ({isSelected, onClick, children}: React.PropsWithChildren<INavigationItemProps>) => {
return (
<button
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 cursor-pointer ${isSelected ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
onClick={onClick}>
{children}
</button>
);
};

View File

@@ -5,11 +5,12 @@ import { Header } from '../Header';
export interface IPageLayoutProps {
header?: React.ReactNode;
folders?: string[] | undefined
totalPages?: number | undefined
folders?: string[] | undefined;
totalPages?: number | undefined;
contentClass?: string;
}
export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header, folders, totalPages, children }: React.PropsWithChildren<IPageLayoutProps>) => {
export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header, folders, totalPages, contentClass, children }: React.PropsWithChildren<IPageLayoutProps>) => {
const settings = useRecoilValue(SettingsSelector);
return (
@@ -20,7 +21,7 @@ export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header,
totalPages={totalPages}
settings={settings} />
<div className="w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4">
<div className={contentClass || "w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4"}>
{ children }
</div>
</div>

View File

@@ -0,0 +1,3 @@
export * from './NavigationBar';
export * from './NavigationItem';
export * from './PageLayout';

View File

@@ -23,7 +23,8 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
Messenger.send(DashboardMessage.runCustomScript, {script, path: selectedFolder});
};
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder && !script.hidden);
if (scripts.length > 0) {
return (
<div className="flex flex-1 justify-end">

View File

@@ -3,7 +3,7 @@ import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TerminalIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
@@ -18,6 +18,8 @@ import { QuickAction } from '../Menu/QuickAction';
import { Alert } from '../Modals/Alert';
import { InfoDialog } from '../Modals/InfoDialog';
import { DetailsSlideOver } from './DetailsSlideOver';
import { usePopper } from 'react-popper';
import { MediaSnippetForm } from './MediaSnippetForm';
export interface IItemProps {
media: MediaInfo;
@@ -25,17 +27,27 @@ export interface IItemProps {
export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWithChildren<IItemProps>) => {
const [ , setLightbox ] = useRecoilState(LightboxAtom);
const [ showAlert, setShowAlert ] = React.useState(false);
const [ showForm, setShowForm ] = React.useState(false);
const [ showSnippetSelection, setShowSnippetSelection ] = React.useState(false);
const [ showDetails, setShowDetails ] = React.useState(false);
const [ caption, setCaption ] = React.useState(media.caption);
const [ alt, setAlt ] = React.useState(media.alt);
const [ filename, setFilename ] = React.useState<string | null>(null);
const [ showAlert, setShowAlert ] = useState(false);
const [ showForm, setShowForm ] = useState(false);
const [ showSnippetSelection, setShowSnippetSelection ] = useState(false);
const [ snippet, setSnippet ] = useState<Snippet | undefined>(undefined);
const [ showDetails, setShowDetails ] = useState(false);
const [ showSnippetFormDialog, setShowSnippetFormDialog ] = useState(false);
const [ mediaData, setMediaData ] = useState<any | undefined>(undefined);
const [ caption, setCaption ] = useState(media.caption);
const [ alt, setAlt ] = useState(media.alt);
const [ filename, setFilename ] = useState<string | null>(null);
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const [referenceElement, setReferenceElement] = useState<any>(null);
const [popperElement, setPopperElement] = useState<any>(null);
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
placement: 'bottom-end',
strategy: 'fixed'
})
const mediaSnippets = useMemo(() => {
if (!settings?.snippets) {
return [];
@@ -128,13 +140,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}
}, [mediaSnippets]);
/**
* Process the snippet
*/
const processSnippet = useCallback((snippet: Snippet) => {
setShowSnippetSelection(false);
const relPath = getRelPath();
const fieldData = {
mediaUrl: parseWinPath(relPath) || "",
mediaUrl: (parseWinPath(relPath) || "").replace(/ /g, "%20"),
alt: alt || "",
caption: caption || "",
title: media.title || "",
@@ -143,7 +158,24 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
mediaHeight: media?.dimensions?.height?.toString() || "",
};
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
if (snippet.fields.length === 0) {
setShowSnippetFormDialog(false);
setMediaData(undefined);
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
insertMediaSnippetToArticle(output);
} else {
setSnippet(snippet);
setShowSnippetFormDialog(true);
setMediaData(fieldData);
}
}, [alt, caption, media, settings, viewData, mediaSnippets]);
/**
* Insert the media snippet
*/
const insertMediaSnippetToArticle = useCallback((output: string) => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertMedia, {
relPath: parseWinPath(relPath) || "",
@@ -152,7 +184,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
position: viewData?.data?.position || null,
snippet: output
});
}, [alt, caption, media, settings, viewData, mediaSnippets]);
}, [viewData]);
const deleteMedia = () => {
setShowAlert(true);
@@ -225,7 +257,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
};
const customScriptActions = () => {
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile && !script.hidden).map(script => (
<MenuItem
key={script.title}
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
@@ -248,7 +280,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}, [media]);
const isImageFile = useMemo(() => {
if (media.mimeType?.startsWith("image/")) {
if (media.mimeType?.startsWith("image/") && !media.mimeType?.startsWith("image/vnd.adobe.photoshop")) {
return true;
}
return false;
@@ -258,12 +290,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const path = media.fsPath;
const extension = path.split('.').pop();
let icon = <DocumentIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
if (isImageFile) {
return <PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
let icon = <DocumentIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
if (isVideoFile) {
icon = <VideoCameraIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
}
@@ -292,6 +324,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return null;
}, [media]);
const clearFormData = () => {
setShowSnippetFormDialog(false);
setSnippet(undefined);
setMediaData(undefined);
}
useEffect(() => {
if (media.alt !== alt) {
setAlt(media.alt);
@@ -311,6 +349,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}
}, [media.fsPath]);
useEffect(() => {
if (!viewData?.data?.filePath) {
clearFormData();
}
}, [viewData]);
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50">
@@ -380,51 +424,63 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
</QuickAction>
</div>
<ActionMenuButton title={`Menu`} />
<div ref={setReferenceElement} className={`flex`}>
<ActionMenuButton title={`Menu`} />
</div>
<MenuItems widthClass='w-40'>
<MenuItem
title={<div className='flex items-center'><PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span></div>}
onClick={updateMetadata}
/>
<div className='menu_items__wrapper z-20' ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<MenuItems widthClass='w-40'>
<MenuItem
title={(
<div className='flex items-center'>
<PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span>
</div>
)}
onClick={updateMetadata}
/>
{
viewData?.data?.filePath ? (
<>
<MenuItem
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
onClick={insertToArticle} />
{
viewData?.data?.filePath ? (
<>
<MenuItem
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
onClick={insertToArticle} />
{
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
<MenuItem
key={idx}
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
onClick={() => processSnippet(snippet)} />
))
}
{
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
<MenuItem
key={idx}
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
onClick={() => processSnippet(snippet)} />
))
}
{ customScriptActions() }
</>
) : (
<>
<MenuItem
title={`Copy media path`}
onClick={copyToClipboard} />
{ customScriptActions() }
</>
) : (
<>
<MenuItem
title={(
<div className='flex items-center'>
<ClipboardIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Copy media path</span>
</div>
)}
onClick={copyToClipboard} />
{ customScriptActions() }
</>
)
}
{ customScriptActions() }
</>
)
}
<MenuItem
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
onClick={revealMedia} />
<MenuItem
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
onClick={revealMedia} />
<MenuItem
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
onClick={deleteMedia} />
</MenuItems>
<MenuItem
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
onClick={deleteMedia} />
</MenuItems>
</div>
</Menu>
</div>
@@ -520,6 +576,18 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
trigger={confirmDeletion} />
)
}
{
(showSnippetFormDialog && snippet && mediaData) && (
<MediaSnippetForm
media={media}
mediaData={mediaData}
snippet={snippet}
onDismiss={clearFormData}
onInsert={insertMediaSnippetToArticle}
/>
)
}
</>
);
};
};

View File

@@ -17,7 +17,7 @@ import useMedia from '../../hooks/useMedia';
import { TelemetryEvent } from '../../../constants';
import { PageLayout } from '../Layout/PageLayout';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { extname, join } from 'path';
import { basename, extname, join } from 'path';
export interface IMediaProps {}
@@ -29,14 +29,27 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
const folders = useRecoilValue(MediaFoldersAtom);
const loading = useRecoilValue(LoadingAtom);
const allFolders = React.useMemo(() => {
const contentFolders = React.useMemo(() => {
// Check if content allows page bundle
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/')));
return [];
}
return folders;
let groupedFolders = [];
for (const cFolder of (settings?.contentFolders || [])) {
const foldersPath = parseWinPath(cFolder.path);
groupedFolders.push({
title: cFolder.title || basename(cFolder.path),
folders: folders.filter(f => parseWinPath(f).startsWith(foldersPath))
});
}
return groupedFolders;
}, [folders, viewData, settings?.contentFolders]);
const publicFolders = React.useMemo(() => {
return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/')));
}, [folders, viewData, settings?.staticFolder]);
const allMedia = React.useMemo(() => {
@@ -126,11 +139,33 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
}
{
allFolders && allFolders.length > 0 && (
contentFolders && contentFolders.length > 0 && contentFolders.map((group, idx) => (
group.folders && group.folders.length > 0 && (
<div key={`group-${idx}`} className={`mb-8`}>
<h2 className='text-lg mb-8 first-letter:uppercase'>Content folder: <b>{group.title}</b></h2>
<List gap={0}>
{
group.folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
</div>
)
))
}
{
publicFolders && publicFolders.length > 0 && (
<div className={`mb-8`}>
{
contentFolders && contentFolders.length > 0 && (<h2 className='text-lg mb-8'>Public folder{settings?.staticFolder && (<span>: <b>{settings?.staticFolder}</b></span>)}</h2>)
}
<List gap={0}>
{
allFolders.map((folder) => (
publicFolders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}

View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { MediaInfo, Snippet } from '../../../models';
import { ViewDataSelector } from '../../state';
import { FormDialog } from '../Modals/FormDialog';
import SnippetForm, { SnippetFormHandle } from '../SnippetsView/SnippetForm';
export interface IMediaSnippetFormProps {
media: MediaInfo;
snippet: Snippet;
mediaData: any;
onDismiss: () => void;
onInsert: (output: string) => void;
}
export const MediaSnippetForm: React.FunctionComponent<IMediaSnippetFormProps> = ({ media, snippet, mediaData, onDismiss, onInsert }: React.PropsWithChildren<IMediaSnippetFormProps>) => {
const viewData = useRecoilValue(ViewDataSelector);
const formRef = useRef<SnippetFormHandle>(null);
const insertToArticle = () => {
formRef.current?.onSave();
onDismiss();
};
return (
<FormDialog
title={`Insert media: ${media.title || media.filename}`}
description={`Insert the ${media.title || media.filename} media file into the current article`}
isSaveDisabled={false}
trigger={insertToArticle}
dismiss={onDismiss}
okBtnText='Insert'
cancelBtnText='Cancel'>
<SnippetForm
ref={formRef}
snippet={snippet}
mediaData={mediaData}
selection={viewData?.data?.selection}
onInsert={onInsert} />
</FormDialog>
);
};

View File

@@ -5,11 +5,15 @@ import * as React from 'react';
export interface IActionMenuButtonProps {
title: string;
disabled?: boolean;
ref?: (instance: Element | null) => void;
}
export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IActionMenuButtonProps>) => {
export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> = ({ title, disabled, ref }: React.PropsWithChildren<IActionMenuButtonProps>) => {
return (
<Menu.Button disabled={disabled} className={`group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600 ${disabled ? 'opacity-50' : ''}`}>
<Menu.Button
ref={ref || null}
disabled={disabled}
className={`group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600 ${disabled ? 'opacity-50' : ''}`}>
<span className="sr-only">{title}</span>
<DotsVerticalIcon className="w-4 h-4" aria-hidden="true" />
</Menu.Button>

View File

@@ -5,12 +5,15 @@ import { Fragment } from 'react';
export interface IMenuItemsProps {
widthClass?: string;
marginTopClass?: string;
updatePopper?: () => void;
disablePopper?: boolean
}
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, marginTopClass, children}: React.PropsWithChildren<IMenuItemsProps>) => {
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, marginTopClass, children, updatePopper, disablePopper}: React.PropsWithChildren<IMenuItemsProps>) => {
return (
<Transition
as={Fragment}
beforeEnter={() => updatePopper ? updatePopper() : null}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
@@ -18,7 +21,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-20 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} ${ disablePopper ? "origin-top-right absolute right-0 z-20" : ""} rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<div className="py-1">
{children}
</div>

View File

@@ -7,7 +7,6 @@ import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { Snippet, Snippets } from '../../../models';
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
import { QuickAction } from '../Menu';

View File

@@ -13,13 +13,15 @@ import { SnippetInputField } from './SnippetInputField';
export interface ISnippetFormProps {
snippet: Snippet;
selection: string | undefined;
mediaData?: any;
onInsert?: (mediaData: any) => void;
}
export interface SnippetFormHandle {
onSave: () => void;
}
const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFormProps> = ({ snippet, selection }, ref) => {
const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFormProps> = ({ snippet, selection, mediaData, onInsert }, ref) => {
const viewData = useRecoilValue(ViewDataSelector);
const [ fields, setFields ] = useState<SnippetField[]>([]);
const settings = useRecoilValue(SettingsAtom);
@@ -38,6 +40,16 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
return value;
}, [selection]);
const insertValueFromMedia = useCallback((fieldName: string) => {
if (!mediaData) {
return "";
}
if (mediaData[fieldName]) {
return mediaData[fieldName];
}
}, [mediaData]);
const snippetBody = useMemo(() => {
let body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`);
@@ -63,10 +75,14 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
return;
}
Messenger.send(DashboardMessage.insertSnippet, {
file: viewData?.data?.filePath,
snippet: snippetBody
});
if (!onInsert) {
Messenger.send(DashboardMessage.insertSnippet, {
file: viewData?.data?.filePath,
snippet: snippetBody
});
} else {
onInsert(snippetBody);
}
}
}));
@@ -79,21 +95,27 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
const allFields: SnippetField[] = [];
const snippetFields = snippet.fields || [];
for (const fieldName of placeholders) {
const field = snippetFields.find(f => f.name === fieldName);
if (field) {
// Loop over all fields to check if they are present in the snippet
for (const field of snippetFields) {
const idx = placeholders.findIndex(fieldName => fieldName === field.name);
if (idx > -1) {
allFields.push({
...field,
value: insertPlaceholderValues(field.default || "")
});
} else {
}
}
// Loop over all placeholders to find the ones that are not present in the snippet fields
for (const fieldName of placeholders) {
const idx = snippetFields.findIndex(field => field.name === fieldName);
if (idx === -1) {
allFields.push({
name: fieldName,
title: fieldName,
type: "string",
single: true,
value: ""
value: insertValueFromMedia(fieldName)
});
}
}

View File

@@ -83,7 +83,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
</FeatureFlag>
)}>
<div className="flex flex-col">
<div className="flex flex-col h-full">
{
viewData?.data?.filePath && (
<div className={`text-xl text-center mb-6`}>

View File

@@ -12,7 +12,7 @@ export interface ISponsorMsgProps {
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isBacker, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
return (
<p className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
<footer className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
{
isBacker ? (
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
@@ -28,6 +28,6 @@ export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isB
</>
)
}
</p>
</footer>
);
};

View File

@@ -33,6 +33,7 @@ const Folder = ({ wsFolder, folder, folders, addFolder }: { wsFolder: string, fo
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
const [framework, setFramework] = useState<string | null>(null);
const [taxImported, setTaxImported] = useState<boolean>(false);
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
@@ -47,6 +48,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
const reload = () => {
const crntState: any = Messenger.getState() || {};
Messenger.setState({
...crntState,
isWelcomeConfiguring: false
@@ -55,14 +57,21 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
Messenger.send(DashboardMessage.reload);
};
const importTaxonomy = () => {
Messenger.send(DashboardMessage.importTaxonomy);
setTaxImported(true);
}
const steps = [
{
id: `welcome-init`,
name: 'Initialize project',
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
status: settings.initialized ? Status.Completed : Status.NotStarted,
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
},
{
id: `welcome-framework`,
name: 'Framework presets',
description: (
<div>
@@ -106,6 +115,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
onClick: undefined
},
{
id: `welcome-content-folders`,
name: 'Register content folder(s)',
description: (
<>
@@ -136,7 +146,15 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
),
status: settings.contentFolders && settings.contentFolders.length > 0 ? Status.Completed : Status.NotStarted
},
{
id: `welcome-import`,
name: 'Import all tags and categories (optional)',
description: <>Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?</>,
status: taxImported ? Status.Completed : Status.NotStarted,
onClick: settings.contentFolders && settings.contentFolders.length > 0 ? importTaxonomy : undefined
},
{
id: `welcome-show-dashboard`,
name: 'Show the dashboard',
description: <>Once all actions are completed, the dashboard can be loaded.</>,
status: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? Status.Active : Status.NotStarted,
@@ -154,7 +172,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
<nav aria-label="Progress">
<ol role="list">
{steps.map((step, stepIdx) => (
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
<li key={step.id} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`} data-test={step.id}>
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />
</li>
))}

View File

@@ -0,0 +1,103 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { ArrowCircleUpIcon, ArrowUpIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback } from 'react';
import { MergeIcon } from '../../../components/icons/MergeIcon';
import { DashboardMessage } from '../../DashboardMessage';
export interface ITaxonomyActionsProps {
field: string | null;
value: string;
unmapped?: boolean;
}
export const TaxonomyActions: React.FunctionComponent<ITaxonomyActionsProps> = ({field, value, unmapped}: React.PropsWithChildren<ITaxonomyActionsProps>) => {
const onEdit = useCallback(() => {
Messenger.send(DashboardMessage.editTaxonomy, {
type: field,
value
});
}, [field, value]);
const onAdd = useCallback(() => {
Messenger.send(DashboardMessage.addToTaxonomy, {
type: field,
value
});
}, [field, value]);
const onMerge = useCallback(() => {
Messenger.send(DashboardMessage.mergeTaxonomy, {
type: field,
value
});
}, [field, value]);
const onMove = useCallback(() => {
Messenger.send(DashboardMessage.moveTaxonomy, {
type: field,
value
});
}, [field, value]);
const onDelete = useCallback(() => {
Messenger.send(DashboardMessage.deleteTaxonomy, {
type: field,
value
});
}, [field, value]);
return (
<div className={`space-x-2`}>
{
unmapped && (
<button
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
type={`button`}
title={`Add ${value} to taxonomy settings`}
onClick={onAdd}>
<PlusIcon className={`w-4 h-4`} aria-hidden={true} />
<span className='sr-only'>Add to settings</span>
</button>
)
}
<button
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
type={`button`}
title={`Edit ${value}`}
onClick={onEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden={true} />
<span className='sr-only'>Edit</span>
</button>
<button
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
type={`button`}
title={`Merge ${value}`}
onClick={onMerge}>
<MergeIcon className={`w-4 h-4`} aria-hidden={true} />
<span className='sr-only'>Merge</span>
</button>
<button
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
type={`button`}
title={`Move to another taxonomy type`}
onClick={onMove}>
<ArrowCircleUpIcon className={`w-4 h-4`} aria-hidden={true} />
<span className='sr-only'>Move to another taxonomy type</span>
</button>
<button
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
type={`button`}
title={`Delete ${value}`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden={true} />
<span className='sr-only'>Delete</span>
</button>
</div>
);
};

View File

@@ -0,0 +1,66 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { useRecoilValue } from 'recoil';
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
import { useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
export interface ITaxonomyLookupProps {
taxonomy: string | null;
value: string;
pages: Page[];
}
export const TaxonomyLookup: React.FunctionComponent<ITaxonomyLookupProps> = ({ taxonomy, value, pages }: React.PropsWithChildren<ITaxonomyLookupProps>) => {
const settings = useRecoilValue(SettingsSelector);
const navigate = useNavigate();
const total: number | undefined = useMemo(() => {
if (!taxonomy || !value || !pages || !settings?.contentTypes) {
return undefined;
}
return pages.filter(page => {
if (taxonomy === "tags") {
return (page.fmTags || []).includes(value);
} else if (taxonomy === "categories") {
return (page.fmCategories || []).includes(value);
}
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
if (!contentType) {
return false;
}
let fieldName = getTaxonomyField(taxonomy, contentType);
return fieldName && page[fieldName] ? page[fieldName].includes(value) : false;
}).length;
}, [taxonomy, value, pages, settings?.contentTypes]);
const onNavigate = useCallback(() => {
if (total) {
navigate(`${routePaths.contents}?taxonomy=${taxonomy}&value=${value}`);
}
}, [total, navigate]);
if (taxonomy === "tags" || taxonomy === "categories") {
return (
<button
className={total ? `text-teal-900 hover:text-teal-600 font-bold` : ``}
title={total ? `Show contents with ${value} in ${taxonomy}` : ``}
onClick={onNavigate}>
{total || `-`}
</button>
);
}
return (
<span>
{total || `-`}
</span>
);
};

View File

@@ -0,0 +1,184 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { ExclamationIcon, PlusSmIcon, TagIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { TaxonomyData } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
import { TaxonomyActions } from './TaxonomyActions';
import { TaxonomyLookup } from './TaxonomyLookup';
export interface ITaxonomyManagerProps {
data: TaxonomyData | undefined;
taxonomy: string | null;
pages: Page[];
}
export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = ({ data, taxonomy, pages }: React.PropsWithChildren<ITaxonomyManagerProps>) => {
const settings = useRecoilValue(SettingsSelector);
const onCreate = () => {
Messenger.send(DashboardMessage.createTaxonomy, {
type: taxonomy
});
};
const items = useMemo(() => {
if (data && taxonomy) {
let crntItems: string[] = [];
if (taxonomy === "tags" || taxonomy === "categories") {
crntItems = data[taxonomy];
} else {
crntItems = data.customTaxonomy.find(c => c.id === taxonomy)?.options || [];
}
// Alphabetically sort the items
crntItems = Object.assign([], crntItems).sort((a: string, b: string) => {
a = a || "";
b = b || "";
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
}
if (a.toLowerCase() > b.toLowerCase()) {
return 1;
}
return 0;
});
return crntItems;
}
return [];
}, [data, taxonomy]);
const unmappedItems = useMemo(() => {
let unmapped: string[] = [];
if (!pages || !settings?.contentTypes || !taxonomy) {
return unmapped;
}
for (const page of pages) {
let values: string[] = [];
if (taxonomy === "tags") {
values = page.fmTags || [];
} else if (taxonomy === "categories") {
values = page.fmCategories || [];
} else {
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
if (!contentType) {
return false;
}
let fieldName = getTaxonomyField(taxonomy, contentType);
if (fieldName && page[fieldName]) {
values = page[fieldName];
}
}
for (const value of values) {
if (!items.includes(value)) {
unmapped.push(value);
}
}
}
return [...new Set(unmapped)];
}, [items, taxonomy, pages, settings?.contentTypes]);
return (
<div className={`py-6 px-4 flex flex-col h-full overflow-hidden`}>
<div className={`flex w-full justify-between flex-shrink-0`}>
<div>
<h2 className={`text-lg text-gray-500 dark:text-whisper-900 first-letter:uppercase`}>{taxonomy}</h2>
<p className={`mt-2 text-sm text-gray-500 dark:text-whisper-900 first-letter:uppercase`}>Create, edit, and manage the {taxonomy} of your site</p>
</div>
<div>
<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 a new ${taxonomy} value`}
onClick={onCreate}>
<PlusSmIcon className={`mr-2 h-6 w-6`} />
<span className={`text-sm`}>Create a new {taxonomy} value</span>
</button>
</div>
</div>
<div className="mt-6 pb-6 -mr-4 pr-4 flex flex-col flex-grow overflow-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-vulcan-300">
<thead>
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Count</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-vulcan-300">
{
items && items.length > 0 ?
items.map((item, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
<TagIcon className="inline-block h-4 w-4 mr-2" />
<span>{item}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
<TaxonomyLookup
taxonomy={taxonomy}
value={item}
pages={pages} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<TaxonomyActions
field={taxonomy}
value={item} />
</td>
</tr>
)) : (
!unmappedItems || unmappedItems.length === 0 && (
<tr>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200" colSpan={4}>No {taxonomy} found</td>
</tr>
)
)
}
{
unmappedItems && unmappedItems.length > 0 &&
unmappedItems.map((item, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200" title='Missing in your settings'>
<ExclamationIcon className="inline-block h-4 w-4 mr-2" />
<span>{item}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
<TaxonomyLookup
taxonomy={taxonomy}
value={item}
pages={pages} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<TaxonomyActions
field={taxonomy}
value={item}
unmapped />
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>
);
};

View File

@@ -0,0 +1,97 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { ChevronRightIcon, DownloadIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { TelemetryEvent } from '../../../constants';
import { TaxonomyData } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { NavigationBar, NavigationItem } from '../Layout';
import { PageLayout } from '../Layout/PageLayout';
import { SponsorMsg } from '../SponsorMsg';
import { TaxonomyManager } from './TaxonomyManager';
export interface ITaxonomyViewProps {
pages: Page[];
}
export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({ pages }: React.PropsWithChildren<ITaxonomyViewProps>) => {
const settings = useRecoilValue(SettingsSelector);
const [ taxonomySettings, setTaxonomySettings ] = useState<TaxonomyData>();
const [ selectedTaxonomy, setSelectedTaxonomy ] = useState<string | null>(`tags`);
const onImport = () => {
Messenger.send(DashboardMessage.importTaxonomy);
};
useEffect(() => {
setTaxonomySettings({
tags: settings?.tags || [],
categories: settings?.categories || [],
customTaxonomy: settings?.customTaxonomy || [],
});
}, [settings?.tags, settings?.categories, settings?.customTaxonomy]);
useEffect(() => {
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewTaxonomyDashboard
});
}, []);
return (
<PageLayout
contentClass={`relative w-full flex-grow flex flex-col mx-auto overflow-hidden`}>
<div className={`h-full w-full flex`}>
<NavigationBar
title='Select the taxonomy'
bottom={(
<button
className={`-mb-4 text-xs opacity-80 flex items-center text-gray-500 dark:text-whisper-900 hover:text-gray-700 dark:hover:text-whisper-500`}
title="Import taxonomy"
onClick={onImport}>
<DownloadIcon className={`w-5 mr-2`} />
<span>Import taxonomy</span>
</button>
)}>
<NavigationItem
isSelected={selectedTaxonomy === "tags"}
onClick={() => setSelectedTaxonomy(`tags`)}>
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
<span>Tags</span>
</NavigationItem>
<NavigationItem
isSelected={selectedTaxonomy === "categories"}
onClick={() => setSelectedTaxonomy(`categories`)}>
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
<span>Categories</span>
</NavigationItem>
{
taxonomySettings?.customTaxonomy && taxonomySettings.customTaxonomy.map((taxonomy, index) => (
<NavigationItem
key={`${taxonomy.id}-${index}`}
isSelected={selectedTaxonomy === taxonomy.id}
onClick={() => setSelectedTaxonomy(taxonomy.id)}>
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
<span className={`first-letter:uppercase`}>{taxonomy.id}</span>
</NavigationItem>
))
}
</NavigationBar>
<div className={`w-10/12 h-full overflow-hidden`}>
<TaxonomyManager
data={taxonomySettings}
taxonomy={selectedTaxonomy}
pages={pages} />
</div>
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
</PageLayout>
);
};

View File

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

View File

@@ -0,0 +1,18 @@
import { StopIcon } from '@heroicons/react/outline';
import * as React from 'react';
export interface IUnknownViewProps {}
export const UnknownView: React.FunctionComponent<IUnknownViewProps> = (props: React.PropsWithChildren<IUnknownViewProps>) => {
return (
<div className={`w-full h-full flex items-center justify-center`}>
<div className='flex flex-col items-center text-gray-500 dark:text-whisper-900'>
<StopIcon className='w-32 h-32' />
<p className='text-3xl mt-2'>View does not exist</p>
<p className='text-xl mt-4'>
You seem to have ended up on a view that doesn't exist. Please re-open the dashboard.
</p>
</div>
</div>
);
};

View File

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

View File

@@ -4,8 +4,9 @@ import { useState, useEffect, useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../models';
import { DashboardCommand } from '../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SearchSelector, SelectedMediaFolderAtom } from '../state';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SelectedMediaFolderAtom } from '../state';
import Fuse from 'fuse.js';
import { PAGE_LIMIT } from '../components/Header/Pagination';
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
keys: [
@@ -18,11 +19,9 @@ const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
includeScore: true
};
export const LIMIT = 16;
export default function useMedia() {
const [ media, setMedia ] = useState<MediaInfo[]>([]);
const [ page, setPage ] = useRecoilState(PageAtom);
const page = useRecoilValue(PageAtom);
const [ searchedMedia, setSearchedMedia ] = useState<MediaInfo[]>([]);
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
@@ -31,7 +30,7 @@ export default function useMedia() {
const search = useRecoilValue(SearchAtom);
const getMedia = useCallback(() => {
return searchedMedia.slice(page * LIMIT, ((page + 1) * LIMIT));
return searchedMedia.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT));
}, [searchedMedia, page]);
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {

View File

@@ -18,7 +18,7 @@ export default function useMessages() {
const [, setView] = useRecoilState(DashboardViewAtom);
const [, setSearchReady] = useRecoilState(SearchReadyAtom);
Messenger.listen((event: MessageEvent<EventData<any>>) => {
const messageListener = (event: MessageEvent<EventData<any>>) => {
const message = event.data;
switch (message.command) {
@@ -33,6 +33,8 @@ export default function useMessages() {
setView(NavigationType.Contents);
} else if (message.data?.type === NavigationType.Data) {
setView(NavigationType.Data);
} else if (message.data?.type === NavigationType.Taxonomy) {
setView(NavigationType.Taxonomy);
} else if (message.data?.type === NavigationType.Snippets) {
setView(NavigationType.Snippets);
}
@@ -51,14 +53,20 @@ export default function useMessages() {
setMode(message.data);
break;
}
});
};
useEffect(() => {
Messenger.listen(messageListener);
setLoading(true);
Messenger.send(DashboardMessage.getViewType);
Messenger.send(DashboardMessage.getTheme);
Messenger.send(DashboardMessage.getData);
Messenger.send(DashboardMessage.getMode);
return () => {
Messenger.unlisten(messageListener);
}
}, ['']);
return {

View File

@@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react';
import { SortOption } from '../constants/SortOption';
import { Tab } from '../constants/Tab';
import { Page } from '../models/Page';
import Fuse from 'fuse.js';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabInfoAtom, TabSelector, TagSelector } from '../state';
import { SortOrder, SortType } from '../../models';
@@ -46,45 +45,6 @@ export default function usePages(pages: Page[]) {
});
}
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesToShow.length;
// Filter by draft status
if (draftField && draftField.type === 'choice') {
const draftChoices = settings?.draftField?.choices;
for (const choice of (draftChoices || [])) {
if (choice) {
draftTypes[choice] = pagesToShow.filter(page => page.fmDraft === choice).length;
}
}
if (tab !== Tab.All) {
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
} else {
pagesToShow = searchedPages;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesToShow.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesToShow.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesToShow = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesToShow = draftField?.invert ? published : drafts;
} else {
pagesToShow = searchedPages;
}
}
// Set the tab information
setTabInfo(draftTypes);
// Sort the pages
let pagesSorted: Page[] = Object.assign([], pagesToShow);
if (!search) {
@@ -107,6 +67,8 @@ export default function usePages(pages: Page[]) {
pagesSorted = pagesSorted.sort(Sorting.alphabetically(name));
} else if (type === SortType.date) {
pagesSorted = pagesSorted.sort(Sorting.date(name));
} else if (type === SortType.number) {
pagesSorted = pagesSorted.sort(Sorting.number(name));
}
if (order === SortOrder.desc) {
@@ -131,6 +93,47 @@ export default function usePages(pages: Page[]) {
pagesSorted = pagesSorted.filter(page => page.fmCategories && page.fmCategories.includes(category));
}
// Process the tab data
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesSorted.length;
// Filter by draft status
if (draftField && draftField.type === 'choice') {
const draftChoices = settings?.draftField?.choices;
for (const choice of (draftChoices || [])) {
if (choice) {
draftTypes[choice] = pagesSorted.filter(page => page.fmDraft === choice).length;
}
}
if (tab !== Tab.All) {
pagesSorted = pagesSorted.filter(page => page.fmDraft === tab);
} else {
pagesSorted = pagesSorted;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesSorted.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesSorted.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesSorted = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesSorted = draftField?.invert ? published : drafts;
} else {
pagesSorted = pagesSorted;
}
}
// Set the tab information
setTabInfo(draftTypes);
// Set the pages
setPageItems(pagesSorted);
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
@@ -165,7 +168,7 @@ export default function usePages(pages: Page[]) {
} else {
processPages(searchedPages);
}
}, [ settings?.draftField, pages, sorting, search, tab ]);
}, [ settings?.draftField, pages, sorting, search, tab, tag, category, folder ]);
useEffect(() => {
Messenger.listen(searchListener);

View File

@@ -1,10 +1,11 @@
import * as React from "react";
import { render } from "react-dom";
import { RecoilRoot } from "recoil";
import { Dashboard } from "./components/Dashboard";
import { App } from "./components/App";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { SENTRY_LINK } from "../constants";
import { MemoryRouter } from "react-router-dom";
import './styles.css';
import { Preview } from "./components/Preview";
@@ -14,6 +15,15 @@ declare const acquireVsCodeApi: <T = unknown>() => {
postMessage: (msg: unknown) => void;
};
export const routePaths: { [name: string]: string } = {
welcome: "/welcome",
contents: "/contents",
media: "/media",
snippets: "/snippets",
data: "/data",
taxonomy: "/taxonomy",
};
const elm = document.querySelector("#app");
if (elm) {
const welcome = elm?.getAttribute("data-showWelcome");
@@ -30,14 +40,25 @@ if (elm) {
tracesSampleRate: 0, // No performance tracing required
release: version || "",
environment: environment || "",
ignoreErrors: ['ResizeObserver loop limit exceeded']
ignoreErrors: [
'ResizeObserver loop limit exceeded',
"Cannot read properties of undefined (reading 'unobserve')"
]
});
}
if (type === "preview") {
render(<Preview url={url} />, elm);
} else {
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
render((
<RecoilRoot>
<MemoryRouter
initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]}
initialIndex={1}>
<App showWelcome={!!welcome} />
</MemoryRouter>
</RecoilRoot>
), elm);
}
}

View File

@@ -3,4 +3,5 @@ export enum NavigationType {
Media = "media",
Data = "data",
Snippets = "snippets",
Taxonomy = "taxonomy",
}

View File

@@ -11,6 +11,7 @@ export interface Page {
fmPreviewImage: string;
fmTags: string[];
fmCategories: string[];
fmContentType: string;
title: string;
slug: string;

View File

@@ -1,18 +1,20 @@
import { DataType } from './../../models/DataType';
import { VersionInfo } from '../../models/VersionInfo';
import { ContentFolder } from '../../models/ContentFolder';
import { ContentType, CustomScript, DraftField, Framework, Snippets, SortingSetting } from '../../models';
import { ContentType, CustomScript, CustomTaxonomy, DraftField, Framework, GitSettings, Snippets, SortingSetting } from '../../models';
import { SortingOption } from './SortingOption';
import { DashboardViewType } from '.';
import { DataFile } from '../../models/DataFile';
export interface Settings {
git: GitSettings;
beta: boolean;
initialized: boolean;
wsFolder: string;
staticFolder: string;
tags: string[];
categories: string[];
customTaxonomy: CustomTaxonomy[];
openOnStart: boolean | null;
versionInfo: VersionInfo;
pageViewType: DashboardViewType | undefined;
@@ -41,6 +43,8 @@ export interface ContentsViewState {
sorting: SortingOption | null | undefined;
defaultSorting: string | null | undefined;
tags: string | null | undefined;
templatesEnabled: boolean | null | undefined;
pagination: boolean | null | undefined;
}
export interface MediaViewState extends ContentsViewState {

View File

@@ -7,7 +7,7 @@ import { TagType } from '../panelWebView/TagType';
import { WebviewHelper } from '@estruyf/vscode';
import { Extension } from '../helpers/Extension';
import { Telemetry } from '../helpers/Telemetry';
import { ModeListener } from '../listeners/general';
import { GitListener, ModeListener } from '../listeners/general';
export class ExplorerView implements WebviewViewProvider, Disposable {
public static readonly viewType = "frontMatter.explorer";
@@ -82,6 +82,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
SettingsListener.process(msg);
TaxonomyListener.process(msg);
ModeListener.process(msg);
GitListener.process(msg);
});
webviewView.onDidChangeVisibility(() => {

View File

@@ -1,3 +1,4 @@
import { GitListener } from './listeners/general/GitListener';
import * as vscode from 'vscode';
import { Telemetry } from './helpers/Telemetry';
import { ContentType } from './helpers/ContentType';
@@ -14,7 +15,7 @@ import { TagType } from './panelWebView/TagType';
import { ExplorerView } from './explorerView/ExplorerView';
import { Extension } from './helpers/Extension';
import { DashboardData } from './models/DashboardData';
import { Logger, Settings as SettingsHelper } from './helpers';
import { debounceCallback, Logger, Settings as SettingsHelper } from './helpers';
import { Content } from './commands/Content';
import ContentProvider from './providers/ContentProvider';
import { Wysiwyg } from './commands/Wysiwyg';
@@ -79,6 +80,11 @@ export async function activate(context: vscode.ExtensionContext) {
Telemetry.send(TelemetryEvent.openDataDashboard);
Dashboard.open({ type: NavigationType.Data });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardTaxonomy, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openTaxonomyDashboard);
Dashboard.open({ type: NavigationType.Taxonomy });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.closeDashboard);
@@ -126,16 +132,7 @@ export async function activate(context: vscode.ExtensionContext) {
const setLastModifiedDate = vscode.commands.registerCommand(COMMAND_NAME.setLastModifiedDate, Article.setLastModifiedDate);
const generateSlug = vscode.commands.registerCommand(COMMAND_NAME.generateSlug, Article.generateSlug);
const createFromTemplate = vscode.commands.registerCommand(COMMAND_NAME.createFromTemplate, (folder: vscode.Uri) => {
const folderPath = Folders.getFolderPath(folder);
if (folderPath) {
Template.create(folderPath);
}
});
let createTemplate = vscode.commands.registerCommand(COMMAND_NAME.createTemplate, Template.generate);
const generateSlug = vscode.commands.registerCommand(COMMAND_NAME.generateSlug, Article.updateSlug);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.initTemplate, () => Project.createSampleTemplate(true))
@@ -154,6 +151,20 @@ export async function activate(context: vscode.ExtensionContext) {
const createFolder = vscode.commands.registerCommand(COMMAND_NAME.createFolder, Folders.addMediaFolder);
/**
* Template creation
*/
const createTemplate = vscode.commands.registerCommand(COMMAND_NAME.createTemplate, Template.generate);
const createFromTemplate = vscode.commands.registerCommand(COMMAND_NAME.createFromTemplate, (folder: vscode.Uri) => {
const folderPath = Folders.getFolderPath(folder);
if (folderPath) {
Template.create(folderPath);
}
});
/**
* Content creation
*/
const createByContentType = vscode.commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent);
const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create);
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create);
@@ -171,14 +182,15 @@ export async function activate(context: vscode.ExtensionContext) {
);
// Initialize command
Template.init();
const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
await Project.init();
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
await Project.init();
if (cb) {
cb();
}
});
if (cb) {
cb();
}
})
);
// Settings promotion command
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, SettingsHelper.promote));
@@ -190,12 +202,12 @@ export async function activate(context: vscode.ExtensionContext) {
// Things to do when configuration changes
SettingsHelper.onConfigChange((global?: any) => {
Template.init();
Preview.init();
GitListener.init();
SettingsListener.getSettings();
DataListener.getFoldersAndFiles();
MarkdownFoldingProvider.triggerHighlighting();
MarkdownFoldingProvider.triggerHighlighting(true);
ModeSwitch.register();
});
@@ -251,6 +263,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Diagnostics
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.diagnostics, Diagnostics.show));
// Git
GitListener.init();
// Subscribe all commands
subscriptions.push(
insertTags,
@@ -270,7 +285,6 @@ export async function activate(context: vscode.ExtensionContext) {
createContent,
createByContentType,
createByTemplate,
projectInit,
collapseAll,
createFolder
);
@@ -287,14 +301,4 @@ const handleAutoDateUpdate = (e: vscode.TextDocumentWillSaveEvent) => {
const triggerShowDraftStatus = (location: string) => {
Logger.info(`Triggering draft status update: ${location}`);
statusDebouncer(() => { StatusListener.verify(frontMatterStatusBar, collection); }, 1000);
};
const debounceCallback = () => {
let timeout: NodeJS.Timeout;
return (fnc: any, time: number) => {
const functionCall = (...args: any[]) => fnc.apply(args);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time) as any;
};
};

View File

@@ -1,9 +1,11 @@
import * as jsoncParser from 'jsonc-parser';
import { CustomPlaceholder } from './../models/CustomPlaceholder';
import { Uri, workspace } from 'vscode';
import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
import * as vscode from 'vscode';
import * as fs from "fs";
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD } from '../constants';
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD, DefaultFieldValues } from '../constants';
import { DumpOptions } from 'js-yaml';
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
import { Extension, Logger, Settings, SlugHelper } from '.';
@@ -22,6 +24,8 @@ import { fromMarkdown } from 'mdast-util-from-markdown';
import { Link, Parent } from 'mdast-util-from-markdown/lib';
import { Content } from 'mdast';
import { processKnownPlaceholders } from './PlaceholderHelper';
import { CustomScript } from './CustomScript';
import { Folders } from '../commands/Folders';
export class ArticleHelper {
private static notifiedFiles: string[] = [];
@@ -368,7 +372,7 @@ export class ArticleHelper {
* @param title
* @returns
*/
public static updatePlaceholders(data: any, title: string) {
public static async updatePlaceholders(data: any, title: string, filePath: string) {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
const fmData = Object.assign({}, data);
@@ -384,7 +388,7 @@ export class ArticleHelper {
}
fmData[fieldName] = processKnownPlaceholders(fmData[fieldName], title, dateFormat);
fmData[fieldName] = this.processCustomPlaceholders(fmData[fieldName], title);
fmData[fieldName] = await this.processCustomPlaceholders(fmData[fieldName], title, filePath);
}
return fmData;
@@ -396,16 +400,52 @@ export class ArticleHelper {
* @param title
* @returns
*/
public static processCustomPlaceholders(value: string, title: string) {
public static async processCustomPlaceholders(value: string, title: string, filePath: string) {
if (value && typeof value === "string") {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS);
const placeholders = Settings.get<CustomPlaceholder[]>(SETTING_CONTENT_PLACEHOLDERS);
if (placeholders && placeholders.length > 0) {
for (const placeholder of placeholders) {
if (value.includes(`{{${placeholder.id}}}`)) {
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
const updatedValue = processKnownPlaceholders(placeholder.value, title, dateFormat);
value = value.replace(regex, updatedValue);
try {
let placeHolderValue = placeholder.value || "";
if (placeholder.script) {
const wsFolder = Folders.getWorkspaceFolder();
const script = { title: placeholder.id, script: placeholder.script, command: placeholder.command };
let output: string | any = await CustomScript.executeScript(script, wsFolder?.fsPath || "", `'${wsFolder?.fsPath}' '${filePath}' '${title}'`);
if (output) {
// Check if the output needs to be parsed
if (output.includes("{") && output.includes("}")) {
try {
output = jsoncParser.parse(output);
} catch (e) {
// Do nothing
}
} else {
output = output.split("\n");
}
placeHolderValue = output;
}
}
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
const updatedValue = processKnownPlaceholders(placeHolderValue, title, dateFormat);
if (value === `{{${placeholder.id}}}`) {
value = updatedValue;
} else {
value = value.replace(regex, updatedValue);
}
} catch (e) {
Notifications.error(`Error while processing the ${placeholder.id} placeholder`);
Logger.error((e as Error).message);
value = DefaultFieldValues.faultyCustomPlaceholder;
}
}
}
}

View File

@@ -1,9 +1,9 @@
import { ModeListener } from './../listeners/general/ModeListener';
import { PagesListener } from './../listeners/dashboard';
import { ArticleHelper, Settings } from ".";
import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field } from '../models';
import { Uri, commands, window } from 'vscode';
import { ArticleHelper, CustomScript, Settings } from ".";
import { DefaultFieldValues, FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field, FieldGroup, FieldType, ScriptType } from '../models';
import { Uri, commands, window, ProgressLocation, workspace } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { existsSync, writeFileSync } from "fs";
@@ -12,6 +12,7 @@ import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { Telemetry } from './Telemetry';
import { processKnownPlaceholders } from './PlaceholderHelper';
import { basename } from 'path';
import { ParsedFrontMatter } from '../parsers';
export class ContentType {
@@ -47,7 +48,7 @@ export class ContentType {
fieldValue = data[draftSetting.name];
}
if (draftSetting && fieldValue) {
if (draftSetting && fieldValue !== null) {
if (draftSetting.type === "boolean") {
return fieldValue ? "Draft" : "Published";
} else {
@@ -115,9 +116,9 @@ export class ContentType {
}
const override = await window.showQuickPick(["Yes", "No"], {
title: "Override default content type",
placeHolder: "Do you want to override the default content type?",
ignoreFocusOut: true,
title: "Override default content type"
ignoreFocusOut: true
});
const overrideBool = override === "Yes";
@@ -126,10 +127,10 @@ export class ContentType {
// Ask for the new content type name
if (!overrideBool) {
contentTypeName = await window.showInputBox({
ignoreFocusOut: true,
title: "Generate Content Type",
placeHolder: "Enter the name of the content type to generate",
prompt: "Enter the name of the content type to generate",
title: "Generate Content Type",
ignoreFocusOut: true,
validateInput: (value: string) => {
if (!value) {
return "Please enter a name for the content type";
@@ -155,9 +156,9 @@ export class ContentType {
const fileName = filePath ? basename(filePath) : undefined;
if (fileName?.startsWith(`index.`)) {
const pageBundleAnswer = await window.showQuickPick(["Yes", "No"], {
title: "Use as page bundle",
placeHolder: "Do you want to use this content type as a page bundle?",
ignoreFocusOut: true,
title: "Use as page bundle"
ignoreFocusOut: true
});
pageBundle = pageBundleAnswer === "Yes";
}
@@ -271,6 +272,192 @@ export class ContentType {
ArticleHelper.update(editor!, content);
}
/**
* Retrieve the field value
* @param data
* @param parents
* @returns
*/
public static getFieldValue(data: any, parents: string[]): string | string[] {
let fieldValue = [];
let crntPageData = data;
for (let i = 0; i < parents.length; i++) {
const crntField = parents[i];
if (i === parents.length - 1) {
fieldValue = crntPageData[crntField];
} else {
if (!crntPageData[crntField]) {
continue;
}
crntPageData = crntPageData[crntField];
}
}
return fieldValue;
}
/**
* Set the field value
* @param data
* @param parents
* @returns
*/
public static setFieldValue(data: any, parents: string[], value: any) {
let crntPageData = data;
for (let i = 0; i < parents.length; i++) {
const crntField = parents[i];
if (i === parents.length - 1) {
crntPageData[crntField] = value;
} else {
if (!crntPageData[crntField]) {
continue;
}
crntPageData = crntPageData[crntField];
}
}
return data;
}
/**
* Find the field by its type
* @param fields
* @param type
* @param parents
* @returns
*/
public static findFieldByType(fields: Field[], type: FieldType, parents: string[] = []): string[] {
for (const field of fields) {
if (field.type === type) {
parents = [...parents, field.name];
return parents;
} else if (field.type === "fields" && field.fields) {
const subFields = this.findFieldByType(field.fields, type, parents);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
/**
* Find the preview field in the fields
* @param ctFields
* @param parents
* @returns
*/
public static findPreviewField(ctFields: Field[], parents: string[] = []): string[] {
for (const field of ctFields) {
if (field.isPreviewImage && field.type === "image") {
parents = [...parents, field.name];
return parents;
} else if (field.type === "fields" && field.fields) {
const subFields = this.findPreviewField(field.fields);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
} else if (field.type === "block") {
const subFields = this.findPreviewInBlockField(field);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
/**
* Find the required fields
*/
public static findEmptyRequiredFields(article: ParsedFrontMatter): Field[][] | undefined {
const contentType = ArticleHelper.getContentType(article.data);
if (!contentType) {
return;
}
const allRequiredFields = ContentType.findRequiredFieldsDeep(contentType.fields);
let emptyFields: Field[][] = [];
for (const fields of allRequiredFields) {
const fieldValue = this.getFieldValue(article.data, fields.map(f => f.name));
if ((fieldValue === null || fieldValue === undefined || fieldValue === "") || fieldValue.length === 0 || fieldValue === DefaultFieldValues.faultyCustomPlaceholder) {
emptyFields.push(fields);
}
}
return emptyFields || [];
}
/**
* Find all the required fields in the content type
* @param fields
* @param parents
* @returns
*/
private static findRequiredFieldsDeep(fields: Field[], parents: Field[][] = [], parentFields: Field[] = []): Field[][] {
for (const field of fields) {
if (field.required) {
parents.push([...parentFields, field]);
}
if (field.type === "fields" && field.fields) {
this.findRequiredFieldsDeep(field.fields, parents, [...parentFields, field]);
}
}
return parents;
}
/**
* Look for the preview image in the block field
* @param field
* @param parents
* @returns
*/
private static findPreviewInBlockField(field: Field) {
const groups = field.fieldGroup && Array.isArray(field.fieldGroup) ? field.fieldGroup : [field.fieldGroup];
if (!groups) {
return [];
}
const blocks = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS);
if (!blocks) {
return [];
}
let found = false;
for (const group of groups) {
const block = blocks.find(block => block.id === group);
if (!block) {
continue;
}
let newParents: string[] = [];
if (!found) {
newParents = this.findPreviewField(block?.fields, []);
}
if (newParents.length > 0) {
found = true;
return newParents;
}
}
return [];
}
/**
* Generate the fields from the data
* @param data
@@ -357,46 +544,72 @@ export class ContentType {
* @returns
*/
private static async create(contentType: IContentType, folderPath: string) {
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
return;
}
window.withProgress({
location: ProgressLocation.Notification,
title: "Front Matter: Creating content...",
cancellable: false
}, async () => {
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
return;
}
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
if (!newFilePath) {
return;
}
let templatePath = contentType.template;
let templateData: ParsedFrontMatter | null = null;
if (templatePath) {
templatePath = Folders.getAbsFilePath(templatePath);
templateData = ArticleHelper.getFrontMatterByPath(templatePath);
}
if (contentType.name === "default") {
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
if (crntFramework?.toLowerCase() === "jekyll") {
const idx = contentType.fields.findIndex(f => f.name === "draft");
if (idx > -1) {
contentType.fields.splice(idx, 1);
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
if (!newFilePath) {
return;
}
if (contentType.name === "default") {
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
if (crntFramework?.toLowerCase() === "jekyll") {
const idx = contentType.fields.findIndex(f => f.name === "draft");
if (idx > -1) {
contentType.fields.splice(idx, 1);
}
}
}
}
let data: any = this.processFields(contentType, titleValue, {});
let data: any = await this.processFields(contentType, titleValue, templateData?.data || {}, newFilePath);
data = ArticleHelper.updateDates(Object.assign({}, data));
data = ArticleHelper.updateDates(Object.assign({}, data));
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
data['type'] = contentType.name;
}
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
data['type'] = contentType.name;
}
const content = ArticleHelper.stringifyFrontMatter(``, data);
const content = ArticleHelper.stringifyFrontMatter(templateData?.content || ``, data);
writeFileSync(newFilePath, content, { encoding: "utf8" });
writeFileSync(newFilePath, content, { encoding: "utf8" });
await commands.executeCommand('vscode.open', Uri.file(newFilePath));
// Check if the content type has a post script to execute
if (contentType.postScript) {
const scripts = await CustomScript.getScripts();
const script = scripts.find(s => s.id === contentType.postScript);
if (script && (script.type === ScriptType.Content || !script?.type)) {
await CustomScript.run(script, newFilePath);
Notifications.info(`Your new content has been created.`);
const doc = await workspace.openTextDocument(Uri.file(newFilePath));
await doc.save();
}
}
Telemetry.send(TelemetryEvent.createContentFromContentType);
await commands.executeCommand('vscode.open', Uri.file(newFilePath));
// Trigger a refresh for the dashboard
PagesListener.refresh();
Notifications.info(`Your new content has been created.`);
Telemetry.send(TelemetryEvent.createContentFromContentType);
// Trigger a refresh for the dashboard
PagesListener.refresh();
})
}
/**
@@ -404,29 +617,40 @@ export class ContentType {
* @param contentType
* @param data
*/
private static processFields(obj: IContentType | Field, titleValue: string, data: any) {
private static async processFields(obj: IContentType | Field, titleValue: string, data: any, filePath: string, isRoot: boolean = true): Promise<any> {
if (obj.fields) {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
for (const field of obj.fields) {
if (field.name === "title") {
if (field.default) {
data[field.name] = processKnownPlaceholders(field.default, titleValue, dateFormat);
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
} else {
data[field.name] = await ArticleHelper.processCustomPlaceholders(data[field.name], titleValue, filePath);
} else if (isRoot) {
data[field.name] = titleValue;
} else {
data[field.name] = ""
}
} else {
if (field.type === "fields") {
data[field.name] = this.processFields(field, titleValue, {});
data[field.name] = await this.processFields(field, titleValue, {}, filePath, false);
} else {
const defaultValue = field.default;
if (typeof defaultValue === "string") {
data[field.name] = processKnownPlaceholders(defaultValue, titleValue, dateFormat);
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
data[field.name] = await ArticleHelper.processCustomPlaceholders(data[field.name], titleValue, filePath);
} else if (typeof defaultValue !== "undefined") {
data[field.name] = defaultValue;
} else {
data[field.name] = typeof defaultValue !== "undefined" ? defaultValue : "";
const draftField = ContentType.getDraftField();
if (field.type === "draft" && (draftField?.type === "boolean" || draftField?.type === undefined)) {
data[field.name] = true;
} else {
data[field.name] = "";
}
}
}
}

View File

@@ -1,3 +1,4 @@
import { Settings } from './SettingsHelper';
import { CommandType } from './../models/PanelSettings';
import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings';
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
@@ -12,9 +13,25 @@ import { Dashboard } from '../commands/Dashboard';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { ParsedFrontMatter } from '../parsers';
import { TelemetryEvent } from '../constants/TelemetryEvent';
import { SETTING_CUSTOM_SCRIPTS } from '../constants';
import { existsSync } from 'fs';
export class CustomScript {
/**
* Retrieve all scripts
* @returns
*/
public static async getScripts(): Promise<ICustomScript[]> {
const scripts = Settings.get<ICustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [];
return scripts;
}
/**
* Run a script
* @param script
* @param path
*/
public static async run(script: ICustomScript, path: string | null = null): Promise<void> {
const wsFolder = Folders.getWorkspaceFolder();
@@ -24,19 +41,19 @@ export class CustomScript {
if (script.type === ScriptType.MediaFile || script.type === ScriptType.MediaFolder) {
Telemetry.send(TelemetryEvent.runMediaScript);
CustomScript.runMediaScript(wsPath, path, script);
await CustomScript.runMediaScript(wsPath, path, script);
} else {
Telemetry.send(TelemetryEvent.runCustomScript);
if (script.bulk) {
// Run script on all files
CustomScript.bulkRun(wsPath, script);
await CustomScript.bulkRun(wsPath, script);
} else if (path) {
// Run script for provided path
CustomScript.singleRun(wsPath, script, path);
await CustomScript.singleRun(wsPath, script, path);
} else {
// Run script on current file.
CustomScript.singleRun(wsPath, script);
await CustomScript.singleRun(wsPath, script);
}
}
}
@@ -64,13 +81,13 @@ export class CustomScript {
}
if (articlePath && article) {
window.withProgress({
return window.withProgress({
location: ProgressLocation.Notification,
title: `Executing: ${script.title}`,
cancellable: false
}, async () => {
const output = await CustomScript.runScript(wsPath, article, articlePath as string, script);
CustomScript.showOutput(output, script, articlePath);
await CustomScript.showOutput(output, script, articlePath);
});
} else {
Notifications.warning(`${script.title}: Article couldn't be retrieved.`);
@@ -116,7 +133,7 @@ export class CustomScript {
}
}
CustomScript.showOutput(output.join(`\n`), script);
await CustomScript.showOutput(output.join(`\n`), script);
});
}
@@ -142,7 +159,7 @@ export class CustomScript {
try {
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${path}"`);
CustomScript.showOutput(output, script);
await CustomScript.showOutput(output, script);
Dashboard.postWebviewMessage({
command: DashboardCommand.mediaUpdate
@@ -188,7 +205,7 @@ export class CustomScript {
* @param output
* @param script
*/
private static showOutput(output: string | null, script: ICustomScript, articlePath?: string | null): void {
private static async showOutput(output: string | null, script: ICustomScript, articlePath?: string | null): Promise<void> {
if (output) {
try {
const data = JSON.parse(output);
@@ -212,9 +229,9 @@ export class CustomScript {
}
if (articlePath) {
ArticleHelper.updateByPath(articlePath, article);
await ArticleHelper.updateByPath(articlePath, article);
} else if (editor) {
ArticleHelper.update(editor, article);
await ArticleHelper.update(editor, article);
} else {
throw new Error(`Couldn't update article.`);
}
@@ -246,7 +263,7 @@ export class CustomScript {
* @param args
* @returns
*/
private static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
public static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
return new Promise((resolve, reject) => {
// Check the command to use
@@ -256,6 +273,12 @@ export class CustomScript {
}
const scriptPath = join(wsPath, script.script);
if (!existsSync(scriptPath)) {
reject(new Error(`Script not found: ${scriptPath}`));
return;
}
const fullScript = `${command} ${scriptPath} ${args}`;
Logger.info(`Executing: ${fullScript}`);
@@ -264,6 +287,11 @@ export class CustomScript {
reject(error.message);
}
if (stdout && stdout.endsWith(`\n`)) {
// Remove empty line at the end of the string
stdout = stdout.slice(0, -1);
}
resolve(stdout);
});
});

View File

@@ -1,9 +1,9 @@
import { GitListener } from './../listeners/general/GitListener';
import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Project } from "../commands/Project";
import { Template } from "../commands/Template";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TEMPLATES_ENABLED, SETTING_GIT_ENABLED, SETTING_DASHBOARD_CONTENT_PAGINATION } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
@@ -12,6 +12,7 @@ import { DataType } from "../models/DataType";
import { Extension } from "./Extension";
import { FrameworkDetector } from "./FrameworkDetector";
import { Settings } from "./SettingsHelper";
import { parseWinPath } from './parseWinPath';
export class DashboardSettings {
@@ -20,14 +21,21 @@ export class DashboardSettings {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = Project.isInitialized();
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
const pagination = Settings.get<boolean>(SETTING_DASHBOARD_CONTENT_PAGINATION)
return {
git: {
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
actions: gitActions || false
},
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER),
staticFolder: Folders.getStaticFolderRelativePath(),
initialized: isInitialized,
tags: Settings.getTaxonomy(TaxonomyType.Tag),
categories: Settings.getTaxonomy(TaxonomyType.Category),
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<DashboardViewType | undefined>(ExtensionState.PagesView, "workspace"),
@@ -46,6 +54,8 @@ export class DashboardSettings {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTING_CONTENT_SORTING_DEFAULT),
tags: Settings.get<string>(SETTING_DASHBOARD_CONTENT_TAGS),
templatesEnabled: Settings.get<boolean>(SETTING_TEMPLATES_ENABLED),
pagination: pagination !== undefined ? pagination : true
},
media: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
@@ -69,7 +79,7 @@ export class DashboardSettings {
* @returns
*/
private static async getDataFiles(): Promise<DataFile[]> {
const wsPath = Folders.getWorkspaceFolder()?.fsPath;
const wsPath = parseWinPath(Folders.getWorkspaceFolder()?.fsPath);
const files = Settings.get<DataFile[]>(SETTING_DATA_FILES);
const folders = Settings.get<DataFolder[]>(SETTING_DATA_FOLDERS);
@@ -85,14 +95,14 @@ export class DashboardSettings {
continue;
}
let dataFolderPath = join(folderPath.replace((wsPath || ''), ''));
let dataFolderPath = parseWinPath(join(folderPath.replace((wsPath || ''), '')));
if (dataFolderPath.startsWith('/')) {
dataFolderPath = dataFolderPath.substring(1);
}
const dataJsonFiles = await workspace.findFiles(join(dataFolderPath, '*.json'));
const dataYmlFiles = await workspace.findFiles(join(dataFolderPath, '*.yml'));
const dataYamlFiles = await workspace.findFiles(join(dataFolderPath, '*.yaml'));
const dataJsonFiles = await workspace.findFiles(parseWinPath(join(dataFolderPath, '*.json')));
const dataYmlFiles = await workspace.findFiles(parseWinPath(join(dataFolderPath, '*.yml')));
const dataYamlFiles = await workspace.findFiles(parseWinPath(join(dataFolderPath, '*.yaml')));
const dataFiles = [...dataJsonFiles, ...dataYmlFiles, ...dataYamlFiles];
for (let dataFile of dataFiles) {

View File

@@ -0,0 +1,9 @@
export const debounceCallback = () => {
let timeout: NodeJS.Timeout;
return (fnc: any, time: number) => {
const functionCall = (...args: any[]) => fnc.apply(args);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time) as any;
};
};

View File

@@ -104,10 +104,20 @@ export class Extension {
return this.ctx.extensionMode === ExtensionMode.Production;
}
/**
* Get the diagnostic collection for the extension
*/
public get diagnosticCollection(): DiagnosticCollection {
return this._collection;
}
/**
* Get extension subscriptions
*/
public get subscriptions() {
return this.ctx.subscriptions;
}
/**
* Set the current version information for the extension
*/
@@ -260,6 +270,10 @@ export class Extension {
return true;
}
public asAbsolutePath(path: string) {
return this.ctx.asAbsolutePath(path);
}
public get packageJson() {
const frontMatter = extensions.getExtension(this.isBetaVersion() ? EXTENSION_BETA_ID : EXTENSION_ID)!;
return frontMatter.packageJSON;

View File

@@ -1,21 +1,32 @@
import { Notifications } from './Notifications';
import { Uri, workspace } from 'vscode';
import { Folders } from '../commands/Folders';
import { isValidFile } from './isValidFile';
export class FilesHelper {
/**
* Retrieve all markdown files from the current project
*/
public static async getMdFiles(): Promise<Uri[] | null> {
const mdFiles = await workspace.findFiles('**/*.md', "**/node_modules/**,**/archetypes/**");
const markdownFiles = await workspace.findFiles('**/*.markdown', "**/node_modules/**,**/archetypes/**");
const mdxFiles = await workspace.findFiles('**/*.mdx', "**/node_modules/**,**/archetypes/**");
if (!mdFiles && !markdownFiles) {
Notifications.info(`No MD files found.`);
public static async getAllFiles(): Promise<Uri[] | null> {
const folderInfo = await Folders.getInfo();
const pages: Uri[] = [];
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (isValidFile(file.fileName)) {
pages.push(Uri.file(file.filePath));
}
}
}
}
if (pages.length === 0) {
Notifications.warning(`No files found.`);
return null;
}
const allMdFiles = [...mdFiles, ...markdownFiles, ...mdxFiles];
return allMdFiles;
return pages;
}
}

View File

@@ -1,3 +1,4 @@
import * as jsoncParser from 'jsonc-parser';
import { existsSync, readFileSync } from "fs";
import jsyaml = require("js-yaml");
import { join, resolve } from "path";
@@ -29,7 +30,7 @@ export class FrameworkDetector {
if (existsSync(pkgFile)) {
let packageJson: any = readFileSync(pkgFile, "utf8");
if (packageJson) {
packageJson = typeof packageJson === "string" ? JSON.parse(packageJson) : packageJson;
packageJson = typeof packageJson === "string" ? jsoncParser.parse(packageJson) : packageJson;
dependencies = packageJson.dependencies || null;
devDependencies = packageJson.devDependencies || null;

View File

@@ -4,8 +4,6 @@ import { dirname, join } from "path";
import { Field } from '../models';
import { existsSync } from 'fs';
import { Folders } from '../commands/Folders';
import { Settings } from './SettingsHelper';
import { SETTING_CONTENT_STATIC_FOLDER } from '../constants';
import { parseWinPath } from './parseWinPath';
export class ImageHelper {
@@ -51,7 +49,7 @@ export class ImageHelper {
*/
public static relToAbs(filePath: string, value: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER);
const staticFolder = Folders.getStaticFolderRelativePath();
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
@@ -73,7 +71,7 @@ export class ImageHelper {
*/
public static absToRel(imgValue: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER);
const staticFolder = Folders.getStaticFolderRelativePath();
let relPath = imgValue || "";
if (imgValue) {

Some files were not shown because too many files have changed in this diff Show More