Compare commits

..

69 Commits

Author SHA1 Message Date
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
5e23d4446b 7.2.0 2022-04-11 10:42:01 +02:00
Elio Struyf
6ef1ba5b57 Updated preview 2022-04-07 16:24:31 +02:00
115 changed files with 3110 additions and 659 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: ''
---

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

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

View File

@@ -1,10 +1,63 @@
# Change Log
## [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
- [#316](https://github.com/estruyf/vscode-front-matter/issues/316): Fix draft tab navigation
- [#315](https://github.com/estruyf/vscode-front-matter/issues/315): Fix draft tab navigation
## [7.1.1] - 2022-04-08

View File

@@ -221,60 +221,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 +301,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 +590,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 +598,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 +613,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 +728,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 {

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

111
package-lock.json generated
View File

@@ -1,9 +1,27 @@
{
"name": "vscode-front-matter-beta",
"version": "7.1.2",
"version": "7.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@actions/core": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
"dev": true,
"requires": {
"@actions/http-client": "^2.0.1"
}
},
"@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"dev": true,
"requires": {
"tunnel": "^0.0.6"
}
},
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@@ -642,6 +660,15 @@
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"dev": true,
"requires": {
"parchment": "^1.1.2"
}
},
"@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
@@ -1432,6 +1459,12 @@
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"dev": true
},
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
"dev": true
},
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -2286,6 +2319,12 @@
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
@@ -2301,6 +2340,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"dev": true
},
"fast-glob": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
@@ -2574,12 +2619,12 @@
"dev": true
},
"gray-matter": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
"integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"requires": {
"js-yaml": "^3.11.0",
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
@@ -4224,6 +4269,12 @@
"tslib": "^1.10.0"
}
},
"parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"dev": true
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4606,6 +4657,39 @@
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true
},
"quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"dev": true,
"requires": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
},
"dependencies": {
"eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=",
"dev": true
}
}
},
"quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"dev": true,
"requires": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -4707,6 +4791,17 @@
"warning": "^4.0.2"
}
},
"react-quill": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz",
"integrity": "sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q==",
"dev": true,
"requires": {
"@types/quill": "^1.3.10",
"lodash": "^4.17.4",
"quill": "^1.3.7"
}
},
"react-sortable-hoc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
@@ -5734,6 +5829,12 @@
"tslib": "^1.8.1"
}
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"dev": true
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

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.2",
"version": "7.3.1",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -137,6 +137,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",
@@ -225,8 +229,7 @@
"additionalProperties": {
"type": "object",
"required": [
"body",
"fields"
"body"
],
"properties": {
"body": {
@@ -255,6 +258,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 +350,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 +377,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 +416,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 +454,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 +467,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 +545,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 +590,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 +650,7 @@
"panel.globalSettings",
"panel.seo",
"panel.actions",
"panel.contentType",
"panel.metadata",
"panel.recentlyModified",
"panel.otherActions",
@@ -744,6 +797,7 @@
"datetime",
"boolean",
"image",
"file",
"choice",
"taxonomy",
"tags",
@@ -751,7 +805,8 @@
"draft",
"fields",
"json",
"block"
"block",
"dataFile"
],
"description": "Define the type of field"
},
@@ -802,6 +857,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 +882,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 +928,21 @@
"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"
}
},
"additionalProperties": false,
@@ -869,6 +951,35 @@
"name"
],
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "dataFile"
}
}
},
"then": {
"required": [
"dataFileId",
"dataFileKey"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "file"
}
}
},
"then": {
"required": [
"fileExtensions"
]
}
},
{
"if": {
"properties": {
@@ -1063,7 +1174,7 @@
},
"additionalProperties": false,
"required": [
"name",
"id",
"fields"
]
}
@@ -1170,6 +1281,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 +1296,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 +1365,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 +1437,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",
@@ -1463,7 +1600,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 +1757,7 @@
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
"command": "frontMatter.insertMedia",
"when": "frontMatter:file:isValid == true"
},
{
@@ -1654,6 +1791,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,7 +1814,7 @@
{
"command": "frontMatter.mode.switch",
"group": "navigation@1",
"when": "view == frontMatter.explorer"
"when": "view == frontMatter.explorer && frontMatter:has:modes == true"
},
{
"command": "frontMatter.dashboard",
@@ -1744,9 +1893,10 @@
"start:site": "cd ./docs && npm run dev"
},
"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",
@@ -1780,7 +1930,7 @@
"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",
@@ -1802,6 +1952,7 @@
"react-datepicker": "4.2.1",
"react-dom": "17.0.1",
"react-dropzone": "^11.3.4",
"react-quill": "^2.0.0-beta.4",
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^8.1.0",
"recoil": "^0.4.1",

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) {
@@ -324,7 +325,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 +376,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,14 @@
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);
}
const options: QuickPickItem[] = [{
label: "Create content by content type",
@@ -15,7 +20,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,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]]`;
@@ -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();
}
}
@@ -284,11 +293,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 +375,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,15 @@
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 { Extension, Settings } from '../helpers';
import { 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';
export class Preview {
@@ -81,59 +82,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

@@ -5,7 +5,7 @@ import * as fs from "fs";
import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Logger, Settings } from "../helpers";
import { FrameworkDetector, Logger, Settings } from "../helpers";
import { SETTING_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
import { SettingsListener } from '../listeners/dashboard';
@@ -24,33 +24,32 @@ 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();
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 +58,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

@@ -17,7 +17,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 +37,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;
@@ -149,7 +154,8 @@ 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,11 +181,16 @@ 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;
}
@@ -226,7 +238,7 @@ export class Settings {
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, {
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any), { encoding: "utf8" });
}

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

@@ -25,6 +25,7 @@ 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"),
@@ -37,8 +38,10 @@ export const COMMAND_NAME = {
diagnostics: getCommandName("diagnostics"),
modeSwitch: getCommandName("mode.switch"),
showOutputChannel: getCommandName("showOutputChannel"),
// Insert dashboards
insertImage: getCommandName("insertImage"),
insertMedia: getCommandName("insertMedia"),
insertSnippet: getCommandName("insertSnippet"),
// WYSIWYG
@@ -53,4 +56,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: {

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

@@ -22,9 +22,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',

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

@@ -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',
@@ -38,8 +44,6 @@ export enum DashboardMessage {
// Other
getTheme = 'getTheme',
updateSetting = 'updateSetting',
initializeProject = 'initializeProject',
setFramework = 'setFramework',
setState = 'setState',
runCustomScript = 'runCustomScript',
sendTelemetry = 'sendTelemetry',

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`}
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);
@@ -51,7 +53,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 +98,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

@@ -12,6 +12,7 @@ import { DataView } from './DataView';
import { Snippets } from './SnippetsView/Snippets';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IDashboardProps {
showWelcome: boolean;
@@ -23,15 +24,17 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
const mode = useRecoilValue(ModeAtom);
useDarkMode();
const viewState: any = Messenger.getState() || {};
if (!settings) {
return <Spinner />;
}
if (showWelcome) {
if (showWelcome || viewState.isWelcomeConfiguring) {
return <WelcomeScreen settings={settings} />;
}
if (!settings.initialized || settings.folders?.length === 0) {
if (!settings.initialized || settings.contentFolders?.length === 0) {
return <WelcomeScreen settings={settings} />;
}

View File

@@ -149,9 +149,9 @@ 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) => (
dataFiles.map((dataFile, idx) => (
<button
key={dataFile.id}
key={`${dataFile.id}-${idx}`}
type='button'
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
onClick={() => setSchema(dataFile)}>

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();
@@ -74,39 +85,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 +210,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 +224,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 +251,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 +310,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 +349,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 +380,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 +388,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 +414,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 +428,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 +463,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 +498,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 { extname, join } from 'path';
export interface IMediaProps {}
@@ -40,12 +40,23 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
}, [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[]) => {

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

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

@@ -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,17 +4,33 @@ 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);
@@ -23,7 +39,21 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
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 steps = [
{
@@ -76,15 +106,41 @@ 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
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
},
{
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
}
];

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

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

View File

@@ -10,6 +10,7 @@ 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[]>([]);
@@ -24,10 +25,27 @@ 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);
// 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;
});
}
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesToShow.length;
@@ -46,15 +64,19 @@ export default function usePages(pages: Page[]) {
pagesToShow = searchedPages;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesToShow.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesToShow.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = pagesToShow.filter(page => !!page[draftFieldName]).length;
draftTypes[Tab.Published] = pagesToShow.filter(page => !page[draftFieldName]).length;
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
pagesToShow = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
pagesToShow = draftField?.invert ? published : drafts;
} else {
pagesToShow = searchedPages;
}

View File

@@ -6,6 +6,7 @@ import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { SENTRY_LINK } from "../constants";
import './styles.css';
import { Preview } from "./components/Preview";
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
@@ -19,6 +20,8 @@ if (elm) {
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 +34,11 @@ if (elm) {
});
}
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
if (type === "preview") {
render(<Preview url={url} />, elm);
} else {
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
}
}
// Webpack HMR

View File

@@ -15,7 +15,7 @@ export interface Page {
title: string;
slug: string;
date: string | Date;
draft: string;
draft: boolean | string;
description: string;
preview?: string;

View File

@@ -10,14 +10,12 @@ export interface Settings {
beta: boolean;
initialized: boolean;
wsFolder: string;
staticFolder: string;
folders: ContentFolder[];
staticFolder: string;
tags: string[];
categories: string[];
openOnStart: boolean | null;
versionInfo: VersionInfo;
pageViewType: DashboardViewType | undefined;
mediaSnippet: string[];
contentTypes: ContentType[];
contentFolders: ContentFolder[];
crntFramework: string;
@@ -36,6 +34,7 @@ export interface Settings {
export interface DashboardState {
contents: ContentsViewState;
media: MediaViewState;
welcome: WelcomeViewState;
}
export interface ContentsViewState {
@@ -47,4 +46,8 @@ export interface ContentsViewState {
export interface MediaViewState extends ContentsViewState {
selectedFolder: string | null | undefined;
mimeTypes: string[] | null | undefined;
}
export interface WelcomeViewState {
contentFolders: string[];
}

View File

@@ -136,6 +136,10 @@ export async function activate(context: vscode.ExtensionContext) {
});
let createTemplate = vscode.commands.registerCommand(COMMAND_NAME.createTemplate, Template.generate);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.initTemplate, () => Project.createSampleTemplate(true))
);
const toggleDraftCommand = COMMAND_NAME.toggleDraft;
const toggleDraft = vscode.commands.registerCommand(toggleDraftCommand, async () => {
@@ -154,6 +158,18 @@ export async function activate(context: vscode.ExtensionContext) {
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 +209,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 +234,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,17 @@
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 { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field } from '../models';
import { Uri, commands } from 'vscode';
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';
export class ContentType {
@@ -92,6 +94,262 @@ 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);
}
/**
* 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
@@ -109,6 +367,16 @@ export class ContentType {
return;
}
if (contentType.name === "default") {
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
if (crntFramework?.toLowerCase() === "jekyll") {
const idx = contentType.fields.findIndex(f => f.name === "draft");
if (idx > -1) {
contentType.fields.splice(idx, 1);
}
}
}
let data: any = this.processFields(contentType, titleValue, {});
data = ArticleHelper.updateDates(Object.assign({}, data));
@@ -167,4 +435,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,11 @@
import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Project } from "../commands/Project";
import { Template } from "../commands/Template";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_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 { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { 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 +19,18 @@ 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(),
initialized: isInitialized,
tags: Settings.getTaxonomy(TaxonomyType.Tag),
categories: Settings.getTaxonomy(TaxonomyType.Category),
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),
@@ -53,6 +52,9 @@ export class DashboardSettings {
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;
}
}
}

View File

@@ -1,8 +1,9 @@
import { basename } from "path";
import { extensions, Uri, ExtensionContext, window, workspace, commands, ExtensionMode, DiagnosticCollection, languages } from "vscode";
import { Folders } from "../commands/Folders";
import { EXTENSION_NAME, GITHUB_LINK, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS } from "../constants";
import { ContentFolder } from "../models";
import { Template } from "../commands/Template";
import { EXTENSION_NAME, GITHUB_LINK, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_CONTENT_SNIPPETS, SETTING_TEMPLATES_ENABLED } from "../constants";
import { ContentFolder, Snippet } from "../models";
import { Notifications } from "./Notifications";
import { Settings } from "./SettingsHelper";
@@ -189,6 +190,41 @@ export class Extension {
}
}
}
if (major <= 7 && minor < 3) {
const mediaSnippet = Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET);
if (mediaSnippet && mediaSnippet.length > 0) {
let snippet = mediaSnippet.join(`\n`);
snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`);
snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`);
snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`);
snippet = snippet.replace(`{caption}`, `[[&caption]]`);
snippet = snippet.replace(`{alt}`, `[[alt]]`);
snippet = snippet.replace(`{filename}`, `[[filename]]`);
snippet = snippet.replace(`{title}`, `[[title]]`);
const snippets = Settings.get<Snippet[]>(SETTING_CONTENT_SNIPPETS) || {} as any;
snippets[`Media snippet (migrated)`] = {
body: snippet.split(`\n`),
isMediaSnippet: true,
description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting`
}
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
}
const templates = await Template.getTemplates();
if (templates && templates.length > 0) {
const answer = await window.showQuickPick(["Yes", "No"], {
title: "Front Matter - Templates",
placeHolder: "Do you want to keep on using the template functionality?",
ignoreFocusOut: true
});
Settings.update(SETTING_TEMPLATES_ENABLED, answer?.toLocaleLowerCase() === "yes", true);
}
}
}
public async setState<T>(propKey: string, propValue: T, type: "workspace" | "global" = "global"): Promise<void> {

View File

@@ -1,7 +1,12 @@
import { existsSync } from "fs";
import { resolve } from "path";
import { existsSync, readFileSync } from "fs";
import jsyaml = require("js-yaml");
import { join, resolve } from "path";
import { commands, Uri } from "vscode";
import { Folders } from "../commands/Folders";
import { COMMAND_NAME } from "../constants";
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
import { Extension } from "./Extension";
import { Framework } from "../models";
import { Logger } from "./Logger";
export class FrameworkDetector {
@@ -14,18 +19,52 @@ export class FrameworkDetector {
}
private static check(folder: string) {
const { dependencies, devDependencies } = Extension.getInstance().packageJson;
let dependencies = null;
let devDependencies = null;
let gemContent = null;
// Try fetching the package JSON file
try {
const pkgFile = join(folder, 'package.json');
if (existsSync(pkgFile)) {
let packageJson: any = readFileSync(pkgFile, "utf8");
if (packageJson) {
packageJson = typeof packageJson === "string" ? JSON.parse(packageJson) : packageJson;
dependencies = packageJson.dependencies || null;
devDependencies = packageJson.devDependencies || null;
}
}
} catch (e) {
// do nothing
}
// Try fetching the Gemfile
try {
const gemFile = join(folder, 'Gemfile');
if (existsSync(gemFile)) {
gemContent = readFileSync(gemFile, "utf8");
}
} catch (e) {
// do nothing
}
for (const detector of FrameworkDetectors) {
if (detector && folder) {
// Verify by dependencies
for (const dependency of detector.requiredDependencies ?? []) {
// Checks for package.json dependencies
const inDependencies = dependencies && dependencies[dependency]
const inDevDependencies = devDependencies && devDependencies[dependency]
if (inDependencies || inDevDependencies) {
return detector.framework;
}
// Checks for Gemfile
if (gemContent && gemContent.includes(dependency)) {
return detector.framework;
}
}
// Verify by files
@@ -40,4 +79,50 @@ export class FrameworkDetector {
return undefined;
}
public static checkDefaultSettings(framework: Framework) {
if (framework.name.toLowerCase() === "jekyll") {
FrameworkDetector.jekyll();
}
}
private static jekyll() {
try {
const wsFolder = Folders.getWorkspaceFolder();
const jekyllConfig = join(wsFolder?.fsPath || "", '_config.yml');
let collectionDir = "";
if (existsSync(jekyllConfig)) {
const content = readFileSync(jekyllConfig, "utf8");
// Convert YAML to JSON
const config = jsyaml.safeLoad(content);
if (config.collections_dir) {
collectionDir = config.collections_dir;
}
}
const draftsPath = join(wsFolder?.fsPath || "", collectionDir, "_drafts");
const postsPath = join(wsFolder?.fsPath || "", collectionDir, "_posts");
if (existsSync(draftsPath)) {
const folderUri = Uri.file(draftsPath);
commands.executeCommand(COMMAND_NAME.registerFolder, {
title: "drafts",
path: folderUri
});
}
if (existsSync(postsPath)) {
const folderUri = Uri.file(postsPath);
commands.executeCommand(COMMAND_NAME.registerFolder, {
title: "posts",
path: folderUri
});
}
} catch (e) {
Logger.error(`Something failed while processing your Jekyll configuration. ${(e as Error).message}`);
}
}
}

View File

@@ -1,14 +1,16 @@
import { Extension } from './Extension';
import { OutputChannel, window } from 'vscode';
import { commands, OutputChannel, window } from 'vscode';
import { format } from 'date-fns';
import { COMMAND_NAME } from '../constants';
export class Logger {
private static instance: Logger;
private static channel: OutputChannel | null = null;
public static channel: OutputChannel | null = null;
private constructor() {
const displayName = Extension.getInstance().displayName;
Logger.channel = window.createOutputChannel(displayName);
commands.registerCommand(COMMAND_NAME.showOutputChannel, () => { Logger.channel?.show(); });
}
public static getInstance(): Logger {

View File

@@ -1,4 +1,4 @@
import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { decodeBase64, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
@@ -228,7 +228,7 @@ export class MediaHelpers {
}
const staticPath = join(absFolderPath, fileName);
const imgData = decodeBase64Image(contents);
const imgData = decodeBase64(contents);
if (imgData) {
writeFileSync(staticPath, imgData.data);
@@ -270,7 +270,7 @@ export class MediaHelpers {
* @param data
*/
public static async insertMediaToMarkdown(data: any) {
if (data?.file && data?.image) {
if (data?.file && data?.relPath) {
if (!data?.position) {
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
}
@@ -281,12 +281,12 @@ export class MediaHelpers {
const editor = window.activeTextEditor;
const wsFolder = Folders.getWorkspaceFolder();
const filePath = data.file;
let imgPath = data.image;
let relPath = data.relPath;
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
const articleCt = article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), relPath);
const fileDir = parseWinPath(dirname(filePath));
const imgDir = parseWinPath(dirname(absImgPath));
const contentFolders = Folders.get();
@@ -303,11 +303,11 @@ export class MediaHelpers {
if (existsInContent) {
const relImgPath = relative(fileDir, imgDir);
imgPath = join(relImgPath, basename(imgPath));
relPath = join(relImgPath, basename(relPath));
// Snippets are already parsed, so update the URL of the image
if (data.snippet) {
data.snippet = data.snippet.replace(data.image, imgPath);
data.snippet = data.snippet.replace(data.relPath, relPath);
}
}
}
@@ -319,7 +319,16 @@ export class MediaHelpers {
if (line) {
const selection = editor?.selection;
await editor?.edit(builder => {
const snippet = data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`;
const mimeType = lookup(relPath)
let isFile = true;
if (mimeType) {
isFile = !mimeType.startsWith('image');
}
const caption = isFile ? `${data.title || ""}` : `${data.alt || data.caption || ""}`;
const snippet = data.snippet || `${isFile ? "" : "!"}[${caption}](${relPath})`;
if (selection !== undefined) {
builder.replace(selection, snippet);
} else {
@@ -333,7 +342,7 @@ export class MediaHelpers {
DataListener.updateMetadata({
field: data.fieldName,
value: imgPath,
value: relPath,
parents: data.parents,
blockData: data.blockData
});

View File

@@ -1,43 +0,0 @@
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
import { CommandToCode } from "../panelWebView/CommandToCode";
interface ClientVsCode<T> {
getState: () => T;
setState: (data: T) => void;
postMessage: (msg: unknown) => void;
}
declare const acquireVsCodeApi: <T = unknown>() => ClientVsCode<T>;
export class MessageHelper {
private static vscode: ClientVsCode<any>;
public static getVsCodeAPI() {
if (!MessageHelper.vscode) {
MessageHelper.vscode = acquireVsCodeApi();
}
return MessageHelper.vscode;
}
public static sendMessage = (command: CommandToCode | DashboardMessage, data?: any) => {
const vscode = MessageHelper.getVsCodeAPI();
if (data) {
vscode.postMessage({ command, data });
} else {
vscode.postMessage({ command });
}
}
public static getState = () => {
const vscode = MessageHelper.getVsCodeAPI();
return vscode.getState();
}
public static setState = (data: any) => {
const vscode = MessageHelper.getVsCodeAPI();
vscode.setState({
...data
});
}
}

View File

@@ -5,6 +5,7 @@ import { Settings } from "./SettingsHelper";
export class Notifications {
private static notifications: string[] = [];
public static info(message: string, ...items: any): Thenable<string | undefined> {
Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO");
@@ -36,6 +37,16 @@ export class Notifications {
return Promise.resolve(undefined);
}
public static async errorShowOnce(message: string, ...items: any): Promise<string | undefined> {
if (this.notifications.includes(message)) {
return;
}
this.notifications.push(message);
return this.error(message, ...items);
}
private static shouldShow(level: "INFO" | "WARNING" | "ERROR"): boolean {
let levels = Settings.get<string[]>(SETTING_GLOBAL_NOTIFICATIONS);

View File

@@ -11,7 +11,7 @@ export class Questions {
* @returns
*/
public static async yesOrNo(placeholder: string) {
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: false });
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: true });
return answer === "yes";
}
@@ -23,7 +23,8 @@ export class Questions {
public static async ContentTitle(showWarning: boolean = true): Promise<string | undefined> {
const title = await window.showInputBox({
prompt: `What would you like to use as a title for the content to create?`,
placeHolder: `Content title`
placeHolder: `Content title`,
ignoreFocusOut: true
});
if (!title && showWarning) {
@@ -82,7 +83,8 @@ export class Questions {
const selectedOption = await window.showQuickPick(options, {
placeHolder: `Select the content type to create your new content`,
canPickMany: false
canPickMany: false,
ignoreFocusOut: true
});
if (!selectedOption && showWarning) {

View File

@@ -170,6 +170,15 @@ export class Settings {
await Settings.config.update(name, value);
}
/**
* Checks if the project contains the frontmatter.json file
*/
public static hasProjectFile() {
const wsFolder = Folders.getWorkspaceFolder();
const configPath = join(wsFolder?.fsPath || "", Settings.globalFile);
return existsSync(configPath);
}
/**
* Create team settings
*/
@@ -328,7 +337,7 @@ export class Settings {
* Get the project config path
* @returns
*/
private static get projectConfigPath() {
public static get projectConfigPath() {
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
const fmConfig = join(wsFolder.fsPath, Settings.globalFile);

View File

@@ -1,13 +1,17 @@
export const decodeBase64Image = (dataString: string) => {
const matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
let response: any = {};
export const decodeBase64 = (dataString: string) => {
const dataParts = dataString.split(';base64,');
if (matches?.length !== 3) {
if (dataParts?.length < 2) {
return null;
}
response.type = matches[1];
response.data = Buffer.from(matches[2], 'base64');
const typePart = dataParts[0].split(':').pop() as string;
const dataPart = dataParts.pop() as string;
let response: any = {};
response.type = typePart;
response.data = Buffer.from(dataPart, 'base64');
return response;
}

View File

@@ -2,6 +2,7 @@ export * from './ArticleHelper';
export * from './ContentType';
export * from './CustomScript';
export * from './DashboardSettings';
export * from './DataFileHelper';
export * from './DateHelper';
export * from './Extension';
export * from './FilesHelper';
@@ -11,13 +12,15 @@ export * from './ImageHelper';
export * from './Logger';
export * from './MediaHelpers';
export * from './MediaLibrary';
export * from './MessageHelper';
export * from './Notifications';
export * from './PanelSettings';
export * from './PlaceholderHelper';
export * from './Questions';
export * from './Sanitize';
export * from './SeoHelper';
export * from './SettingsHelper';
export * from './SlugHelper';
export * from './SnippetParser';
export * from './Sorting';
export * from './StringHelpers';
export * from './Telemetry';

View File

@@ -21,7 +21,7 @@ export default function useContentType(settings: PanelSettings | Settings | unde
setContentType(ct || DEFAULT_CONTENT_TYPE)
}
}, [settings?.contentTypes, metadata?.data]);
}, [settings?.contentTypes, metadata?.type]);
return contentType;
}

View File

@@ -3,11 +3,10 @@ import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { BaseListener } from "./BaseListener";
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
import { Folders } from '../../commands/Folders';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { dirname } from 'path';
import * as yaml from 'js-yaml';
import { Logger, Notifications } from '../../helpers';
import { commands } from 'vscode';
import { DataFileHelper } from '../../helpers';
export class DataListener extends BaseListener {
@@ -57,38 +56,7 @@ export class DataListener extends BaseListener {
* @param msgData
*/
private static async processDataFile(msgData: DataFile) {
try {
const { file } = msgData;
const dataFile = this.getDataFile(file);
if (msgData.fileType === "yaml") {
const entries = yaml.safeLoad(dataFile || "");
this.sendMsg(DashboardCommand.dataFileEntries, entries);
} else {
const jsonData = dataFile ? JSON.parse(dataFile) : [];
this.sendMsg(DashboardCommand.dataFileEntries, jsonData);
}
} catch (ex) {
Logger.error(`DataListener::processDataFile: ${(ex as Error).message}`);
const btnClick = await Notifications.error(`Something went wrong while processing the data file. Check your file and output log for more information.`, 'Open output');
if (btnClick && btnClick === 'Open output') {
commands.executeCommand(`workbench.panel.output.focus`);
}
}
}
/**
* Retrieve the file data
* @param file
* @returns
*/
private static getDataFile(file: string) {
const absPath = Folders.getAbsFilePath(file);
if (existsSync(absPath)) {
return readFileSync(absPath, 'utf8');
}
return null;
const entries = await DataFileHelper.process(msgData);
this.sendMsg(DashboardCommand.dataFileEntries, entries);
}
}

View File

@@ -36,10 +36,14 @@ export class MediaListener extends BaseListener {
case DashboardMessage.revealMedia:
this.openFileInFinder(msg?.data?.file);
break;
case DashboardMessage.insertPreviewImage:
case DashboardMessage.insertMedia:
Telemetry.send(TelemetryEvent.insertMediaToContent);
MediaHelpers.insertMediaToMarkdown(msg?.data);
break;
case DashboardMessage.insertFile:
Telemetry.send(TelemetryEvent.insertFileToContent);
MediaHelpers.insertMediaToMarkdown(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
Telemetry.send(TelemetryEvent.updateMediaMetadata);
this.update(msg.data);

View File

@@ -174,6 +174,10 @@ export class PagesListener extends BaseListener {
}
} catch (error: any) {
if ((error as Error)?.message.toLowerCase() === "webview is disposed") {
continue;
}
Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`);
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}

View File

@@ -1,4 +1,7 @@
import { SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID } from "../../constants";
import { join } from "path";
import { commands, Uri } from "vscode";
import { Folders } from "../../commands/Folders";
import { COMMAND_NAME, SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { DashboardSettings, Settings } from "../../helpers";
@@ -26,6 +29,9 @@ export class SettingsListener extends BaseListener {
case DashboardMessage.setFramework:
this.setFramework(msg?.data);
break;
case DashboardMessage.addFolder:
this.addFolder(msg?.data);
break;
}
}
@@ -53,7 +59,7 @@ export class SettingsListener extends BaseListener {
* Set the current site-generator or framework + related settings
* @param frameworkId
*/
private static setFramework(frameworkId: string | null) {
public static setFramework(frameworkId: string | null) {
Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
if (frameworkId) {
@@ -61,9 +67,21 @@ export class SettingsListener extends BaseListener {
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
if (framework) {
Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
FrameworkDetector.checkDefaultSettings(framework);
} else {
Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
}
}
SettingsListener.getSettings();
}
private static addFolder(folder: string) {
if (folder) {
const wsFolder = Folders.getWorkspaceFolder();
const folderUri = Uri.file(join(wsFolder?.fsPath || "", folder));
commands.executeCommand(COMMAND_NAME.registerFolder, folderUri);
}
}
}

View File

@@ -4,6 +4,7 @@ import { Dashboard } from "../../commands/Dashboard";
import { SETTING_CONTENT_SNIPPETS } from "../../constants";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Notifications, Settings } from "../../helpers";
import { Snippet } from "../../models";
import { BaseListener } from "./BaseListener";
import { SettingsListener } from "./SettingsListener";
@@ -27,7 +28,7 @@ export class SnippetListener extends BaseListener {
}
private static async addSnippet(data: any) {
const { title, description, body, fields } = data;
const { title, description, body, fields, isMediaSnippet } = data;
if (!title || !body) {
Notifications.warning("Snippet missing title or body");
@@ -42,11 +43,18 @@ export class SnippetListener extends BaseListener {
const snippetLines = body.split("\n");
snippets[title] = {
const snippetContent: any = {
description,
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines,
fields: fields || []
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
};
if (isMediaSnippet) {
snippetContent.isMediaSnippet = true;
} else {
snippetContent.fields = fields || []
}
snippets[title] = snippetContent;
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
SettingsListener.getSettings();

View File

@@ -1,21 +1,31 @@
import { GeneralCommands } from './../../constants';
import { GeneralCommands } from './../../constants/GeneralCommands';
import { Dashboard } from "../../commands/Dashboard";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { ExplorerView } from "../../explorerView/ExplorerView";
import { Extension } from "../../helpers";
import { Logger } from "../../helpers/Logger";
import { CommandToCode } from '../../panelWebView/CommandToCode';
import { commands, Uri } from 'vscode';
export abstract class BaseListener {
public static process(msg: { command: DashboardMessage, data: any }) {}
public static process(msg: { command: DashboardMessage | CommandToCode | string , data: any }) {
switch(msg.command) {
case GeneralCommands.toVSCode.openLink:
if (msg.data) {
commands.executeCommand('vscode.open', Uri.parse(msg.data));
}
break;
}
}
/**
* Send a message to the webview
* @param command
* @param data
*/
public static sendMsg(command: GeneralCommands, data: any) {
public static sendMsg(command: string, data: any) {
Logger.info(`Sending message to webview (panel&dashboard): ${command}`);
const extPath = Extension.getInstance().extensionPath;

View File

@@ -35,7 +35,7 @@ export class ModeListener extends BaseListener {
const activeMode = ModeSwitch.getMode();
if (activeMode) {
const mode = modes.find(m => m.id === activeMode);
this.sendMsg(GeneralCommands.setMode as any, mode);
this.sendMsg(GeneralCommands.toWebview.setMode as any, mode);
// Check the commands that need to be enabled/disabled
const snippetsView = mode?.features.find(f => f === FEATURE_FLAG.dashboard.snippets.view);
@@ -44,13 +44,37 @@ export class ModeListener extends BaseListener {
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, !!snippetsView);
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, !!dataView);
} else {
this.sendMsg(GeneralCommands.setMode as any, undefined);
this.sendMsg(GeneralCommands.toWebview.setMode as any, undefined);
// Enable dashboards
await this.resetEnablement();
}
}
/**
* Check if the mode has the feature enabled
* @param feature
* @returns
*/
public static async hasFeature(feature: string) {
const modes = Settings.get<Mode[]>(SETTING_GLOBAL_MODES);
if (!modes || modes.length === 0) {
return true;
}
const activeMode = ModeSwitch.getMode();
if (activeMode) {
const mode = modes.find(m => m.id === activeMode);
return mode?.features.find(f => f === feature);
}
return true;
}
/**
* Reset the context
*/
public static async resetEnablement() {
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, true);
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, true);

View File

@@ -1,3 +1,4 @@
import { DataFileHelper } from './../../helpers/DataFileHelper';
import { BlockFieldData } from './../../models/BlockFieldData';
import { ImageHelper } from './../../helpers/ImageHelper';
import { Folders } from "../../commands/Folders";
@@ -43,6 +44,18 @@ export class DataListener extends BaseListener {
case CommandToCode.updatePlaceholder:
this.updatePlaceholder(msg?.data?.field, msg?.data?.value, msg?.data?.title);
break;
case CommandToCode.generateContentType:
commands.executeCommand(COMMAND_NAME.generateContentType);
break;
case CommandToCode.addMissingFields:
commands.executeCommand(COMMAND_NAME.addMissingFields);
break;
case CommandToCode.setContentType:
commands.executeCommand(COMMAND_NAME.setContentType);
break;
case CommandToCode.getDataEntries:
this.getDataFileEntries(msg.data);
break;
}
}
@@ -95,7 +108,7 @@ export class DataListener extends BaseListener {
// Get the current content type
const contentType = ArticleHelper.getContentType(updatedMetadata);
if (contentType) {
ImageHelper.processImageFields(updatedMetadata, contentType.fields)
ImageHelper.processImageFields(updatedMetadata, contentType.fields);
}
}
@@ -147,12 +160,14 @@ export class DataListener extends BaseListener {
const contentType = ArticleHelper.getContentType(article.data);
const dateFields = contentType.fields.filter((f) => f.type === "datetime");
const imageFields = contentType.fields.filter((f) => f.type === "image" && f.multiple);
const fileFields = contentType.fields.filter((f) => f.type === "file" && f.multiple);
// Support multi-level fields
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
const isDateField = dateFields.some(f => f.name === field);
const isMultiImageField = imageFields.some(f => f.name === field);
const isMultiFileField = fileFields.some(f => f.name === field);
if (isDateField) {
for (const dateField of dateFields) {
@@ -160,9 +175,11 @@ export class DataListener extends BaseListener {
parentObj[field] = Article.formatDate(new Date(value));
}
}
} else if (isMultiImageField) {
for (const imageField of imageFields) {
if (field === imageField.name) {
} else if (isMultiImageField || isMultiFileField) {
const fields = isMultiImageField ? imageFields : fileFields;
for (const crntField of fields) {
if (field === crntField.name) {
// If value is an array, it means it comes from the explorer view itself (deletion)
if (Array.isArray(value)) {
parentObj[field] = value || [];
@@ -268,6 +285,17 @@ export class DataListener extends BaseListener {
}
}
/**
* Retrieve the data entries from local data files
* @param data
*/
private static async getDataFileEntries(data: any) {
const entries = await DataFileHelper.getById(data);
if (entries) {
this.sendMsg(Command.dataFileEntries, entries);
}
}
/**
* Open a terminal and run the passed command
* @param command

View File

@@ -22,6 +22,9 @@ export class MediaListener extends BaseListener {
case CommandToCode.selectImage:
this.selectMedia(msg);
break;
case CommandToCode.selectFile:
this.selectMedia(msg);
break;
case CommandToCode.getImageUrl:
this.generateUrl(msg.data);
break;

View File

@@ -19,4 +19,8 @@ export interface ViewData {
parents?: string[];
multiple?: string[];
value?: string;
// File fields
type: "file" | "media";
fileExtensions?: string[];
}

View File

@@ -2,4 +2,5 @@ export interface DraftField {
name: string;
type: "boolean" | "choice";
choices?: string[];
invert?: boolean;
}

View File

@@ -12,6 +12,7 @@ export interface MediaInfo {
fsPath: string;
vsPath: string | undefined;
dimensions?: ISizeCalculationResult | undefined;
title?: string | undefined;
caption?: string | undefined;
alt?: string | undefined;
mimeType?: string | undefined;

View File

@@ -48,7 +48,7 @@ export interface ContentType {
pageBundle?: boolean;
}
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block";
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file" | "dataFile";
export interface Field {
title?: string;
@@ -56,6 +56,7 @@ export interface Field {
type: FieldType;
choices?: string[] | Choice[];
single?: boolean;
wysiwyg?: boolean;
multiple?: boolean;
isPreviewImage?: boolean;
hidden?: boolean;
@@ -65,10 +66,16 @@ export interface Field {
fieldGroup?: string | string[];
dataType?: string | string[];
taxonomyLimit?: number;
fileExtensions?: string[];
// Date fields
isPublishDate?: boolean;
isModifiedDate?: boolean;
// Data file
dataFileId?: string;
dataFileKey?: string;
dataFileValue?: string;
}
export interface DateInfo {
@@ -109,6 +116,7 @@ export interface CustomScript {
output?: "notification" | "editor";
outputType?: string;
type?: ScriptType;
command?: CommandType;
}
export interface PreviewSettings {
@@ -125,4 +133,12 @@ export enum ScriptType {
Content = "content",
MediaFolder = "mediaFolder",
MediaFile = "mediaFile"
}
export enum CommandType {
Node = "node",
Shell = "shell",
PowerShell = "powershell",
Python = "python",
Python3 = "python3"
}

View File

@@ -10,6 +10,7 @@ export interface Snippet {
fields: SnippetField[];
openingTags?: string;
closingTags?: string;
isMediaSnippet?: boolean;
}
export type SnippetSpecialPlaceholders = "FM_SELECTED_TEXT" | string;

View File

@@ -9,4 +9,5 @@ export enum Command {
mediaSelectionData = "mediaSelectionData",
sendMediaUrl = "sendMediaUrl",
updatePlaceholder = "updatePlaceholder",
dataFileEntries = "dataFileEntries",
}

View File

@@ -26,6 +26,7 @@ export enum CommandToCode {
updateMetadata = "update-metadata",
openDashboard = "open-dashboard",
selectImage = "select-image",
selectFile = "select-file",
updateCustomTaxonomy = "updateCustomTaxonomy",
addToCustomTaxonomy = "addToCustomTaxonomy",
frameworkCommand = "framework-command",
@@ -33,4 +34,8 @@ export enum CommandToCode {
getImageUrl = "get-image-url",
updatePlaceholder = "update-placeholder",
getMode = "get-mode",
generateContentType = "generate-content-type",
addMissingFields = "add-missing-fields",
setContentType = "set-content-type",
getDataEntries = "get-data-entries",
}

View File

@@ -70,7 +70,8 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React
settings={settings}
metadata={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus} />
unsetFocus={unsetFocus}
features={mode?.features || []} />
</FeatureFlag>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.recentlyModified}>

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { CustomScript, FolderInfo, Mode, PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Collapsible } from './Collapsible';
import { GlobalSettings } from './GlobalSettings';
import { OtherActions } from './OtherActions';
@@ -10,6 +9,7 @@ import { SponsorMsg } from './SponsorMsg';
import { StartServerButton } from './StartServerButton';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants/Features';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IBaseViewProps {
settings: PanelSettings | undefined;
@@ -20,23 +20,23 @@ export interface IBaseViewProps {
const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles, mode}: React.PropsWithChildren<IBaseViewProps>) => {
const openDashboard = () => {
MessageHelper.sendMessage(CommandToCode.openDashboard);
Messenger.send(CommandToCode.openDashboard);
};
const initProject = () => {
MessageHelper.sendMessage(CommandToCode.initProject);
Messenger.send(CommandToCode.initProject);
};
const createContent = () => {
MessageHelper.sendMessage(CommandToCode.createContent);
Messenger.send(CommandToCode.createContent);
};
const openPreview = () => {
MessageHelper.sendMessage(CommandToCode.openPreview);
Messenger.send(CommandToCode.openPreview);
};
const runBulkScript = (script: CustomScript) => {
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title: script.title, script });
Messenger.send(CommandToCode.runCustomScript, { title: script.title, script });
};
const customActions: any[] = (settings?.scripts || []).filter(s => s.bulk && (s.type === "content" || !s.type));

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useEffect } from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Command } from '../Command';
import { VsCollapsible } from './VscodeComponents';
@@ -16,7 +16,7 @@ const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children,
const collapseKey = `collapse-${id}`;
useEffect(() => {
const prevState = MessageHelper.getState();
const prevState: any = Messenger.getState();
if (!prevState || !prevState[collapseKey] || prevState[collapseKey] === null || prevState[collapseKey] === 'true') {
setIsOpen(true);
updateStorage(true);
@@ -32,8 +32,8 @@ const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children,
}, ['']);
const updateStorage = (value: boolean) => {
const prevState = MessageHelper.getState();
MessageHelper.setState({
const prevState: any = Messenger.getState();
Messenger.setState({
...prevState,
[collapseKey]: value.toString()
});

View File

@@ -0,0 +1,74 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { VSCodeButton, VSCodeDivider } from '@vscode/webview-ui-toolkit/react';
import * as React from 'react';
import { useMemo } from 'react';
import { Field } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { IMetadata } from '../Metadata';
import { VsLabel } from '../VscodeComponents';
export interface IContentTypeValidatorProps {
fields: Field[];
metadata: IMetadata
}
const fieldsToIgnore = [`filePath`, `articleDetails`, `slug`];
export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidatorProps> = ({ fields, metadata}: React.PropsWithChildren<IContentTypeValidatorProps>) => {
const isValid = useMemo(() => {
const metadataFields = Object.keys(metadata).filter(key => !fieldsToIgnore.includes(key));
for (const mField of metadataFields) {
if (!fields.find(field => field.name === mField)) {
return false;
}
}
return true;
}, [fields, metadata]);
const generateContentType = () => {
Messenger.send(CommandToCode.generateContentType);
};
const addMissingFields = () => {
Messenger.send(CommandToCode.addMissingFields);
};
const setContentType = () => {
Messenger.send(CommandToCode.setContentType);
};
if (isValid) {
return null;
}
return (
<div className='hint'>
<VsLabel>
<div className={`metadata_field__label metadata_field__alert`}>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fillRule="evenodd" clipRule="evenodd" d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"/></svg>
<span>Content type</span>
</div>
</VsLabel>
<p className='inline_hint'>We noticed field differences between the content type and the front matter data.</p>
<p className='inline_hint'>Would you like to create, update, or set the content type for this content?</p>
<div className='hint__buttons'>
<VSCodeButton appearance={`secondary`} onClick={generateContentType}>Create content type</VSCodeButton>
<VSCodeButton appearance={`secondary`} onClick={addMissingFields}>Add missing fields</VSCodeButton>
<VSCodeButton appearance={`secondary`} onClick={setContentType}>Set content type</VSCodeButton>
</div>
<VSCodeDivider />
</div>
);
};

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { ActionButton } from './ActionButton';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface ICustomScriptProps {
title: string;
@@ -11,7 +11,7 @@ export interface ICustomScriptProps {
const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title, script}: React.PropsWithChildren<ICustomScriptProps>) => {
const runCustomScript = () => {
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title, script });
Messenger.send(CommandToCode.runCustomScript, { title, script });
};
return (

View File

@@ -0,0 +1,181 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { ChevronDownIcon, DatabaseIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Command } from '../../Command';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
import Downshift from 'downshift';
import { ChoiceButton } from './ChoiceButton';
export interface IDataFileFieldProps {
label: string;
dataFileId?: string;
dataFileKey?: string;
dataFileValue?: string;
selected: string | string[];
multiSelect?: boolean;
onChange: (value: string | string[]) => void;
}
export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ label, dataFileId, dataFileKey, dataFileValue, selected, multiSelect, onChange }: React.PropsWithChildren<IDataFileFieldProps>) => {
const [ dataEntries, setDataEntries ] = useState<string[] | null>(null);
const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>();
const dsRef = React.useRef<Downshift<string> | null>(null);
const messageListener = (message: MessageEvent<EventData<any>>) => {
const { command, data } = message.data;
if (command === Command.dataFileEntries) {
setDataEntries(data || null);
}
};
const onValueChange = useCallback((txtValue: string) => {
if (multiSelect) {
const newValue = [...(crntSelected || []) as string[], txtValue];
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected(txtValue);
onChange(txtValue);
}
}, [crntSelected, multiSelect, onChange]);
const removeSelected = useCallback((txtValue: string) => {
if (multiSelect) {
const newValue = [...(crntSelected || [])].filter(v => v !== txtValue);
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected("");
onChange("");
}
}, [crntSelected, multiSelect, onChange]);
const allChoices = useMemo(() => {
if (dataEntries && dataFileKey) {
return dataEntries.map((r: any) => ({
id: r[dataFileKey],
title: r[dataFileValue || dataFileKey] || r[dataFileKey]
})).filter(r => r.id);
}
return [];
}, [crntSelected, dataEntries, dataFileKey, dataFileValue]);
const availableChoices = useMemo(() => {
if (allChoices) {
return allChoices.filter(choice => {
if (choice) {
if (typeof crntSelected === 'string') {
return crntSelected !== choice.id;
} else if (crntSelected instanceof Array) {
return crntSelected.indexOf(choice.id) === -1;
}
return true;
}
return false;
});
}
return [];
}, [allChoices]);
const getChoiceValue = useCallback((id: string) => {
const choice = allChoices.find(r => r.id === id);
if (choice) {
return choice.title;
}
return "";
}, [allChoices]);
useEffect(() => {
if (selected) {
if (multiSelect) {
setCrntSelected(typeof selected === 'string' ? [selected] : selected);
return;
} else {
setCrntSelected(selected instanceof Array ? selected[0] : selected);
return;
}
}
setCrntSelected(multiSelect ? [] : "");
}, [selected, multiSelect]);
useEffect(() => {
if (dataFileId) {
Messenger.send(CommandToCode.getDataEntries, dataFileId);
}
}, [dataFileId]);
useEffect(() => {
Messenger.listen(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, []);
return (
<div className={`metadata_field`}>
<VsLabel>
<div className={`metadata_field__label`}>
<DatabaseIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>
<Downshift
ref={dsRef}
onSelect={(selected) => onValueChange(selected || "")}
itemToString={item => (item ? item : '')}>
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
<div {...getRootProps(undefined, {suppressRefError: true})} className={`metadata_field__choice`}>
<button
{...getToggleButtonProps({
className: `metadata_field__choice__toggle`,
disabled: availableChoices.length === 0
})}>
<span>{`Select ${label}`}</span>
<ChevronDownIcon className="icon" />
</button>
<ul className={`metadata_field__choice_list ${isOpen ? "open" : "closed" }`} {...getMenuProps()}>
{
isOpen ? availableChoices.map((choice, index) => (
<li {...getItemProps({
key: choice.id,
index,
item: choice.id,
})}>
{ choice.title || <span className={`metadata_field__choice_list__item`}>Clear value</span> }
</li>
)) : null
}
</ul>
</div>
)}
</Downshift>
{
crntSelected instanceof Array ? crntSelected.map((value: string) => (
<ChoiceButton
key={value}
value={value}
title={getChoiceValue(value)}
onClick={removeSelected} />
)) : (
crntSelected && (
<ChoiceButton
key={crntSelected}
value={crntSelected}
title={getChoiceValue(crntSelected)}
onClick={removeSelected} />
)
)
}
</div>
);
};

View File

@@ -0,0 +1,102 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { DocumentIcon, PaperClipIcon, TrashIcon } from '@heroicons/react/outline';
import { basename } from 'path';
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { BlockFieldData } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
export interface IFileFieldProps {
label: string;
fieldName: string;
filePath: string;
multiple?: boolean;
value: string | string[] | null;
fileExtensions?: string[];
parents?: string[];
blockData?: BlockFieldData;
onChange: (value: string | string[] | null) => void;
}
const File = ({ value, onRemove }: { value: string, onRemove: (value: string) => void }) => {
return (
<div className='metadata_field__file__list__item' title={value}>
<div className='metadata_field__file__item__icon'>
<PaperClipIcon style={{ width: "16px", height: "16px" }} />
</div>
<span className='metadata_field__file__item__text' style={{ lineHeight: "16px" }}>{basename(value)}</span>
<button className='metadata_field__file__item__remove' type='button' onClick={() => onRemove(value)}>
<TrashIcon style={{ width: "16px", height: "16px" }} />
<span className='sr-only'>Delete file</span>
</button>
</div>
)
}
export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange }: React.PropsWithChildren<IFileFieldProps>) => {
const selectFile = useCallback(() => {
Messenger.send(CommandToCode.selectFile, {
filePath,
fieldName,
value,
multiple,
parents,
blockData,
fileExtensions,
type: "file"
});
}, [filePath, fieldName, value, multiple, parents]);
const onRemove = useCallback((crntValue) => {
const newValue = value && Array.isArray(value) ? value.filter(v => v !== crntValue) : null;
onChange(newValue);
} , [value]);
const isEmpty = useMemo(() => {
return !value || (Array.isArray(value) && value.length === 0);
}, [value]);
return (
<div className={`metadata_field`}>
<VsLabel>
<div className={`metadata_field__label`}>
<DocumentIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>
<div className={`metadata_field__file`}>
{
(isEmpty || multiple) && (
<button className={`metadata_field__file__button ${isEmpty ? "" : "not_empty"}`} title={`Add your ${label?.toLowerCase() || "file"}`} type="button" onClick={selectFile}>
<DocumentIcon />
<span>Add your {label?.toLowerCase() || "file"}</span>
</button>
)
}
{
value && !Array.isArray(value) && (
<div className='metadata_field__file__list'>
<File value={value} onRemove={onRemove} />
</div>
)
}
{
multiple && value && Array.isArray(value) && (
<div className='metadata_field__file__list multiple'>
{
value.map((v, idx) => (
<File key={idx} value={v} onRemove={onRemove} />
))
}
</div>
)
}
</div>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { Command } from '../../Command';
import { CommandToCode } from '../../CommandToCode';
import { ImageFallback } from './ImageFallback';
@@ -29,7 +29,7 @@ export const PreviewImage: React.FunctionComponent<IPreviewImageProps> = ({ valu
if (value?.webviewUrl) {
setImgUrl(value.webviewUrl);
} else {
MessageHelper.sendMessage(CommandToCode.getImageUrl, value)
Messenger.send(CommandToCode.getImageUrl, value)
}
}, [value]);

View File

@@ -1,7 +1,7 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import {PhotographIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback } from 'react';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { BlockFieldData } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
@@ -35,14 +35,15 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
const selectImage = useCallback(() => {
MessageHelper.sendMessage(CommandToCode.selectImage, {
Messenger.send(CommandToCode.selectImage, {
filePath: filePath,
fieldName,
value,
multiple,
metadataInsert: true,
parents,
blockData
blockData,
type: "media"
});
}, [filePath, fieldName, value, multiple, parents]);
@@ -64,7 +65,7 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
(!value || multiple) && (
<button className={`metadata_field__preview_image__button`} title={`Add your ${label?.toLowerCase() || "image"}`} type="button" onClick={selectImage}>
<PhotographIcon />
<span className="mt-2 block text-sm font-medium text-gray-900">Add your {label?.toLowerCase() || "image"}</span>
<span>Add your {label?.toLowerCase() || "image"}</span>
</button>
)
}

View File

@@ -6,12 +6,15 @@ export interface ITextFieldProps {
label: string;
value: string | null;
singleLine: boolean | undefined;
wysiwyg: boolean | undefined;
limit: number | undefined;
rows?: number;
onChange: (txtValue: string) => void;
}
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
const WysiwygField = React.lazy(() => import('./WysiwygField'));
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, wysiwyg, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
const [ text, setText ] = React.useState<string | null>(value);
React.useEffect(() => {
@@ -37,25 +40,31 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
<PencilIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>
{
singleLine ? (
<input
className={`metadata_field__input`}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
wysiwyg ? (
<React.Suspense fallback={(<div>Loading field</div>)}>
<WysiwygField text={text || ""} onChange={onTextChange} />
</React.Suspense>
) : (
<textarea
className={`metadata_field__textarea`}
rows={rows || 2}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
singleLine ? (
<input
className={`metadata_field__input`}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
) : (
<textarea
className={`metadata_field__textarea`}
rows={rows || 2}
value={text || ""}
onChange={(e) => onTextChange(e.currentTarget.value)}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
}} />
)
)
}

View File

@@ -1,9 +1,9 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { BlockFieldData, Field, PanelSettings } from '../../../models';
import { Command } from '../../Command';
import { CommandToCode } from '../../CommandToCode';
@@ -17,8 +17,10 @@ import { IMetadata } from '../Metadata';
import { TagPicker } from '../TagPicker';
import { VsLabel } from '../VscodeComponents';
import { ChoiceField } from './ChoiceField';
import { DataFileField } from './DataFileField';
import { DateTimeField } from './DateTimeField';
import { DraftField } from './DraftField';
import { FileField } from './FileField';
import { NumberField } from './NumberField';
import { PreviewImageField, PreviewImageValue } from './PreviewImageField';
import { TextField } from './TextField';
@@ -97,7 +99,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
// Check if the field value contains a placeholder
if (value && typeof value === "string" && value.includes(`{{`) && value.includes(`}}`)) {
window.addEventListener('message', listener);
MessageHelper.sendMessage(CommandToCode.updatePlaceholder, {
Messenger.send(CommandToCode.updatePlaceholder, {
field: field.name,
title: metadata["title"],
value
@@ -153,6 +155,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
label={field.title || field.name}
singleLine={field.single}
limit={limit}
wysiwyg={field.wysiwyg}
rows={3}
onChange={(value) => onSendUpdate(field.name, value, parentFields)}
value={fieldValue as string || null} />
@@ -187,6 +190,21 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
onChange={(value) => onSendUpdate(field.name, value, parentFields)} />
</FieldBoundary>
);
} else if (field.type === 'file') {
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<FileField
label={field.title || field.name}
fieldName={field.name}
multiple={field.multiple}
fileExtensions={field.fileExtensions}
filePath={metadata.filePath as string}
value={fieldValue as string | string[] | null}
parents={parentFields}
blockData={blockData}
onChange={(value) => onSendUpdate(field.name, value, parentFields)} />
</FieldBoundary>
);
} else if (field.type === 'choice') {
const choices = field.choices || [];
@@ -329,6 +347,19 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
onSubmit={(value) => onSendUpdate(field.name, value, parentFields)} />
</FieldBoundary>
);
} else if (field.type === 'dataFile') {
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<DataFileField
label={field.title || field.name}
dataFileId={field.dataFileId}
dataFileKey={field.dataFileKey}
dataFileValue={field.dataFileValue}
selected={fieldValue as string}
multiSelect={field.multiple}
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
</FieldBoundary>
);
} else {
console.warn(`Unknown field type: ${field.type}`);
return null;

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
const ReactQuill = require('react-quill');
import 'react-quill/dist/quill.snow.css';
export interface IWysiwygFieldProps {
text: string;
onChange: (txtValue: string) => void;
}
const WysiwygField: React.FunctionComponent<IWysiwygFieldProps> = ({ text, onChange }: React.PropsWithChildren<IWysiwygFieldProps>) => {
const modules = {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{'list': 'ordered'}, {'list': 'bullet'}],
['clean']
]
};
return (
<ReactQuill
modules={modules}
value={text || ""}
onChange={onChange} />
);
};
export default WysiwygField;

View File

@@ -1,7 +1,7 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useMemo } from 'react';
import { DEFAULT_FILE_TYPES } from '../../constants/DefaultFileTypes';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { FileIcon } from './Icons/FileIcon';
import { MarkdownIcon } from './Icons/MarkdownIcon';
@@ -15,11 +15,11 @@ export interface IFileItemProps {
const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, folderName, path }: React.PropsWithChildren<IFileItemProps>) => {
const openFile = () => {
MessageHelper.sendMessage(CommandToCode.openInEditor, path);
Messenger.send(CommandToCode.openInEditor, path);
};
const itemName = useMemo(() => {
if (folderName && name === 'index.md') {
if (folderName && name.includes("index.")) {
return folderName;
}

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