Compare commits

..

130 Commits

Author SHA1 Message Date
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
17150a53bc 7.3.4 2022-06-13 10:23:57 +02:00
Elio
fbf1990045 Update version in changelog 2022-06-13 10:23:51 +02:00
Elio
c5881d7905 Update changelog 2022-06-13 10:20:47 +02:00
Elio
b83f7beb30 #354 - Windows file parsing fix 2022-06-13 10:19:20 +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
7a2b45f031 Fix double pages on contents dashboard 2022-06-11 20:02:35 +02:00
Elio Struyf
8ed64691c4 7.3.3 2022-06-11 20:00:16 +02:00
Elio Struyf
844971cdd9 Fix card render 2022-06-11 20:00:03 +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
Elio Struyf
e695bad1c6 Merge pull request #347 from estruyf/dev 2022-06-01 12:01:18 +02:00
Elio Struyf
fe31081907 7.3.2 2022-06-01 12:00:45 +02:00
Elio Struyf
248ccb3718 Update changelog 2022-06-01 12:00:36 +02:00
Elio Struyf
2260174ec2 Merge branch 'main' into dev 2022-06-01 11:59:30 +02:00
Elio Struyf
cd3a867422 #346 - Fix media refresh 2022-06-01 11:59:02 +02:00
Elio Struyf
05a63dd110 Create PULL_REQUEST_TEMPLATE.md 2022-05-30 13:32:48 +02:00
Elio Struyf
cfc0c3d5a1 Create CONTRIBUTING.md 2022-05-30 13:29:48 +02:00
Elio Struyf
d6dbca25ce Create CODE_OF_CONDUCT.md 2022-05-30 13:16:10 +02:00
Elio Struyf
d21ad14e89 Merge pull request #344 from estruyf/dev 2022-05-26 18:58:30 +02:00
Elio Struyf
8d00726322 Update changelog 2022-05-26 18:57:22 +02:00
Elio Struyf
af11c304d3 7.3.1 2022-05-26 18:53:35 +02:00
Elio Struyf
223276f6af #343 - Fix in schema for frontMatter.taxonomy.fieldGroups setting 2022-05-26 18:53:27 +02:00
Elio Struyf
a6fdfe0dfa Merge pull request #342 from estruyf/dev 2022-05-25 13:13:50 +02:00
Elio Struyf
6154164b1c Updated changelog 2022-05-25 13:09:16 +02:00
Elio Struyf
1cdb6c56a5 #336 - Fix for updating status field 2022-05-25 13:08:39 +02:00
Elio Struyf
c63310a2db Initialize template folder 2022-05-25 12:51:39 +02:00
Elio Struyf
9f681a7459 Remove template creation 2022-05-25 11:46:18 +02:00
Elio Struyf
2610032a38 Update for the dataFile field 2022-05-25 10:12:05 +02:00
Elio Struyf
d11e8112e0 Fix for data view 2022-05-25 10:09:21 +02:00
Elio Struyf
df5e346cf1 Changelog update 2022-05-24 12:03:30 +02:00
Elio Struyf
9892d14a62 default labels for issues 2022-05-20 16:55:51 +02:00
Elio Struyf
8c61f79885 #340 - Show notification for not existing content folders 2022-05-20 16:46:39 +02:00
Elio Struyf
ffa6638d3d #339 - Fix for content folders without a title 2022-05-20 16:25:08 +02:00
Elio Struyf
f9ef12bd3a #338 - Disable templates setting 2022-05-20 16:22:46 +02:00
Elio Struyf
b79216f2b5 Added project label flow 2022-05-18 16:52:43 +02:00
Elio Struyf
2597a63718 Updated workflow 2022-05-18 14:13:07 +02:00
Elio Struyf
126a21a6b5 Added github action info 2022-05-18 13:58:50 +02:00
Elio Struyf
3abfbd5302 Message handler updates 2022-05-18 13:20:06 +02:00
Elio Struyf
efdbce2d08 #332 - Adding the new fileData field 2022-05-18 13:19:55 +02:00
Elio Struyf
09eea16d60 Remove logging 2022-05-18 10:29:23 +02:00
Elio Struyf
71697a09b6 Update dependency version 2022-05-17 16:23:23 +02:00
Elio Struyf
3583a2b962 #337 - Add support for other fm types 2022-05-17 16:19:40 +02:00
Elio Struyf
2825d5ddd8 #336 - support for draft field invert 2022-05-13 09:01:30 +02:00
Elio Struyf
2e66174c4a Setting focus for questions 2022-05-13 08:26:30 +02:00
Elio Struyf
e78069ad17 #333 - collection and published field support 2022-05-13 08:18:42 +02:00
Elio Struyf
4c97993c5f #333 - better support for Jekyll 2022-05-12 20:53:47 +02:00
Elio Struyf
a452173d9a Added icons to snippets 2022-05-12 17:59:17 +02:00
Elio Struyf
60a38be923 #335 - Merge media snippets to content snippets 2022-05-12 15:51:50 +02:00
Elio Struyf
6c3d286282 #331 - Added functionality to run other type of scripts 2022-05-10 16:26:59 +02:00
Elio Struyf
32c7bbd3f9 #334 - Fix for locked content folders retrieval 2022-05-09 09:00:36 +02:00
Elio Struyf
426dbc2e46 Update beta script 2022-05-08 20:02:35 +02:00
Elio Struyf
9882dea960 Update beta release script 2022-05-08 19:52:31 +02:00
Elio Struyf
eb9a05e90c update beta release script 2022-05-08 18:18:14 +02:00
Elio Struyf
4e59e736ed Parse windows path 2022-05-08 18:09:19 +02:00
Elio Struyf
9f91ebf289 7.3.0 2022-05-05 17:41:32 +02:00
Elio Struyf
b80de402bd #330 - Update front matter from script 2022-05-05 17:41:25 +02:00
Elio Struyf
e3c535276c Merge pull request #328 from estruyf/dev 2022-05-02 20:33:02 +02:00
Elio Struyf
add22b0bd0 updated changelog 2022-05-02 20:24:29 +02:00
Elio Struyf
48f855144e Limit the snippet pre height 2022-04-28 16:00:01 +02:00
Elio Struyf
67291f0cbe style fixes in date picker 2022-04-27 16:31:54 +02:00
Elio Struyf
45b302c698 Status radius change 2022-04-26 16:40:13 +02:00
Elio Struyf
f897edab5f Enhanced tags 2022-04-26 16:35:18 +02:00
Elio Struyf
dffb9f3dd8 #326 - New content type actions 2022-04-26 12:08:37 +02:00
Elio Struyf
573e1966ae Merge branch 'poc/generate-ct' into dev 2022-04-26 12:05:28 +02:00
Elio Struyf
d161aa98a0 Updates for button colors 2022-04-26 12:05:08 +02:00
Elio Struyf
f10d93c22e Add new mode for the content type actions 2022-04-25 20:47:44 +02:00
Elio Struyf
17a98fba68 Add content type create, update, setting 2022-04-25 14:57:59 +02:00
Elio Struyf
dee28397cb Override default content type 2022-04-22 11:18:42 +02:00
Elio Struyf
c4055eb37c Content type generation 2022-04-21 21:18:24 +02:00
Elio Struyf
91049bebd9 #325 - Better welcome experience 2022-04-21 17:11:07 +02:00
Elio Struyf
9f6c35b9ec Automatically set the recognized framework 2022-04-21 12:49:56 +02:00
Elio Struyf
f0ed7c0b39 #323 - Added additional SSGs 2022-04-21 12:42:57 +02:00
Elio Struyf
e64c4fc0f8 #324 - Fix for the framework selection 2022-04-21 12:27:45 +02:00
Elio Struyf
cd19cec4f7 #308 - Updates to media insertion + title metadata 2022-04-21 09:01:30 +02:00
Elio Struyf
070fc53685 Media dashboard fixes + improvements 2022-04-20 20:58:03 +02:00
Elio Struyf
b232c55843 Fix for mode switch 2022-04-20 11:19:23 +02:00
Elio Struyf
55a14b3fbe #308 - Implementation of the file field 2022-04-20 11:02:32 +02:00
Elio Struyf
60fde1711e Merge branch 'release/v7.2.0' into dev 2022-04-12 17:07:46 +02:00
Elio Struyf
31ca9d4e8b #322 - Show folder name for index page 2022-04-12 17:07:28 +02:00
Elio Struyf
bd47a09d1e Small fix for index files in the list 2022-04-12 15:34:46 +02:00
Elio Struyf
744322a398 #263 - WYSIWYG string field 2022-04-12 15:12:14 +02:00
Elio Struyf
70de0e3ebd Merge branch 'poc/preview' into release/v7.2.0 2022-04-11 14:10:11 +02:00
Elio Struyf
935ef83c4f #316 - Surpress disposed webview notifications 2022-04-11 13:22:20 +02:00
Elio Struyf
93370095e9 Merge branch 'dev' into v7.2.0 2022-04-11 13:13:15 +02:00
Elio Struyf
e8e9a5a5d3 Updated tag 2022-04-11 13:09:47 +02:00
Elio Struyf
a5fbf6991c Merge pull request #318 from estruyf/dev 2022-04-11 13:07:16 +02:00
Elio Struyf
a5f8017ab7 7.1.2 2022-04-11 13:05:56 +02:00
Elio Struyf
201fa4d564 #316 - Fix draft tab navigation 2022-04-11 13:05:48 +02:00
Elio Struyf
5e23d4446b 7.2.0 2022-04-11 10:42:01 +02:00
Elio Struyf
ef4e3fe28e Added sponsor image 2022-04-11 09:55:44 +02:00
Elio Struyf
6d6d86692a Merge pull request #313 from estruyf/dev 2022-04-08 10:03:42 +02:00
Elio Struyf
ada9724e54 update changelog 2022-04-08 10:03:13 +02:00
Elio Struyf
6347b728e4 Merge branch 'dev' 2022-04-08 10:01:09 +02:00
Elio Struyf
5079812745 7.1.1 2022-04-08 10:00:53 +02:00
Elio Struyf
c2ed9b2577 Fix for stopPropagation 2022-04-08 10:00:50 +02:00
Elio Struyf
6ef1ba5b57 Updated preview 2022-04-07 16:24:31 +02:00
170 changed files with 18613 additions and 1279 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report
about: Create a report to help us improve
title: 'Issue: '
labels: ''
labels: 'bug'
assignees: ''
---

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: 'Enhancement: '
labels: ''
labels: 'enhancement'
assignees: ''
---

33
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,33 @@
# PR Details
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Related Issue
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
## How Has This Been Tested
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Docs change / refactoring / dependency upgrade
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

27
.github/workflows/project-labelling.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Project labelling
on:
project_card:
types: [created, moved, deleted]
jobs:
automate-issues-labels:
runs-on: ubuntu-latest
steps:
- name: Fetch project data
run: |
echo 'PROJECT_DATA<<EOF' >> $GITHUB_ENV
curl --request GET --url '${{ github.event.project_card.project_url }}' --header 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Add the project label
uses: andymckay/labeler@master
if: ${{ contains(github.event.action, 'created') || contains(github.event.action, 'moved') }}
with:
add-labels: "Project: ${{ fromJSON(env.PROJECT_DATA).name }}"
- name: Remove the project label
uses: andymckay/labeler@master
if: ${{ contains(github.event.action, 'deleted') }}
with:
remove-labels: "Project: ${{ fromJSON(env.PROJECT_DATA).name }}"

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

View File

@@ -2,6 +2,7 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"ms-vscode.vscode-typescript-tslint-plugin"
"ms-vscode.vscode-typescript-tslint-plugin",
"eliostruyf.vscode-typescript-exportallmodules"
]
}

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

View File

@@ -10,8 +10,4 @@
"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"
]
}

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,115 @@
# Change Log
## [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
- [#354](https://github.com/estruyf/vscode-front-matter/issues/354): Fix Windows file path parsing for inserting media files
## [7.3.3] - 2022-06-11
### 🐞 Fixes
- Card render when taxonomy is not an array value
- Double pages on contents dashboard
## [7.3.2] - 2022-06-01
### 🐞 Fixes
- [#346](https://github.com/estruyf/vscode-front-matter/issues/346): Fix media dashboard refresh action
## [7.3.1] - 2022-05-26
### 🐞 Fixes
- [#343](https://github.com/estruyf/vscode-front-matter/issues/343): Fix in the schema for the `frontMatter.taxonomy.fieldGroups` setting
## [7.3.0] - 2022-05-25 - [Release notes](https://beta.frontmatter.codes/updates/v7.3.0)
### 🎨 Enhancements
- JSON schema enhancements for working with data files
- [#330](https://github.com/estruyf/vscode-front-matter/issues/330): Allow custom scripts to easily update front matter
- [#331](https://github.com/estruyf/vscode-front-matter/issues/331): Added functionality to run other type of scripts
- [#332](https://github.com/estruyf/vscode-front-matter/issues/332): New `dataFile` field which allows you to create data file references
- [#333](https://github.com/estruyf/vscode-front-matter/issues/333): Automatically mark Jekyll posts in `_drafts` folder as draft
- [#335](https://github.com/estruyf/vscode-front-matter/issues/335): Merge media snippets with content snippets to allow you to define multiple media snippets and use these in your content
- [#336](https://github.com/estruyf/vscode-front-matter/issues/336): Support added for inverting the draft field so that SSGs/authors can use a published field instead
- [#337](https://github.com/estruyf/vscode-front-matter/issues/337): Allow multiple front matter types to be used
- [#338](https://github.com/estruyf/vscode-front-matter/issues/338): Ability to disable the templates functionality (default is disabled)
- [#340](https://github.com/estruyf/vscode-front-matter/issues/340): Show an error message when there is a content folder registered that does not exist in the project
### 🐞 Fixes
- [#334](https://github.com/estruyf/vscode-front-matter/issues/334): Fix for locked content folders retrieval
- [#339](https://github.com/estruyf/vscode-front-matter/issues/339): Fix for content folders without a title
## [7.2.0] - 2022-05-02 - [Release notes](https://beta.frontmatter.codes/updates/v7.2.0)
### 🎨 Enhancements
- New tag design for the tags, category, and taxonomy fields
- [#263](https://github.com/estruyf/vscode-front-matter/issues/263): WYSIWYG string field option
- [#308](https://github.com/estruyf/vscode-front-matter/issues/308): New `File` field
- [#314](https://github.com/estruyf/vscode-front-matter/issues/314): New preview actions to open the page in the browser and refresh the preview
- [#322](https://github.com/estruyf/vscode-front-matter/issues/322): Show parent folder name when file is an index page (`index.md` / `_index.md`)
- [#323](https://github.com/estruyf/vscode-front-matter/issues/323): Added 11ty, jekyll, and docusaurus to the framework selection list
- [#325](https://github.com/estruyf/vscode-front-matter/issues/325): Better welcome experience that allows you to add content folders straight from the welcome view
- [#326](https://github.com/estruyf/vscode-front-matter/issues/326): Content type actions to create, update, or set according to the current file
### ⚡️ Optimizations
- [#316](https://github.com/estruyf/vscode-front-matter/issues/316): Suppress file parsing errors when closing the dashboard
### 🐞 Fixes
- Updated JSON schema link to supported version by VS Code (draft-07)
- Hide the view mode action from the Front Matter panel if no custom modes are defined
- Fix in decode base64 uploaded video files
- Fix for a lightbox on other types of documents (pdf, etc.)
- Fix for hiding the image preview on slide-over for none image documents
- [#324](https://github.com/estruyf/vscode-front-matter/issues/324): Fix for the framework selection on the welcome screen
## [7.1.2] - 2022-04-11
### 🐞 Fixes
- [#315](https://github.com/estruyf/vscode-front-matter/issues/315): Fix draft tab navigation
## [7.1.1] - 2022-04-08
### 🐞 Fixes
- Fix in menu item with `stopPropagation` not defined.
## [7.1.0] - 2022-04-07 - [Release notes](https://beta.frontmatter.codes/updates/v7.1.0)
### 🎨 Enhancements

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
elio@struyfconsulting.be.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

59
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,59 @@
# Contributing to Front Matter
First of all, it is amazing you want to contribute to Front Matter 💚.
There are various ways in how you can contribute to the project, it can be as simple from opening a bug report to implementing fixes or features.
## How you can help us
- Testing out the extension and providing feedback
- Reporting issues and bugs
- Suggesting new features
- Fixing an issue
- Updating documentation
- UI improvements
- Tutorials
- etc.
Eager to start contributing? Great 🤩, you can contribute to the following projects:
- [Extension](https://github.com/estruyf/vscode-front-matter)
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
- [Sample Projects](https://github.com/FrontMatter/project-samples)
## How to get started
- Start by forking this project;
- Clone your fork to your local machine;
- Run `npm i`;
- Open the project in VS Code;
- To start developing, run `npm run dev:ext` and press `f5` to start the debugging session.
### Tips
- Ensure that the main branch on your fork is in sync with the original **vscode-front-matter** repository
```bash
# assuming you are in the folder of your locally cloned fork....
git checkout main
# assuming you have a remote named `upstream` pointing to the official **vscode-front-matter** repo
git fetch upstream
# update your local main to be a mirror of what's in the main repo
git pull --rebase upstream main
```
- Create a feature branch in your fork. In case you get stuck, or have issues with merging your PR, this will allow you to have a clean main branch that you can use for contributing other changes.
```bash
git checkout -b issue/<id>
```
## Pull request
Once you are done with implementing the fix or feature. Please create a PR to our `dev` branch.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.

View File

@@ -166,27 +166,7 @@ You can open showcase issues for the following things:
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/jmatthewpryor" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/850570" />
</a>
<a href="https://github.com/apowell656" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
</a>
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
<a href="https://github.com/grahampcharles" title="Graham Charles">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/3606679?v=4" />
</a>
<a href="https://github.com/zivbk1" title="Bryan Klein">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
</a>
<a href="https://github.com/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>
<a href="https://github.com/themefisher" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/10640964" />
</a>
<img src="https://frontmatter.codes/api/img-sponsors" />
</p>
<br />

View File

@@ -164,27 +164,7 @@ You can open showcase issues for the following things:
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/jmatthewpryor" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/850570" />
</a>
<a href="https://github.com/apowell656" title="Andre Powell">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1969515" />
</a>
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
<a href="https://github.com/grahampcharles" title="Graham Charles">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/3606679?v=4" />
</a>
<a href="https://github.com/zivbk1" title="Bryan Klein">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
</a>
<a href="https://github.com/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>
<a href="https://github.com/themefisher" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/10640964" />
</a>
<img src="https://frontmatter.codes/api/img-sponsors" />
</p>
<br />

View File

@@ -163,14 +163,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);
@@ -182,17 +174,6 @@
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);
@@ -221,60 +202,6 @@
margin-top: 1rem;
}
.article__tags__items__item {
display: inline-flex;
margin-bottom: .5rem;
margin-right: .5rem;
}
.article__tags__items__item {
display: inline-block;
margin-bottom: .5rem;
margin-right: .5rem;
}
.article__tags__items__item_add,
.article__tags__items__item_delete {
display: inline-block;
width: auto;
}
.article__tags__items__item svg {
display: inline;
vertical-align: bottom;
}
.article__tags__items__item_delete span {
margin-left: .5rem;
}
.article__tags__items__pill_notexists {
color: var(--vscode-inputValidation-errorForeground);
background-color: var(--vscode-inputValidation-errorBackground);
padding-left: .5rem;
}
.article__tags__items__pill_notexists:hover {
color: var(--vscode-inputValidation-errorForeground);
background-color: var(--vscode-inputValidation-errorBackground);
filter: contrast(60%);
}
.article__tags__items__item_add {
color: var(--vscode-inputValidation-infoForeground);
background-color: var(--vscode-inputValidation-infoBackground);
border-right: 1px solid var(--vscode-inputValidation-infoBorder);
}
.article__tags__items__item_add:hover {
color: var(--vscode-inputValidation-infoForeground);
background-color: var(--vscode-inputValidation-infoBackground);
border-right: 1px solid var(--vscode-inputValidation-infoBorder);
filter: contrast(60%);
}
.article__actions > * + *,
.other_actions > * + *,
.base__actions > * + *,
@@ -355,6 +282,11 @@
color: var(--vscode-button-secondaryForeground);
}
.ext_link_block a:hover,
.ext_link_block button:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.table__cell {
overflow: hidden;
}
@@ -639,6 +571,7 @@ input:checked + .field__toggle__slider:before {
max-height: 16rem;
}
.metadata_field__file__button,
.metadata_field__preview_image__button {
background-color: transparent;
border: 1px dashed var(--vscode-button-background);
@@ -646,11 +579,13 @@ input:checked + .field__toggle__slider:before {
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;
@@ -659,6 +594,7 @@ input:checked + .field__toggle__slider:before {
margin: 0 auto;
}
.metadata_field__file__button span,
.metadata_field__preview_image__button span {
color: var(--vscode-foreground);
display: inline-block;
@@ -773,8 +709,12 @@ input:checked + .field__toggle__slider:before {
}
/* Timepicker */
.react-datepicker button {
outline: 0;
}
.react-datepicker button:hover {
background-color: none !important;
background: none !important;
}
.react-datepicker__triangle {

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

View File

@@ -50,6 +50,28 @@
],
"openingTags": "{{",
"closingTags": "}}"
},
"Issue link": {
"description": "Link to a GitHub issue",
"body": "- [#{{id}}](https://github.com/estruyf/vscode-front-matter/issues/{{id}}): {{title}}",
"fields": [
{
"name": "id",
"title": "Issue ID",
"type": "string",
"single": true,
"default": ""
},
{
"name": "title",
"title": "Title",
"type": "string",
"single": true,
"default": ""
}
],
"openingTags": "{{",
"closingTags": "}}"
}
}
}

13087
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.1.0",
"version": "8.0.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -22,6 +22,9 @@
"href": "https://www.buymeacoffee.com/zMeFRy9"
}
],
"sponsor": {
"url": "https://github.com/sponsors/estruyf"
},
"engines": {
"vscode": "^1.63.0"
},
@@ -137,6 +140,10 @@
"type": "string",
"description": "Name of the field to use"
},
"invert": {
"type": "boolean",
"description": "By default the draft field is set to true when the content is a draft. Set this to true to set it to false."
},
"choices": {
"type": "array",
"description": "List of choices for the field",
@@ -179,6 +186,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,
@@ -225,8 +240,7 @@
"additionalProperties": {
"type": "object",
"required": [
"body",
"fields"
"body"
],
"properties": {
"body": {
@@ -255,6 +269,11 @@
"description": "The snippet closing tags.",
"type": "string",
"default": "]]"
},
"isMediaSnippet": {
"description": "Specify if the snippet is to be used for media files.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
@@ -342,7 +361,7 @@
},
"nodeBin": {
"type": "string",
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use."
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use. (deprecated: use the command property instead)"
},
"bulk": {
"type": "boolean",
@@ -369,6 +388,25 @@
"mediaFile"
],
"description": "The type for which the script will be used."
},
"command": {
"type": "string",
"oneOf": [
{
"enum": [
"node",
"bash",
"powershell",
"python",
"python3"
]
},
{
"type": "string"
}
],
"description": "The type of script you want to execute.",
"default": "node"
}
},
"additionalProperties": false,
@@ -389,6 +427,7 @@
"type": "array",
"default": [],
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
"deprecationMessage": "This setting is deprecated and will be removed in the next major version. Please define your media snippet in the `frontMatter.content.snippet` setting.",
"items": {
"type": "string",
"description": "Use the `{mediaUrl}`, `{caption}`, `{alt}`, `{filename}`, `{mediaHeight}`, and `{mediaWidth}` placeholders in your snippet to automatically insert the media information."
@@ -426,7 +465,8 @@
},
"file": {
"type": "string",
"description": "Path to the file to load. Only JSON or YAML files are supported."
"description": "Path to the file to load. Only JSON or YAML files are supported.",
"default": "[[workspace]]/"
},
"fileType": {
"type": "string",
@@ -438,10 +478,38 @@
"description": "Defines how you want to parse the file. JSON is the default."
},
"schema": {
"$id": "#dataFileSchema",
"type": "object",
"default": {},
"description": "The JSON schema for your data which will be used to render the data form.",
"additionalProperties": true
"additionalProperties": true,
"required": [
"type",
"properties"
],
"properties": {
"title": {
"type": "string",
"description": "Title of the form."
},
"type": {
"type": "string",
"description": "Defines the type of the form. Default is 'object'.",
"default": "object"
},
"required": {
"type": "array",
"description": "Defines the required fields for the form.",
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"description": "Defines the fields of the form.",
"additionalProperties": true
}
}
},
"type": {
"type": "string",
@@ -488,13 +556,11 @@
},
"path": {
"type": "string",
"description": "Path to the folder to load files."
"description": "Path to the folder to load files.",
"default": "[[workspace]]/"
},
"schema": {
"type": "object",
"default": {},
"description": "The JSON schema for your data which will be used to render the data form.",
"additionalProperties": true
"$ref": "#dataFileSchema"
},
"type": {
"type": "string",
@@ -535,10 +601,7 @@
"description": "Your unique ID you want to use for your data type."
},
"schema": {
"type": "object",
"default": {},
"description": "The JSON schema for your data which will be used to render the data form.",
"additionalProperties": true
"$ref": "#dataFileSchema"
}
},
"required": [
@@ -598,6 +661,7 @@
"panel.globalSettings",
"panel.seo",
"panel.actions",
"panel.contentType",
"panel.metadata",
"panel.recentlyModified",
"panel.otherActions",
@@ -744,6 +808,7 @@
"datetime",
"boolean",
"image",
"file",
"choice",
"taxonomy",
"tags",
@@ -751,7 +816,10 @@
"draft",
"fields",
"json",
"block"
"block",
"list",
"dataFile",
"slug"
],
"description": "Define the type of field"
},
@@ -802,6 +870,11 @@
"default": false,
"description": "Is a single line field"
},
"wysiwyg": {
"type": "boolean",
"default": false,
"description": "Is a WYSIWYG field (HTML output)"
},
"multiple": {
"type": "boolean",
"default": false,
@@ -822,6 +895,13 @@
"default": "",
"description": "The ID of your taxonomy field"
},
"fileExtensions": {
"type": "array",
"description": "Specify the file extensions to allow for the file picker",
"items": {
"type": "string"
}
},
"fields": {
"$ref": "#contenttypefield"
},
@@ -861,6 +941,26 @@
"type": "boolean",
"default": false,
"description": "Specify if the field is the modified date field"
},
"dataFileId": {
"type": "string",
"default": "",
"description": "Specify the ID of the data file to use for this field"
},
"dataFileKey": {
"type": "string",
"default": "",
"description": "Specify the key of the data file to use for this field"
},
"dataFileValue": {
"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"
}
},
"additionalProperties": false,
@@ -869,6 +969,35 @@
"name"
],
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "dataFile"
}
}
},
"then": {
"required": [
"dataFileId",
"dataFileKey"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "file"
}
}
},
"then": {
"required": [
"fileExtensions"
]
}
},
{
"if": {
"properties": {
@@ -954,6 +1083,11 @@
],
"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."
}
},
"additionalProperties": false,
@@ -1057,13 +1191,17 @@
"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"
}
},
"additionalProperties": false,
"required": [
"name",
"id",
"fields"
]
}
@@ -1170,6 +1308,12 @@
"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"
}
}
},
@@ -1179,6 +1323,21 @@
"title": "Authenticate",
"category": "Front matter"
},
{
"command": "frontMatter.contenttype.generate",
"title": "Generate content type from current file",
"category": "Front matter"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"title": "Add missing fields from front matter to content type",
"category": "Front matter"
},
{
"command": "frontMatter.contenttype.setContentType",
"title": "Set the content type to use for the current file",
"category": "Front matter"
},
{
"command": "frontMatter.markup.blockquote",
"title": "Blockquote",
@@ -1233,9 +1392,14 @@
"dark": "assets/icons/close-dark.svg"
}
},
{
"command": "frontMatter.initTemplate",
"title": "Initialize the template folder",
"category": "Front matter"
},
{
"command": "frontMatter.createTemplate",
"title": "Create a template from current file",
"title": "Create template from current file",
"category": "Front matter"
},
{
@@ -1300,8 +1464,8 @@
"category": "Front matter"
},
{
"command": "frontMatter.insertImage",
"title": "Insert image into your content",
"command": "frontMatter.insertMedia",
"title": "Insert media into your content",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/media-dark.svg",
@@ -1367,6 +1531,15 @@
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.taxonomy",
"title": "Open taxonomy dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.markup.orderedlist",
"title": "Ordered list",
@@ -1463,7 +1636,7 @@
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
"command": "frontMatter.insertMedia",
"group": "navigation@-128",
"when": "frontMatter:file:isValid == true"
},
@@ -1620,7 +1793,7 @@
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
"command": "frontMatter.insertMedia",
"when": "frontMatter:file:isValid == true"
},
{
@@ -1654,6 +1827,18 @@
{
"command": "frontMatter.generateSlug",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.generate",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.setContentType",
"when": "frontMatter:file:isValid == true"
}
],
"view/title": [
@@ -1665,12 +1850,12 @@
{
"command": "frontMatter.mode.switch",
"group": "navigation@1",
"when": "view == frontMatter.explorer"
"when": "view == frontMatter.explorer && frontMatter:has:modes == true"
},
{
"command": "frontMatter.dashboard",
"group": "navigation@2",
"when": "view == frontMatter.explorer"
"when": "view == frontMatter.explorer || view == explorer"
}
]
},
@@ -1741,18 +1926,22 @@
"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",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "0.0.3",
"@headlessui/react": "^1.5.0",
"@headlessui/react": "1.5.0",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@octokit/rest": "^18.12.0",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/chai": "^4.3.1",
"@types/glob": "7.1.3",
"@types/invariant": "^2.2.35",
"@types/js-yaml": "3.12.1",
@@ -1760,7 +1949,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",
@@ -1775,12 +1964,13 @@
"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",
"fuse.js": "6.5.3",
"glob": "7.1.6",
"gray-matter": "4.0.2",
"gray-matter": "4.0.3",
"html-loader": "1.3.2",
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
@@ -1791,6 +1981,7 @@
"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",
@@ -1802,10 +1993,13 @@
"react-datepicker": "4.2.1",
"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",
"style-loader": "2.0.0",
"tailwindcss": "^2.2.7",
"tailwindcss-nested-groups": "^1.2.4",
@@ -1817,6 +2011,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",
@@ -1828,4 +2023,4 @@
"dependencies": {
"node-fetch": "^2.6.7"
}
}
}

View File

@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
const core = require('@actions/core');
const packageJson = require('../package.json');
const version = packageJson.version.split('.');
@@ -14,7 +15,24 @@ packageJson.homepage = "https://beta.frontmatter.codes";
console.log(packageJson.version);
core.summary.addHeading(`Version info`).addDetails(`${packageJson.version}`);
const scripts = packageJson.scripts;
for (const key in scripts) {
if (key.startsWith(`prod:`)) {
scripts[key] = scripts[key].replace("production", "development");
}
}
console.log(JSON.stringify(packageJson.scripts, null, 2));
fs.writeFileSync(path.join(path.resolve('.'), 'package.json'), JSON.stringify(packageJson, null, 2));
let readme = fs.readFileSync(path.join(__dirname, '../README.beta.md'), 'utf8');
fs.writeFileSync(path.join(__dirname, '../README.md'), readme);
fs.writeFileSync(path.join(__dirname, '../README.md'), readme);
// Update the .vscodeignore file
const ignoreFilePath = path.join(path.resolve('.'), '.vscodeignore');
let vscodeignore = fs.readFileSync(ignoreFilePath, 'utf8');
vscodeignore = vscodeignore.replace(`**/*.map`, '');
fs.writeFileSync(ignoreFilePath, vscodeignore);

View File

@@ -30,7 +30,7 @@ export class Article {
return;
}
const article = Article.getCurrent();
const article = ArticleHelper.getCurrent();
if (!article) {
return;
@@ -69,7 +69,8 @@ export class Article {
const selectedOptions = await vscode.window.showQuickPick(options, {
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
canPickMany: true
canPickMany: true,
ignoreFocusOut: true
});
if (selectedOptions) {
@@ -167,13 +168,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;
@@ -190,17 +212,16 @@ 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
@@ -226,7 +247,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}`;
@@ -324,7 +345,7 @@ export class Article {
/**
* Insert an image from the media dashboard into the article
*/
public static async insertImage() {
public static async insertMedia() {
let editor = vscode.window.activeTextEditor;
if (!editor) {
return;
@@ -375,23 +396,6 @@ export class Article {
} as DashboardData);
}
/**
* Get the current article
*/
private static getCurrent(): ParsedFrontMatter | undefined {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
return article;
}
/**
* Update the article date and return it
* @param article

View File

@@ -1,9 +1,15 @@
import { commands, QuickPickItem, window } from 'vscode';
import { COMMAND_NAME } from '../constants';
import { COMMAND_NAME, SETTING_TEMPLATES_ENABLED } from '../constants';
import { Settings } from '../helpers';
export class Content {
public static async create() {
const templatesEnabled = await Settings.get(SETTING_TEMPLATES_ENABLED);
if (!templatesEnabled) {
commands.executeCommand(COMMAND_NAME.createByContentType);
return;
}
const options: QuickPickItem[] = [{
label: "Create content by content type",
@@ -15,7 +21,8 @@ export class Content {
const selectedOption = await window.showQuickPick(options, {
placeHolder: `Select how you want to create your new content`,
canPickMany: false
canPickMany: false,
ignoreFocusOut: true
});
if (selectedOption) {

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,7 +7,7 @@ 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';
@@ -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,7 @@ export class Dashboard {
TelemetryListener.process(msg);
SnippetListener.process(msg);
ModeListener.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

@@ -1,20 +1,21 @@
import { Questions } from './../helpers/Questions';
import { SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_STATIC_FOLDER, SETTING_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants';
import { commands, Uri, workspace, window } from "vscode";
import { basename, dirname, join, sep } from "path";
import { basename, dirname, join, relative, sep } from "path";
import { ContentFolder, FileInfo, FolderInfo } from "../models";
import uniqBy = require("lodash.uniqby");
import { Template } from "./Template";
import { Notifications } from "../helpers/Notifications";
import { Settings } from "../helpers";
import { Logger, Settings } from "../helpers";
import { existsSync, mkdirSync } from 'fs';
import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
import { parseWinPath } from '../helpers/parseWinPath';
import { MediaHelpers } from '../helpers/MediaHelpers';
import { MediaListener, PagesListener } from '../listeners/dashboard';
import { MediaListener, PagesListener, SettingsListener } from '../listeners/dashboard';
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
import { Telemetry } from '../helpers/Telemetry';
import { glob } from 'glob';
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
@@ -26,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 = "";
@@ -97,7 +98,10 @@ export class Folders {
* Register the new folder path
* @param folder
*/
public static async register(folder: Uri) {
public static async register(folderInfo: { title: string, path: Uri } | Uri) {
let folderName = folderInfo instanceof Uri ? undefined : folderInfo.title;
const folder = folderInfo instanceof Uri ? folderInfo : folderInfo.path;
if (folder && folder.fsPath) {
const wslPath = folder.fsPath.replace(/\//g, '\\');
@@ -110,11 +114,14 @@ export class Folders {
return;
}
const folderName = await window.showInputBox({
prompt: `Which name would you like to specify for this folder?`,
placeHolder: `Folder name`,
value: basename(folder.fsPath)
});
if (!folderName) {
folderName = await window.showInputBox({
prompt: `Which name would you like to specify for this folder?`,
placeHolder: `Folder name`,
value: basename(folder.fsPath),
ignoreFocusOut: true
});
}
folders.push({
title: folderName,
@@ -127,6 +134,8 @@ export class Folders {
Notifications.info(`Folder registered`);
Telemetry.send(TelemetryEvent.registerFolder);
SettingsListener.getSettings();
}
}
@@ -144,6 +153,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
@@ -222,7 +250,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[] = [];
@@ -284,11 +312,30 @@ export class Folders {
public static get(): ContentFolder[] {
const wsFolder = Folders.getWorkspaceFolder();
const folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
const contentFolders = folders.map(folder => {
if (!folder.title) {
folder.title = basename(folder.path);
}
let folderPath = Folders.absWsFolder(folder, wsFolder);
if (!existsSync(folderPath)) {
Notifications.errorShowOnce(`Folder "${folder.title} (${folder.path})" does not exist. Please remove it from the settings.`, "Remove folder").then(answer => {
if (answer === "Remove folder") {
let folders = Folders.get();
Folders.update(folders.filter(f => f.path !== folder.path));
}
});
return null;
}
return {
...folder,
path: folderPath
}
})
return folders.map(folder => ({
...folder,
path: Folders.absWsFolder(folder, wsFolder)
}));
return contentFolders.filter(folder => folder !== null) as ContentFolder[];
}
/**
@@ -347,4 +394,46 @@ export class Folders {
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
return absPath;
}
/**
* Find the content folders
*/
public static async getContentFolders() {
// Find folders that contain files
const wsFolder = Folders.getWorkspaceFolder();
const supportedFiles = Settings.get<string[]>(SETTING_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES;
const patterns = supportedFiles.map(fileType => `${join(parseWinPath(wsFolder?.fsPath || ""), "**", `*${fileType.startsWith('.') ? '' : '.'}${fileType}`)}`);
let folders: string[] = [];
for (const pattern of patterns) {
try {
folders = [...folders, ...(await this.findFolders(pattern))];
} catch (e) {
Logger.error(`Something went wrong while searching for folders with pattern "${pattern}": ${(e as Error).message}`);
}
}
// Filter out the workspace folder
if (wsFolder) {
folders = folders.filter(folder => folder !== wsFolder.fsPath);
}
const uniqueFolders = [...new Set(folders)];
return uniqueFolders.map(folder => relative(wsFolder?.path || "", folder));
}
/**
* Retrieve all content folders
* @param pattern
* @returns
*/
private static findFolders(pattern: string): Promise<string[]> {
return new Promise(resolve => {
glob(pattern, { ignore: "**/node_modules/**" }, (err, files) => {
const allFolders = files.map(file => dirname(file));
const uniqueFolders = [...new Set(allFolders)];
resolve(uniqueFolders);
});
});
}
}

View File

@@ -1,14 +1,16 @@
import { Telemetry } from './../helpers/Telemetry';
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent } from './../constants';
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent, PreviewCommands } from './../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
import { commands, env, Uri, ViewColumn, window } from "vscode";
import { 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 {
@@ -36,6 +38,28 @@ 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) {
if (filePath.startsWith(folder.path)) {
if (!selectedFolder || selectedFolder.path.length < folder.path.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) {
@@ -81,59 +105,62 @@ export class Preview {
const cspSource = webView.webview.cspSource;
webView.webview.html = `<!DOCTYPE html>
<head>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; frame-src ${localhostUrl} ${cspSource} http: https:; img-src ${localhostUrl} ${cspSource} http: https:; script-src ${localhostUrl} ${cspSource} 'unsafe-inline'; style-src ${localhostUrl} ${cspSource} 'self' 'unsafe-inline' http: https:;"
/>
<style>
html,body {
margin: 0;
padding: 0;
background: white;
height: 100%;
width: 100%;
}
body {
margin: 0;
padding: 0;
webView.webview.onDidReceiveMessage(message => {
switch (message.command) {
case PreviewCommands.toVSCode.open:
if (message.data) {
commands.executeCommand('vscode.open', message.data);
}
return;
}
});
iframe {
width: 100%;
height: calc(100% - 30px);
border: 0;
margin-top: 30px;
}
.slug {
width: 100%;
position: fixed;
height: 30px;
display: flex;
align-items: center;
background-color: var(--vscode-editor-background);
border-bottom: 1px solid var(--vscode-focusBorder);
}
const dashboardFile = "dashboardWebView.js";
const localPort = `9000`;
const localServerUrl = `localhost:${localPort}`;
input {
color: var(--vscode-editor-foreground);
padding: 0.25rem 0.5rem;
background: none;
border: 0;
width: 100%;
}
</style>
</head>
<body>
<div class="slug">
<input type="text" value="${urlJoin(localhostUrl.toString(), slug || '')}" disabled />
</div>
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
</body>
</html>`;
const nonce = WebviewHelper.getNonce();
const ext = Extension.getInstance();
const isProd = ext.isProductionMode;
const version = ext.getVersion();
const isBeta = ext.isBetaVersion();
const extensionUri = ext.extensionPath;
const csp = [
`default-src 'none';`,
`img-src ${localhostUrl} ${cspSource} http: https:;`,
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`} 'unsafe-eval'`,
`style-src ${cspSource} 'self' 'unsafe-inline' http: https:`,
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
`frame-src ${localhostUrl} ${cspSource} http: https:;`,
];
let scriptUri = "";
if (isProd) {
scriptUri = webView.webview.asWebviewUri(Uri.joinPath(extensionUri, 'dist', dashboardFile)).toString();
} else {
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}
webView.webview.html = `
<!DOCTYPE html>
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Matter Preview</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden">
<div id="app" data-type="preview" data-url="${urlJoin(localhostUrl.toString(), slug || '')}" data-isProd="${isProd}" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;"></div>
<script ${isProd ? `nonce="${nonce}"` : ""} src="${scriptUri}"></script>
</body>
</html>
`;
Telemetry.send(TelemetryEvent.openPreview);
}

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";
@@ -5,8 +6,8 @@ import * as fs from "fs";
import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Logger, Settings } from "../helpers";
import { SETTING_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
import { FrameworkDetector, Logger, Settings } from "../helpers";
import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { SettingsListener } from '../listeners/dashboard';
export class Project {
@@ -24,33 +25,35 @@ categories: []
---
`;
public static isInitialized() {
return Settings.hasProjectFile();
}
/**
* Initialize a new "Project" instance.
*/
public static async init(sampleTemplate: boolean = true) {
public static async init(sampleTemplate?: boolean) {
try {
Settings.createTeamSettings();
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
const folder = Template.getSettings();
const templatePath = Project.templatePath();
// Add the default content type
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true);
if (!folder || !templatePath) {
return;
}
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
if (!fs.existsSync(templatePath.fsPath)) {
await workspace.fs.createDirectory(templatePath);
}
if (sampleTemplate) {
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
if (sampleTemplate !== undefined) {
await Project.createSampleTemplate();
} else {
Notifications.info("Project initialized successfully.");
}
Telemetry.send(TelemetryEvent.initialization);
Telemetry.send(TelemetryEvent.initialization)
// Check if you can find the framework
const wsFolder = Folders.getWorkspaceFolder();
const framework = FrameworkDetector.get(wsFolder?.fsPath || "");
if (framework) {
SettingsListener.setFramework(framework.name);
}
SettingsListener.getSettings();
} catch (err: any) {
@@ -59,6 +62,33 @@ categories: []
}
}
/**
* Creates the templates folder + sample if needed
* @param sampleTemplate
* @returns
*/
public static async createSampleTemplate(sampleTemplate?: boolean) {
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
const folder = Template.getSettings();
const templatePath = Project.templatePath();
if (!folder || !templatePath) {
return;
}
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
if (!fs.existsSync(templatePath.fsPath)) {
await workspace.fs.createDirectory(templatePath);
}
if (sampleTemplate) {
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
Notifications.info("Sample template created.");
}
}
/**
* Get the template path for the current project
*/

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 {
@@ -17,7 +16,8 @@ export class Settings {
public static async create(type: TaxonomyType) {
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"}`
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
ignoreFocusOut: true
});
if (newOption) {
@@ -36,7 +36,11 @@ export class Settings {
await SettingsHelper.updateTaxonomy(type, options);
// Ask if the new term needs to be added to the page
const addToPage = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?` });
const addToPage = await vscode.window.showQuickPick(["yes", "no"], {
canPickMany: false,
placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?`,
ignoreFocusOut: true
});
if (addToPage && addToPage === "yes") {
const editor = vscode.window.activeTextEditor;
@@ -71,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;
}
@@ -149,8 +153,10 @@ export class Settings {
"Category"
], {
placeHolder: `What do you want to remap?`,
canPickMany: false
canPickMany: false,
ignoreFocusOut: true
});
if (!taxType) {
return;
}
@@ -165,7 +171,8 @@ export class Settings {
const selectedOption = await vscode.window.showQuickPick(options, {
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
canPickMany: false
canPickMany: false,
ignoreFocusOut: true
});
if (!selectedOption) {
@@ -174,86 +181,25 @@ export class Settings {
const newOptionValue = await vscode.window.showInputBox({
prompt: `Specify the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} with which you want to remap "${selectedOption}". Leave the input <blank> if you want to remove the ${type === TaxonomyType.Tag ? "tag" : "category"} from all articles.`,
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
ignoreFocusOut: true
});
if (!newOptionValue) {
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?` });
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], {
canPickMany: false,
placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?`,
ignoreFocusOut: true
});
if (deleteAnswer === "no") {
return;
}
}
// 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, {
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

@@ -64,7 +64,8 @@ export class Template {
const titleValue = await vscode.window.showInputBox({
prompt: `What name would you like to give your template?`,
placeHolder: `article`
placeHolder: `article`,
ignoreFocusOut: true
});
if (!titleValue) {
@@ -77,6 +78,7 @@ export class Template {
{
canPickMany: false,
placeHolder: `Do you want to keep the contents for the template?`,
ignoreFocusOut: true
}
);
@@ -98,11 +100,24 @@ export class Template {
}
}
/**
* Retrieve all templates
*/
public static async getTemplates() {
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
if (!folder) {
Notifications.warning(`No templates found.`);
return;
}
return await vscode.workspace.findFiles(`${folder}/**/*`, "**/node_modules/**,**/archetypes/**");
}
/**
* Create from a template
*/
public static async create(folderPath: string) {
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
const contentTypes = ContentType.getAll();
if (!folderPath) {
@@ -110,19 +125,15 @@ export class Template {
return;
}
if (!folder) {
Notifications.warning(`No templates found.`);
return;
}
const templates = await vscode.workspace.findFiles(`${folder}/**/*`, "**/node_modules/**,**/archetypes/**");
const templates = await Template.getTemplates();
if (!templates || templates.length === 0) {
Notifications.warning(`No templates found.`);
return;
}
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
placeHolder: `Select the content template to use`
placeHolder: `Select the content template to use`,
ignoreFocusOut: true
});
if (!selectedTemplate) {
Notifications.warning(`No template selected.`);

View File

@@ -59,8 +59,8 @@ export class Wysiwyg {
const option = await window.showQuickPick([ ...qpItems ], {
placeHolder: "Which type of markup would you like to insert?",
canPickMany: false,
ignoreFocusOut: false,
canPickMany: false,
ignoreFocusOut: true
});
if (option) {
@@ -161,8 +161,8 @@ export class Wysiwyg {
"Heading 6"
], {
canPickMany: false,
placeHolder: "Which heading level do you want to insert?",
ignoreFocusOut: false
placeHolder: "Which heading level do you want to insert?",
ignoreFocusOut: true
});
if (headingLvl) {

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

@@ -25,20 +25,24 @@ export const COMMAND_NAME = {
createByContentType: getCommandName("createByContentType"),
createByTemplate: getCommandName("createByTemplate"),
createTemplate: getCommandName("createTemplate"),
initTemplate: getCommandName("initTemplate"),
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
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"),
diagnostics: getCommandName("diagnostics"),
modeSwitch: getCommandName("mode.switch"),
showOutputChannel: getCommandName("showOutputChannel"),
// Insert dashboards
insertImage: getCommandName("insertImage"),
insertMedia: getCommandName("insertMedia"),
insertSnippet: getCommandName("insertSnippet"),
// WYSIWYG
@@ -53,4 +57,9 @@ export const COMMAND_NAME = {
orderedlist: getCommandName("markup.orderedlist"),
taskList: getCommandName("markup.tasklist"),
options: getCommandName("markup.options"),
// Content types
generateContentType: getCommandName("contenttype.generate"),
addMissingFields: getCommandName("contenttype.addMissingFields"),
setContentType: getCommandName("contenttype.setContentType"),
};

View File

@@ -8,6 +8,7 @@ export const FEATURE_FLAG = {
metadata: "panel.metadata",
recentlyModified: "panel.recentlyModified",
otherActions: "panel.otherActions",
contentType: "panel.contentType",
},
dashboard: {
snippets: {
@@ -16,6 +17,9 @@ export const FEATURE_FLAG = {
},
data: {
view: "dashboard.data.view",
},
taxonomy: {
view: "dashboard.taxonomy.view"
}
}
};

View File

@@ -1,33 +1,89 @@
export const FrameworkDetectors = [
{
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
"requiredFiles": ["gatsby-config.js"],
"requiredDependencies": ["gatsby"],
"commands": {
"start": "npx gatsby develop"
export const FrameworkDetectors = [{
framework: {
name: "gatsby",
dist: "public",
static: "static",
build: "gatsby build"
},
requiredFiles: ["gatsby-config.js"],
requiredDependencies: ["gatsby"],
commands: {
start: "npx gatsby develop"
}
},
{
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
"requiredFiles": ["config.toml", "config.yaml", "config.yml"],
"commands": {
"start": "hugo server -D"
framework: {
name: "hugo",
dist: "public",
static: "static",
build: "hugo"
},
requiredFiles: ["config.toml", "config.yaml", "config.yml"],
commands: {
start: "hugo server -D"
}
},
{
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
"requiredFiles": ["next.config.js"],
"requiredDependencies": ["next"],
"commands": {
"start": "npx next dev"
framework: {
name: "next",
dist: ".next",
static: "public",
build: "next build"
},
requiredFiles: ["next.config.js"],
requiredDependencies: ["next"],
commands: {
start: "npx next dev"
}
},
{
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
"requiredFiles": ["nuxt.config.js"],
"requiredDependencies": ["nuxt"],
"commands": {
"start": "npx nuxt"
framework: {
name: "nuxt",
dist: "dist",
static: "static",
build: "nuxt"
},
requiredFiles: ["nuxt.config.js"],
requiredDependencies: ["nuxt"],
commands: {
start: "npx nuxt"
}
},
{
framework: {
name: "jekyll",
dist: "_site",
static: "assets",
build: "bundle exec jekyll build"
},
requiredFiles: ["Gemfile"],
requiredDependencies: ["jekyll"],
commands: {
start: "bundle exec jekyll serve --livereload"
}
},
{
framework: {
name: "docusaurus",
dist: "build",
static: "static",
build: "npx docusaurus build"
},
requiredFiles: ["docusaurus.config.js"],
requiredDependencies: ["@docusaurus/core"],
commands: {
start: "npx docusaurus start"
}
},
{
framework: {
name: "11ty",
dist: "_site",
build: "npx @11ty/eleventy"
},
requiredDependencies: ["@11ty/eleventy"],
commands: {
start: "npx @11ty/eleventy --serve"
}
}
];

View File

@@ -1,5 +1,10 @@
export enum GeneralCommands{
setMode = "setMode"
export const GeneralCommands = {
toWebview: {
setMode: "setMode",
},
toVSCode: {
openLink: "openLink",
}
};

View File

@@ -0,0 +1,8 @@
export const PreviewCommands = {
toVSCode: {
open: `preview.open`
},
fromVSCode: {}
};

View File

@@ -10,6 +10,7 @@ export const TelemetryEvent = {
openMediaDashboard: 'openMediaDashboard',
openDataDashboard: 'openDataDashboard',
openSnippetsDashboard: 'openSnippetsDashboard',
openTaxonomyDashboard: 'openTaxonomyDashboard',
closeDashboard: 'closeDashboard',
// Other actions
@@ -22,9 +23,15 @@ export const TelemetryEvent = {
refreshMedia: 'refreshMedia',
deleteMedia: 'deleteMedia',
insertMediaToContent: 'insertMediaToContent',
insertFileToContent: 'insertFileToContent',
updateMediaMetadata: 'updateMediaMetadata',
openExplorerView: 'openExplorerView',
// Content types
generateContentType: 'generateContentType',
addMissingFields: 'addMissingFields',
setContentType: 'setContentType',
// Custom scripts
runCustomScript: 'runCustomScript',
runMediaScript: 'runMediaScript',
@@ -35,4 +42,5 @@ export const TelemetryEvent = {
webviewDataView: 'webviewDataView',
webviewContentsView: 'webviewContentsView',
webviewSnippetsView: 'webviewSnippetsView',
webviewTaxonomyDashboard: 'webviewTaxonomyDashboard',
};

View File

@@ -9,6 +9,8 @@ export const CONTEXT = {
backer: "frontMatter:backers:supporter",
isValidFile: "frontMatter:file:isValid",
hasViewModes: "frontMatter:has:modes",
isSnippetsDashboardEnabled: "frontMatter:dashboard:snippets:enabled",
isDataDashboardEnabled: "frontMatter:dashboard:data:enabled",
};

View File

@@ -9,6 +9,7 @@ export * from './GeneralCommands';
export * from './Links';
export * from './LocalStore';
export * from './Navigation';
export * from './PreviewCommands';
export * from './TelemetryEvent';
export * from './charCode';
export * from './charMap';

View File

@@ -32,6 +32,7 @@ export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
export const SETTING_TEMPLATES_FOLDER = "templates.folder";
export const SETTING_TEMPLATES_PREFIX = "templates.prefix";
export const SETTING_TEMPLATES_ENABLED = "templates.enabled";
export const SETTING_TELEMETRY_DISABLE = "telemetry.disable";
@@ -61,7 +62,6 @@ export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
export const SETTING_DATA_FILES = "data.files";
@@ -89,3 +89,8 @@ export const SETTING_DATE_FIELD = "taxonomy.dateField";
* Use the `isModifiedDate` property on the content type datetime field instead
*/
export const SETTING_MODIFIED_FIELD = "taxonomy.modifiedField";
/**
* @deprecated
* Use the `frontMatter.content.snippets` setting instead
*/
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";

View File

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

View File

@@ -5,6 +5,11 @@ export enum DashboardMessage {
getMode = 'getMode',
showWarning = 'showWarning',
// Welcome view
initializeProject = 'initializeProject',
setFramework = 'setFramework',
addFolder = 'addFolder',
// Content dashboard
getData = 'getData',
createContent = 'createContent',
@@ -22,9 +27,10 @@ export enum DashboardMessage {
uploadMedia = 'uploadMedia',
deleteMedia = 'deleteMedia',
revealMedia = 'revealMedia',
insertPreviewImage = 'insertPreviewImage',
insertMedia = 'insertMedia',
updateMediaMetadata = 'updateMediaMetadata',
createMediaFolder = 'createMediaFolder',
insertFile = 'insertFile',
// Data dashboard
getDataEntries = 'getDataEntries',
@@ -35,11 +41,19 @@ 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',
initializeProject = 'initializeProject',
setFramework = 'setFramework',
setState = 'setState',
runCustomScript = 'runCustomScript',
sendTelemetry = 'sendTelemetry',

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

@@ -12,10 +12,11 @@ export interface IChoiceButtonProps {
onClick: () => void;
}[];
disabled?: boolean;
isTemplatesEnabled?: boolean;
onClick: () => void;
}
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, isTemplatesEnabled, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
return (
<span className="relative z-50 inline-flex shadow-sm rounded-md">
<button
@@ -27,36 +28,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>
{
isTemplatesEnabled && (
<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`}>
<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

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { EyeIcon, TrashIcon } from '@heroicons/react/outline';
import { EyeIcon, TerminalIcon, TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { CustomScript, ScriptType } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
@@ -43,7 +43,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
<MenuItem
key={script.title}
title={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>}
onClick={(value, e) => runCustomScript(e, script)} />
))
}, [scripts]);
@@ -70,15 +70,15 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
<ActionMenuButton title={`Menu`} />
<MenuItems widthClass='w-40' marginTopClass='mt-6'>
<MenuItems widthClass='w-44' marginTopClass='mt-6'>
<MenuItem
title={`View`}
onClick={onView} />
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 }
<MenuItem
title={`Delete`}
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>
</Menu>

View File

@@ -9,14 +9,16 @@ import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardViewType } from '../../models';
import { ContentActions } from './ContentActions';
import { useMemo } from 'react';
export interface IItemProps extends Page {}
const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
const view = useRecoilValue(ViewSelector);
const settings = useRecoilValue(SettingsSelector);
const draftField = useMemo(() => settings?.draftField, [settings]);
const openFile = () => {
Messenger.send(DashboardMessage.openFile, fmFilePath);
@@ -28,7 +30,20 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
}
const tagField = settings.dashboardState.contents.tags;
return pageData[tagField] || [];
if (tagField === "tags") {
return pageData.fmTags;
} else if (tagField === "categories") {
return pageData.fmCategories;
}
const tagsValue = pageData[tagField] || [];
if (Array.isArray(tagsValue)) {
return tagsValue;
}
return [tagsValue];
}, [settings, pageData]);
if (view === DashboardViewType.Grid) {
@@ -51,7 +66,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<div className="relative p-4 w-full">
<div className={`flex justify-between items-center`}>
<Status draft={draft} />
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
<DateField className={`mr-4`} value={date} />
@@ -96,7 +111,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<DateField value={date} />
</div>
<div className="col-span-2">
<Status draft={draft} />
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
</div>
</button>
</li>

View File

@@ -25,7 +25,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settin
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
{
settings && settings?.folders?.length > 0 ? (
settings && settings?.contentFolders?.length > 0 ? (
<p className={`text-xl font-medium`}>No Markdown to show</p>
) : (
<>

View File

@@ -1,69 +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';
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();
if (!settings) {
return <Spinner />;
}
if (showWelcome) {
return <WelcomeScreen settings={settings} />;
}
if (!settings.initialized || settings.folders?.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 {}
@@ -149,15 +150,14 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
{
(dataFiles && dataFiles.length > 0) && (
dataFiles.map((dataFile) => (
<button
key={dataFile.id}
type='button'
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
dataFiles.map((dataFile, idx) => (
<NavigationItem
key={`${dataFile.id}-${idx}`}
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

@@ -10,7 +10,7 @@ import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
import { CategoryAtom, SortingAtom, TagAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -19,6 +19,9 @@ 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 } from 'react';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -34,8 +37,9 @@ 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 resetSorting = useResetRecoilState(SortingAtom);
const location = useLocation();
const navigate = useNavigate();
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
@@ -50,7 +54,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
};
const updateView = (view: NavigationType) => {
setView(view);
navigate(routePaths[view]);
resetSorting();
}
@@ -68,6 +72,27 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
onClick: () => runBulkScript(s)
}));
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,7 +101,7 @@ 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 />
@@ -109,6 +134,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
...customActions
]}
onClick={createContent}
isTemplatesEnabled={settings?.dashboardState?.contents?.templatesEnabled || undefined}
disabled={!settings?.initialized} />
</div>
</div>
@@ -141,7 +167,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
}
{
view === NavigationType.Media && (
location.pathname === routePaths.media && (
<>
<MediaHeaderTop />

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

@@ -3,7 +3,7 @@ import { PencilAltIcon, XIcon } from '@heroicons/react/outline';
import { format } from 'date-fns';
import { basename } from 'path';
import * as React from 'react';
import { Fragment, useMemo } from 'react';
import { Fragment, useCallback, useMemo } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { MediaInfo } from '../../../models';
import { Messenger } from '@estruyf/vscode/dist/client';
@@ -18,14 +18,16 @@ export interface IDetailsSlideOverProps {
folder: string;
media: MediaInfo;
showForm: boolean;
isImageFile: boolean;
onEdit: () => void;
onEditClose: () => void;
onDismiss: () => void;
}
export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> = ({ imgSrc, size, dimensions, media, folder, showForm, onEdit, onEditClose, onDismiss }: React.PropsWithChildren<IDetailsSlideOverProps>) => {
export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> = ({ imgSrc, size, dimensions, media, folder, showForm, onEdit, onEditClose, onDismiss, isImageFile }: React.PropsWithChildren<IDetailsSlideOverProps>) => {
const [ filename, setFilename ] = React.useState<string>(media.filename);
const [ caption, setCaption ] = React.useState<string | undefined>(media.caption);
const [ title, setTitle ] = React.useState<string | undefined>(media.title);
const [ alt, setAlt ] = React.useState(media.alt);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const page = useRecoilValue(PageSelector);
@@ -37,20 +39,22 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
const extension = fileInfo?.pop();
const name = fileInfo?.join('.');
const onSubmitMetadata = () => {
const onSubmitMetadata = useCallback(() => {
Messenger.send(DashboardMessage.updateMediaMetadata, {
file: media.fsPath,
filename,
caption,
alt,
title,
folder: selectedFolder,
page
});
onEditClose();
};
}, [media, filename, caption, alt, title, selectedFolder, page])
React.useEffect(() => {
setTitle(media.title);
setAlt(media.alt);
setCaption(media.caption);
setFilename(media.filename);
@@ -93,9 +97,13 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
<div className="relative mt-6 flex-1 px-4 sm:px-6">
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
<div>
<div className="block w-full aspect-w-10 aspect-h-7 overflow-hidden border-gray-200 dark:border-vulcan-200 border">
<img src={imgSrc} alt={media.filename} className="object-cover" />
</div>
{
isImageFile && (
<div className="block w-full aspect-w-10 aspect-h-7 overflow-hidden border-gray-200 dark:border-vulcan-200 border">
<img src={imgSrc} alt={media.filename} className="object-cover" />
</div>
)
}
<div className="mt-4 flex items-start justify-between">
<div>
<h2 className="text-lg font-medium text-vulcan-300 dark:text-whisper-500"><span className="sr-only">Details for </span>{media.filename}</h2>
@@ -128,31 +136,51 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Caption
</label>
<div className="mt-1">
<textarea
rows={3}
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={caption || ''}
onChange={(e) => setCaption(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Alt tag value
Title
</label>
<div className="mt-1">
<input
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={alt || ''}
onChange={(e) => setAlt(e.target.value)}
value={title || ''}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</div>
{
isImageFile && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Caption
</label>
<div className="mt-1">
<textarea
rows={3}
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={caption || ''}
onChange={(e) => setCaption(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Alt tag value
</label>
<div className="mt-1">
<input
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={alt || ''}
onChange={(e) => setAlt(e.target.value)}
/>
</div>
</div>
</>
)
}
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
@@ -192,16 +220,27 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
<dt className="text-vulcan-100 dark:text-whisper-900">Filename</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.filename}</dd>
</div>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Caption</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.caption || ""}</dd>
<dt className="text-vulcan-100 dark:text-whisper-900">Title</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.title}</dd>
</div>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Alternate text</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.alt || ""}</dd>
</div>
{
isImageFile && (
<>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Caption</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.caption || ""}</dd>
</div>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Alternate text</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.alt || ""}</dd>
</div>
</>
)
}
</dl>
</>
)

View File

@@ -1,21 +1,22 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
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 { useRecoilState, useRecoilValue } from 'recoil';
import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { ScriptType } from '../../../models';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { ScriptType, Snippet } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { ActionMenuButton } from '../Menu/ActionMenuButton';
import { QuickAction } from '../Menu/QuickAction';
import { Alert } from '../Modals/Alert';
import { InfoDialog } from '../Modals/InfoDialog';
import { DetailsSlideOver } from './DetailsSlideOver';
export interface IItemProps {
@@ -26,6 +27,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
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);
@@ -34,6 +36,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const mediaSnippets = useMemo(() => {
if (!settings?.snippets) {
return [];
}
const keys = Object.keys(settings.snippets);
return keys.filter(key => (settings.snippets || {})[key].isMediaSnippet).map(key => ({ title: key, ...(settings.snippets || {})[key]}));
}, [settings]);
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
let relPath = media.fsPath.split(settings.wsFolder).pop();
@@ -50,10 +61,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const getRelPath = () => {
let relPath: string | undefined = "";
if (settings?.wsFolder && media.fsPath) {
relPath = media.fsPath.split(settings.wsFolder).pop();
const wsFolderParsed = parseWinPath(settings.wsFolder);
const mediaParsed = parseWinPath(media.fsPath);
relPath = mediaParsed.split(wsFolderParsed).pop();
if (settings.staticFolder && relPath) {
relPath = relPath.split(settings.staticFolder).pop();
const staticFolderParsed = parseWinPath(settings.staticFolder);
relPath = relPath.split(staticFolderParsed).pop();
}
}
return relPath;
@@ -74,39 +89,70 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const insertToArticle = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
alt: alt || "",
caption: caption || ""
});
if (viewData?.data?.type === "file") {
Messenger.send(DashboardMessage.insertFile, {
relPath: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
title: media.title
});
} else {
Messenger.send(DashboardMessage.insertMedia, {
relPath: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
alt: alt || "",
caption: caption || "",
title: media.title || ""
});
}
};
const insertSnippet = () => {
const insertSnippet = useCallback(() => {
if (mediaSnippets.length === 1) {
processSnippet(mediaSnippets[0]);
} else {
// Show dialog to select
setShowSnippetSelection(true);
}
}, [mediaSnippets]);
const processSnippet = useCallback((snippet: Snippet) => {
setShowSnippetSelection(false);
const relPath = getRelPath();
let snippet = settings?.mediaSnippet.join("\n");
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
snippet = snippet?.replace("{alt}", alt || "");
snippet = snippet?.replace("{caption}", caption || "");
snippet = snippet?.replace("{filename}", basename(relPath || ""));
snippet = snippet?.replace("{mediaWidth}", media?.dimensions?.width?.toString() || "");
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
const fieldData = {
mediaUrl: parseWinPath(relPath) || "",
alt: alt || "",
caption: caption || "",
title: media.title || "",
filename: basename(relPath || ""),
mediaWidth: media?.dimensions?.width?.toString() || "",
mediaHeight: media?.dimensions?.height?.toString() || "",
};
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
Messenger.send(DashboardMessage.insertMedia, {
relPath: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
position: viewData?.data?.position || null,
snippet
snippet: output
});
};
}, [alt, caption, media, settings, viewData, mediaSnippets]);
const deleteMedia = () => {
setShowAlert(true);
@@ -168,7 +214,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
};
const openLightbox = useCallback(() => {
if (!isVideoFile() && !isAudioFile()) {
if (isImageFile) {
setLightbox(media.vsPath || "");
}
}, [media.vsPath]);
@@ -182,26 +228,26 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
<MenuItem
key={script.title}
title={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>}
onClick={() => runCustomScript(script)} />
))
}
const isVideoFile = useCallback(() => {
const isVideoFile = useMemo(() => {
if (media.mimeType?.startsWith("video/")) {
return true;
}
return false;
}, [media]);
const isAudioFile = useCallback(() => {
const isAudioFile = useMemo(() => {
if (media.mimeType?.startsWith("audio/")) {
return true;
}
return false;
}, [media]);
const isImageFile = useCallback(() => {
const isImageFile = useMemo(() => {
if (media.mimeType?.startsWith("image/")) {
return true;
}
@@ -209,27 +255,37 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}, [media]);
const renderMediaIcon = useMemo(() => {
if (isVideoFile()) {
return <VideoCameraIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
if (isAudioFile()) {
return <MusicNoteIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
const path = media.fsPath;
const extension = path.split('.').pop();
if (isImageFile()) {
if (isImageFile) {
return <PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}
return <DocumentIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
}, [media]);
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`} />;
}
if (isAudioFile) {
icon = <MusicNoteIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
}
return (
<div className='w-full h-full flex justify-center items-center'>
{icon}
<span className='text-2xl font-bold absolute top-0 right-0 bottom-0 left-0 flex justify-center items-center'>{extension}</span>
</div>
);
}, [media, isImageFile, isVideoFile, isAudioFile]);
const renderMedia = useMemo(() => {
if (isVideoFile() || isAudioFile()) {
if (isVideoFile || isAudioFile) {
return null;
}
if (isImageFile()) {
if (isImageFile) {
return <img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />;
}
@@ -258,7 +314,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
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">
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<button className={`relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImageFile ? "cursor-pointer" : "cursor-default"}`} onClick={openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
{
renderMediaIcon
@@ -297,7 +353,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
</QuickAction>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
(viewData?.data?.position && mediaSnippets.length > 0) && (
<QuickAction
title='Insert snippet'
onClick={insertSnippet}>
@@ -328,7 +384,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<MenuItems widthClass='w-40'>
<MenuItem
title={`Edit metadata`}
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}
/>
@@ -336,15 +392,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
viewData?.data?.filePath ? (
<>
<MenuItem
title={`Insert image markdown`}
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 && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
<MenuItem
title={`Insert snippet`}
onClick={insertSnippet} />
)
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() }
@@ -361,11 +418,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}
<MenuItem
title={`Reveal media`}
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={`Delete`}
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>
</Menu>
@@ -375,6 +432,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center break-all">
{basename(parseWinPath(media.fsPath) || "")}
</p>
{
!isImageFile && media.title && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-2`}>Title:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.title}</span>
</p>
)
}
{
media.caption && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
@@ -402,6 +467,32 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
</div>
</li>
{
showSnippetSelection && (
<InfoDialog
icon={<CodeIcon className="h-6 w-6" aria-hidden="true" />}
title='Insert snippet'
description='Select the media snippet to use for the current media file.'
dismiss={() => setShowSnippetSelection(false)}>
<ul className='flex justify-center'>
{
mediaSnippets.map((snippet, idx) => (
<li key={idx} className="inline-flex items-center pb-2 mr-2">
<button
className="w-full inline-flex justify-center border border-transparent shadow-sm px-4 py-2 bg-teal-600 text-base font-medium text-white hover:bg-teal-700 dark:hover:bg-teal-900 focus:outline-none sm:w-auto sm:text-sm disabled:opacity-30"
onClick={() => processSnippet(snippet)}>
{snippet.title}
</button>
</li>
))
}
</ul>
</InfoDialog>
)
}
{
showDetails && (
<DetailsSlideOver
@@ -411,6 +502,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
folder={getFolder()}
media={media}
showForm={showForm}
isImageFile={isImageFile}
onEdit={() => setShowForm(true)}
onEditClose={() => setShowForm(false)}
onDismiss={() => { setShowDetails(false); setShowForm(false); }} />

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 { join } from 'path';
import { basename, extname, join } from 'path';
export interface IMediaProps {}
@@ -29,23 +29,47 @@ 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(() => {
let mediaFiles = media;
// Check if content allows page bundle
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
return media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
}
return media;
if (viewData && viewData.data && viewData.data.type === "file" && viewData.data.fileExtensions && viewData.data.fileExtensions.length > 0) {
const supportedExtensions = viewData.data.fileExtensions;
mediaFiles = mediaFiles.filter(m => {
const ext = extname(m.fsPath);
// Remove the dot from the extension
const extWithoutDot = ext.substring(1);
return supportedExtensions.includes(extWithoutDot);
});
}
return mediaFiles;
}, [media, viewData, settings?.staticFolder]);
const onDrop = useCallback((acceptedFiles: File[]) => {
@@ -115,11 +139,33 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
}
{
allFolders && allFolders.length > 0 && (
contentFolders && contentFolders.length > 0 && contentFolders.map(group => (
group.folders && group.folders.length > 0 && (
<div 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

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

@@ -0,0 +1,72 @@
import { Dialog, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment } from 'react';
export interface IInfoDialogProps {
icon?: JSX.Element;
title: string;
description: string;
dismiss: () => void;
}
export const InfoDialog: React.FunctionComponent<IInfoDialogProps> = ({dismiss, icon, title, description, children}: React.PropsWithChildren<IInfoDialogProps>) => {
return (
<Transition.Root show={true} as={Fragment}>
<Dialog className="fixed z-10 inset-0 overflow-y-auto" onClose={dismiss}>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white dark:bg-vulcan-500 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 border-whisper-900">
<div className="sm:flex sm:items-start">
{
icon && (
<div className="mt-3 sm:mr-4 mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:mx-0 sm:h-10 sm:w-10 bg-gray-50 dark:bg-vulcan-400">
{icon}
</div>
)
}
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-vulcan-300 dark:text-whisper-900">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-vulcan-500 dark:text-whisper-500">
{description}
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4">
{children}
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Tab } from '../constants/Tab';
import { SettingsAtom, TabAtom } from '../state';
import { SettingsAtom, TabAtom, TabInfoAtom } from '../state';
export interface INavigationProps {
totalPages: number;
@@ -15,8 +15,9 @@ export const tabs = [
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
const tabInfo = useRecoilValue(TabInfoAtom);
const settings = useRecoilValue(SettingsAtom);
return (
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{
@@ -28,7 +29,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPage
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
{tab.name}{(tabInfo && tabInfo[tab.id]) ? ` (${tabInfo[tab.id]})` : ''}
</button>
))
) : (
@@ -38,7 +39,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPage
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tabs[0].id)}
>
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
{tabs[0].name}{(tabInfo && tabInfo[tabs[0].id]) ? ` (${tabInfo[tabs[0].id]})` : ''}
</button>
{
@@ -49,7 +50,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPage
aria-current={value === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(value)}
>
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
{value}{(tabInfo && tabInfo[value]) ? ` (${tabInfo[value]})` : ''}
</button>
))
}

View File

@@ -0,0 +1,69 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { ExternalLinkIcon, RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRef } from 'react';
import { PreviewCommands } from '../../../constants';
export interface IPreviewProps {
url: string | null;
}
export const Preview: React.FunctionComponent<IPreviewProps> = ({url}: React.PropsWithChildren<IPreviewProps>) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const onRefresh = () => {
if (iframeRef.current?.src) {
iframeRef.current.src = iframeRef.current.src;
}
};
const openInBrowser = () => {
Messenger.send(PreviewCommands.toVSCode.open, url);
};
return (
<div className='w-full h-full bg-white'>
<div
className="slug fixed w-full top-0 flex items-center"
style={{
height: "30px",
background: "var(--vscode-editor-background)",
borderBottom: "1px solid var(--vscode-focusBorder)"
}}
>
<input
type="text"
value={url || ""}
className="w-full h-full border-0 bg-transparent text-xs py-1 px-2"
style={{
color: "var(--vscode-editor-foreground)"
}}
disabled />
<div
className="actions absolute right-0 top-0 bottom-0 flex items-center space-x-2 px-2"
style={{
background: "var(--vscode-editor-background)",
}}
>
<button title="Refresh" onClick={onRefresh}>
<RefreshIcon className='w-4 h-4' aria-hidden="true" />
</button>
<button title="Open" onClick={openInBrowser} className="mr-2">
<ExternalLinkIcon className='w-4 h-4' aria-hidden="true" />
</button>
</div>
</div>
<iframe
ref={iframeRef}
src={url || ""}
className={`w-full border-0`}
style={{
height: "calc(100% - 30px)",
marginTop: "30px"
}}></iframe>
</div>
);
};

View File

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

View File

@@ -1,12 +1,13 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { CodeIcon, DocumentTextIcon, DotsHorizontalIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
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';
@@ -31,9 +32,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
const [ snippetTitle, setSnippetTitle ] = useState<string>('');
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
const [ snippetOriginalBody, setSnippetOriginalBody ] = useState<string>('');
const [ mediaSnippet, setMediaSnippet ] = useState<boolean>(false);
const formRef = useRef<SnippetFormHandle>(null);
const insertToContent = useMemo(() => viewData?.data?.filePath, [ viewData ]);
const insertToArticle = () => {
formRef.current?.onSave();
setShowInsertDialog(false);
@@ -44,6 +48,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
setSnippetTitle('');
setSnippetDescription('');
setSnippetOriginalBody('');
setMediaSnippet(false);
};
const onOpenEdit = useCallback(() => {
@@ -51,6 +56,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
setSnippetDescription(snippet.description);
setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`));
setShowEditDialog(true);
setMediaSnippet(!!snippet.isMediaSnippet);
}, [snippet]);
const onSnippetUpdate = useCallback(() => {
@@ -68,11 +74,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
const snippetContents: Snippet = {
...crntSnippet,
fields,
description: snippetDescription || '',
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
};
if (!mediaSnippet) {
snippetContents.fields = fields;
} else {
snippetContents.isMediaSnippet = true;
}
// Check if new or update
if (title === snippetTitle) {
snippets[title] = snippetContents;
@@ -84,7 +95,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
Messenger.send(DashboardMessage.updateSnippet, { snippets });
reset();
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody]);
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]);
const onDelete = useCallback(() => {
const snippets = Object.assign({}, settings?.snippets || {});
@@ -102,13 +113,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
<CodeIcon className='w-64 h-64 opacity-5 text-vulcan-200 dark:text-gray-400' />
</div>
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<h2 className="mt-2 mb-2 font-bold flex items-center" title={snippet.isMediaSnippet ? "Media snippet" : "Content snippet"}>
{ snippet.isMediaSnippet ? <PhotographIcon className='w-5 h-5 mr-1' aria-hidden={true} /> : <DocumentTextIcon className='w-5 h-5 mr-1' aria-hidden={true} /> }
{title}
</h2>
<FeatureFlag
features={mode?.features || []}
flag={FEATURE_FLAG.dashboard.snippets.manage}
alternative={(
viewData?.data?.filePath ? (
insertToContent ? (
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='group-hover:hidden'>
@@ -135,7 +150,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
<div className='hidden group-hover:flex'>
{
viewData?.data?.filePath && (
insertToContent && !snippet.isMediaSnippet && (
<>
<QuickAction
title={`Insert snippet`}
@@ -170,7 +185,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
<FormDialog
title={`Insert snippet: ${title}`}
description={`Insert the ${title.toLowerCase()} snippet into the current article`}
isSaveDisabled={!viewData?.data?.filePath}
isSaveDisabled={!insertToContent}
trigger={insertToArticle}
dismiss={() => setShowInsertDialog(false)}
okBtnText='Insert'
@@ -200,6 +215,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
title={snippetTitle}
description={snippetDescription}
body={snippetOriginalBody}
isMediaSnippet={mediaSnippet}
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
onTitleUpdate={(value: string) => setSnippetTitle(value)}
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
onBodyUpdate={(value: string) => setSnippetOriginalBody(value)} />

View File

@@ -1,16 +1,24 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { GeneralCommands } from '../../../constants';
export interface INewFormProps {
title: string;
description: string;
body: string;
isMediaSnippet: boolean;
onMediaSnippetUpdate: (value: boolean) => void;
onTitleUpdate: (value: string) => void;
onDescriptionUpdate: (value: string) => void;
onBodyUpdate: (value: string) => void;
}
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, isMediaSnippet, onMediaSnippetUpdate, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
const openLink = () => {
Messenger.send(GeneralCommands.toVSCode.openLink, "https://frontmatter.codes/docs/markdown#placeholders");
}
return (
<div className='space-y-4'>
@@ -60,6 +68,36 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, descrip
/>
</div>
</div>
<div>
<label htmlFor={`snippet`} className="block text-sm font-medium">
Is a media snippet?
</label>
<div className="mt-1 relative flex items-start">
<div className="flex items-center h-5">
<input
id="isMediaSnippet"
aria-describedby="isMediaSnippet-description"
name="isMediaSnippet"
type="checkbox"
checked={isMediaSnippet}
onChange={(e) => onMediaSnippetUpdate(e.currentTarget.checked)}
className="focus:ring-teal-500 h-4 w-4 text-teal-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="isMediaSnippet" className="font-medium text-vulcan-100 dark:text-whisper-900">
Media snippet
</label>
<p id="isMediaSnippet-description" className="text-vulcan-300 dark:text-whisper-500">
Use the current snippet for inserting media files into your content.
</p>
<p>
Check our <button className='text-teal-700 hover:text-teal-500' onClick={openLink} title='media snippet placeholders'>media snippet placeholders</button> documentation to know which placeholders you can use.
</p>
</div>
</div>
</div>
</div>
);
};

View File

@@ -103,7 +103,7 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
return (
<div>
<pre className='border border-opacity-40 p-2 whitespace-pre-wrap break-words'>
<pre className='border border-opacity-40 p-2 whitespace-pre-wrap break-words max-h-64 overflow-auto'>
{snippetBody}
</pre>

View File

@@ -36,8 +36,9 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
<textarea
name={field.name}
value={field.value || ""}
className="focus:outline-none block w-full sm:text-sm border-gray-300 text-vulcan-500"
className="focus:outline-none block w-full sm:text-sm border-gray-300 text-vulcan-500 h-auto"
onChange={(e) => onValueChange(field, e.currentTarget.value)}
rows={4}
/>
)
}

View File

@@ -25,6 +25,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
const [ snippetBody, setSnippetBody ] = useState<string>('');
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
const [ mediaSnippet, setMediaSnippet ] = useState(false);
const snippets = settings?.snippets || {};
const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]);
@@ -41,11 +42,12 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
title: snippetTitle,
description: snippetDescription || '',
body: snippetBody,
fields
fields,
isMediaSnippet: mediaSnippet
});
reset();
}, [snippetTitle, snippetDescription, snippetBody]);
}, [snippetTitle, snippetDescription, snippetBody, mediaSnippet]);
const reset = () => {
setShowCreateDialog(false);
@@ -81,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`}>
@@ -127,6 +129,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
title={snippetTitle}
description={snippetDescription}
body={snippetBody}
isMediaSnippet={mediaSnippet}
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
onTitleUpdate={(value: string) => setSnippetTitle(value)}
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
onBodyUpdate={(value: string) => setSnippetBody(value)} />

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

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsAtom } from '../state';
@@ -9,15 +10,29 @@ export interface IStatusProps {
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
const settings = useRecoilValue(SettingsAtom);
const draftField = useMemo(() => settings?.draftField, [settings]);
const draftValue = useMemo(() => {
if (draftField && draftField.type === 'choice') {
return draft;
} else if (draftField && typeof draftField.invert !== 'undefined' && draftField.invert) {
return !draft;
} else {
return draft;
}
}, [draftField, draft]);
if (settings?.draftField && settings.draftField.type === "choice") {
if (draft) {
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
if (draftValue) {
return <span className={`inline-block px-2 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draftValue}</span>;
} else {
return null;
}
}
return (
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
<span className={`inline-block px-2 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draftValue ? "bg-red-500" : "bg-teal-500"}`}>
{draftValue ? "Draft" : "Published"}
</span>
);
};

View File

@@ -47,7 +47,7 @@ export const Step: React.FunctionComponent<IStepProps> = ({name, description, st
<span className="ml-4 min-w-0 flex flex-col">
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
<div className="text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
<div className="mt-1 text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
</span>
</>
);

View File

@@ -4,35 +4,74 @@ import { DashboardMessage } from '../../DashboardMessage';
import { Settings } from '../../models/Settings';
import { Status } from '../../models/Status';
import { Step } from './Step';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Menu } from '@headlessui/react';
import { MenuItem } from '../Menu';
import { Framework } from '../../../models';
import {ChevronDownIcon} from '@heroicons/react/outline';
import { ContentFolder, Framework } from '../../../models';
import {CheckCircleIcon, ChevronDownIcon} from '@heroicons/react/outline';
import {CheckCircleIcon as CheckCircleIconSolid} from '@heroicons/react/solid';
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
import { join } from 'path';
export interface IStepsToGetStartedProps {
settings: Settings;
}
const Folder = ({ wsFolder, folder, folders, addFolder }: { wsFolder: string, folder: string, folders: ContentFolder[], addFolder: (folder: string) => void}) => {
const isAdded = useMemo(() => folders.find(f => f.path.toLowerCase() === join(wsFolder, folder).toLowerCase()), [folder, folders, wsFolder]);
return (
<div className={`text-sm flex items-center ${isAdded ? "text-teal-800" : "text-vulcan-300 dark:text-whisper-800" }`}>
<button onClick={() => addFolder(folder)} className='mr-2 hover:text-teal-500' title={`Add as a content folder to Front Matter`}>
{ isAdded ? <CheckCircleIconSolid className={`h-4 w-4`} /> : <CheckCircleIcon className={`h-4 w-4`} /> }
</button>
<span>{folder}</span>
</div>
)
}
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);
const setFrameworkAndSendMessage = (framework: string) => {
setFramework(framework);
Messenger.send(DashboardMessage.setFramework, framework);
};
const addFolder = (folder: string) => {
Messenger.send(DashboardMessage.addFolder, folder);
};
const reload = () => {
const crntState: any = Messenger.getState() || {};
Messenger.setState({
...crntState,
isWelcomeConfiguring: false
});
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>
@@ -76,15 +115,50 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
onClick: undefined
},
{
name: 'Register content folders (manual action)',
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
id: `welcome-content-folders`,
name: 'Register content folder(s)',
description: (
<>
<p>Add one of the folders we found in your project as a content folder. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</p>
{
settings?.dashboardState?.welcome?.contentFolders?.length > 0 && (
<div className="mt-4">
<div className="text-sm">
Folders containing content:
</div>
<div className="mt-1 space-y-1">
{settings?.dashboardState?.welcome?.contentFolders?.map((folder) => (
<Folder
key={folder}
folder={folder}
addFolder={addFolder}
wsFolder={settings.wsFolder}
folders={settings.contentFolders} />
))}
</div>
</div>
)
}
<p className='mt-4 text-vulcan-300 dark:text-gray-400'><b>IMPORTANT</b>: You can perform this action by <b>right-clicking on the folder in the explorer view</b>, and selecting <b>register folder</b>.</p>
</>
),
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 both actions are completed, click on this action to load the dashboard.</>,
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
description: <>Once all actions are completed, the dashboard can be loaded.</>,
status: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? Status.Active : Status.NotStarted,
onClick: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? reload : undefined
}
];
@@ -98,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,67 @@
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,181 @@
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) => {
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

@@ -15,18 +15,23 @@ export interface IWelcomeScreenProps {
export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({settings}: React.PropsWithChildren<IWelcomeScreenProps>) => {
React.useEffect(() => {
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewWelcomeScreen
});
const crntState: any = Messenger.getState() || {};
Messenger.setState({
...crntState,
isWelcomeConfiguring: true
});
return () => {
Messenger.send(DashboardMessage.reload)
};
}, []);
return (
<div className={`h-full overflow-auto py-24`}>
<div className={`h-full overflow-auto py-8`}>
<main>
<div className="mx-auto max-w-7xl">
<div className="grid grid-cols-12 gap-8">
@@ -82,7 +87,7 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({set
<StepsToGetStarted settings={settings} />
<p className="mt-5 text-sm text-vulcan-300 dark:text-whisper-700">
Once you completed both actions, the dashboard will show its full potential. You can also use the extension from the <b>Front Matter</b> side panel. There you will find the actions you can perform specifically for your pages.
You can also use the extension from the <b>Front Matter</b> side panel. There you will find the actions you can perform specifically for your pages.
</p>
</div>
</div>

View File

@@ -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);
}
@@ -47,7 +49,7 @@ export default function useMessages() {
case DashboardCommand.searchReady:
setSearchReady(true);
break;
case GeneralCommands.setMode:
case GeneralCommands.toWebview.setMode:
setMode(message.data);
break;
}

View File

@@ -2,18 +2,19 @@ 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, TabSelector, TagSelector } from '../state';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabInfoAtom, TabSelector, TagSelector } from '../state';
import { SortOrder, SortType } from '../../models';
import { Sorting } from '../../helpers/Sorting';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../DashboardMessage';
import { EventData } from '@estruyf/vscode/dist/models';
import { parseWinPath } from '../../helpers/parseWinPath';
export default function usePages(pages: Page[]) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
const [ sorting, setSorting ] = useRecoilState(SortingAtom);
const [ tabInfo , setTabInfo ] = useRecoilState(TabInfoAtom);
const settings = useRecoilValue(SettingsSelector);
const tab = useRecoilValue(TabSelector);
const folder = useRecoilValue(FolderSelector);
@@ -23,25 +24,25 @@ export default function usePages(pages: Page[]) {
const processPages = useCallback((searchedPages: Page[]) => {
const draftField = settings?.draftField;
const framework = settings?.crntFramework;
// Filter the pages
let pagesToShow: Page[] = Object.assign([], searchedPages);
if (draftField && draftField.type === 'choice') {
if (tab !== Tab.All) {
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
} else {
pagesToShow = searchedPages;
}
} else {
const draftFieldName = draftField?.name || "draft";
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
} else {
pagesToShow = searchedPages;
}
// Framework specific actions
if (framework?.toLowerCase() === "jekyll") {
pagesToShow = pagesToShow.map(page => {
// https://jekyllrb.com/docs/posts/#drafts
const filePath = parseWinPath(page.fmFilePath);
page.draft = filePath.indexOf(`/_drafts/`) > -1;
// Published field: https://jekyllrb.com/docs/front-matter/#predefined-global-variables
if (typeof page.published !== "undefined") {
page.draft = !page.published;
}
return page;
});
}
// Sort the pages
@@ -90,8 +91,49 @@ 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 ]);
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
const searchListener = (message: MessageEvent<EventData<any>>) => {
@@ -124,7 +166,7 @@ export default function usePages(pages: Page[]) {
} else {
processPages(searchedPages);
}
}, [ settings?.draftField, pages, sorting, search ]);
}, [ settings?.draftField, pages, sorting, search, tab, tag, category, folder ]);
useEffect(() => {
Messenger.listen(searchListener);

View File

@@ -1,11 +1,13 @@
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";
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
@@ -13,12 +15,23 @@ 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");
const version = elm?.getAttribute("data-version");
const environment = elm?.getAttribute("data-environment");
const isProd = elm?.getAttribute("data-isProd");
const type = elm?.getAttribute("data-type");
const url = elm?.getAttribute("data-url");
if (isProd === "true") {
Sentry.init({
@@ -31,7 +44,19 @@ if (elm) {
});
}
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
if (type === "preview") {
render(<Preview url={url} />, elm);
} else {
render((
<RecoilRoot>
<MemoryRouter
initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]}
initialIndex={1}>
<App showWelcome={!!welcome} />
</MemoryRouter>
</RecoilRoot>
), elm);
}
}
// Webpack HMR

View File

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

View File

@@ -11,11 +11,12 @@ export interface Page {
fmPreviewImage: string;
fmTags: string[];
fmCategories: string[];
fmContentType: string;
title: string;
slug: string;
date: string | Date;
draft: string;
draft: boolean | string;
description: string;
preview?: string;

View File

@@ -1,7 +1,7 @@
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, Snippets, SortingSetting } from '../../models';
import { SortingOption } from './SortingOption';
import { DashboardViewType } from '.';
import { DataFile } from '../../models/DataFile';
@@ -10,14 +10,13 @@ export interface Settings {
beta: boolean;
initialized: boolean;
wsFolder: string;
staticFolder: string;
folders: ContentFolder[];
staticFolder: string;
tags: string[];
categories: string[];
customTaxonomy: CustomTaxonomy[];
openOnStart: boolean | null;
versionInfo: VersionInfo;
pageViewType: DashboardViewType | undefined;
mediaSnippet: string[];
contentTypes: ContentType[];
contentFolders: ContentFolder[];
crntFramework: string;
@@ -36,15 +35,21 @@ export interface Settings {
export interface DashboardState {
contents: ContentsViewState;
media: MediaViewState;
welcome: WelcomeViewState;
}
export interface ContentsViewState {
sorting: SortingOption | null | undefined;
defaultSorting: string | null | undefined;
tags: string | null | undefined;
templatesEnabled: boolean | null | undefined;
}
export interface MediaViewState extends ContentsViewState {
selectedFolder: string | null | undefined;
mimeTypes: string[] | null | undefined;
}
export interface WelcomeViewState {
contentFolders: string[];
}

View File

@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const TabInfoAtom = atom<{ [tab: string]: number } | null>({
key: 'TabInfoAtom',
default: {}
});

View File

@@ -14,6 +14,7 @@ export * from './SelectedMediaFolderAtom';
export * from './SettingsAtom';
export * from './SortingAtom';
export * from './TabAtom';
export * from './TabInfoAtom';
export * from './TagAtom';
export * from './ViewAtom';
export * from './ViewDataAtom';

View File

@@ -79,6 +79,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 +131,11 @@ 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))
);
const toggleDraftCommand = COMMAND_NAME.toggleDraft;
const toggleDraft = vscode.commands.registerCommand(toggleDraftCommand, async () => {
@@ -150,10 +150,36 @@ 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);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate)
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.addMissingFields, ContentType.addMissingFields)
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.setContentType, ContentType.setContentType)
);
// Initialize command
Template.init();
const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
@@ -193,7 +219,14 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => triggerShowDraftStatus(`onDidChangeActiveTextEditor`)));
subscriptions.push(vscode.window.onDidChangeTextEditorSelection((e) => {
if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) {
triggerShowDraftStatus(`onDidChangeTextEditorSelection`);
statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200);
}
}));
subscriptions.push(vscode.workspace.onDidChangeTextDocument((TextDocumentChangeEvent) => {
const filePath = TextDocumentChangeEvent.document.uri.fsPath;
if (filePath && !filePath.toLowerCase().startsWith(`extension-output`)) {
MarkdownFoldingProvider.triggerHighlighting();
statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200);
}
}));
@@ -211,7 +244,7 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
// Inserting an image in Markdown
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia));
// Inserting a snippet in Markdown
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet));

View File

@@ -1,3 +1,4 @@
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';
@@ -44,6 +45,23 @@ export class ArticleHelper {
return ArticleHelper.parseFile(fileContents, document.fileName);
}
/**
* Get the current article
*/
public static getCurrent(): ParsedFrontMatter | undefined {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
return article;
}
/**
* Retrieve the file's front matter by its path
* @param filePath
@@ -65,6 +83,23 @@ export class ArticleHelper {
await editor.edit(builder => builder.replace(update.range, update.newText));
}
/**
* Store the new information for the article path
*
* @param path
* @param article
*/
public static async updateByPath(path: string, article: ParsedFrontMatter) {
const file = await workspace.openTextDocument(Uri.parse(path));
const editor = await window.showTextDocument(file);
if (file && editor) {
const update = this.generateUpdate(file, article);
await editor.edit(builder => builder.replace(update.range, update.newText));
}
}
/**
* Generate the update to be applied to the article.
* @param article
@@ -80,7 +115,7 @@ export class ArticleHelper {
const lastLine = lines.pop();
const endsWithNewLine = lastLine !== undefined && lastLine.trim() === "";
let newMarkdown = this.stringifyFrontMatter(article.content, Object.assign({}, article.data));
let newMarkdown = this.stringifyFrontMatter(article.content, Object.assign({}, article.data), document?.getText());
// Logic to not include a new line at the end of the file
if (!endsWithNewLine) {
@@ -115,8 +150,9 @@ export class ArticleHelper {
*
* @param content
* @param data
* @param originalContent
*/
public static stringifyFrontMatter(content: string, data: any) {
public static stringifyFrontMatter(content: string, data: any, originalContent?: string) {
const indentArray = Settings.get(SETTING_INDENT_ARRAY) as boolean;
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
@@ -130,7 +166,7 @@ export class ArticleHelper {
}
}
return FrontMatterParser.toFile(content, data, ({
return FrontMatterParser.toFile(content, data, originalContent, ({
noArrayIndent: !indentArray,
skipInvalid: true,
noCompatMode: true,

View File

@@ -1,15 +1,18 @@
import { ModeListener } from './../listeners/general/ModeListener';
import { PagesListener } from './../listeners/dashboard';
import { ArticleHelper, Settings } from ".";
import { SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field } from '../models';
import { Uri, commands } from 'vscode';
import { 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 } from '../models';
import { Uri, commands, window } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { writeFileSync } from "fs";
import { existsSync, writeFileSync } from "fs";
import { Notifications } from "./Notifications";
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 {
@@ -92,6 +95,403 @@ export class ContentType {
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
}
/**
* Generate a content type
*/
public static async generate() {
if (!(await ContentType.verify())) {
return;
}
Telemetry.send(TelemetryEvent.generateContentType);
const content = ArticleHelper.getCurrent();
const editor = window.activeTextEditor;
const filePath = editor?.document.uri.fsPath;
if (!content || !content.data) {
Notifications.warning(`No front matter data found to generate a content type.`);
return;
}
const override = await window.showQuickPick(["Yes", "No"], {
placeHolder: "Do you want to override the default content type?",
ignoreFocusOut: true,
title: "Override default content type"
});
const overrideBool = override === "Yes";
let contentTypeName: string | undefined = `default`;
// Ask for the new content type name
if (!overrideBool) {
contentTypeName = await window.showInputBox({
ignoreFocusOut: true,
placeHolder: "Enter the name of the content type to generate",
prompt: "Enter the name of the content type to generate",
title: "Generate Content Type",
validateInput: (value: string) => {
if (!value) {
return "Please enter a name for the content type";
}
const contentTypes = ContentType.getAll();
if (contentTypes && contentTypes.find(ct => ct.name.toLowerCase() === value.toLowerCase())) {
return "A content type with this name already exists";
}
return null;
}
});
if (!contentTypeName) {
Notifications.warning(`You didn't specify a name for the content type.`);
return;
}
}
// Ask if the content type needs to be used as a page bundle
let pageBundle = false;
const fileName = filePath ? basename(filePath) : undefined;
if (fileName?.startsWith(`index.`)) {
const pageBundleAnswer = await window.showQuickPick(["Yes", "No"], {
placeHolder: "Do you want to use this content type as a page bundle?",
ignoreFocusOut: true,
title: "Use as page bundle"
});
pageBundle = pageBundleAnswer === "Yes";
}
const fields = ContentType.generateFields(content.data);
if (!overrideBool && !fields.some(f => f.name === "type")) {
fields.push({
name: "type",
type: "string",
default: contentTypeName,
hidden: true
} as Field);
}
// Update the type field in the page
if (!overrideBool && editor) {
content.data["type"] = contentTypeName;
ArticleHelper.update(editor, content);
}
const newContentType: IContentType = {
name: contentTypeName,
pageBundle,
fields
};
const contentTypes = ContentType.getAll() || [];
if (overrideBool) {
const index = contentTypes.findIndex(ct => ct.name === contentTypeName);
contentTypes[index].fields = fields;
} else {
contentTypes.push(newContentType);
}
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = Settings.projectConfigPath;
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
/**
* Add missing fields to the content type
*/
public static async addMissingFields() {
if (!(await ContentType.verify())) {
return;
}
Telemetry.send(TelemetryEvent.addMissingFields);
const content = ArticleHelper.getCurrent();
if (!content || !content.data) {
Notifications.warning(`No front matter data found to add missing fields.`);
return;
}
const contentType = ArticleHelper.getContentType(content?.data);
const updatedFields = ContentType.generateFields(content.data, contentType.fields);
const contentTypes = ContentType.getAll() || [];
const index = contentTypes.findIndex(ct => ct.name === contentType.name);
contentTypes[index].fields = updatedFields;
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = Settings.projectConfigPath;
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
/**
* Set the content type to be used for the current file
*/
public static async setContentType() {
if (!(await ContentType.verify())) {
return;
}
Telemetry.send(TelemetryEvent.setContentType);
const content = ArticleHelper.getCurrent();
const contentTypes = ContentType.getAll() || [];
if (!content || !content.data) {
Notifications.warning(`No front matter data found to set the content type.`);
return;
}
const ctAnswer = await window.showQuickPick(contentTypes.map(ct => ct.name), {
title: "Select the content type",
ignoreFocusOut: true,
placeHolder: "Which content type would you like to use?"
});
if (!ctAnswer) {
return;
}
content.data.type = ctAnswer;
const editor = window.activeTextEditor;
ArticleHelper.update(editor!, content);
}
/**
* Retrieve the field value
* @param data
* @param parents
* @returns
*/
public static getFieldValue(data: any, parents: 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;
}
/**
* 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
* @param fields
* @returns
*/
private static generateFields(data: any, fields: any[] = []) {
for (const field in data) {
const fieldData = data[field];
if (fields.some(f => f.name === field)) {
continue;
}
if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "string") {
if (field.toLowerCase() === "tag" || field.toLowerCase() === "tags") {
fields.push({
title: field,
name: field,
type: "tags",
} as Field);
} else if (field.toLowerCase() === "category" || field.toLowerCase() === "categories") {
fields.push({
title: field,
name: field,
type: "categories",
} as Field);
} else {
fields.push({
title: field,
name: field,
type: "choice",
choices: fieldData
} as Field);
}
} else if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "object") {
const newFields = ContentType.generateFields(fieldData);
fields.push({
title: field,
name: field,
type: "block",
fields: newFields
} as Field);
} else if (fieldData && fieldData instanceof Object) {
const newFields = ContentType.generateFields(fieldData);
fields.push({
title: field,
name: field,
type: "fields",
fields: newFields
} as Field);
} else {
if (!isNaN(new Date(fieldData).getDate())) {
fields.push({
title: field,
name: field,
type: "datetime"
} as Field);
} else if (field.toLowerCase() === "draft") {
fields.push({
title: field,
name: field,
type: "draft"
} as Field);
} else if (field.toLowerCase() === "slug") {
// Do nothing
} else {
fields.push({
title: field,
name: field,
type: typeof fieldData
} as Field);
}
}
}
return fields;
}
/**
* Create a new file with the specified content type
* @param contentType
@@ -104,12 +504,29 @@ export class ContentType {
return;
}
let templatePath = contentType.template;
let templateData: ParsedFrontMatter | null = null;
if (templatePath) {
templatePath = Folders.getAbsFilePath(templatePath);
templateData = ArticleHelper.getFrontMatterByPath(templatePath);
}
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
if (!newFilePath) {
return;
}
let data: any = this.processFields(contentType, titleValue, {});
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, templateData?.data || {});
data = ArticleHelper.updateDates(Object.assign({}, data));
@@ -117,7 +534,7 @@ export class ContentType {
data['type'] = contentType.name;
}
const content = ArticleHelper.stringifyFrontMatter(``, data);
const content = ArticleHelper.stringifyFrontMatter(templateData?.content || ``, data);
writeFileSync(newFilePath, content, { encoding: "utf8" });
@@ -167,4 +584,18 @@ export class ContentType {
return data;
}
/**
* Verify if the content type feature is enabled
* @returns
*/
private static async verify() {
const hasFeature = await ModeListener.hasFeature(FEATURE_FLAG.panel.contentType);
if (!hasFeature) {
Notifications.warning(`The content type actions are not available in this mode.`);
return false;
}
return true;
}
}

View File

@@ -1,6 +1,7 @@
import { CommandType } from './../models/PanelSettings';
import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings';
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
import { ArticleHelper, Telemetry } from '.';
import { ArticleHelper, Logger, Telemetry } from '.';
import { Folders } from '../commands/Folders';
import { exec } from 'child_process';
import * as os from 'os';
@@ -41,6 +42,13 @@ export class CustomScript {
}
}
/**
* Run the script on the current file
* @param wsPath
* @param script
* @param path
* @returns
*/
private static async singleRun(wsPath: string, script: ICustomScript, path: string | null = null): Promise<void> {
let articlePath: string | null = path;
let article: ParsedFrontMatter | null = null;
@@ -62,13 +70,19 @@ export class CustomScript {
cancellable: false
}, async () => {
const output = await CustomScript.runScript(wsPath, article, articlePath as string, script);
CustomScript.showOutput(output, script);
CustomScript.showOutput(output, script, articlePath);
});
} else {
Notifications.warning(`${script.title}: Article couldn't be retrieved.`);
}
}
/**
* Run the script on multiple files
* @param wsPath
* @param script
* @returns
*/
private static async bulkRun(wsPath: string, script: ICustomScript): Promise<void> {
const folders = await Folders.getInfo();
@@ -106,6 +120,13 @@ export class CustomScript {
});
}
/**
* Run a script for a media file
* @param wsPath
* @param path
* @param script
* @returns
*/
private static async runMediaScript(wsPath: string, path: string | null, script: ICustomScript): Promise<void> {
if (!path) {
Notifications.error(`${script.title}: There was no folder or media path specified.`);
@@ -118,28 +139,34 @@ export class CustomScript {
title: `Executing: ${script.title}`,
cancellable: false
}, async () => {
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${path}"`, (error, stdout) => {
if (error) {
Notifications.error(`${script.title}: ${error.message}`);
resolve();
return;
}
CustomScript.showOutput(stdout, script);
try {
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${path}"`);
CustomScript.showOutput(output, script);
Dashboard.postWebviewMessage({
command: DashboardCommand.mediaUpdate
});
resolve();
return;
});
} catch (e) {
Notifications.error(`${script.title}: ${(e as Error).message}`);
return;
}
});
});
}
/**
* Script runner
* @param wsPath
* @param article
* @param contentPath
* @param script
* @returns
*/
private static async runScript(wsPath: string, article: ParsedFrontMatter | null, contentPath: string, script: ICustomScript): Promise<string | null> {
return new Promise((resolve, reject) => {
try {
let articleData = "";
if (os.type() === "Windows_NT") {
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
@@ -148,31 +175,97 @@ export class CustomScript {
articleData = `'${articleData}'`;
}
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => {
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${contentPath}" ${articleData}`);
return output;
} catch (e) {
Notifications.error(`${script.title}: ${(e as Error).message}`);
return null;
}
}
/**
* Show/process the output of the script
* @param output
* @param script
*/
private static showOutput(output: string | null, script: ICustomScript, articlePath?: string | null): void {
if (output) {
try {
const data = JSON.parse(output);
if (data.frontmatter) {
let article = null;
const editor = window.activeTextEditor;
if (!articlePath) {
if (!editor) return;
articlePath = editor.document.uri.fsPath;
article = ArticleHelper.getFrontMatter(editor);
} else {
article = ArticleHelper.getFrontMatterByPath(articlePath);
}
if (article && article.data) {
for (const key in data.frontmatter) {
article.data[key] = data.frontmatter[key];
}
if (articlePath) {
ArticleHelper.updateByPath(articlePath, article);
} else if (editor) {
ArticleHelper.update(editor, article);
} else {
throw new Error(`Couldn't update article.`);
}
Notifications.info(`${script.title}: front matter updated.`);
}
} else {
throw new Error(`No frontmatter found.`);
}
} catch (error) {
if (script.output === "editor") {
ContentProvider.show(output, script.title, script.outputType || "text");
} else {
window.showInformationMessage(`${script.title}: ${output}`, 'Copy output').then(value => {
if (value === 'Copy output') {
vscodeEnv.clipboard.writeText(output);
}
});
}
}
} else {
Notifications.info(`${script.title}: Executed your custom script.`);
}
}
/**
* Execute script
* @param script
* @param wsPath
* @param args
* @returns
*/
private static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
return new Promise((resolve, reject) => {
// Check the command to use
let command = script.nodeBin || "node";
if (script.command && script.command !== CommandType.Node) {
command = script.command;
}
const scriptPath = join(wsPath, script.script);
const fullScript = `${command} ${scriptPath} ${args}`;
Logger.info(`Executing: ${fullScript}`);
exec(fullScript, (error, stdout) => {
if (error) {
Notifications.error(`${script.title}: ${error.message}`);
resolve(null);
return;
reject(error.message);
}
resolve(stdout);
});
});
}
private static showOutput(output: string | null, script: ICustomScript): void {
if (output) {
if (script.output === "editor") {
ContentProvider.show(output, script.title, script.outputType || "text");
} else {
window.showInformationMessage(`${script.title}: ${output}`, 'Copy output').then(value => {
if (value === 'Copy output') {
vscodeEnv.clipboard.writeText(output);
}
});
}
} else {
Notifications.info(`${script.title}: Executed your custom script.`);
}
}
}

View File

@@ -1,10 +1,10 @@
import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
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_MEDIA_SNIPPET, 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 { Project } from "../commands/Project";
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 } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
import { DataFolder } from "../models/DataFolder";
import { DataType } from "../models/DataType";
@@ -18,20 +18,19 @@ export class DashboardSettings {
public static async get() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = await Template.isInitialized();
const isInitialized = Project.isInitialized();
return {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER),
folders: Folders.get(),
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"),
mediaSnippet: Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
@@ -47,12 +46,16 @@ 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)
},
media: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTING_MEDIA_SORTING_DEFAULT),
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace"),
mimeTypes: Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES)
},
welcome: {
contentFolders: await Folders.getContentFolders()
}
},
dataFiles: await this.getDataFiles(),

View File

@@ -0,0 +1,74 @@
import { existsSync, readFileSync } from "fs";
import { Folders } from "../commands/Folders";
import { DataFile } from "../models";
import * as yaml from 'js-yaml';
import { Logger } from "./Logger";
import { Notifications } from "./Notifications";
import { commands } from "vscode";
import { COMMAND_NAME, SETTING_DATA_FILES } from "../constants";
import { Settings } from "./SettingsHelper";
export class DataFileHelper {
/**
* Retrieve the file data
* @param filePath
* @returns
*/
public static get(filePath: string) {
const absPath = Folders.getAbsFilePath(filePath);
if (existsSync(absPath)) {
return readFileSync(absPath, 'utf8');
}
return null;
}
/**
* Get by the id of the data file
* @param id
*/
public static getById(id: string) {
const files = Settings.get<DataFile[]>(SETTING_DATA_FILES);
if (!files || files.length === 0) {
return;
}
const file = files.find(f => f.id === id);
if (!file) {
return;
}
return DataFileHelper.process(file);
}
/**
* Process the data file
* @param data
* @returns
*/
public static async process(data: DataFile) {
try {
const { file, fileType } = data;
const dataFile = DataFileHelper.get(file);
if (fileType === "yaml") {
return yaml.safeLoad(dataFile || "");
} else {
return dataFile ? JSON.parse(dataFile) : undefined;
}
} catch (ex) {
Logger.error(`DataFileHelper::process: ${(ex as Error).message}`);
const btnClick = await Notifications.error(`Something went wrong while processing the data file. Check your file and output log for more information.`, 'Open output');
if (btnClick && btnClick === 'Open output') {
commands.executeCommand(COMMAND_NAME.showOutputChannel);
}
return;
}
}
}

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