forked from iarv/vscode-front-matter
Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb9984396b | ||
|
|
b7b79024e1 | ||
|
|
d17cc901ff | ||
|
|
1fe03197e3 | ||
|
|
a1eaa5baca | ||
|
|
b83b2205c0 | ||
|
|
989d20c474 | ||
|
|
2cf3ff93c5 | ||
|
|
67b44dce42 | ||
|
|
c182a67daa | ||
|
|
2494e4c6c5 | ||
|
|
efc230f81e | ||
|
|
e455fa764b | ||
|
|
c6273fa9c1 | ||
|
|
9f37ff773e | ||
|
|
9b21e15c63 | ||
|
|
fe41d9a751 | ||
|
|
5e91a0e7af | ||
|
|
e00186890c | ||
|
|
b2b017efc0 | ||
|
|
51b11b66ab | ||
|
|
2275c1b9cc | ||
|
|
bf98ff9a1d | ||
|
|
23c5a7bc18 | ||
|
|
4d05c660c8 | ||
|
|
83d4427c09 | ||
|
|
45285d3cf2 | ||
|
|
f46fdb9fb0 | ||
|
|
3557360297 | ||
|
|
600c225265 | ||
|
|
7fa814ca1b | ||
|
|
e4f44def47 | ||
|
|
08aa73f9c3 | ||
|
|
fcb564b054 | ||
|
|
ac4aea68eb | ||
|
|
ad6c37f62d | ||
|
|
bc3d5cb6b2 | ||
|
|
88c8cc82c8 | ||
|
|
69e0dc3343 | ||
|
|
dda9b88752 | ||
|
|
5b712e64d7 | ||
|
|
af1cc15d3d | ||
|
|
76e3c08405 | ||
|
|
ebae16f724 | ||
|
|
911adaa5d6 | ||
|
|
1766c19133 | ||
|
|
4a8bbaf82e | ||
|
|
fa7b9f3ad1 | ||
|
|
ecc9c74091 | ||
|
|
282c95be29 | ||
|
|
9325ce3638 | ||
|
|
a4da46ca21 | ||
|
|
44f30f70d5 | ||
|
|
2ef39cb2ed | ||
|
|
c8ecc92309 | ||
|
|
3ca6609ace | ||
|
|
670791fcf6 | ||
|
|
30dc33a859 | ||
|
|
07ed95793c | ||
|
|
9a9ec33f9f | ||
|
|
89aab6c74e | ||
|
|
c0e6c79c67 | ||
|
|
7badfda41b | ||
|
|
9445ce6d37 | ||
|
|
1aa8f77590 | ||
|
|
dab6a46d98 | ||
|
|
6b940e2f24 | ||
|
|
8c0ce05133 | ||
|
|
937494f81c | ||
|
|
7392d7ea0d | ||
|
|
9fcc231a7a | ||
|
|
149703a5df | ||
|
|
463455121e | ||
|
|
43ae9a6ba2 | ||
|
|
c24cc2165f | ||
|
|
f177a61d4f | ||
|
|
f13b9e8ea5 | ||
|
|
c04dd79778 | ||
|
|
00273a8c86 | ||
|
|
231ef804dc | ||
|
|
44dc22c792 | ||
|
|
830fc550bd | ||
|
|
6c7567a15c | ||
|
|
be9797cc77 | ||
|
|
a78d9c5906 | ||
|
|
8f4fe45d9e | ||
|
|
79157feed5 | ||
|
|
09888d5657 | ||
|
|
a0371167bc | ||
|
|
0dc2623ded | ||
|
|
7d81a83672 | ||
|
|
17150a53bc | ||
|
|
fbf1990045 | ||
|
|
c5881d7905 | ||
|
|
b83f7beb30 | ||
|
|
d2c5a850ef | ||
|
|
5b334db3c9 | ||
|
|
69aa7a7648 | ||
|
|
97e4313d93 | ||
|
|
3f7acd7e26 | ||
|
|
7a2b45f031 | ||
|
|
8ed64691c4 | ||
|
|
844971cdd9 | ||
|
|
cf376cdda7 | ||
|
|
1a6acce77f | ||
|
|
e9258e1a7f | ||
|
|
61b461661d | ||
|
|
a12a3852d2 | ||
|
|
0c94b33606 | ||
|
|
23f3fbfadf | ||
|
|
434e87b074 | ||
|
|
081fb7ce2e | ||
|
|
bd43ba8a6d | ||
|
|
bd2860e225 | ||
|
|
daeaf0a59d | ||
|
|
9cc7ea09d6 | ||
|
|
4b6f283bf3 | ||
|
|
e695bad1c6 | ||
|
|
fe31081907 | ||
|
|
248ccb3718 | ||
|
|
2260174ec2 | ||
|
|
cd3a867422 | ||
|
|
05a63dd110 | ||
|
|
cfc0c3d5a1 | ||
|
|
d6dbca25ce | ||
|
|
d21ad14e89 | ||
|
|
8d00726322 | ||
|
|
af11c304d3 | ||
|
|
223276f6af | ||
|
|
a6fdfe0dfa | ||
|
|
6154164b1c | ||
|
|
1cdb6c56a5 | ||
|
|
c63310a2db | ||
|
|
9f681a7459 | ||
|
|
2610032a38 | ||
|
|
d11e8112e0 | ||
|
|
df5e346cf1 | ||
|
|
9892d14a62 | ||
|
|
8c61f79885 | ||
|
|
ffa6638d3d | ||
|
|
f9ef12bd3a | ||
|
|
b79216f2b5 | ||
|
|
2597a63718 | ||
|
|
126a21a6b5 | ||
|
|
3abfbd5302 | ||
|
|
efdbce2d08 | ||
|
|
09eea16d60 | ||
|
|
71697a09b6 | ||
|
|
3583a2b962 | ||
|
|
2825d5ddd8 | ||
|
|
2e66174c4a | ||
|
|
e78069ad17 | ||
|
|
4c97993c5f | ||
|
|
a452173d9a | ||
|
|
60a38be923 | ||
|
|
6c3d286282 | ||
|
|
32c7bbd3f9 | ||
|
|
426dbc2e46 | ||
|
|
9882dea960 | ||
|
|
eb9a05e90c | ||
|
|
4e59e736ed | ||
|
|
9f91ebf289 | ||
|
|
b80de402bd | ||
|
|
e3c535276c | ||
|
|
add22b0bd0 | ||
|
|
48f855144e | ||
|
|
67291f0cbe | ||
|
|
45b302c698 | ||
|
|
f897edab5f | ||
|
|
dffb9f3dd8 | ||
|
|
573e1966ae | ||
|
|
d161aa98a0 | ||
|
|
f10d93c22e | ||
|
|
17a98fba68 | ||
|
|
dee28397cb | ||
|
|
c4055eb37c | ||
|
|
91049bebd9 | ||
|
|
9f6c35b9ec | ||
|
|
f0ed7c0b39 | ||
|
|
e64c4fc0f8 | ||
|
|
cd19cec4f7 | ||
|
|
070fc53685 | ||
|
|
b232c55843 | ||
|
|
55a14b3fbe | ||
|
|
60fde1711e | ||
|
|
31ca9d4e8b | ||
|
|
bd47a09d1e | ||
|
|
744322a398 | ||
|
|
70de0e3ebd | ||
|
|
935ef83c4f | ||
|
|
93370095e9 | ||
|
|
e8e9a5a5d3 | ||
|
|
5e23d4446b | ||
|
|
6ef1ba5b57 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'Issue: '
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://frontmatter.codes/docs
|
||||
about: See our documentation.
|
||||
- name: Changelog
|
||||
url: https://frontmatter.codes/updates
|
||||
about: See our changelog.
|
||||
- name: Front Matter website
|
||||
url: https://frontmatter.codes
|
||||
about: Our website.
|
||||
- name: Support Front Matter
|
||||
url: https://github.com/sponsors/estruyf
|
||||
about: Support Front Matter development.
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'Enhancement: '
|
||||
labels: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# PR Details
|
||||
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!--- This project only accepts pull requests related to open issues -->
|
||||
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
|
||||
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
|
||||
<!--- Please link to the issue here: -->
|
||||
|
||||
## Motivation and Context
|
||||
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
|
||||
## How Has This Been Tested
|
||||
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
<!--- see how your change affects other areas of the code, etc. -->
|
||||
|
||||
## Types of changes
|
||||
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] Docs change / refactoring / dependency upgrade
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
27
.github/workflows/project-labelling.yml
vendored
Normal file
27
.github/workflows/project-labelling.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Project labelling
|
||||
|
||||
on:
|
||||
project_card:
|
||||
types: [created, moved, deleted]
|
||||
|
||||
jobs:
|
||||
automate-issues-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch project data
|
||||
run: |
|
||||
echo 'PROJECT_DATA<<EOF' >> $GITHUB_ENV
|
||||
curl --request GET --url '${{ github.event.project_card.project_url }}' --header 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
|
||||
- name: Add the project label
|
||||
uses: andymckay/labeler@master
|
||||
if: ${{ contains(github.event.action, 'created') || contains(github.event.action, 'moved') }}
|
||||
with:
|
||||
add-labels: "Project: ${{ fromJSON(env.PROJECT_DATA).name }}"
|
||||
|
||||
- name: Remove the project label
|
||||
uses: andymckay/labeler@master
|
||||
if: ${{ contains(github.event.action, 'deleted') }}
|
||||
with:
|
||||
remove-labels: "Project: ${{ fromJSON(env.PROJECT_DATA).name }}"
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,4 +4,8 @@ node_modules
|
||||
*.vsix
|
||||
.DS_Store
|
||||
dist
|
||||
todo.md
|
||||
todo.md
|
||||
|
||||
e2e/storage
|
||||
e2e/extensions
|
||||
e2e/sample
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -2,6 +2,7 @@
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"eliostruyf.vscode-typescript-exportallmodules"
|
||||
]
|
||||
}
|
||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -29,20 +29,6 @@
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Extension Tests",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionTestsPath=${workspaceFolder}/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/test/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: test-compile"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
43
.vscode/settings.json
vendored
43
.vscode/settings.json
vendored
@@ -10,8 +10,43 @@
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"eliostruyf.writingstyleguide.terms.isDisabled": true,
|
||||
"eliostruyf.writingstyleguide.biasFree.isDisabled": true,
|
||||
"exportall.config.folderListener": [
|
||||
"/src/pagesView/state/atom",
|
||||
"/src/pagesView/state/selectors"
|
||||
]
|
||||
"squarl.groups": [
|
||||
{
|
||||
"id": "dashboard",
|
||||
"name": "Dashboard"
|
||||
},
|
||||
{
|
||||
"id": "panel",
|
||||
"name": "Panel"
|
||||
}
|
||||
],
|
||||
"squarl.bookmarks": [
|
||||
{
|
||||
"name": "App.tsx",
|
||||
"path": "src/dashboardWebView/components/App.tsx",
|
||||
"description": "Start of dashboard",
|
||||
"type": "file",
|
||||
"groupId": "dashboard"
|
||||
},
|
||||
{
|
||||
"name": "ViewPanel.tsx",
|
||||
"path": "src/panelWebView/ViewPanel.tsx",
|
||||
"description": "Start of panel",
|
||||
"type": "file",
|
||||
"groupId": "panel"
|
||||
},
|
||||
{
|
||||
"name": "styles.css",
|
||||
"path": "src/panelWebView/styles.css",
|
||||
"description": "Panel styles",
|
||||
"type": "file",
|
||||
"groupId": "panel"
|
||||
},
|
||||
{
|
||||
"name": "settings.ts",
|
||||
"path": "src/constants/settings.ts",
|
||||
"description": "Settings names",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -26,4 +26,6 @@ dist/*.html
|
||||
frontmatter.json
|
||||
.frontmatter
|
||||
webpack
|
||||
README.beta.md
|
||||
README.beta.md
|
||||
e2e
|
||||
storage
|
||||
158
CHANGELOG.md
158
CHANGELOG.md
@@ -1,10 +1,166 @@
|
||||
# Change Log
|
||||
|
||||
## [8.1.1] - 2022-09-23
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#422](https://github.com/estruyf/vscode-front-matter/issues/422): Fix in panel initialization logic
|
||||
|
||||
## [8.1.0] - 2022-09-22 - [Release notes](https://beta.frontmatter.codes/updates/v8.1.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#369](https://github.com/estruyf/vscode-front-matter/issues/369): New `required` property to specify if a content-type field is required
|
||||
- [#376](https://github.com/estruyf/vscode-front-matter/issues/376): Ability to run scripts after content was created
|
||||
- [#377](https://github.com/estruyf/vscode-front-matter/issues/377): Git sync actions added on panel and content dashboard (pull and push your changes to remote)
|
||||
- [#379](https://github.com/estruyf/vscode-front-matter/issues/377): New `frontMatter.config.reload` command to reload the configuration file + reinitialize its listeners
|
||||
- [#391](https://github.com/estruyf/vscode-front-matter/issues/391): New `description` property to show a message underneath the input field
|
||||
- [#401](https://github.com/estruyf/vscode-front-matter/issues/401): Content dashboard now has pagination enabled and can be disabled via the `frontMatter.dashboard.content.pagination` setting
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#352](https://github.com/estruyf/vscode-front-matter/issues/352): Custom placeholders now support scripting
|
||||
- [#370](https://github.com/estruyf/vscode-front-matter/issues/370): Define the tags and categories as reserved keywords for custom taxonomy
|
||||
- [#372](https://github.com/estruyf/vscode-front-matter/issues/372): Rename Taxonomy tab to Taxonomies
|
||||
- [#374](https://github.com/estruyf/vscode-front-matter/issues/374): Hide the front matter section to use the panel instead
|
||||
- [#383](https://github.com/estruyf/vscode-front-matter/issues/383): Add the item menu to the content list view
|
||||
- [#385](https://github.com/estruyf/vscode-front-matter/issues/385): If no default value for the draft field is defined, the field value will be set to `true`
|
||||
- [#388](https://github.com/estruyf/vscode-front-matter/issues/388): New stop server action has been added to the panel
|
||||
- [#390](https://github.com/estruyf/vscode-front-matter/issues/390): Implement another JSON parser in order to be able to parse the `frontmatter.json` file better
|
||||
- [#394](https://github.com/estruyf/vscode-front-matter/issues/394): Ordering of snippet fields is based on their field definition
|
||||
- [#395](https://github.com/estruyf/vscode-front-matter/issues/395): Added support for custom snippet fields on media snippets
|
||||
- [#402](https://github.com/estruyf/vscode-front-matter/issues/402): Custom sorting of content now supports `number` fields
|
||||
- [#417](https://github.com/estruyf/vscode-front-matter/issues/417): New `hyperlink` wysiwyg option
|
||||
- [#418](https://github.com/estruyf/vscode-front-matter/issues/418): New `heading` and `divider` fields for your content-type definition
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- Internal post message optimizations to the webviews
|
||||
- Preview tab now shows the title of the page/content if present
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#378](https://github.com/estruyf/vscode-front-matter/issues/378): Fix last modified update only to content in content folders
|
||||
- [#384](https://github.com/estruyf/vscode-front-matter/issues/384): Fix issue `title` field in sub-fields
|
||||
- [#393](https://github.com/estruyf/vscode-front-matter/issues/393): Fix Windows file path for retrieving the preview path
|
||||
- [#396](https://github.com/estruyf/vscode-front-matter/issues/396): Fix for `index` and `_index` page previews
|
||||
- [#398](https://github.com/estruyf/vscode-front-matter/issues/398): Fix Windows folder path parsing in data folder retrieval
|
||||
- [#400](https://github.com/estruyf/vscode-front-matter/issues/400): Fix for draft/published content grouping
|
||||
- [#403](https://github.com/estruyf/vscode-front-matter/issues/403): Fix for media files with spaces on importing in article content
|
||||
- [#404](https://github.com/estruyf/vscode-front-matter/issues/404): Fix for published sorting option in media dashboard
|
||||
- [#408](https://github.com/estruyf/vscode-front-matter/issues/408): Fix for missing `dashboard.taxonomy.view` view mode in the JSON schema
|
||||
|
||||
## [8.0.1] - 2022-07-13
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Fix `PSD` media card icon image
|
||||
- Fix missing clipboard icon for the media card action
|
||||
- Fix in tags rendering on content cards
|
||||
|
||||
## [8.0.0] - 2022-07-11 - [Release notes](https://beta.frontmatter.codes/updates/v8.0.0)
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- [#291](https://github.com/estruyf/vscode-front-matter/issues/291): New taxonomy dashboard for managing tags, categories, and custom taxonomies
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Ignore the SEO `keywords` field for missing content type field
|
||||
- [#307](https://github.com/estruyf/vscode-front-matter/issues/307): New `list` field which allows to create a list of items
|
||||
- [#345](https://github.com/estruyf/vscode-front-matter/issues/345): Media dashboard UI improvements to visualize the content and public folders
|
||||
- [#349](https://github.com/estruyf/vscode-front-matter/issues/349): New `slug` field which allows you to manage the slug of your post from the Front Matter panel
|
||||
- [#350](https://github.com/estruyf/vscode-front-matter/issues/350): New `previewPath` property for the `frontMatter.content.pageFolders` setting. This allows you to specify a section prefix for all content created in that directory.
|
||||
- [#351](https://github.com/estruyf/vscode-front-matter/issues/351): New `template` property for content types which allows you to combine templates and content types for content creation
|
||||
- [#353](https://github.com/estruyf/vscode-front-matter/issues/353): Add the default content type on project initialization
|
||||
- [#366](https://github.com/estruyf/vscode-front-matter/issues/366): Better support for using block fields in another block field
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#348](https://github.com/estruyf/vscode-front-matter/issues/348): Fix media dashboard breadcrumb when multiple page folders are in use
|
||||
- [#356](https://github.com/estruyf/vscode-front-matter/issues/356): Re-introduce the `labelField` to the `frontMatter.taxonomy.fieldGroups` setting
|
||||
- [#358](https://github.com/estruyf/vscode-front-matter/issues/358): Fix for relative path of the public folder
|
||||
- [#364](https://github.com/estruyf/vscode-front-matter/issues/364): Honour file ending rules in data files
|
||||
- [#365](https://github.com/estruyf/vscode-front-matter/issues/365): Show spinner on the initial load of the content dashboard
|
||||
|
||||
## [7.3.4] - 2022-06-13
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#354](https://github.com/estruyf/vscode-front-matter/issues/354): Fix Windows file path parsing for inserting media files
|
||||
|
||||
## [7.3.3] - 2022-06-11
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Card render when taxonomy is not an array value
|
||||
- Double pages on contents dashboard
|
||||
|
||||
## [7.3.2] - 2022-06-01
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#346](https://github.com/estruyf/vscode-front-matter/issues/346): Fix media dashboard refresh action
|
||||
|
||||
|
||||
## [7.3.1] - 2022-05-26
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#343](https://github.com/estruyf/vscode-front-matter/issues/343): Fix in the schema for the `frontMatter.taxonomy.fieldGroups` setting
|
||||
|
||||
## [7.3.0] - 2022-05-25 - [Release notes](https://beta.frontmatter.codes/updates/v7.3.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- JSON schema enhancements for working with data files
|
||||
- [#330](https://github.com/estruyf/vscode-front-matter/issues/330): Allow custom scripts to easily update front matter
|
||||
- [#331](https://github.com/estruyf/vscode-front-matter/issues/331): Added functionality to run other type of scripts
|
||||
- [#332](https://github.com/estruyf/vscode-front-matter/issues/332): New `dataFile` field which allows you to create data file references
|
||||
- [#333](https://github.com/estruyf/vscode-front-matter/issues/333): Automatically mark Jekyll posts in `_drafts` folder as draft
|
||||
- [#335](https://github.com/estruyf/vscode-front-matter/issues/335): Merge media snippets with content snippets to allow you to define multiple media snippets and use these in your content
|
||||
- [#336](https://github.com/estruyf/vscode-front-matter/issues/336): Support added for inverting the draft field so that SSGs/authors can use a published field instead
|
||||
- [#337](https://github.com/estruyf/vscode-front-matter/issues/337): Allow multiple front matter types to be used
|
||||
- [#338](https://github.com/estruyf/vscode-front-matter/issues/338): Ability to disable the templates functionality (default is disabled)
|
||||
- [#340](https://github.com/estruyf/vscode-front-matter/issues/340): Show an error message when there is a content folder registered that does not exist in the project
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#334](https://github.com/estruyf/vscode-front-matter/issues/334): Fix for locked content folders retrieval
|
||||
- [#339](https://github.com/estruyf/vscode-front-matter/issues/339): Fix for content folders without a title
|
||||
|
||||
|
||||
## [7.2.0] - 2022-05-02 - [Release notes](https://beta.frontmatter.codes/updates/v7.2.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- New tag design for the tags, category, and taxonomy fields
|
||||
- [#263](https://github.com/estruyf/vscode-front-matter/issues/263): WYSIWYG string field option
|
||||
- [#308](https://github.com/estruyf/vscode-front-matter/issues/308): New `File` field
|
||||
- [#314](https://github.com/estruyf/vscode-front-matter/issues/314): New preview actions to open the page in the browser and refresh the preview
|
||||
- [#322](https://github.com/estruyf/vscode-front-matter/issues/322): Show parent folder name when file is an index page (`index.md` / `_index.md`)
|
||||
- [#323](https://github.com/estruyf/vscode-front-matter/issues/323): Added 11ty, jekyll, and docusaurus to the framework selection list
|
||||
- [#325](https://github.com/estruyf/vscode-front-matter/issues/325): Better welcome experience that allows you to add content folders straight from the welcome view
|
||||
- [#326](https://github.com/estruyf/vscode-front-matter/issues/326): Content type actions to create, update, or set according to the current file
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- [#316](https://github.com/estruyf/vscode-front-matter/issues/316): Suppress file parsing errors when closing the dashboard
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Updated JSON schema link to supported version by VS Code (draft-07)
|
||||
- Hide the view mode action from the Front Matter panel if no custom modes are defined
|
||||
- Fix in decode base64 uploaded video files
|
||||
- Fix for a lightbox on other types of documents (pdf, etc.)
|
||||
- Fix for hiding the image preview on slide-over for none image documents
|
||||
- [#324](https://github.com/estruyf/vscode-front-matter/issues/324): Fix for the framework selection on the welcome screen
|
||||
|
||||
## [7.1.2] - 2022-04-11
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#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
|
||||
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
elio@struyfconsulting.be.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
59
CONTRIBUTING.md
Normal file
59
CONTRIBUTING.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Contributing to Front Matter
|
||||
|
||||
First of all, it is amazing you want to contribute to Front Matter 💚.
|
||||
|
||||
There are various ways in how you can contribute to the project, it can be as simple from opening a bug report to implementing fixes or features.
|
||||
|
||||
## How you can help us
|
||||
|
||||
- Testing out the extension and providing feedback
|
||||
- Reporting issues and bugs
|
||||
- Suggesting new features
|
||||
- Fixing an issue
|
||||
- Updating documentation
|
||||
- UI improvements
|
||||
- Tutorials
|
||||
- etc.
|
||||
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following projects:
|
||||
|
||||
- [Extension](https://github.com/estruyf/vscode-front-matter)
|
||||
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
- [Sample Projects](https://github.com/FrontMatter/project-samples)
|
||||
|
||||
## How to get started
|
||||
|
||||
- Start by forking this project;
|
||||
- Clone your fork to your local machine;
|
||||
- Run `npm i`;
|
||||
- Open the project in VS Code;
|
||||
- To start developing, run `npm run dev:ext` and press `f5` to start the debugging session.
|
||||
|
||||
### Tips
|
||||
|
||||
- Ensure that the main branch on your fork is in sync with the original **vscode-front-matter** repository
|
||||
|
||||
```bash
|
||||
# assuming you are in the folder of your locally cloned fork....
|
||||
git checkout main
|
||||
|
||||
# assuming you have a remote named `upstream` pointing to the official **vscode-front-matter** repo
|
||||
git fetch upstream
|
||||
|
||||
# update your local main to be a mirror of what's in the main repo
|
||||
git pull --rebase upstream main
|
||||
```
|
||||
|
||||
- Create a feature branch in your fork. In case you get stuck, or have issues with merging your PR, this will allow you to have a clean main branch that you can use for contributing other changes.
|
||||
|
||||
```bash
|
||||
git checkout -b issue/<id>
|
||||
```
|
||||
|
||||
## Pull request
|
||||
|
||||
Once you are done with implementing the fix or feature. Please create a PR to our `dev` branch.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
@@ -54,6 +54,12 @@ A couple of our extension highlights that hopefully get you interested in giving
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 8**
|
||||
|
||||
The taxonomy dashboard got introduced on which you can manage your tags, categories, and custom taxonomy.
|
||||
|
||||

|
||||
|
||||
**Version 7**
|
||||
|
||||
Snippets support for Front Matter has been added!
|
||||
|
||||
@@ -52,6 +52,12 @@ A couple of our extension highlights that hopefully get you interested in giving
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 8**
|
||||
|
||||
The taxonomy dashboard got introduced on which you can manage your tags, categories, and custom taxonomy.
|
||||
|
||||

|
||||
|
||||
**Version 7**
|
||||
|
||||
Snippets support for Front Matter has been added!
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
}
|
||||
|
||||
.collapsible__body,
|
||||
.ext_settings {
|
||||
.ext_settings,
|
||||
.git_actions,
|
||||
.initialize_actions {
|
||||
padding: 1rem 1.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -163,36 +165,6 @@
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.article__tags__input input {
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
}
|
||||
|
||||
.article__tags__input input:disabled {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.article__tags__input.freeform {
|
||||
position: relative;
|
||||
outline: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.article__tags__input.freeform input {
|
||||
padding-right: 35px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.article__tags__input button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.article__tags ul {
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
@@ -221,60 +193,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 +273,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;
|
||||
}
|
||||
@@ -449,162 +372,6 @@ input:checked + .field__toggle__slider:before {
|
||||
}
|
||||
|
||||
/* Metadata */
|
||||
.metadata_field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__box {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: .5rem;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__label.metadata_field__label_parent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metadata_field__label svg {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__error {
|
||||
color: var(--vscode-errorForeground);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata_field__error button {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.metadata_field__error button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__input, .metadata_field__input:focus,
|
||||
.metadata_field__textarea, .metadata_field__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.metadata_field__limit {
|
||||
color: var(--vscode-inputValidation-warningBorder);
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__number {
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
width: 100%;
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
background-color: var(--vscode-input-background);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle:hover,
|
||||
.metadata_field__choice__toggle:focus,
|
||||
.metadata_field__choice__toggle:active,
|
||||
.metadata_field__choice__toggle:disabled {
|
||||
background-color: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle span {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle svg.icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-left: .25rem;
|
||||
|
||||
position: absolute;
|
||||
right: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list {
|
||||
width: 90%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list.open {
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li {
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li:active {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-selected="true"] {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-disabled="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list__item {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button {
|
||||
margin-top: .5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button_icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__datetime {
|
||||
display: flex;
|
||||
@@ -627,71 +394,6 @@ input:checked + .field__toggle__slider:before {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__multiple_images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--vscode-button-background);
|
||||
padding: 1.5rem;
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button svg {
|
||||
color: var(--vscode-foreground);
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button span {
|
||||
color: var(--vscode-foreground);
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove:hover {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
/* File list */
|
||||
.file_list vscode-label {
|
||||
border-bottom: 1px solid var(--vscode-foreground);
|
||||
@@ -773,8 +475,12 @@ input:checked + .field__toggle__slider:before {
|
||||
}
|
||||
|
||||
/* Timepicker */
|
||||
.react-datepicker button {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-datepicker button:hover {
|
||||
background-color: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.react-datepicker__triangle {
|
||||
|
||||
80
e2e/src/command.test.ts
Normal file
80
e2e/src/command.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { By, VSBrowser, EditorView, WebView, Workbench, Notification, StatusBar, NotificationType } from "vscode-extension-tester";
|
||||
import { expect } from "chai";
|
||||
import { sleep } from "./utils";
|
||||
import { join } from "path";
|
||||
|
||||
// https://github.com/microsoft/vscode-java-dependency/blob/4256fa6adcaff5ec24dbdbb8d9a516fad21431c5/test/ui/index.ts
|
||||
// https://github.com/microsoft/vscode-java-dependency/blob/4256fa6adcaff5ec24dbdbb8d9a516fad21431c5/test/ui/command.test.ts
|
||||
|
||||
describe("Initialization testing", function() {
|
||||
this.timeout(2 * 60 * 1000 /*ms*/);
|
||||
|
||||
let workbench: Workbench;
|
||||
let view: WebView;
|
||||
|
||||
before(async function() {
|
||||
await VSBrowser.instance.openResources(join(__dirname, '../sample'));
|
||||
await sleep(3000);
|
||||
workbench = new Workbench();
|
||||
|
||||
await workbench.executeCommand("frontMatter.dashboard");
|
||||
await sleep(3000);
|
||||
|
||||
await new EditorView().openEditor(`FrontMatter Dashboard`);
|
||||
|
||||
view = new WebView();
|
||||
await view.switchToFrame();
|
||||
});
|
||||
|
||||
it("1. Open welcome dashboard", async function() {
|
||||
const element = await view.findWebElement(By.css('h1'));
|
||||
|
||||
const title = await element.getText();
|
||||
|
||||
expect(title).has.string(`Front Matter`);
|
||||
});
|
||||
|
||||
it("2. Initialize project", async function() {
|
||||
const btn = await view.findWebElement(By.css('[data-test="welcome-init"] button'));
|
||||
expect(btn).to.exist;
|
||||
|
||||
await btn.click();
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
await VSBrowser.instance.driver.wait(() => {
|
||||
return notificationExists(workbench, 'Front Matter:');
|
||||
}, 2000) as Notification;
|
||||
|
||||
const notifications = await workbench.getNotifications();
|
||||
|
||||
let notification!: Notification;
|
||||
for (const not of notifications) {
|
||||
console.log(not);
|
||||
|
||||
// const message = await not.get;
|
||||
// console.log(message);
|
||||
// if (message.includes('Front Matter:')) {
|
||||
// notification = not;
|
||||
// }
|
||||
}
|
||||
|
||||
expect(await notification.getMessage()).has.string(`Project initialized successfully.`);
|
||||
});
|
||||
|
||||
it("3. Check if project file is created", async function() {});
|
||||
});
|
||||
|
||||
|
||||
async function notificationExists(workbench: Workbench, text: string): Promise<Notification | undefined> {
|
||||
const notifications = await (await (new StatusBar()).openNotificationsCenter()).getNotifications(NotificationType.Info);
|
||||
console.log(`Notifications:`, notifications.length);
|
||||
|
||||
for (const notification of notifications) {
|
||||
const message = await notification.getMessage();
|
||||
console.log(message)
|
||||
if (message.indexOf(text) >= 0) {
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
e2e/src/runTests.ts
Normal file
33
e2e/src/runTests.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as path from 'path';
|
||||
import * as semver from "semver";
|
||||
import { ExTester, ReleaseQuality } from "vscode-extension-tester";
|
||||
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const vsCodeVersion: semver.SemVer = new semver.SemVer(`1.66.0`);
|
||||
const version = vsCodeVersion.version;
|
||||
|
||||
const storageFolder = path.join(__dirname, "..", "storage");
|
||||
const extFolder = path.join(__dirname, "..", "extensions");
|
||||
|
||||
try {
|
||||
const testPath = path.join(__dirname, "command.test.js");
|
||||
|
||||
const exTester = new ExTester(storageFolder, ReleaseQuality.Stable, extFolder);
|
||||
await exTester.downloadCode(version);
|
||||
await exTester.installVsix();
|
||||
// await exTester.installFromMarketplace("eliostruyf.vscode-front-matter");
|
||||
await exTester.downloadChromeDriver(version);
|
||||
// await exTester.setupRequirements({vscodeVersion: version});
|
||||
const result = await exTester.runTests(testPath, {
|
||||
vscodeVersion: version
|
||||
});
|
||||
|
||||
process.exit(result);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
1
e2e/src/utils/index.ts
Normal file
1
e2e/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sleep';
|
||||
3
e2e/src/utils/sleep.ts
Normal file
3
e2e/src/utils/sleep.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function sleep(time: number) {
|
||||
await new Promise((resolve) => setTimeout(resolve, time));
|
||||
}
|
||||
@@ -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": "}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
13453
package-lock.json
generated
13453
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
478
package.json
478
package.json
@@ -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": "8.1.1",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -22,6 +22,10 @@
|
||||
"href": "https://www.buymeacoffee.com/zMeFRy9"
|
||||
}
|
||||
],
|
||||
"sponsor": {
|
||||
"url": "https://github.com/sponsors/estruyf"
|
||||
},
|
||||
"qna": "https://github.com/estruyf/vscode-front-matter/discussions",
|
||||
"engines": {
|
||||
"vscode": "^1.63.0"
|
||||
},
|
||||
@@ -56,7 +60,7 @@
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "frontmatter-explorer",
|
||||
"title": "FrontMatter",
|
||||
"title": "Front Matter",
|
||||
"icon": "assets/frontmatter-short-min.svg"
|
||||
}
|
||||
]
|
||||
@@ -65,9 +69,9 @@
|
||||
"frontmatter-explorer": [
|
||||
{
|
||||
"id": "frontMatter.explorer",
|
||||
"name": "FrontMatter",
|
||||
"name": "Front Matter",
|
||||
"icon": "assets/frontmatter-short-min.svg",
|
||||
"contextualTitle": "FrontMatter",
|
||||
"contextualTitle": "Front Matter",
|
||||
"type": "webview"
|
||||
}
|
||||
]
|
||||
@@ -78,7 +82,7 @@
|
||||
"frontMatter.content.autoUpdateDate": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
|
||||
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page (only content located in your content folder). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.defaultFileType": {
|
||||
@@ -137,6 +141,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",
|
||||
@@ -160,6 +168,17 @@
|
||||
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.fmhighlight)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.hideFm": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Specify if you want to hide the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.hidefm)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.hideFmMessage": {
|
||||
"type": "string",
|
||||
"default": "Use the editor panel to make front matter changes",
|
||||
"markdownDescription": "Specify the message to display when the Front Matter is hidden. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.hidefmMessage)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.pageFolders": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -179,6 +198,14 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude sub-directories"
|
||||
},
|
||||
"previewPath": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
],
|
||||
"default": null,
|
||||
"description": "Defines a custom preview path for the folder."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -203,12 +230,18 @@
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The placeholder its value"
|
||||
},
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "The script to execute to get the value of the placeholder"
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#scriptCommand"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"value"
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
@@ -225,8 +258,7 @@
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"body",
|
||||
"fields"
|
||||
"body"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
@@ -255,6 +287,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
|
||||
@@ -292,7 +329,8 @@
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"date"
|
||||
"date",
|
||||
"number"
|
||||
],
|
||||
"description": "Type of the field value"
|
||||
}
|
||||
@@ -332,6 +370,10 @@
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the script."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title you want to give to your script. Will be shown as the title of the button."
|
||||
@@ -342,7 +384,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 +411,31 @@
|
||||
"mediaFile"
|
||||
],
|
||||
"description": "The type for which the script will be used."
|
||||
},
|
||||
"command": {
|
||||
"$id": "#scriptCommand",
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"node",
|
||||
"bash",
|
||||
"powershell",
|
||||
"python",
|
||||
"python3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "The type of script you want to execute.",
|
||||
"default": "node"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "Hide the action from the UI",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -379,6 +446,12 @@
|
||||
},
|
||||
"scope": "Custom scripts"
|
||||
},
|
||||
"frontMatter.dashboard.content.pagination": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Specify if you want to enable/disable pagination for your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.dashboard.content.cardTags": {
|
||||
"type": "string",
|
||||
"default": "tags",
|
||||
@@ -389,6 +462,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 +500,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 +513,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 +591,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 +636,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": [
|
||||
@@ -567,6 +665,16 @@
|
||||
"default": null,
|
||||
"markdownDescription": "Specify the command you want to use to start your static site generator or framework. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.startcommand)"
|
||||
},
|
||||
"frontMatter.git.enabled": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Specify if you want to use the Git actions for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.git.enabled)",
|
||||
"default": false
|
||||
},
|
||||
"frontMatter.git.commitMessage": {
|
||||
"type": "string",
|
||||
"markdownDescription": "Specify the commit message you want to use for the sync. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.git.commitmessage)",
|
||||
"default": "Synced by Front Matter"
|
||||
},
|
||||
"frontMatter.global.activeMode": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -598,12 +706,14 @@
|
||||
"panel.globalSettings",
|
||||
"panel.seo",
|
||||
"panel.actions",
|
||||
"panel.contentType",
|
||||
"panel.metadata",
|
||||
"panel.recentlyModified",
|
||||
"panel.otherActions",
|
||||
"dashboard.snippets.view",
|
||||
"dashboard.snippets.manage",
|
||||
"dashboard.data.view"
|
||||
"dashboard.data.view",
|
||||
"dashboard.taxonomy.view"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -633,6 +743,14 @@
|
||||
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.global.disabledNotificaitons": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "This is an array with the notifications types that can be disabled for Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.disablednotifications)",
|
||||
"enum": [
|
||||
"requiredFieldValidation"
|
||||
]
|
||||
},
|
||||
"frontMatter.media.defaultSorting": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
@@ -744,6 +862,7 @@
|
||||
"datetime",
|
||||
"boolean",
|
||||
"image",
|
||||
"file",
|
||||
"choice",
|
||||
"taxonomy",
|
||||
"tags",
|
||||
@@ -751,7 +870,12 @@
|
||||
"draft",
|
||||
"fields",
|
||||
"json",
|
||||
"block"
|
||||
"block",
|
||||
"list",
|
||||
"dataFile",
|
||||
"slug",
|
||||
"divider",
|
||||
"heading"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -763,6 +887,10 @@
|
||||
"type": "string",
|
||||
"description": "Title to show in the UI"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description to show in the UI"
|
||||
},
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -802,6 +930,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,
|
||||
@@ -820,7 +953,27 @@
|
||||
"taxonomyId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The ID of your taxonomy field"
|
||||
"description": "The ID of your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
"const": "tags"
|
||||
},
|
||||
{
|
||||
"const": "categories"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"fileExtensions": {
|
||||
"type": "array",
|
||||
"description": "Specify the file extensions to allow for the file picker",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"$ref": "#contenttypefield"
|
||||
@@ -861,6 +1014,31 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the field is the modified date field"
|
||||
},
|
||||
"dataFileId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Specify the ID of the data file to use for this field"
|
||||
},
|
||||
"dataFileKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Specify the key of the data file to use for this field"
|
||||
},
|
||||
"dataFileValue": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Specify the property name that will be used to show the value for the field"
|
||||
},
|
||||
"editable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Specify if the field is editable"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the field is required"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -869,6 +1047,49 @@
|
||||
"name"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "divider"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "dataFile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"dataFileId",
|
||||
"dataFileKey"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"fileExtensions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
@@ -954,6 +1175,16 @@
|
||||
],
|
||||
"default": null,
|
||||
"description": "Defines a custom preview path for the content type."
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "An optional template that can be used for creating new content."
|
||||
},
|
||||
"postScript": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "An optional post script that can be used after new content creation."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1017,11 +1248,24 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID for your taxonomy field"
|
||||
"description": "ID for your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
"const": "tags"
|
||||
},
|
||||
{
|
||||
"const": "categories"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "Options from which you can pick",
|
||||
"description": "Options from which you can pick.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1057,13 +1301,17 @@
|
||||
"type": "string",
|
||||
"description": "The name of the field group"
|
||||
},
|
||||
"labelField": {
|
||||
"type": "string",
|
||||
"description": "The name of the field to be used as display value"
|
||||
},
|
||||
"fields": {
|
||||
"$ref": "#contenttypefield"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"id",
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
@@ -1159,6 +1407,12 @@
|
||||
"default": false,
|
||||
"markdownDescription": "Specify if you want to disable the telemetry. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.telemetry.disable)"
|
||||
},
|
||||
"frontMatter.templates.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.enabled)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.templates.folder": {
|
||||
"type": "string",
|
||||
"default": ".frontmatter/templates",
|
||||
@@ -1174,15 +1428,35 @@
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "frontMatter.config.reload",
|
||||
"title": "Reload config",
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.authenticate",
|
||||
"title": "Authenticate",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.generate",
|
||||
"title": "Generate content type from current file",
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/blockquote-light.svg",
|
||||
"dark": "assets/icons/blockquote-dark.svg"
|
||||
@@ -1191,7 +1465,7 @@
|
||||
{
|
||||
"command": "frontMatter.markup.bold",
|
||||
"title": "Bold",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/bold-light.svg",
|
||||
"dark": "assets/icons/bold-dark.svg"
|
||||
@@ -1200,7 +1474,7 @@
|
||||
{
|
||||
"command": "frontMatter.dashboard.close",
|
||||
"title": "Close dashboard",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-teal.svg",
|
||||
"light": "/assets/icons/frontmatter-small-teal.svg"
|
||||
@@ -1209,7 +1483,7 @@
|
||||
{
|
||||
"command": "frontMatter.markup.code",
|
||||
"title": "Code",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/code-light.svg",
|
||||
"dark": "assets/icons/code-dark.svg"
|
||||
@@ -1218,50 +1492,61 @@
|
||||
{
|
||||
"command": "frontMatter.markup.codeblock",
|
||||
"title": "Codeblock",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/codeblock-light.svg",
|
||||
"dark": "assets/icons/codeblock-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.hyperlink",
|
||||
"title": "Hyperlink",
|
||||
"category": "Front Matter",
|
||||
"icon": "$(link)"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"title": "Collapse sections",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/close-light.svg",
|
||||
"dark": "assets/icons/close-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.initTemplate",
|
||||
"title": "Initialize the template folder",
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"title": "Create a template from current file",
|
||||
"category": "Front matter"
|
||||
"title": "Create template from current file",
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createCategory",
|
||||
"title": "Create category",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createContent",
|
||||
"title": "Create new content from defined content type or template",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTag",
|
||||
"title": "Create tag",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.diagnostics",
|
||||
"title": "Diagnostic logging",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.exportTaxonomy",
|
||||
"title": "Export all tags & categories to your settings",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
@@ -1278,12 +1563,12 @@
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"title": "Generate slug based on content title",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.heading",
|
||||
"title": "Heading",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/heading-light.svg",
|
||||
"dark": "assets/icons/heading-dark.svg"
|
||||
@@ -1292,17 +1577,17 @@
|
||||
{
|
||||
"command": "frontMatter.init",
|
||||
"title": "Initialize project",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertCategories",
|
||||
"title": "Insert categories",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"title": "Insert image into your content",
|
||||
"category": "Front matter",
|
||||
"command": "frontMatter.insertMedia",
|
||||
"title": "Insert media into your content",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/media-dark.svg",
|
||||
"light": "/assets/icons/media-light.svg"
|
||||
@@ -1311,7 +1596,7 @@
|
||||
{
|
||||
"command": "frontMatter.insertSnippet",
|
||||
"title": "Insert snippet into your content",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/scissors-dark.svg",
|
||||
"light": "/assets/icons/scissors-light.svg"
|
||||
@@ -1320,12 +1605,12 @@
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
"title": "Insert tags",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.italic",
|
||||
"title": "Italic",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/italic-light.svg",
|
||||
"dark": "assets/icons/italic-dark.svg"
|
||||
@@ -1334,7 +1619,7 @@
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"title": "Open dashboard",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
@@ -1343,7 +1628,7 @@
|
||||
{
|
||||
"command": "frontMatter.dashboard.data",
|
||||
"title": "Open data dashboard",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
@@ -1352,7 +1637,7 @@
|
||||
{
|
||||
"command": "frontMatter.dashboard.media",
|
||||
"title": "Open media dashboard",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
@@ -1361,7 +1646,16 @@
|
||||
{
|
||||
"command": "frontMatter.dashboard.snippets",
|
||||
"title": "Open snippets dashboard",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.taxonomy",
|
||||
"title": "Open taxonomies dashboard",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
@@ -1370,7 +1664,7 @@
|
||||
{
|
||||
"command": "frontMatter.markup.orderedlist",
|
||||
"title": "Ordered list",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/ordered-list-light.svg",
|
||||
"dark": "assets/icons/ordered-list-dark.svg"
|
||||
@@ -1379,7 +1673,7 @@
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"title": "Other markup options",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/options-light.svg",
|
||||
"dark": "assets/icons/options-dark.svg"
|
||||
@@ -1388,27 +1682,27 @@
|
||||
{
|
||||
"command": "frontMatter.preview",
|
||||
"title": "Preview content",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.promoteSettings",
|
||||
"title": "Promote settings from local to team level",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.remap",
|
||||
"title": "Remap or remove tag/category in all articles",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.setLastModifiedDate",
|
||||
"title": "Set lastmod date",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.strikethrough",
|
||||
"title": "Strikethrough",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/strikethrough-light.svg",
|
||||
"dark": "assets/icons/strikethrough-dark.svg"
|
||||
@@ -1417,22 +1711,27 @@
|
||||
{
|
||||
"command": "frontMatter.mode.switch",
|
||||
"title": "Switch mode",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": "$(preview)"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.tasklist",
|
||||
"title": "Task list",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.unorderedlist",
|
||||
"title": "Unordered list",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/unordered-list-light.svg",
|
||||
"dark": "assets/icons/unordered-list-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.git.sync",
|
||||
"title": "Sync",
|
||||
"category": "Front Matter"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -1453,7 +1752,7 @@
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.strikethrough",
|
||||
"command": "frontMatter.markup.hyperlink",
|
||||
"group": "navigation@-130",
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
|
||||
},
|
||||
@@ -1463,7 +1762,7 @@
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"command": "frontMatter.insertMedia",
|
||||
"group": "navigation@-128",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
@@ -1551,6 +1850,10 @@
|
||||
"command": "frontMatter.dashboard.snippets",
|
||||
"when": "frontMatter:dashboard:snippets:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.git.sync",
|
||||
"when": "frontMatter:git:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
@@ -1620,7 +1923,7 @@
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"command": "frontMatter.insertMedia",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
@@ -1654,6 +1957,18 @@
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.generate",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.addMissingFields",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.setContentType",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
@@ -1665,12 +1980,12 @@
|
||||
{
|
||||
"command": "frontMatter.mode.switch",
|
||||
"group": "navigation@1",
|
||||
"when": "view == frontMatter.explorer"
|
||||
"when": "view == frontMatter.explorer && frontMatter:has:modes == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"group": "navigation@2",
|
||||
"when": "view == frontMatter.explorer"
|
||||
"when": "view == frontMatter.explorer || view == explorer"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1741,18 +2056,23 @@
|
||||
"prod:panel": "webpack --mode production --config ./webpack/panel.config.js",
|
||||
"test-compile": "tsc -p ./",
|
||||
"clean": "rimraf dist",
|
||||
"start:site": "cd ./docs && npm run dev"
|
||||
"start:site": "cd ./docs && npm run dev",
|
||||
"clean:test": "rm ./e2e/sample/frontmatter.json || exit 0 && rm -rf ./e2e/sample/.frontmatter || exit 0",
|
||||
"test": "tsc -p tsconfig.e2e.json && npm run clean:test && node ./e2e/out/runTests.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "^1.8.2",
|
||||
"@bendera/vscode-webview-elements": "0.6.2",
|
||||
"@estruyf/vscode": "0.0.3",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@headlessui/react": "1.5.0",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"@tailwindcss/forms": "^0.3.3",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/glob": "7.1.3",
|
||||
"@types/invariant": "^2.2.35",
|
||||
"@types/js-yaml": "3.12.1",
|
||||
@@ -1760,7 +2080,7 @@
|
||||
"@types/lodash.uniqby": "4.7.6",
|
||||
"@types/lodash.xor": "^4.5.6",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "10.17.48",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
@@ -1775,22 +2095,25 @@
|
||||
"ajv": "^8.8.2",
|
||||
"array-move": "^4.0.0",
|
||||
"autoprefixer": "^10.3.2",
|
||||
"chai": "^4.3.6",
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.23.0",
|
||||
"downshift": "6.0.6",
|
||||
"fuse.js": "6.5.3",
|
||||
"glob": "7.1.6",
|
||||
"gray-matter": "4.0.2",
|
||||
"gray-matter": "4.0.3",
|
||||
"html-loader": "1.3.2",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"image-size": "^1.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.uniqby": "4.7.0",
|
||||
"lodash.xor": "^4.5.0",
|
||||
"mdast-util-from-markdown": "1.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mocha": "^10.0.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-json-db": "^1.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -1802,10 +2125,14 @@
|
||||
"react-datepicker": "4.2.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-dropzone": "^11.3.4",
|
||||
"react-quill": "^2.0.0-beta.4",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-toastify": "^8.1.0",
|
||||
"recoil": "^0.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.7",
|
||||
"simple-git": "^3.10.0",
|
||||
"style-loader": "2.0.0",
|
||||
"tailwindcss": "^2.2.7",
|
||||
"tailwindcss-nested-groups": "^1.2.4",
|
||||
@@ -1817,6 +2144,7 @@
|
||||
"uniforms-bridge-json-schema": "^3.7.0",
|
||||
"uniforms-unstyled": "^3.7.0",
|
||||
"url-join-ts": "^1.0.5",
|
||||
"vscode-extension-tester": "^4.2.5",
|
||||
"wc-react": "github:estruyf/wc-react",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
|
||||
@@ -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`).addRaw(`Version: ${packageJson.version}`).write();
|
||||
|
||||
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);
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Folders } from './Folders';
|
||||
import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType';
|
||||
import { isValidFile } from './../helpers/isValidFile';
|
||||
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_CONTENT_PLACEHOLDERS, TelemetryEvent } from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { Field, TaxonomyType } from "../models";
|
||||
import { CustomPlaceholder, Field, TaxonomyType } from "../models";
|
||||
import { format } from "date-fns";
|
||||
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
@@ -30,7 +31,7 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Article.getCurrent();
|
||||
const article = ArticleHelper.getCurrent();
|
||||
|
||||
if (!article) {
|
||||
return;
|
||||
@@ -69,7 +70,8 @@ export class Article {
|
||||
|
||||
const selectedOptions = await vscode.window.showQuickPick(options, {
|
||||
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
|
||||
canPickMany: true
|
||||
canPickMany: true,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (selectedOptions) {
|
||||
@@ -167,13 +169,34 @@ export class Article {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slug based on the article title
|
||||
* Generate the new slug
|
||||
*/
|
||||
public static async generateSlug() {
|
||||
Telemetry.send(TelemetryEvent.generateSlug);
|
||||
|
||||
public static generateSlug(title: string) {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = Settings.get(SETTING_SLUG_PREFIX) as string;
|
||||
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
|
||||
|
||||
const slug = SlugHelper.createSlug(title);
|
||||
|
||||
if (slug) {
|
||||
return {
|
||||
slug,
|
||||
slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}`
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slug based on the article title
|
||||
*/
|
||||
public static async updateSlug() {
|
||||
Telemetry.send(TelemetryEvent.generateSlug);
|
||||
|
||||
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
|
||||
const filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
@@ -190,22 +213,21 @@ export class Article {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const titleField = "title";
|
||||
const articleTitle: string = article.data[titleField];
|
||||
const slugInfo = Article.generateSlug(articleTitle);
|
||||
|
||||
const slug = SlugHelper.createSlug(articleTitle);
|
||||
if (slug) {
|
||||
let slugFieldValue = `${prefix}${slug}${suffix}`;
|
||||
article.data["slug"] = slugFieldValue;
|
||||
if (slugInfo && slugInfo.slug && slugInfo.slugWithPrefixAndSuffix) {
|
||||
article.data["slug"] = slugInfo.slugWithPrefixAndSuffix;
|
||||
|
||||
if (contentType) {
|
||||
// Update the fields containing the slug placeholder
|
||||
let fieldsToUpdate: Field[] = contentType.fields.filter(f => f.default === "{{slug}}");
|
||||
for (const field of fieldsToUpdate) {
|
||||
article.data[field.name] = slug;
|
||||
article.data[field.name] = slugInfo.slug;
|
||||
}
|
||||
|
||||
// Update the fields containing a custom placeholder that depends on slug
|
||||
const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS);
|
||||
const customPlaceholders = placeholders?.filter(p => p.value.includes("{{slug}}"));
|
||||
const placeholders = Settings.get<CustomPlaceholder[]>(SETTING_CONTENT_PLACEHOLDERS);
|
||||
const customPlaceholders = placeholders?.filter(p => p.value && p.value.includes("{{slug}}"));
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
for (const customPlaceholder of (customPlaceholders || [])) {
|
||||
const customPlaceholderFields = contentType.fields.filter(f => f.default === `{{${customPlaceholder.id}}}`);
|
||||
@@ -226,7 +248,7 @@ export class Article {
|
||||
const ext = extname(editor.document.fileName);
|
||||
const fileName = basename(editor.document.fileName);
|
||||
|
||||
let slugName = slug.startsWith("/") ? slug.substring(1) : slug;
|
||||
let slugName = slugInfo.slug.startsWith("/") ? slugInfo.slug.substring(1) : slugInfo.slug;
|
||||
slugName = slugName.endsWith("/") ? slugName.substring(0, slugName.length - 1) : slugName;
|
||||
|
||||
let newFileName = `${slugName}${ext}`;
|
||||
@@ -302,6 +324,14 @@ export class Article {
|
||||
if (document && ArticleHelper.isSupportedFile(document)) {
|
||||
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
|
||||
|
||||
// Is article located in one of the content folders
|
||||
const folders = Folders.get();
|
||||
const documentPath = parseWinPath(document.fileName);
|
||||
const folder = folders.find(f => documentPath.startsWith(f.path));
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoUpdate) {
|
||||
event.waitUntil(Article.setLastModifiedDateOnSave(document));
|
||||
}
|
||||
@@ -324,7 +354,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;
|
||||
@@ -334,6 +364,7 @@ export class Article {
|
||||
const contentType = article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
|
||||
|
||||
const position = editor.selection.active;
|
||||
const selectionText = editor.document.getText(editor.selection);
|
||||
|
||||
await vscode.commands.executeCommand(COMMAND_NAME.dashboard, {
|
||||
type: "media",
|
||||
@@ -341,7 +372,8 @@ export class Article {
|
||||
pageBundle: !!contentType.pageBundle,
|
||||
filePath: editor.document.uri.fsPath,
|
||||
fieldName: basename(editor.document.uri.fsPath),
|
||||
position
|
||||
position,
|
||||
selection: selectionText
|
||||
}
|
||||
} as DashboardData);
|
||||
|
||||
@@ -375,23 +407,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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { CONTEXT } from '../constants';
|
||||
import { COMMAND_NAME, CONTEXT } from '../constants';
|
||||
import { Extension } from '../helpers';
|
||||
import { Credentials } from "../services/Credentials";
|
||||
import fetch from "node-fetch";
|
||||
@@ -17,7 +17,7 @@ export class Backers {
|
||||
Backers.tryUsernameCheck();
|
||||
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('frontMatter.authenticate', async () => {
|
||||
commands.registerCommand(COMMAND_NAME.authenticate, async () => {
|
||||
Backers.tryUsernameCheck();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { commands, QuickPickItem, window } from 'vscode';
|
||||
import { COMMAND_NAME } from '../constants';
|
||||
import { COMMAND_NAME, SETTING_TEMPLATES_ENABLED } from '../constants';
|
||||
import { Settings } from '../helpers';
|
||||
|
||||
export class Content {
|
||||
|
||||
public static async create() {
|
||||
const templatesEnabled = await Settings.get(SETTING_TEMPLATES_ENABLED);
|
||||
if (!templatesEnabled) {
|
||||
commands.executeCommand(COMMAND_NAME.createByContentType);
|
||||
return;
|
||||
}
|
||||
|
||||
const options: QuickPickItem[] = [{
|
||||
label: "Create content by content type",
|
||||
@@ -14,8 +20,10 @@ export class Content {
|
||||
} as QuickPickItem];
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
title: "Create content",
|
||||
placeHolder: `Select how you want to create your new content`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (selectedOption) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SETTING_DASHBOARD_OPENONSTART, CONTEXT } from '../constants';
|
||||
import { SETTING_DASHBOARD_OPENONSTART, CONTEXT, ExtensionState } from '../constants';
|
||||
import { join } from "path";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode";
|
||||
import { Logger, Settings as SettingsHelper } from '../helpers';
|
||||
@@ -7,9 +7,9 @@ import { Extension } from '../helpers/Extension';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { MediaLibrary } from '../helpers/MediaLibrary';
|
||||
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener } from '../listeners/dashboard';
|
||||
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener } from '../listeners/dashboard';
|
||||
import { MediaListener as PanelMediaListener } from '../listeners/panel'
|
||||
import { ModeListener } from '../listeners/general';
|
||||
import { GitListener, ModeListener } from '../listeners/general';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
@@ -74,6 +74,7 @@ export class Dashboard {
|
||||
public static reload() {
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.webview?.dispose();
|
||||
Extension.getInstance().setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace")
|
||||
|
||||
setTimeout(() => {
|
||||
Dashboard.open();
|
||||
@@ -145,6 +146,8 @@ export class Dashboard {
|
||||
TelemetryListener.process(msg);
|
||||
SnippetListener.process(msg);
|
||||
ModeListener.process(msg);
|
||||
GitListener.process(msg);
|
||||
TaxonomyListener.process(msg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ ${folderData.join("\n")}
|
||||
let projectStart = folder.path.split(projectName).pop();
|
||||
projectStart = projectStart || "";
|
||||
projectStart = projectStart?.replace(/\\/g, '/');
|
||||
projectStart = projectStart?.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
projectStart = projectStart?.startsWith('/') ? projectStart.substring(1) : projectStart;
|
||||
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import { SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_STATIC_FOLDER, SETTING_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants';
|
||||
import { commands, Uri, workspace, window } from "vscode";
|
||||
import { basename, dirname, join, sep } from "path";
|
||||
import { basename, dirname, join, relative, sep } from "path";
|
||||
import { ContentFolder, FileInfo, FolderInfo } from "../models";
|
||||
import uniqBy = require("lodash.uniqby");
|
||||
import { Template } from "./Template";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Settings } from "../helpers";
|
||||
import { Logger, Settings } from "../helpers";
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { MediaHelpers } from '../helpers/MediaHelpers';
|
||||
import { MediaListener, PagesListener } from '../listeners/dashboard';
|
||||
import { MediaListener, PagesListener, SettingsListener } from '../listeners/dashboard';
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
import { glob } from 'glob';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
@@ -26,7 +27,7 @@ export class Folders {
|
||||
*/
|
||||
public static async addMediaFolder(data?: {selectedFolder?: string}) {
|
||||
let wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER);
|
||||
let staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
let startPath = "";
|
||||
|
||||
@@ -41,6 +42,7 @@ export class Folders {
|
||||
}
|
||||
|
||||
const folderName = await window.showInputBox({
|
||||
title: `Add media folder`,
|
||||
prompt: `Which name would you like to give to your folder (use "/" to create multi-level folders)?`,
|
||||
value: startPath,
|
||||
ignoreFocusOut: true,
|
||||
@@ -95,9 +97,12 @@ export class Folders {
|
||||
|
||||
/**
|
||||
* Register the new folder path
|
||||
* @param folder
|
||||
* @param folderInfo
|
||||
*/
|
||||
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 +115,15 @@ 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({
|
||||
title: `Register folder`,
|
||||
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 +136,8 @@ export class Folders {
|
||||
Notifications.info(`Folder registered`);
|
||||
|
||||
Telemetry.send(TelemetryEvent.registerFolder);
|
||||
|
||||
SettingsListener.getSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +155,25 @@ export class Folders {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the static folder its relative path
|
||||
* @returns
|
||||
*/
|
||||
public static getStaticFolderRelativePath(): string | undefined {
|
||||
let staticFolder = Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER);
|
||||
|
||||
if (staticFolder && staticFolder.includes(WORKSPACE_PLACEHOLDER)) {
|
||||
staticFolder = Folders.getAbsFilePath(staticFolder);
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const relativePath = relative(parseWinPath(wsFolder.fsPath), parseWinPath(staticFolder));
|
||||
return relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
return staticFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the folder path
|
||||
* @param folder
|
||||
@@ -222,7 +252,7 @@ export class Folders {
|
||||
|
||||
if (projectStart) {
|
||||
projectStart = projectStart.replace(/\\/g, '/');
|
||||
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
projectStart = projectStart.startsWith('/') ? projectStart.substring(1) : projectStart;
|
||||
|
||||
let files: Uri[] = [];
|
||||
|
||||
@@ -284,11 +314,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[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +368,7 @@ export class Folders {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = filePath.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
|
||||
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
|
||||
return absPath;
|
||||
return parseWinPath(absPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,7 +381,7 @@ export class Folders {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
|
||||
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
|
||||
return absPath;
|
||||
return parseWinPath(absPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,4 +396,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Telemetry } from './../helpers/Telemetry';
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent } from './../constants';
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent, PreviewCommands } from './../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { join } from "path";
|
||||
import { commands, env, Uri, ViewColumn, window } from "vscode";
|
||||
import { Settings } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { Extension, parseWinPath, Settings } from '../helpers';
|
||||
import { ContentFolder, PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { Article } from '.';
|
||||
import { urlJoin } from 'url-join-ts';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { Folders } from './Folders';
|
||||
|
||||
|
||||
export class Preview {
|
||||
|
||||
/**
|
||||
/**
|
||||
* Init the preview
|
||||
*/
|
||||
public static async init() {
|
||||
@@ -36,6 +38,29 @@ export class Preview {
|
||||
let slug = article?.data ? article.data.slug : "";
|
||||
|
||||
let pathname = settings.pathname;
|
||||
|
||||
// Check if there is a pathname defined on content folder level
|
||||
const folders = Folders.get();
|
||||
if (folders.length > 0) {
|
||||
const foldersWithPath = folders.filter(folder => folder.previewPath);
|
||||
const filePath = parseWinPath(editor?.document.uri.fsPath);
|
||||
|
||||
let selectedFolder: ContentFolder | null = null;
|
||||
for (const folder of foldersWithPath) {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
if (filePath.startsWith(folderPath)) {
|
||||
if (!selectedFolder || selectedFolder.path.length < folderPath.length) {
|
||||
selectedFolder = folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFolder) {
|
||||
pathname = selectedFolder.previewPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is a pathname defined on content type level
|
||||
if (article?.data) {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (contentType && contentType.previewPath) {
|
||||
@@ -57,10 +82,18 @@ export class Preview {
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there are no backslashes in the slug
|
||||
slug = parseWinPath(slug);
|
||||
|
||||
// Verify if the slug doesn't end with _index or index
|
||||
if (slug.endsWith('_index') || slug.endsWith('index')) {
|
||||
slug = slug.substring(0, slug.endsWith('_index') ? slug.length - 6 : slug.length - 5);
|
||||
}
|
||||
|
||||
// Create the preview webview
|
||||
const webView = window.createWebviewPanel(
|
||||
'frontMatterPreview',
|
||||
'FrontMatter Preview',
|
||||
article?.data?.title ? `Preview: ${article?.data?.title}` : 'FrontMatter Preview',
|
||||
{
|
||||
viewColumn: ViewColumn.Beside,
|
||||
preserveFocus: true
|
||||
@@ -81,59 +114,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);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType';
|
||||
import { Telemetry } from './../helpers/Telemetry';
|
||||
import { workspace, Uri } from "vscode";
|
||||
import { join } from "path";
|
||||
@@ -5,8 +6,8 @@ import * as fs from "fs";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Template } from "./Template";
|
||||
import { Folders } from "./Folders";
|
||||
import { Logger, Settings } from "../helpers";
|
||||
import { SETTING_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
|
||||
import { FrameworkDetector, Logger, Settings } from "../helpers";
|
||||
import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
|
||||
import { SettingsListener } from '../listeners/dashboard';
|
||||
|
||||
export class Project {
|
||||
@@ -24,33 +25,35 @@ categories: []
|
||||
---
|
||||
`;
|
||||
|
||||
public static isInitialized() {
|
||||
return Settings.hasProjectFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new "Project" instance.
|
||||
*/
|
||||
public static async init(sampleTemplate: boolean = true) {
|
||||
public static async init(sampleTemplate?: boolean) {
|
||||
try {
|
||||
Settings.createTeamSettings();
|
||||
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
const folder = Template.getSettings();
|
||||
const templatePath = Project.templatePath();
|
||||
// Add the default content type
|
||||
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true);
|
||||
|
||||
if (!folder || !templatePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
|
||||
|
||||
if (!fs.existsSync(templatePath.fsPath)) {
|
||||
await workspace.fs.createDirectory(templatePath);
|
||||
}
|
||||
|
||||
if (sampleTemplate) {
|
||||
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
if (sampleTemplate !== undefined) {
|
||||
await Project.createSampleTemplate();
|
||||
} else {
|
||||
Notifications.info("Project initialized successfully.");
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.initialization);
|
||||
|
||||
Telemetry.send(TelemetryEvent.initialization)
|
||||
// Check if you can find the framework
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const framework = FrameworkDetector.get(wsFolder?.fsPath || "");
|
||||
|
||||
if (framework) {
|
||||
SettingsListener.setFramework(framework.name);
|
||||
}
|
||||
|
||||
SettingsListener.getSettings();
|
||||
} catch (err: any) {
|
||||
@@ -59,6 +62,33 @@ categories: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the templates folder + sample if needed
|
||||
* @param sampleTemplate
|
||||
* @returns
|
||||
*/
|
||||
public static async createSampleTemplate(sampleTemplate?: boolean) {
|
||||
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
const folder = Template.getSettings();
|
||||
const templatePath = Project.templatePath();
|
||||
|
||||
if (!folder || !templatePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
|
||||
|
||||
if (!fs.existsSync(templatePath.fsPath)) {
|
||||
await workspace.fs.createDirectory(templatePath);
|
||||
}
|
||||
|
||||
if (sampleTemplate) {
|
||||
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
Notifications.info("Sample template created.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template path for the current project
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { TaxonomyHelper } from './../helpers/TaxonomyHelper';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { TaxonomyType } from "../models";
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, EXTENSION_NAME } from '../constants';
|
||||
import { ArticleHelper, Settings as SettingsHelper, FilesHelper } from '../helpers';
|
||||
import { FrontMatterParser } from '../parsers';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
|
||||
export class Settings {
|
||||
@@ -15,9 +14,10 @@ export class Settings {
|
||||
* @param type
|
||||
*/
|
||||
public static async create(type: TaxonomyType) {
|
||||
const newOption = await vscode.window.showInputBox({
|
||||
const newOption = await vscode.window.showInputBox({
|
||||
prompt: `Insert the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} that you want to add to your configuration.`,
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (newOption) {
|
||||
@@ -36,7 +36,11 @@ export class Settings {
|
||||
await SettingsHelper.updateTaxonomy(type, options);
|
||||
|
||||
// Ask if the new term needs to be added to the page
|
||||
const addToPage = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?` });
|
||||
const addToPage = await vscode.window.showQuickPick(["yes", "no"], {
|
||||
canPickMany: false,
|
||||
placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (addToPage && addToPage === "yes") {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
@@ -71,7 +75,7 @@ export class Settings {
|
||||
*/
|
||||
public static async export() {
|
||||
// Retrieve all the Markdown files
|
||||
const allMdFiles = await FilesHelper.getMdFiles();
|
||||
const allMdFiles = await FilesHelper.getAllFiles();
|
||||
if (!allMdFiles) {
|
||||
return;
|
||||
}
|
||||
@@ -147,10 +151,13 @@ export class Settings {
|
||||
const taxType = await vscode.window.showQuickPick([
|
||||
"Tag",
|
||||
"Category"
|
||||
], {
|
||||
], {
|
||||
title: `Remap`,
|
||||
placeHolder: `What do you want to remap?`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!taxType) {
|
||||
return;
|
||||
}
|
||||
@@ -165,7 +172,8 @@ export class Settings {
|
||||
|
||||
const selectedOption = await vscode.window.showQuickPick(options, {
|
||||
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!selectedOption) {
|
||||
@@ -174,86 +182,25 @@ export class Settings {
|
||||
|
||||
const newOptionValue = await vscode.window.showInputBox({
|
||||
prompt: `Specify the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} with which you want to remap "${selectedOption}". Leave the input <blank> if you want to remove the ${type === TaxonomyType.Tag ? "tag" : "category"} from all articles.`,
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!newOptionValue) {
|
||||
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?` });
|
||||
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], {
|
||||
canPickMany: false,
|
||||
placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (deleteAnswer === "no") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve all the markdown files
|
||||
const allMdFiles = await FilesHelper.getMdFiles();
|
||||
if (!allMdFiles) {
|
||||
return;
|
||||
if (newOptionValue) {
|
||||
TaxonomyHelper.process("edit", type, selectedOption, newOptionValue);
|
||||
} else {
|
||||
TaxonomyHelper.process("delete", type, selectedOption, undefined);
|
||||
}
|
||||
|
||||
let progressText = `${EXTENSION_NAME}: Remapping "${selectedOption}" ${type === TaxonomyType.Tag ? "tag" : "category"} to "${newOptionValue}".`;
|
||||
if (!newOptionValue) {
|
||||
progressText = `${EXTENSION_NAME}: Deleting "${selectedOption}" ${type === TaxonomyType.Tag ? "tag" : "category"}.`;
|
||||
}
|
||||
vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: progressText,
|
||||
cancellable: false
|
||||
}, async (progress) => {
|
||||
// Set the initial progress
|
||||
const progressNr = allMdFiles.length/100;
|
||||
progress.report({ increment: 0});
|
||||
|
||||
const matterProp: string = type === TaxonomyType.Tag ? "tags" : "categories";
|
||||
|
||||
let i = 0;
|
||||
for (const file of allMdFiles) {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
const mdFile = fs.readFileSync(file.path, { encoding: "utf8" });
|
||||
if (mdFile) {
|
||||
try {
|
||||
const article = FrontMatterParser.fromFile(mdFile);
|
||||
if (article && article.data) {
|
||||
const { data } = article;
|
||||
let taxonomies: string[] = data[matterProp];
|
||||
if (taxonomies && taxonomies.length > 0) {
|
||||
const idx = taxonomies.findIndex(o => o === selectedOption);
|
||||
if (idx !== -1) {
|
||||
if (newOptionValue) {
|
||||
taxonomies[idx] = newOptionValue;
|
||||
} else {
|
||||
taxonomies = taxonomies.filter(o => o !== selectedOption);
|
||||
}
|
||||
data[matterProp] = [...new Set(taxonomies)].sort();
|
||||
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any), { encoding: "utf8" });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue with the next file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the settings
|
||||
const idx = options.findIndex(o => o === selectedOption);
|
||||
if (newOptionValue) {
|
||||
// Add or update the new option
|
||||
if (idx !== -1) {
|
||||
options[idx] = newOptionValue;
|
||||
} else {
|
||||
options.push(newOptionValue);
|
||||
}
|
||||
} else {
|
||||
// Remove the selected option
|
||||
options = options.filter(o => o !== selectedOption);
|
||||
}
|
||||
await SettingsHelper.updateTaxonomy(type, options);
|
||||
|
||||
Notifications.info(`${newOptionValue ? "Remapping" : "Deleation"} of the ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"} completed.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { CONTEXT, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
|
||||
import { ParsedFrontMatter } from './../parsers/FrontMatterParser';
|
||||
import { CONTEXT, NOTIFICATION_TYPE, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
|
||||
import { ArticleHelper, Notifications, SeoHelper, Settings } from '../helpers';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { DataListener } from '../listeners/panel';
|
||||
import { commands } from 'vscode';
|
||||
import { Field } from '../models';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -42,7 +44,7 @@ export class StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
// Check SEO for title and description length
|
||||
// Check SEO and required fields
|
||||
if (article && article.data) {
|
||||
collection.clear();
|
||||
|
||||
@@ -58,6 +60,9 @@ export class StatusListener {
|
||||
if (article.data[fieldName] && descLength > -1) {
|
||||
SeoHelper.checkLength(editor, collection, article, fieldName, descLength);
|
||||
}
|
||||
|
||||
// Check the required fields
|
||||
StatusListener.verifyRequiredFields(editor, article, collection);
|
||||
}
|
||||
|
||||
const panel = ExplorerView.getInstance();
|
||||
@@ -80,4 +85,98 @@ export class StatusListener {
|
||||
|
||||
frontMatterSB.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the required fields
|
||||
* @param article
|
||||
* @param collection
|
||||
*/
|
||||
private static verifyRequiredFields(editor: vscode.TextEditor, article: ParsedFrontMatter, collection: vscode.DiagnosticCollection) {
|
||||
// Check for missing fields
|
||||
const emptyFields = ContentType.findEmptyRequiredFields(article);
|
||||
const fieldsToReport = [];
|
||||
|
||||
if (emptyFields && emptyFields.length > 0) {
|
||||
const text = editor.document.getText();
|
||||
const markdown = ArticleHelper.stringifyFrontMatter("", article.data);
|
||||
const editorSpaces = vscode.window.activeTextEditor?.options?.tabSize;
|
||||
|
||||
const requiredDiagnostics: vscode.Diagnostic[] = [];
|
||||
|
||||
for (const fields of emptyFields) {
|
||||
let txtIdx = -1;
|
||||
let fieldName = "";
|
||||
let level = 0;
|
||||
|
||||
for (const field of fields) {
|
||||
const totalSpaces = level * (typeof editorSpaces === "string" ? parseInt(editorSpaces) : editorSpaces || 2);
|
||||
const crntIdx = StatusListener.findFieldLine(text, txtIdx, totalSpaces, field);
|
||||
|
||||
if (crntIdx && crntIdx > txtIdx) {
|
||||
txtIdx = crntIdx;
|
||||
fieldName = field.name;
|
||||
}
|
||||
|
||||
++level;
|
||||
}
|
||||
|
||||
if (txtIdx !== -1 && txtIdx < markdown.length) {
|
||||
fieldsToReport.push(fields.map(f => f.title).join("/"));
|
||||
|
||||
const posStart = editor.document.positionAt(txtIdx);
|
||||
const posEnd = editor.document.positionAt(txtIdx + 1 + fieldName.length);
|
||||
|
||||
const diagnostic: vscode.Diagnostic = {
|
||||
code: '',
|
||||
message: `This ${fields.map(f => f.name).join("/")} field is required to contain a value.`,
|
||||
range: new vscode.Range(posStart, posEnd),
|
||||
severity: vscode.DiagnosticSeverity.Error,
|
||||
source: 'Front Matter'
|
||||
};
|
||||
|
||||
requiredDiagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.has(editor.document.uri)) {
|
||||
const otherDiag = collection.get(editor.document.uri) || [];
|
||||
collection.set(editor.document.uri, [...otherDiag, ...requiredDiagnostics]);
|
||||
} else {
|
||||
collection.set(editor.document.uri, [...requiredDiagnostics]);
|
||||
}
|
||||
|
||||
if (fieldsToReport.length > 0) {
|
||||
Notifications.showIfNotDisabled(NOTIFICATION_TYPE.requiredFieldValidation, "ERROR_ONCE", `The following fields are required to contain a value: ${fieldsToReport.join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the line of the field
|
||||
* @param text
|
||||
* @param startIdx
|
||||
* @param totalSpaces
|
||||
* @param field
|
||||
* @returns
|
||||
*/
|
||||
private static findFieldLine(text: string, startIdx: number, totalSpaces: number, field: Field): number | undefined {
|
||||
const crntIdx = text.indexOf(field.name, startIdx === -1 ? 0 : startIdx);
|
||||
|
||||
if (crntIdx > -1) {
|
||||
// Find the linebreak before the current index
|
||||
const txtFromStart = text.substring(0, crntIdx);
|
||||
const splitLineBreaks = txtFromStart.split(/\r?\n/);
|
||||
const lastLine = splitLineBreaks[splitLineBreaks.length - 1];
|
||||
|
||||
if (lastLine.length === totalSpaces) {
|
||||
if (crntIdx > startIdx) {
|
||||
return crntIdx;
|
||||
}
|
||||
} else {
|
||||
return StatusListener.findFieldLine(text, crntIdx + field.name.length, totalSpaces, field);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEv
|
||||
import { ArticleHelper, Settings } from '../helpers';
|
||||
import { Article } from '.';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { CONTEXT } from '../constants';
|
||||
import { Project } from './Project';
|
||||
import { Folders } from './Folders';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
import { PagesListener } from '../listeners/dashboard';
|
||||
@@ -17,39 +15,6 @@ import { Telemetry } from '../helpers/Telemetry';
|
||||
|
||||
export class Template {
|
||||
|
||||
/**
|
||||
* Check if the template folder is available
|
||||
*/
|
||||
public static async init() {
|
||||
const isInitialized = await Template.isInitialized();
|
||||
await vscode.commands.executeCommand('setContext', CONTEXT.canInit, !isInitialized);
|
||||
|
||||
if (isInitialized) {
|
||||
await vscode.commands.executeCommand('setContext', CONTEXT.initialized, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the project is already initialized
|
||||
*/
|
||||
public static async isInitialized() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const folder = Template.getSettings();
|
||||
|
||||
if (!folder || !wsFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const templatePath = vscode.Uri.file(path.join(wsFolder.fsPath, folder));
|
||||
|
||||
try {
|
||||
await vscode.workspace.fs.stat(templatePath);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a template
|
||||
*/
|
||||
@@ -62,9 +27,11 @@ export class Template {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
const clonedArticle = Object.assign({}, article);
|
||||
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
title: `Template title`,
|
||||
prompt: `What name would you like to give your template?`,
|
||||
placeHolder: `article`
|
||||
placeHolder: `article`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!titleValue) {
|
||||
@@ -75,8 +42,10 @@ export class Template {
|
||||
const keepContents = await vscode.window.showQuickPick(
|
||||
["yes", "no"],
|
||||
{
|
||||
title: `Keep contents`,
|
||||
canPickMany: false,
|
||||
placeHolder: `Do you want to keep the contents for the template?`,
|
||||
ignoreFocusOut: true
|
||||
}
|
||||
);
|
||||
|
||||
@@ -98,11 +67,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 +92,16 @@ 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`
|
||||
title: `Select a template`,
|
||||
placeHolder: `Select the content template to use`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (!selectedTemplate) {
|
||||
Notifications.warning(`No template selected.`);
|
||||
@@ -164,7 +143,7 @@ export class Template {
|
||||
}
|
||||
|
||||
if (frontMatter.data) {
|
||||
frontMatter.data = ArticleHelper.updatePlaceholders(frontMatter.data, titleValue);
|
||||
frontMatter.data = await ArticleHelper.updatePlaceholders(frontMatter.data, titleValue, newFilePath);
|
||||
|
||||
frontMatter = Article.updateDate(frontMatter);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { commands, window, Selection, QuickPickItem } from "vscode";
|
||||
import { commands, window, Selection, QuickPickItem, TextEditor } from "vscode";
|
||||
import { COMMAND_NAME, CONTEXT, SETTING_CONTENT_WYSIWYG } from "../constants";
|
||||
import { Settings } from "../helpers";
|
||||
|
||||
@@ -12,7 +12,8 @@ enum MarkupType {
|
||||
heading,
|
||||
unorderedList,
|
||||
orderedList,
|
||||
taskList
|
||||
taskList,
|
||||
hyperlink,
|
||||
}
|
||||
|
||||
export class Wysiwyg {
|
||||
@@ -46,6 +47,9 @@ export class Wysiwyg {
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.orderedlist, () => this.addMarkup(MarkupType.orderedList)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.taskList, () => this.addMarkup(MarkupType.taskList)));
|
||||
|
||||
// Other markup
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.hyperlink, () => this.addMarkup(MarkupType.hyperlink)));
|
||||
|
||||
// Options
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.options, async () => {
|
||||
const qpItems: QuickPickItem[] = [
|
||||
@@ -55,12 +59,14 @@ export class Wysiwyg {
|
||||
{ label: "$(code) Code", detail: "Add inline code snippet", alwaysShow: true },
|
||||
{ label: "$(symbol-namespace) Code block", detail: "Add a code block", alwaysShow: true },
|
||||
{ label: "$(quote) Blockquote", detail: "Add a blockquote", alwaysShow: true },
|
||||
{ label: "$(symbol-text) Strikethrough", detail: "Add a strikethrough", alwaysShow: true },
|
||||
]
|
||||
|
||||
const option = await window.showQuickPick([ ...qpItems ], {
|
||||
const option = await window.showQuickPick([ ...qpItems ], {
|
||||
title: "WYSIWYG Options",
|
||||
placeHolder: "Which type of markup would you like to insert?",
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: false,
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (option) {
|
||||
@@ -76,6 +82,8 @@ export class Wysiwyg {
|
||||
await this.addMarkup(MarkupType.codeblock);
|
||||
} else if (option.label === qpItems[5].label) {
|
||||
await this.addMarkup(MarkupType.blockquote);
|
||||
} else if (option.label === qpItems[6].label) {
|
||||
await this.addMarkup(MarkupType.strikethrough);
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -95,6 +103,10 @@ export class Wysiwyg {
|
||||
const selection = editor.selection;
|
||||
const hasTextSelection = !selection.isEmpty;
|
||||
|
||||
if (type === MarkupType.hyperlink) {
|
||||
return this.addHyperlink(editor, selection);
|
||||
}
|
||||
|
||||
const markers = this.getMarkers(type);
|
||||
if (!markers) {
|
||||
return;
|
||||
@@ -129,6 +141,51 @@ export class Wysiwyg {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hyperlink to the content
|
||||
* @returns void
|
||||
*/
|
||||
private static async addHyperlink(editor: TextEditor, selection: Selection) {
|
||||
const hasTextSelection = !selection.isEmpty;
|
||||
const linkText = hasTextSelection ? editor.document.getText(selection) : "";
|
||||
|
||||
const link = await window.showInputBox({
|
||||
title: "WYSIWYG Hyperlink",
|
||||
placeHolder: "Enter the URL",
|
||||
prompt: "Enter the URL",
|
||||
value: linkText,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
const text = await window.showInputBox({
|
||||
title: "WYSIWYG Text",
|
||||
prompt: "Enter the text for the hyperlink",
|
||||
placeHolder: "Enter the text for the hyperlink",
|
||||
value: linkText,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (link) {
|
||||
const txt = `[${text || link}](${link})`;
|
||||
|
||||
if (hasTextSelection) {
|
||||
editor.edit(builder => {
|
||||
builder.replace(selection, txt);
|
||||
});
|
||||
} else {
|
||||
const crntSelection = selection.active;
|
||||
const markerLength = txt.length;
|
||||
const newPosition = crntSelection.with(crntSelection.line, crntSelection.character + markerLength);
|
||||
|
||||
await editor.edit(builder => {
|
||||
builder.insert(newPosition, txt);
|
||||
});
|
||||
|
||||
editor.selection = new Selection(newPosition, newPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the text will be wrapped
|
||||
* @param type
|
||||
@@ -159,10 +216,11 @@ export class Wysiwyg {
|
||||
"Heading 4",
|
||||
"Heading 5",
|
||||
"Heading 6"
|
||||
], {
|
||||
], {
|
||||
title: "Heading Level",
|
||||
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) {
|
||||
|
||||
13
src/components/icons/MergeIcon.tsx
Normal file
13
src/components/icons/MergeIcon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMergeIconProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const MergeIcon: React.FunctionComponent<IMergeIconProps> = ({className}: React.PropsWithChildren<IMergeIconProps>) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className={className}>
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M7.586 8.00366L4 8.00366C3.44772 8.00366 3 7.55595 3 7.00366C3 6.45138 3.44772 6.00366 4 6.00366L8 6.00366C8.26509 6.00366 8.51933 6.10892 8.70685 6.2963L13.414 11H18.5845L15.2931 7.71103C14.9025 7.32065 14.9023 6.68748 15.2926 6.29681C15.683 5.90615 16.3162 5.90592 16.7068 6.2963L21.7068 11.2926C21.8945 11.4802 22 11.7346 22 11.9998C22 12.2651 21.8947 12.5195 21.7071 12.7071L16.7071 17.7071C16.3166 18.0976 15.6834 18.0976 15.2929 17.7071C14.9024 17.3166 14.9024 16.6834 15.2929 16.2929L18.5858 13H13.4142L8.70711 17.7071C8.51957 17.8947 8.26522 18 8 18H4C3.44772 18 3 17.5523 3 17C3 16.4477 3.44772 16 4 16H7.58579L11.5855 12.0003L7.586 8.00366Z" fill="currentcolor"/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
5
src/constants/DefaultFieldValues.ts
Normal file
5
src/constants/DefaultFieldValues.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const DefaultFieldValues = {
|
||||
faultyCustomPlaceholder: "<failed to process>"
|
||||
}
|
||||
@@ -25,20 +25,24 @@ export const COMMAND_NAME = {
|
||||
createByContentType: getCommandName("createByContentType"),
|
||||
createByTemplate: getCommandName("createByTemplate"),
|
||||
createTemplate: getCommandName("createTemplate"),
|
||||
initTemplate: getCommandName("initTemplate"),
|
||||
collapseSections: getCommandName("collapseSections"),
|
||||
preview: getCommandName("preview"),
|
||||
dashboard: getCommandName("dashboard"),
|
||||
dashboardMedia: getCommandName("dashboard.media"),
|
||||
dashboardSnippets: getCommandName("dashboard.snippets"),
|
||||
dashboardData: getCommandName("dashboard.data"),
|
||||
dashboardTaxonomy: getCommandName("dashboard.taxonomy"),
|
||||
dashboardClose: getCommandName("dashboard.close"),
|
||||
promote: getCommandName("promoteSettings"),
|
||||
createFolder: getCommandName("createFolder"),
|
||||
diagnostics: getCommandName("diagnostics"),
|
||||
modeSwitch: getCommandName("mode.switch"),
|
||||
|
||||
showOutputChannel: getCommandName("showOutputChannel"),
|
||||
|
||||
// Insert dashboards
|
||||
insertImage: getCommandName("insertImage"),
|
||||
insertMedia: getCommandName("insertMedia"),
|
||||
insertSnippet: getCommandName("insertSnippet"),
|
||||
|
||||
// WYSIWYG
|
||||
@@ -52,5 +56,20 @@ export const COMMAND_NAME = {
|
||||
unorderedlist: getCommandName("markup.unorderedlist"),
|
||||
orderedlist: getCommandName("markup.orderedlist"),
|
||||
taskList: getCommandName("markup.tasklist"),
|
||||
hyperlink: getCommandName("markup.hyperlink"),
|
||||
options: getCommandName("markup.options"),
|
||||
|
||||
// Content types
|
||||
generateContentType: getCommandName("contenttype.generate"),
|
||||
addMissingFields: getCommandName("contenttype.addMissingFields"),
|
||||
setContentType: getCommandName("contenttype.setContentType"),
|
||||
|
||||
// Git
|
||||
gitSync: getCommandName("git.sync"),
|
||||
|
||||
// Authenticate
|
||||
authenticate: getCommandName("authenticate"),
|
||||
|
||||
// Config
|
||||
reloadConfig: getCommandName("config.reload"),
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export const FEATURE_FLAG = {
|
||||
metadata: "panel.metadata",
|
||||
recentlyModified: "panel.recentlyModified",
|
||||
otherActions: "panel.otherActions",
|
||||
contentType: "panel.contentType",
|
||||
},
|
||||
dashboard: {
|
||||
snippets: {
|
||||
@@ -16,6 +17,9 @@ export const FEATURE_FLAG = {
|
||||
},
|
||||
data: {
|
||||
view: "dashboard.data.view",
|
||||
},
|
||||
taxonomy: {
|
||||
view: "dashboard.taxonomy.view"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1,5 +1,13 @@
|
||||
|
||||
|
||||
export enum GeneralCommands{
|
||||
setMode = "setMode"
|
||||
export const GeneralCommands = {
|
||||
toWebview: {
|
||||
setMode: "setMode",
|
||||
gitSyncingStart: "gitSyncingStart",
|
||||
gitSyncingEnd: "gitSyncingEnd",
|
||||
},
|
||||
toVSCode: {
|
||||
openLink: "openLink",
|
||||
gitSync: "gitSync",
|
||||
}
|
||||
};
|
||||
@@ -2,5 +2,7 @@ export const GITHUB_LINK = "https://github.com/estruyf/vscode-front-matter";
|
||||
export const ISSUE_LINK = "https://github.com/estruyf/vscode-front-matter/issues";
|
||||
export const SPONSOR_LINK = "https://github.com/sponsors/estruyf";
|
||||
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";
|
||||
export const DOCUMENTATION_LINK = "https://frontmatter.codes/docs";
|
||||
export const DOCUMENTATION_SETTINGS_LINK = "https://frontmatter.codes/docs/settings";
|
||||
|
||||
export const SENTRY_LINK = "https://1ac45704bbe74264a7b4674bdc2abf48@o1022172.ingest.sentry.io/5988293";
|
||||
5
src/constants/NotificationType.ts
Normal file
5
src/constants/NotificationType.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const NOTIFICATION_TYPE = {
|
||||
requiredFieldValidation: "requiredFieldValidation",
|
||||
}
|
||||
8
src/constants/PreviewCommands.ts
Normal file
8
src/constants/PreviewCommands.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const PreviewCommands = {
|
||||
toVSCode: {
|
||||
open: `preview.open`
|
||||
},
|
||||
fromVSCode: {}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ export const TelemetryEvent = {
|
||||
openMediaDashboard: 'openMediaDashboard',
|
||||
openDataDashboard: 'openDataDashboard',
|
||||
openSnippetsDashboard: 'openSnippetsDashboard',
|
||||
openTaxonomyDashboard: 'openTaxonomyDashboard',
|
||||
closeDashboard: 'closeDashboard',
|
||||
|
||||
// Other actions
|
||||
@@ -21,10 +22,17 @@ export const TelemetryEvent = {
|
||||
uploadMedia: 'uploadMedia',
|
||||
refreshMedia: 'refreshMedia',
|
||||
deleteMedia: 'deleteMedia',
|
||||
insertContentSnippet: 'insertContentSnippet',
|
||||
insertMediaToContent: 'insertMediaToContent',
|
||||
insertFileToContent: 'insertFileToContent',
|
||||
updateMediaMetadata: 'updateMediaMetadata',
|
||||
openExplorerView: 'openExplorerView',
|
||||
|
||||
// Content types
|
||||
generateContentType: 'generateContentType',
|
||||
addMissingFields: 'addMissingFields',
|
||||
setContentType: 'setContentType',
|
||||
|
||||
// Custom scripts
|
||||
runCustomScript: 'runCustomScript',
|
||||
runMediaScript: 'runMediaScript',
|
||||
@@ -35,4 +43,8 @@ export const TelemetryEvent = {
|
||||
webviewDataView: 'webviewDataView',
|
||||
webviewContentsView: 'webviewContentsView',
|
||||
webviewSnippetsView: 'webviewSnippetsView',
|
||||
webviewTaxonomyDashboard: 'webviewTaxonomyDashboard',
|
||||
|
||||
// Git
|
||||
gitSync: 'gitSync',
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
export const CONTEXT = {
|
||||
canInit: "frontMatter:CanInit",
|
||||
initialized: "frontMatter:Initialized",
|
||||
canOpenPreview: "frontMatter:CanOpenPreview",
|
||||
canOpenDashboard: "frontMatter:CanOpenDashboard",
|
||||
isEnabled: "frontMatter:enabled",
|
||||
@@ -9,6 +7,10 @@ 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",
|
||||
|
||||
isGitEnabled: "frontMatter:git:enabled",
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './ContentType';
|
||||
export * from './DefaultFieldValues';
|
||||
export * from './DefaultFields';
|
||||
export * from './DefaultFileTypes';
|
||||
export * from './Extension';
|
||||
@@ -9,6 +10,8 @@ export * from './GeneralCommands';
|
||||
export * from './Links';
|
||||
export * from './LocalStore';
|
||||
export * from './Navigation';
|
||||
export * from './NotificationType';
|
||||
export * from './PreviewCommands';
|
||||
export * from './TelemetryEvent';
|
||||
export * from './charCode';
|
||||
export * from './charMap';
|
||||
|
||||
@@ -3,6 +3,7 @@ export const EXTENSION_NAME = "Front Matter";
|
||||
export const CONFIG_KEY = "frontMatter";
|
||||
|
||||
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
|
||||
export const SETTING_GLOBAL_NOTIFICATIONS_DISABLED = "global.disabledNotifications";
|
||||
export const SETTING_GLOBAL_MODES = "global.modes";
|
||||
export const SETTING_GLOBAL_ACTIVE_MODE = "global.activeMode";
|
||||
|
||||
@@ -32,6 +33,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";
|
||||
|
||||
@@ -58,11 +60,14 @@ export const SETTING_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
|
||||
export const SETTING_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
|
||||
export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
|
||||
|
||||
export const SETTING_CONTENT_HIDE_FRONTMATTER = "content.hideFm";
|
||||
export const SETTING_CONTENT_HIDE_FRONTMATTER_MESSAGE = "content.hideFmMessage";
|
||||
|
||||
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
|
||||
|
||||
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
|
||||
export const SETTING_DASHBOARD_CONTENT_PAGINATION = "dashboard.content.pagination";
|
||||
|
||||
export const SETTING_DATA_FILES = "data.files";
|
||||
export const SETTING_DATA_FOLDERS = "data.folders";
|
||||
@@ -75,6 +80,9 @@ export const SETTING_FRAMEWORK_START = "framework.startCommand";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
export const SETTING_GIT_ENABLED = "git.enabled";
|
||||
export const SETTING_GIT_COMMIT_MSG = "git.commitMessage";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@@ -89,3 +97,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";
|
||||
|
||||
@@ -8,4 +8,7 @@ export enum DashboardCommand {
|
||||
mediaUpdate = "mediaUpdate",
|
||||
dataFileEntries = "dataFileEntries",
|
||||
searchReady = "searchReady",
|
||||
|
||||
// Taxonomy dashboard
|
||||
setTaxonomyData = "setTaxonomyData",
|
||||
}
|
||||
@@ -5,6 +5,11 @@ export enum DashboardMessage {
|
||||
getMode = 'getMode',
|
||||
showWarning = 'showWarning',
|
||||
|
||||
// Welcome view
|
||||
initializeProject = 'initializeProject',
|
||||
setFramework = 'setFramework',
|
||||
addFolder = 'addFolder',
|
||||
|
||||
// Content dashboard
|
||||
getData = 'getData',
|
||||
createContent = 'createContent',
|
||||
@@ -22,9 +27,10 @@ export enum DashboardMessage {
|
||||
uploadMedia = 'uploadMedia',
|
||||
deleteMedia = 'deleteMedia',
|
||||
revealMedia = 'revealMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
insertMedia = 'insertMedia',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
insertFile = 'insertFile',
|
||||
|
||||
// Data dashboard
|
||||
getDataEntries = 'getDataEntries',
|
||||
@@ -35,11 +41,19 @@ export enum DashboardMessage {
|
||||
addSnippet = 'addSnippet',
|
||||
updateSnippet = 'updateSnippet',
|
||||
|
||||
// Taxonomy dashboard
|
||||
getTaxonomyData = 'getTaxonomyData',
|
||||
editTaxonomy = "editTaxonomy",
|
||||
mergeTaxonomy = "mergeTaxonomy",
|
||||
deleteTaxonomy = "deleteTaxonomy",
|
||||
addToTaxonomy = "addToTaxonomy",
|
||||
createTaxonomy = "createTaxonomy",
|
||||
importTaxonomy = "importTaxonomy",
|
||||
moveTaxonomy = "moveTaxonomy",
|
||||
|
||||
// Other
|
||||
getTheme = 'getTheme',
|
||||
updateSetting = 'updateSetting',
|
||||
initializeProject = 'initializeProject',
|
||||
setFramework = 'setFramework',
|
||||
setState = 'setState',
|
||||
runCustomScript = 'runCustomScript',
|
||||
sendTelemetry = 'sendTelemetry',
|
||||
|
||||
90
src/dashboardWebView/components/App.tsx
Normal file
90
src/dashboardWebView/components/App.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import { Spinner } from './Spinner';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
import useDarkMode from '../../hooks/useDarkMode';
|
||||
import { WelcomeScreen } from './WelcomeScreen';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardViewSelector, ModeAtom } from '../state';
|
||||
import { Contents } from './Contents/Contents';
|
||||
import { Media } from './Media/Media';
|
||||
import { DataView } from './DataView';
|
||||
import { Snippets } from './SnippetsView/Snippets';
|
||||
import { FEATURE_FLAG } from '../../constants';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { TaxonomyView } from './TaxonomyView';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '..';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { UnknownView } from './UnknownView';
|
||||
|
||||
export interface IAppProps {
|
||||
showWelcome: boolean;
|
||||
}
|
||||
|
||||
export const App: React.FunctionComponent<IAppProps> = ({showWelcome}: React.PropsWithChildren<IAppProps>) => {
|
||||
const { loading, pages, settings } = useMessages();
|
||||
const view = useRecoilValue(DashboardViewSelector);
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
const navigate = useNavigate();
|
||||
useDarkMode();
|
||||
|
||||
const viewState: any = Messenger.getState() || {};
|
||||
|
||||
const isAllowed = (features: string[], flag: string) => {
|
||||
if (!features ||( features.length > 0 && !features.includes(flag))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const allowDataView = useMemo(() => {
|
||||
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.data.view)
|
||||
}, [mode?.features]);
|
||||
|
||||
const allowTaxonomyView = useMemo(() => {
|
||||
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.taxonomy.view)
|
||||
}, [mode?.features]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view && routePaths[view]) {
|
||||
navigate(routePaths[view]);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(routePaths[view]);
|
||||
}, [view]);
|
||||
|
||||
if (!settings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (showWelcome || viewState.isWelcomeConfiguring) {
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (!settings.initialized || settings.contentFolders?.length === 0) {
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Routes>
|
||||
<Route path={routePaths.welcome} element={<WelcomeScreen settings={settings} />} />
|
||||
<Route path={routePaths.contents} element={<Contents pages={pages} loading={loading} />} />
|
||||
<Route path={routePaths.media} element={<Media />} />
|
||||
<Route path={routePaths.snippets} element={<Snippets />} />
|
||||
|
||||
{
|
||||
allowDataView && <Route path={routePaths.data} element={<DataView />} />
|
||||
}
|
||||
|
||||
{
|
||||
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
|
||||
}
|
||||
|
||||
<Route path={`*`} element={<UnknownView />} />
|
||||
</Routes>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -27,36 +27,40 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
|
||||
{title}
|
||||
</button>
|
||||
|
||||
<Menu as="span" className="-ml-px relative block">
|
||||
<Menu.Button
|
||||
className="h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
|
||||
disabled={disabled}>
|
||||
<span className="sr-only">Open options</span>
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
{
|
||||
choices.length > 0 && (
|
||||
<Menu as="span" className="-ml-px relative block">
|
||||
<Menu.Button
|
||||
className="h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
|
||||
disabled={disabled}>
|
||||
<span className="sr-only">Open options</span>
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
|
||||
<MenuItems widthClass={`w-56`}>
|
||||
<div className="py-1">
|
||||
{choices.map((choice, idx) => (
|
||||
<MenuItem
|
||||
key={idx}
|
||||
title={(
|
||||
choice.icon ? (
|
||||
<div className="flex items-center">
|
||||
{choice.icon}
|
||||
<span>{choice.title}</span>
|
||||
</div>
|
||||
) : (
|
||||
choice.title
|
||||
)
|
||||
)}
|
||||
value={null}
|
||||
onClick={choice.onClick}
|
||||
disabled={choice.disabled} />
|
||||
))}
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
<MenuItems widthClass={`w-56`} disablePopper>
|
||||
<div className="py-1">
|
||||
{choices.map((choice, idx) => (
|
||||
<MenuItem
|
||||
key={idx}
|
||||
title={(
|
||||
choice.icon ? (
|
||||
<div className="flex items-center">
|
||||
{choice.icon}
|
||||
<span>{choice.title}</span>
|
||||
</div>
|
||||
) : (
|
||||
choice.title
|
||||
)
|
||||
)}
|
||||
value={null}
|
||||
onClick={choice.onClick}
|
||||
disabled={choice.disabled} />
|
||||
))}
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,32 @@
|
||||
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';
|
||||
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface IContentActionsProps {
|
||||
title: string;
|
||||
path: string;
|
||||
scripts: CustomScript[] | undefined;
|
||||
listView?: boolean;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen }: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen, listView }: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
const [ showDeletionAlert, setShowDeletionAlert ] = React.useState(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<any>(null);
|
||||
const [popperElement, setPopperElement] = useState<any>(null);
|
||||
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
|
||||
placement: listView ? 'right-start' : 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
})
|
||||
|
||||
const onView = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
onOpen();
|
||||
@@ -40,47 +50,55 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
}, [path]);
|
||||
|
||||
const customScriptActions = React.useMemo(() => {
|
||||
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
|
||||
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk && !script.hidden).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={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]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`group-scope absolute top-6 right-0 flex flex-col space-y-4`}>
|
||||
<div className="flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full p-2 -mt-4">
|
||||
<div className={`${listView ? '' : 'group-scope absolute top-6 right-0'} flex flex-col space-y-4`}>
|
||||
<div className={`flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full ${listView ? '' : 'p-2 -mt-4'}`}>
|
||||
|
||||
<Menu as="div" className="relative z-10 flex text-left">
|
||||
<div className='hidden group-scope-hover:flex'>
|
||||
<QuickAction
|
||||
title={`View content`}
|
||||
onClick={onView}>
|
||||
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={`Delete content`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
<Menu as="div" className={`relative flex text-left ${listView ? '' : 'z-10'}`}>
|
||||
{
|
||||
!listView && (
|
||||
<div className='hidden group-scope-hover:flex'>
|
||||
<QuickAction
|
||||
title={`View content`}
|
||||
onClick={onView}>
|
||||
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={`Delete content`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div ref={setReferenceElement} className={`flex`}>
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
</div>
|
||||
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
<div className='menu_items__wrapper z-20' ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<MenuItems updatePopper={forceUpdate || undefined} widthClass='w-44' marginTopClass={listView ? '' : ''}>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
|
||||
onClick={(value, e) => onView(e)} />
|
||||
|
||||
<MenuItems widthClass='w-40' marginTopClass='mt-6'>
|
||||
<MenuItem
|
||||
title={`View`}
|
||||
onClick={(value, e) => onView(e)} />
|
||||
{ customScriptActions }
|
||||
|
||||
{ customScriptActions }
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={(value, e) => onDelete(e)} />
|
||||
</MenuItems>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={(value, e) => onDelete(e)} />
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,16 @@ import { Status } from '../Status';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardViewType } from '../../models';
|
||||
import { ContentActions } from './ContentActions';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface IItemProps extends Page {}
|
||||
|
||||
const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const draftField = useMemo(() => settings?.draftField, [settings]);
|
||||
|
||||
const openFile = () => {
|
||||
Messenger.send(DashboardMessage.openFile, fmFilePath);
|
||||
@@ -28,16 +30,31 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
}
|
||||
|
||||
const tagField = settings.dashboardState.contents.tags;
|
||||
return pageData[tagField] || [];
|
||||
let tagsValue = [];
|
||||
|
||||
if (tagField === "tags") {
|
||||
tagsValue = pageData.fmTags;
|
||||
} else if (tagField === "categories") {
|
||||
tagsValue = pageData.fmCategories;
|
||||
} else {
|
||||
tagsValue = pageData[tagField] || [];
|
||||
}
|
||||
|
||||
if (typeof tagsValue === "string") {
|
||||
return [tagsValue];
|
||||
} else if (Array.isArray(tagsValue)) {
|
||||
return tagsValue;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [settings, pageData]);
|
||||
|
||||
if (view === DashboardViewType.Grid) {
|
||||
return (
|
||||
<li className="relative">
|
||||
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50`}
|
||||
onClick={openFile}>
|
||||
|
||||
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
|
||||
<div className={`group flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50`}>
|
||||
|
||||
<button onClick={openFile} className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200 cursor-pointer">
|
||||
{
|
||||
pageData[PREVIEW_IMAGE_FIELD] ? (
|
||||
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
@@ -47,11 +64,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="relative p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
<Status draft={draft} />
|
||||
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
|
||||
|
||||
<DateField className={`mr-4`} value={date} />
|
||||
|
||||
@@ -62,46 +79,59 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
onOpen={openFile} />
|
||||
</div>
|
||||
|
||||
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
|
||||
<button onClick={openFile} className={`text-left`}><h2 className="mt-2 mb-2 font-bold">{title}</h2></button>
|
||||
|
||||
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p>
|
||||
<button onClick={openFile} className={`text-left`}><p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p></button>
|
||||
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div className="mt-2">
|
||||
{
|
||||
tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block mr-1 mt-1 text-[#5D561D] dark:text-[#F0ECD0] text-xs">
|
||||
#{tag}
|
||||
</span>
|
||||
tag && (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block mr-1 mt-1 text-[#5D561D] dark:text-[#F0ECD0] text-xs">
|
||||
#{tag}
|
||||
</span>
|
||||
)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
} else if (view === DashboardViewType.List) {
|
||||
return (
|
||||
<li className="relative">
|
||||
<button className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`} onClick={openFile}>
|
||||
<div className="col-span-8 font-bold truncate">
|
||||
{title}
|
||||
<div className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`}>
|
||||
<div className="col-span-8 font-bold truncate flex items-center space-x-4">
|
||||
<button
|
||||
title={`Open: ${title}`}
|
||||
onClick={openFile}>
|
||||
{title}
|
||||
</button>
|
||||
|
||||
<ContentActions
|
||||
title={title}
|
||||
path={fmFilePath}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
listView />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Status draft={draft} />
|
||||
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import {ChevronRightIcon} from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { groupBy } from '../../../helpers/GroupBy';
|
||||
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { GroupOption } from '../../constants/GroupOption';
|
||||
import { Page } from '../../models/Page';
|
||||
import { Settings } from '../../models/Settings';
|
||||
import { GroupingSelector } from '../../state';
|
||||
import { GroupingSelector, PageAtom } from '../../state';
|
||||
import { PAGE_LIMIT } from '../Header/Pagination';
|
||||
import { Item } from './Item';
|
||||
import { List } from './List';
|
||||
|
||||
@@ -18,6 +20,23 @@ export interface IOverviewProps {
|
||||
|
||||
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settings}: React.PropsWithChildren<IOverviewProps>) => {
|
||||
const grouping = useRecoilValue(GroupingSelector);
|
||||
const page = useRecoilValue(PageAtom);
|
||||
|
||||
const pagedPages = useMemo(() => {
|
||||
if (settings?.dashboardState.contents.pagination) {
|
||||
return pages.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT));
|
||||
}
|
||||
|
||||
return pages;
|
||||
}, [pages, page, settings]);
|
||||
|
||||
const groupName = useCallback((groupId, groupedPages) => {
|
||||
if (grouping === GroupOption.Draft) {
|
||||
return `${groupId} (${groupedPages[groupId].length})`;
|
||||
}
|
||||
|
||||
return `${GroupOption[grouping]}: ${groupId} (${groupedPages[groupId].length})`;
|
||||
}, [grouping])
|
||||
|
||||
if (!pages || !pages.length) {
|
||||
return (
|
||||
@@ -25,7 +44,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>
|
||||
) : (
|
||||
<>
|
||||
@@ -58,7 +77,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settin
|
||||
<ChevronRightIcon
|
||||
className={`w-8 h-8 mr-1 ${open ? "transform rotate-90" : ""}`}
|
||||
/>
|
||||
{GroupOption[grouping]}: {groupId} ({groupedPages[groupId].length})
|
||||
{ groupName(groupId, groupedPages) }
|
||||
</h2>
|
||||
</Disclosure.Button>
|
||||
|
||||
@@ -80,7 +99,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settin
|
||||
|
||||
return (
|
||||
<List>
|
||||
{pages.map((page, idx) => (
|
||||
{pagedPages.map((page, idx) => (
|
||||
<Item key={`${page.slug}-${idx}`} {...page} />
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Spinner } from './Spinner';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
import useDarkMode from '../../hooks/useDarkMode';
|
||||
import { WelcomeScreen } from './WelcomeScreen';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardViewSelector, ModeAtom } from '../state';
|
||||
import { Contents } from './Contents/Contents';
|
||||
import { Media } from './Media/Media';
|
||||
import { NavigationType } from '../models';
|
||||
import { DataView } from './DataView';
|
||||
import { Snippets } from './SnippetsView/Snippets';
|
||||
import { FeatureFlag } from '../../components/features/FeatureFlag';
|
||||
import { FEATURE_FLAG } from '../../constants';
|
||||
|
||||
export interface IDashboardProps {
|
||||
showWelcome: boolean;
|
||||
}
|
||||
|
||||
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome}: React.PropsWithChildren<IDashboardProps>) => {
|
||||
const { loading, pages, settings } = useMessages();
|
||||
const view = useRecoilValue(DashboardViewSelector);
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
useDarkMode();
|
||||
|
||||
if (!settings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (showWelcome) {
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (!settings.initialized || settings.folders?.length === 0) {
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (view === NavigationType.Snippets) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Snippets />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === NavigationType.Media) {
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Media />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === NavigationType.Data) {
|
||||
return (
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.data.view}>
|
||||
<main className={`h-full w-full`}>
|
||||
<DataView />
|
||||
</main>
|
||||
</FeatureFlag>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`h-full w-full`}>
|
||||
<Contents pages={pages} loading={loading} />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import { ToastContainer, toast, Slide } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { DataType } from '../../../models/DataType';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { NavigationItem } from '../Layout';
|
||||
|
||||
export interface IDataViewProps {}
|
||||
|
||||
@@ -149,15 +150,14 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
(dataFiles && dataFiles.length > 0) && (
|
||||
dataFiles.map((dataFile) => (
|
||||
<button
|
||||
key={dataFile.id}
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
dataFiles.map((dataFile, idx) => (
|
||||
<NavigationItem
|
||||
key={`${dataFile.id}-${idx}`}
|
||||
isSelected={selectedData?.id === dataFile.id}
|
||||
onClick={() => setSchema(dataFile)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>{dataFile.title}</span>
|
||||
</button>
|
||||
</NavigationItem>
|
||||
)
|
||||
))
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
selectedData ? (
|
||||
<>
|
||||
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData.title.toLowerCase()} data items</h2>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData?.title?.toLowerCase() || ""} data items</h2>
|
||||
|
||||
<div className='py-4'>
|
||||
{
|
||||
@@ -245,4 +245,4 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
<ToastContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,14 +38,18 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
|
||||
}
|
||||
}
|
||||
|
||||
let valid = false;
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const folder = contentFolders[i];
|
||||
const contentFolder = parseWinPath(folder.path) as string;
|
||||
const relContentPath = folderPath.replace(contentFolder, '');
|
||||
return relContentPath.length > 1 && folderPath.startsWith(contentFolder);
|
||||
|
||||
if (!valid) {
|
||||
valid = relContentPath.length > 1 && folderPath.startsWith(contentFolder);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return valid;
|
||||
};
|
||||
|
||||
if (!selectedFolder) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { FolderSelector, TagSelector, CategorySelector, SortingAtom, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
|
||||
|
||||
import { DefaultValue } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const guardRecoilDefaultValue = (
|
||||
candidate: any
|
||||
@@ -34,7 +36,7 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props:
|
||||
resetCategory();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
|
||||
setShow(true);
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const Filter: React.FunctionComponent<IFilterProps> = ({label, activeItem
|
||||
)}
|
||||
title={activeItem || DEFAULT_VALUE} />
|
||||
|
||||
<MenuItems>
|
||||
<MenuItems disablePopper>
|
||||
<MenuItem
|
||||
title={DEFAULT_VALUE}
|
||||
value={null}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Folders: React.FunctionComponent<IFoldersProps> = ({}: React.PropsW
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<MenuButton label={`Showing`} title={crntFolder || DEFAULT_TYPE} />
|
||||
|
||||
<MenuItems>
|
||||
<MenuItems disablePopper>
|
||||
<MenuItem
|
||||
title={DEFAULT_TYPE}
|
||||
value={null}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
|
||||
export interface IGroupingProps {}
|
||||
|
||||
export const groupOptions = [
|
||||
export const GROUP_OPTIONS = [
|
||||
{ name: "None", id: GroupOption.none },
|
||||
{ name: "Year", id: GroupOption.Year },
|
||||
{ name: "Draft/Published", id: GroupOption.Draft },
|
||||
@@ -16,15 +16,15 @@ export const groupOptions = [
|
||||
export const Grouping: React.FunctionComponent<IGroupingProps> = ({}: React.PropsWithChildren<IGroupingProps>) => {
|
||||
const [ group, setGroup ] = useRecoilState(GroupingAtom);
|
||||
|
||||
const crntGroup = groupOptions.find(x => x.id === group);
|
||||
const crntGroup = GROUP_OPTIONS.find(x => x.id === group);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<MenuButton label={`Group by`} title={crntGroup?.name || ""} />
|
||||
|
||||
<MenuItems>
|
||||
{groupOptions.map((option) => (
|
||||
<MenuItems disablePopper>
|
||||
{GROUP_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
title={option.name}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { Startup } from '../Startup';
|
||||
import { Navigation } from '../Navigation';
|
||||
import { Grouping } from '.';
|
||||
import { ViewSwitch } from './ViewSwitch';
|
||||
import { useRecoilState, useResetRecoilState } from 'recoil';
|
||||
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
|
||||
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { CategoryAtom, GroupingSelector, SortingAtom, TagAtom } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ClearFilters } from './ClearFilters';
|
||||
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
|
||||
@@ -19,6 +19,12 @@ import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
|
||||
import { Tabs } from './Tabs';
|
||||
import { CustomScript } from '../../../models';
|
||||
import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '../..';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { SyncButton } from './SyncButton';
|
||||
import { PAGE_LIMIT, Pagination } from './Pagination';
|
||||
import { GroupOption } from '../../constants/GroupOption';
|
||||
|
||||
export interface IHeaderProps {
|
||||
header?: React.ReactNode;
|
||||
@@ -34,8 +40,10 @@ export interface IHeaderProps {
|
||||
export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPages, folders, settings }: React.PropsWithChildren<IHeaderProps>) => {
|
||||
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
|
||||
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
|
||||
const [ view, setView ] = useRecoilState(DashboardViewAtom);
|
||||
const resetSorting = useResetRecoilState(SortingAtom)
|
||||
const grouping = useRecoilValue(GroupingSelector);
|
||||
const resetSorting = useResetRecoilState(SortingAtom);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createContent = () => {
|
||||
Messenger.send(DashboardMessage.createContent);
|
||||
@@ -50,7 +58,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
|
||||
};
|
||||
|
||||
const updateView = (view: NavigationType) => {
|
||||
setView(view);
|
||||
navigate(routePaths[view]);
|
||||
resetSorting();
|
||||
}
|
||||
|
||||
@@ -68,6 +76,59 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
|
||||
onClick: () => runBulkScript(s)
|
||||
}));
|
||||
|
||||
const choiceOptions = useMemo(() => {
|
||||
const isEnabled = settings?.dashboardState?.contents?.templatesEnabled || false;
|
||||
|
||||
if (isEnabled) {
|
||||
return [
|
||||
{
|
||||
title: (
|
||||
<div className='flex items-center'>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<span>Create by content type</span>
|
||||
</div>
|
||||
),
|
||||
onClick: createByContentType,
|
||||
disabled: !settings?.initialized
|
||||
}, {
|
||||
title: (
|
||||
<div className='flex items-center'>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<span>Create by template</span>
|
||||
</div>
|
||||
),
|
||||
onClick: createByTemplate,
|
||||
disabled: !settings?.initialized
|
||||
},
|
||||
...customActions
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
}, [settings?.dashboardState?.contents?.templatesEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.search) {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const taxonomy = searchParams.get("taxonomy");
|
||||
const value = searchParams.get("value");
|
||||
|
||||
if (taxonomy && value) {
|
||||
if (taxonomy === "tags") {
|
||||
setCrntTag(value);
|
||||
} else if (taxonomy === "categories") {
|
||||
setCrntCategory(value);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setCrntTag("");
|
||||
setCrntCategory("");
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
|
||||
|
||||
@@ -76,39 +137,20 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
|
||||
</div>
|
||||
|
||||
{
|
||||
view === NavigationType.Contents && (
|
||||
location.pathname === routePaths.contents && (
|
||||
<>
|
||||
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
|
||||
<Searchbox />
|
||||
|
||||
<div className={`flex items-center justify-end space-x-4 flex-1`}>
|
||||
<Startup settings={settings} />
|
||||
|
||||
<SyncButton />
|
||||
|
||||
<ChoiceButton
|
||||
title={`Create content`}
|
||||
choices={[
|
||||
{
|
||||
title: (
|
||||
<div className='flex items-center'>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<span>Create by content type</span>
|
||||
</div>
|
||||
),
|
||||
onClick: createByContentType,
|
||||
disabled: !settings?.initialized
|
||||
}, {
|
||||
title: (
|
||||
<div className='flex items-center'>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<span>Create by template</span>
|
||||
</div>
|
||||
),
|
||||
onClick: createByTemplate,
|
||||
disabled: !settings?.initialized
|
||||
},
|
||||
...customActions
|
||||
]}
|
||||
onClick={createContent}
|
||||
choices={choiceOptions}
|
||||
onClick={createContent}
|
||||
disabled={!settings?.initialized} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,12 +178,20 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
|
||||
|
||||
<Sorting view={NavigationType.Contents} />
|
||||
</div>
|
||||
|
||||
{
|
||||
(settings?.dashboardState.contents.pagination) && (totalPages || 0) > PAGE_LIMIT && (!grouping || grouping === GroupOption.none) && (
|
||||
<div className={`flex justify-center py-2 border-b border-gray-300 dark:border-vulcan-100`}>
|
||||
<Pagination totalPages={totalPages || 0} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
view === NavigationType.Media && (
|
||||
location.pathname === routePaths.media && (
|
||||
<>
|
||||
<MediaHeaderTop />
|
||||
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { LIMIT } from '../../hooks/useMedia';
|
||||
import { routePaths } from '../..';
|
||||
import { MediaTotalSelector, PageAtom } from '../../state';
|
||||
import { PaginationButton } from './PaginationButton';
|
||||
|
||||
export interface IPaginationProps {}
|
||||
export interface IPaginationProps {
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
export const Pagination: React.FunctionComponent<IPaginationProps> = (props: React.PropsWithChildren<IPaginationProps>) => {
|
||||
export const PAGE_LIMIT = 16;
|
||||
|
||||
export const Pagination: React.FunctionComponent<IPaginationProps> = ({ totalPages }: React.PropsWithChildren<IPaginationProps>) => {
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const location = useLocation();
|
||||
|
||||
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
|
||||
const totalItems: number = useMemo(() => {
|
||||
if (location.pathname === routePaths.contents) {
|
||||
return Math.ceil((totalPages || 0) / PAGE_LIMIT) - 1
|
||||
} else {
|
||||
return Math.ceil(totalMedia / PAGE_LIMIT) - 1;
|
||||
}
|
||||
}, [location.pathname, totalPages, totalMedia]);
|
||||
|
||||
const getButtons = (): number[] => {
|
||||
const maxButtons = 5;
|
||||
@@ -19,12 +32,16 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = (props: Rea
|
||||
const end = page + maxButtons;
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i >= 0 && i <= totalPages) {
|
||||
if (i >= 0 && i <= totalItems) {
|
||||
buttons.push(i);
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
|
||||
@@ -54,19 +71,19 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = (props: Rea
|
||||
setPage(button)
|
||||
}
|
||||
}
|
||||
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'} max-h-8`}
|
||||
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'} max-h-8 rounded-sm`}
|
||||
>{button + 1}</button>
|
||||
))}
|
||||
|
||||
<PaginationButton
|
||||
title="Next"
|
||||
disabled={page >= totalPages}
|
||||
disabled={page >= totalItems}
|
||||
onClick={() => setPage(page + 1)} />
|
||||
|
||||
<PaginationButton
|
||||
title="Last"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(totalPages)} />
|
||||
disabled={page >= totalItems}
|
||||
onClick={() => setPage(totalItems)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MediaTotalSelector, PageAtom } from '../../state';
|
||||
import { LIMIT } from '../../hooks/useMedia';
|
||||
import { PAGE_LIMIT } from './Pagination';
|
||||
|
||||
export interface IPaginationStatusProps {}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
|
||||
const page = useRecoilValue(PageAtom);
|
||||
|
||||
const getTotalPage = () => {
|
||||
const mediaItems = ((page + 1) * LIMIT);
|
||||
const mediaItems = ((page + 1) * PAGE_LIMIT);
|
||||
if (totalMedia < mediaItems) {
|
||||
return totalMedia;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
|
||||
return (
|
||||
<div className="hidden sm:flex">
|
||||
<p className="text-sm text-gray-500 dark:text-whisper-900">
|
||||
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
|
||||
Showing <span className="font-medium">{(page * PAGE_LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
|
||||
<span className="font-medium">{totalMedia}</span> results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -18,14 +18,17 @@ export interface ISortingProps {
|
||||
}
|
||||
|
||||
export const sortOptions: SortingOption[] = [
|
||||
{ name: "Published (asc)", id: SortOption.PublishedAsc, order: SortOrder.asc, type: SortType.date },
|
||||
{ name: "Published (desc)", id: SortOption.PublishedDesc, order: SortOrder.desc, type: SortType.date },
|
||||
{ name: "Last modified (asc)", id: SortOption.LastModifiedAsc, order: SortOrder.asc, type: SortType.date },
|
||||
{ name: "Last modified (desc)", id: SortOption.LastModifiedDesc, order: SortOrder.desc, type: SortType.date },
|
||||
{ name: "By filename (asc)", id: SortOption.FileNameAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
const contentSortOptions: SortingOption[] = [
|
||||
{ name: "Published (asc)", id: SortOption.PublishedAsc, order: SortOrder.asc, type: SortType.date },
|
||||
{ name: "Published (desc)", id: SortOption.PublishedDesc, order: SortOrder.desc, type: SortType.date }
|
||||
];
|
||||
|
||||
const mediaSortOptions: SortingOption[] = [
|
||||
{ name: "Size (asc)", id: SortOption.SizeAsc, order: SortOrder.asc, type: SortType.number },
|
||||
{ name: "Size (desc)", id: SortOption.SizeDesc, order: SortOrder.desc, type: SortType.number },
|
||||
@@ -53,6 +56,8 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
|
||||
if (view === NavigationType.Media) {
|
||||
allOptions = [...allOptions, ...mediaSortOptions];
|
||||
} else if (view === NavigationType.Contents) {
|
||||
allOptions = [...contentSortOptions, ...allOptions];
|
||||
}
|
||||
|
||||
allOptions = allOptions.sort(SortingHelpers.alphabetically("name"))
|
||||
@@ -91,7 +96,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
|
||||
|
||||
<MenuItems widthClass="w-48">
|
||||
<MenuItems widthClass="w-48" disablePopper>
|
||||
{allOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
|
||||
55
src/dashboardWebView/components/Header/SyncButton.tsx
Normal file
55
src/dashboardWebView/components/Header/SyncButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { GeneralCommands } from '../../../constants';
|
||||
import { SettingsSelector } from '../../state';
|
||||
|
||||
export interface ISyncButtonProps {}
|
||||
|
||||
export const SyncButton: React.FunctionComponent<ISyncButtonProps> = (props: React.PropsWithChildren<ISyncButtonProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const [ isSyncing, setIsSyncing ] = useState(false);
|
||||
|
||||
const pull = () => {
|
||||
Messenger.send(GeneralCommands.toVSCode.gitSync);
|
||||
};
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
||||
const { command, data } = message.data;
|
||||
|
||||
if (command === GeneralCommands.toWebview.gitSyncingStart) {
|
||||
setIsSyncing(true);
|
||||
} else if (command === GeneralCommands.toWebview.gitSyncingEnd) {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!settings?.git?.actions || !settings?.git.isGitRepo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='git_actions'>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
|
||||
onClick={pull}
|
||||
disabled={isSyncing}
|
||||
>
|
||||
<RefreshIcon className={`w-4 h-4 mr-2 ${isSyncing ? 'animate-reverse-spin' : ''}`} aria-hidden="true" />
|
||||
<span>Sync</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { NavigationType } from '../../models';
|
||||
import { DashboardViewAtom } from '../../state';
|
||||
|
||||
export interface ITabProps {
|
||||
navigationType: NavigationType;
|
||||
@@ -9,11 +8,11 @@ export interface ITabProps {
|
||||
}
|
||||
|
||||
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
|
||||
const view = useRecoilValue(DashboardViewAtom);
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === navigationType ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
|
||||
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${location.pathname === `/${navigationType}` ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="profile"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseIcon, PhotographIcon, ScissorsIcon } from '@heroicons/react/outline';
|
||||
import { DatabaseIcon, PhotographIcon, ScissorsIcon, TagIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlag } from '../../../components/features/FeatureFlag';
|
||||
@@ -49,6 +49,15 @@ export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.
|
||||
</Tab>
|
||||
</li>
|
||||
</FeatureFlag>
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.taxonomy.view}>
|
||||
<li className="mr-2" role="presentation">
|
||||
<Tab
|
||||
navigationType={NavigationType.Taxonomy}
|
||||
onNavigate={onNavigate}>
|
||||
<TagIcon className={`h-6 w-auto mr-2`} /><span>Taxonomies</span>
|
||||
</Tab>
|
||||
</li>
|
||||
</FeatureFlag>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
28
src/dashboardWebView/components/Layout/NavigationBar.tsx
Normal file
28
src/dashboardWebView/components/Layout/NavigationBar.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface INavigationBarProps {
|
||||
title?: string;
|
||||
bottom?: JSX.Element;
|
||||
}
|
||||
|
||||
export const NavigationBar: React.FunctionComponent<INavigationBarProps> = ({title, bottom, children}: React.PropsWithChildren<INavigationBarProps>) => {
|
||||
return (
|
||||
<aside className={`w-2/12 px-4 py-6 h-full flex flex-col flex-grow border-r border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
title && <h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>{title}</h2>
|
||||
}
|
||||
|
||||
<nav className={`flex-1 py-4 -mx-4 h-full`}>
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{
|
||||
bottom && bottom
|
||||
}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
17
src/dashboardWebView/components/Layout/NavigationItem.tsx
Normal file
17
src/dashboardWebView/components/Layout/NavigationItem.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface INavigationItemProps {
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const NavigationItem: React.FunctionComponent<INavigationItemProps> = ({isSelected, onClick, children}: React.PropsWithChildren<INavigationItemProps>) => {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 cursor-pointer ${isSelected ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -5,11 +5,12 @@ import { Header } from '../Header';
|
||||
|
||||
export interface IPageLayoutProps {
|
||||
header?: React.ReactNode;
|
||||
folders?: string[] | undefined
|
||||
totalPages?: number | undefined
|
||||
folders?: string[] | undefined;
|
||||
totalPages?: number | undefined;
|
||||
contentClass?: string;
|
||||
}
|
||||
|
||||
export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header, folders, totalPages, children }: React.PropsWithChildren<IPageLayoutProps>) => {
|
||||
export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header, folders, totalPages, contentClass, children }: React.PropsWithChildren<IPageLayoutProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
return (
|
||||
@@ -20,7 +21,7 @@ export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header,
|
||||
totalPages={totalPages}
|
||||
settings={settings} />
|
||||
|
||||
<div className="w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4">
|
||||
<div className={contentClass || "w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4"}>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3
src/dashboardWebView/components/Layout/index.ts
Normal file
3
src/dashboardWebView/components/Layout/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './NavigationBar';
|
||||
export * from './NavigationItem';
|
||||
export * from './PageLayout';
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -23,7 +23,8 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
|
||||
Messenger.send(DashboardMessage.runCustomScript, {script, path: selectedFolder});
|
||||
};
|
||||
|
||||
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
|
||||
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder && !script.hidden);
|
||||
|
||||
if (scripts.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-1 justify-end">
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
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 { useCallback, useEffect, useMemo, useState } 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';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { MediaSnippetForm } from './MediaSnippetForm';
|
||||
|
||||
export interface IItemProps {
|
||||
media: MediaInfo;
|
||||
@@ -24,16 +27,36 @@ export interface IItemProps {
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWithChildren<IItemProps>) => {
|
||||
const [ , setLightbox ] = useRecoilState(LightboxAtom);
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
const [ showForm, setShowForm ] = React.useState(false);
|
||||
const [ showDetails, setShowDetails ] = React.useState(false);
|
||||
const [ caption, setCaption ] = React.useState(media.caption);
|
||||
const [ alt, setAlt ] = React.useState(media.alt);
|
||||
const [ filename, setFilename ] = React.useState<string | null>(null);
|
||||
const [ showAlert, setShowAlert ] = useState(false);
|
||||
const [ showForm, setShowForm ] = useState(false);
|
||||
const [ showSnippetSelection, setShowSnippetSelection ] = useState(false);
|
||||
const [ snippet, setSnippet ] = useState<Snippet | undefined>(undefined);
|
||||
const [ showDetails, setShowDetails ] = useState(false);
|
||||
const [ showSnippetFormDialog, setShowSnippetFormDialog ] = useState(false);
|
||||
const [ mediaData, setMediaData ] = useState<any | undefined>(undefined);
|
||||
const [ caption, setCaption ] = useState(media.caption);
|
||||
const [ alt, setAlt ] = useState(media.alt);
|
||||
const [ filename, setFilename ] = useState<string | null>(null);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<any>(null);
|
||||
const [popperElement, setPopperElement] = useState<any>(null);
|
||||
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
})
|
||||
|
||||
const mediaSnippets = useMemo(() => {
|
||||
if (!settings?.snippets) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keys = Object.keys(settings.snippets);
|
||||
return keys.filter(key => (settings.snippets || {})[key].isMediaSnippet).map(key => ({ title: key, ...(settings.snippets || {})[key]}));
|
||||
}, [settings]);
|
||||
|
||||
const getFolder = () => {
|
||||
if (settings?.wsFolder && media.fsPath) {
|
||||
let relPath = media.fsPath.split(settings.wsFolder).pop();
|
||||
@@ -50,10 +73,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const getRelPath = () => {
|
||||
let relPath: string | undefined = "";
|
||||
if (settings?.wsFolder && media.fsPath) {
|
||||
relPath = media.fsPath.split(settings.wsFolder).pop();
|
||||
const wsFolderParsed = parseWinPath(settings.wsFolder);
|
||||
const mediaParsed = parseWinPath(media.fsPath);
|
||||
|
||||
relPath = mediaParsed.split(wsFolderParsed).pop();
|
||||
|
||||
if (settings.staticFolder && relPath) {
|
||||
relPath = relPath.split(settings.staticFolder).pop();
|
||||
const staticFolderParsed = parseWinPath(settings.staticFolder);
|
||||
relPath = relPath.split(staticFolderParsed).pop();
|
||||
}
|
||||
}
|
||||
return relPath;
|
||||
@@ -74,39 +101,90 @@ 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]);
|
||||
|
||||
/**
|
||||
* Process the snippet
|
||||
*/
|
||||
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) || "").replace(/ /g, "%20"),
|
||||
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) || "",
|
||||
if (snippet.fields.length === 0) {
|
||||
setShowSnippetFormDialog(false);
|
||||
setMediaData(undefined);
|
||||
|
||||
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
|
||||
insertMediaSnippetToArticle(output);
|
||||
} else {
|
||||
setSnippet(snippet);
|
||||
setShowSnippetFormDialog(true);
|
||||
setMediaData(fieldData);
|
||||
}
|
||||
}, [alt, caption, media, settings, viewData, mediaSnippets]);
|
||||
|
||||
/**
|
||||
* Insert the media snippet
|
||||
*/
|
||||
const insertMediaSnippetToArticle = useCallback((output: string) => {
|
||||
const relPath = getRelPath();
|
||||
|
||||
Messenger.send(DashboardMessage.insertMedia, {
|
||||
relPath: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
position: viewData?.data?.position || null,
|
||||
snippet
|
||||
snippet: output
|
||||
});
|
||||
};
|
||||
}, [viewData]);
|
||||
|
||||
const deleteMedia = () => {
|
||||
setShowAlert(true);
|
||||
@@ -168,7 +246,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
};
|
||||
|
||||
const openLightbox = useCallback(() => {
|
||||
if (!isVideoFile() && !isAudioFile()) {
|
||||
if (isImageFile) {
|
||||
setLightbox(media.vsPath || "");
|
||||
}
|
||||
}, [media.vsPath]);
|
||||
@@ -179,63 +257,79 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
};
|
||||
|
||||
const customScriptActions = () => {
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile && !script.hidden).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={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(() => {
|
||||
if (media.mimeType?.startsWith("image/")) {
|
||||
const isImageFile = useMemo(() => {
|
||||
if (media.mimeType?.startsWith("image/") && !media.mimeType?.startsWith("image/vnd.adobe.photoshop")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [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()) {
|
||||
let icon = <DocumentIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
|
||||
|
||||
if (isImageFile) {
|
||||
return <PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}
|
||||
|
||||
return <DocumentIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}, [media]);
|
||||
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" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [media]);
|
||||
|
||||
const clearFormData = () => {
|
||||
setShowSnippetFormDialog(false);
|
||||
setSnippet(undefined);
|
||||
setMediaData(undefined);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (media.alt !== alt) {
|
||||
setAlt(media.alt);
|
||||
@@ -255,10 +349,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}
|
||||
}, [media.fsPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewData?.data?.filePath) {
|
||||
clearFormData();
|
||||
}
|
||||
}, [viewData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50">
|
||||
<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 +397,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}>
|
||||
@@ -324,50 +424,63 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</QuickAction>
|
||||
</div>
|
||||
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
<div ref={setReferenceElement} className={`flex`}>
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
</div>
|
||||
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={`Edit metadata`}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
<div className='menu_items__wrapper z-20' ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={(
|
||||
<div className='flex items-center'>
|
||||
<PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span>
|
||||
</div>
|
||||
)}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={`Insert image markdown`}
|
||||
onClick={insertToArticle} />
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
|
||||
onClick={insertToArticle} />
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
<MenuItem
|
||||
title={`Insert snippet`}
|
||||
onClick={insertSnippet} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
|
||||
<MenuItem
|
||||
key={idx}
|
||||
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
|
||||
onClick={() => processSnippet(snippet)} />
|
||||
))
|
||||
}
|
||||
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
title={`Copy media path`}
|
||||
onClick={copyToClipboard} />
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
title={(
|
||||
<div className='flex items-center'>
|
||||
<ClipboardIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Copy media path</span>
|
||||
</div>
|
||||
)}
|
||||
onClick={copyToClipboard} />
|
||||
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
)
|
||||
}
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={`Reveal media`}
|
||||
onClick={revealMedia} />
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -375,6 +488,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 +523,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 +558,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); }} />
|
||||
@@ -428,6 +576,18 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
trigger={confirmDeletion} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(showSnippetFormDialog && snippet && mediaData) && (
|
||||
<MediaSnippetForm
|
||||
media={media}
|
||||
mediaData={mediaData}
|
||||
snippet={snippet}
|
||||
onDismiss={clearFormData}
|
||||
onInsert={insertMediaSnippetToArticle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import useMedia from '../../hooks/useMedia';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { join } from 'path';
|
||||
import { basename, extname, join } from 'path';
|
||||
|
||||
export interface IMediaProps {}
|
||||
|
||||
@@ -29,23 +29,47 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
const folders = useRecoilValue(MediaFoldersAtom);
|
||||
const loading = useRecoilValue(LoadingAtom);
|
||||
|
||||
|
||||
const allFolders = React.useMemo(() => {
|
||||
const contentFolders = React.useMemo(() => {
|
||||
// Check if content allows page bundle
|
||||
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
|
||||
return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
return [];
|
||||
}
|
||||
|
||||
return folders;
|
||||
let groupedFolders = [];
|
||||
|
||||
for (const cFolder of (settings?.contentFolders || [])) {
|
||||
const foldersPath = parseWinPath(cFolder.path);
|
||||
groupedFolders.push({
|
||||
title: cFolder.title || basename(cFolder.path),
|
||||
folders: folders.filter(f => parseWinPath(f).startsWith(foldersPath))
|
||||
});
|
||||
}
|
||||
|
||||
return groupedFolders;
|
||||
}, [folders, viewData, settings?.contentFolders]);
|
||||
|
||||
const publicFolders = React.useMemo(() => {
|
||||
return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
}, [folders, viewData, settings?.staticFolder]);
|
||||
|
||||
const allMedia = React.useMemo(() => {
|
||||
let mediaFiles = media;
|
||||
// Check if content allows page bundle
|
||||
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
|
||||
return media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
}
|
||||
|
||||
return media;
|
||||
if (viewData && viewData.data && viewData.data.type === "file" && viewData.data.fileExtensions && viewData.data.fileExtensions.length > 0) {
|
||||
const supportedExtensions = viewData.data.fileExtensions;
|
||||
mediaFiles = mediaFiles.filter(m => {
|
||||
const ext = extname(m.fsPath);
|
||||
// Remove the dot from the extension
|
||||
const extWithoutDot = ext.substring(1);
|
||||
return supportedExtensions.includes(extWithoutDot);
|
||||
});
|
||||
}
|
||||
|
||||
return mediaFiles;
|
||||
}, [media, viewData, settings?.staticFolder]);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
@@ -115,11 +139,33 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
}
|
||||
|
||||
{
|
||||
allFolders && allFolders.length > 0 && (
|
||||
contentFolders && contentFolders.length > 0 && contentFolders.map((group, idx) => (
|
||||
group.folders && group.folders.length > 0 && (
|
||||
<div key={`group-${idx}`} className={`mb-8`}>
|
||||
<h2 className='text-lg mb-8 first-letter:uppercase'>Content folder: <b>{group.title}</b></h2>
|
||||
|
||||
<List gap={0}>
|
||||
{
|
||||
group.folders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
{
|
||||
publicFolders && publicFolders.length > 0 && (
|
||||
<div className={`mb-8`}>
|
||||
{
|
||||
contentFolders && contentFolders.length > 0 && (<h2 className='text-lg mb-8'>Public folder{settings?.staticFolder && (<span>: <b>{settings?.staticFolder}</b></span>)}</h2>)
|
||||
}
|
||||
|
||||
<List gap={0}>
|
||||
{
|
||||
allFolders.map((folder) => (
|
||||
publicFolders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
|
||||
45
src/dashboardWebView/components/Media/MediaSnippetForm.tsx
Normal file
45
src/dashboardWebView/components/Media/MediaSnippetForm.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MediaInfo, Snippet } from '../../../models';
|
||||
import { ViewDataSelector } from '../../state';
|
||||
import { FormDialog } from '../Modals/FormDialog';
|
||||
import SnippetForm, { SnippetFormHandle } from '../SnippetsView/SnippetForm';
|
||||
|
||||
export interface IMediaSnippetFormProps {
|
||||
media: MediaInfo;
|
||||
snippet: Snippet;
|
||||
mediaData: any;
|
||||
onDismiss: () => void;
|
||||
onInsert: (output: string) => void;
|
||||
}
|
||||
|
||||
export const MediaSnippetForm: React.FunctionComponent<IMediaSnippetFormProps> = ({ media, snippet, mediaData, onDismiss, onInsert }: React.PropsWithChildren<IMediaSnippetFormProps>) => {
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
const formRef = useRef<SnippetFormHandle>(null);
|
||||
|
||||
const insertToArticle = () => {
|
||||
formRef.current?.onSave();
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
title={`Insert media: ${media.title || media.filename}`}
|
||||
description={`Insert the ${media.title || media.filename} media file into the current article`}
|
||||
isSaveDisabled={false}
|
||||
trigger={insertToArticle}
|
||||
dismiss={onDismiss}
|
||||
okBtnText='Insert'
|
||||
cancelBtnText='Cancel'>
|
||||
|
||||
<SnippetForm
|
||||
ref={formRef}
|
||||
snippet={snippet}
|
||||
mediaData={mediaData}
|
||||
selection={viewData?.data?.selection}
|
||||
onInsert={onInsert} />
|
||||
|
||||
</FormDialog>
|
||||
);
|
||||
};
|
||||
@@ -5,11 +5,15 @@ import * as React from 'react';
|
||||
export interface IActionMenuButtonProps {
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
ref?: (instance: Element | null) => void;
|
||||
}
|
||||
|
||||
export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IActionMenuButtonProps>) => {
|
||||
export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> = ({ title, disabled, ref }: React.PropsWithChildren<IActionMenuButtonProps>) => {
|
||||
return (
|
||||
<Menu.Button disabled={disabled} className={`group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600 ${disabled ? 'opacity-50' : ''}`}>
|
||||
<Menu.Button
|
||||
ref={ref || null}
|
||||
disabled={disabled}
|
||||
className={`group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600 ${disabled ? 'opacity-50' : ''}`}>
|
||||
<span className="sr-only">{title}</span>
|
||||
<DotsVerticalIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
|
||||
@@ -5,12 +5,15 @@ import { Fragment } from 'react';
|
||||
export interface IMenuItemsProps {
|
||||
widthClass?: string;
|
||||
marginTopClass?: string;
|
||||
updatePopper?: () => void;
|
||||
disablePopper?: boolean
|
||||
}
|
||||
|
||||
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, marginTopClass, children}: React.PropsWithChildren<IMenuItemsProps>) => {
|
||||
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, marginTopClass, children, updatePopper, disablePopper}: React.PropsWithChildren<IMenuItemsProps>) => {
|
||||
return (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
beforeEnter={() => updatePopper ? updatePopper() : null}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
@@ -18,7 +21,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-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"} ${ disablePopper ? "origin-top-right absolute right-0 z-20" : ""} rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
72
src/dashboardWebView/components/Modals/InfoDialog.tsx
Normal file
72
src/dashboardWebView/components/Modals/InfoDialog.tsx
Normal 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">
|
||||
​
|
||||
</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>
|
||||
);
|
||||
};
|
||||
69
src/dashboardWebView/components/Preview/Preview.tsx
Normal file
69
src/dashboardWebView/components/Preview/Preview.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
src/dashboardWebView/components/Preview/index.ts
Normal file
1
src/dashboardWebView/components/Preview/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Preview';
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
@@ -31,9 +31,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 +47,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
setSnippetTitle('');
|
||||
setSnippetDescription('');
|
||||
setSnippetOriginalBody('');
|
||||
setMediaSnippet(false);
|
||||
};
|
||||
|
||||
const onOpenEdit = useCallback(() => {
|
||||
@@ -51,6 +55,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 +73,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 +94,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 +112,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 +149,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 +184,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 +214,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)} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -13,13 +13,15 @@ import { SnippetInputField } from './SnippetInputField';
|
||||
export interface ISnippetFormProps {
|
||||
snippet: Snippet;
|
||||
selection: string | undefined;
|
||||
mediaData?: any;
|
||||
onInsert?: (mediaData: any) => void;
|
||||
}
|
||||
|
||||
export interface SnippetFormHandle {
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFormProps> = ({ snippet, selection }, ref) => {
|
||||
const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFormProps> = ({ snippet, selection, mediaData, onInsert }, ref) => {
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
const [ fields, setFields ] = useState<SnippetField[]>([]);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
@@ -38,6 +40,16 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
|
||||
return value;
|
||||
}, [selection]);
|
||||
|
||||
const insertValueFromMedia = useCallback((fieldName: string) => {
|
||||
if (!mediaData) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (mediaData[fieldName]) {
|
||||
return mediaData[fieldName];
|
||||
}
|
||||
}, [mediaData]);
|
||||
|
||||
const snippetBody = useMemo(() => {
|
||||
let body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`);
|
||||
|
||||
@@ -63,10 +75,14 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
|
||||
return;
|
||||
}
|
||||
|
||||
Messenger.send(DashboardMessage.insertSnippet, {
|
||||
file: viewData?.data?.filePath,
|
||||
snippet: snippetBody
|
||||
});
|
||||
if (!onInsert) {
|
||||
Messenger.send(DashboardMessage.insertSnippet, {
|
||||
file: viewData?.data?.filePath,
|
||||
snippet: snippetBody
|
||||
});
|
||||
} else {
|
||||
onInsert(snippetBody);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -79,21 +95,27 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
|
||||
const allFields: SnippetField[] = [];
|
||||
const snippetFields = snippet.fields || [];
|
||||
|
||||
for (const fieldName of placeholders) {
|
||||
const field = snippetFields.find(f => f.name === fieldName);
|
||||
|
||||
if (field) {
|
||||
// Loop over all fields to check if they are present in the snippet
|
||||
for (const field of snippetFields) {
|
||||
const idx = placeholders.findIndex(fieldName => fieldName === field.name);
|
||||
if (idx > -1) {
|
||||
allFields.push({
|
||||
...field,
|
||||
value: insertPlaceholderValues(field.default || "")
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over all placeholders to find the ones that are not present in the snippet fields
|
||||
for (const fieldName of placeholders) {
|
||||
const idx = snippetFields.findIndex(field => field.name === fieldName);
|
||||
if (idx === -1) {
|
||||
allFields.push({
|
||||
name: fieldName,
|
||||
title: fieldName,
|
||||
type: "string",
|
||||
single: true,
|
||||
value: ""
|
||||
value: insertValueFromMedia(fieldName)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -103,7 +125,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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
|
||||
const [ snippetBody, setSnippetBody ] = useState<string>('');
|
||||
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
|
||||
const [ mediaSnippet, setMediaSnippet ] = useState(false);
|
||||
|
||||
const snippets = settings?.snippets || {};
|
||||
const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]);
|
||||
@@ -41,11 +42,12 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
title: snippetTitle,
|
||||
description: snippetDescription || '',
|
||||
body: snippetBody,
|
||||
fields
|
||||
fields,
|
||||
isMediaSnippet: mediaSnippet
|
||||
});
|
||||
|
||||
reset();
|
||||
}, [snippetTitle, snippetDescription, snippetBody]);
|
||||
}, [snippetTitle, snippetDescription, snippetBody, mediaSnippet]);
|
||||
|
||||
const reset = () => {
|
||||
setShowCreateDialog(false);
|
||||
@@ -81,7 +83,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
</FeatureFlag>
|
||||
)}>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col h-full">
|
||||
{
|
||||
viewData?.data?.filePath && (
|
||||
<div className={`text-xl text-center mb-6`}>
|
||||
@@ -127,6 +129,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
title={snippetTitle}
|
||||
description={snippetDescription}
|
||||
body={snippetBody}
|
||||
isMediaSnippet={mediaSnippet}
|
||||
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
|
||||
onTitleUpdate={(value: string) => setSnippetTitle(value)}
|
||||
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
|
||||
onBodyUpdate={(value: string) => setSnippetBody(value)} />
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ISponsorMsgProps {
|
||||
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isBacker, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
|
||||
return (
|
||||
<p className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
|
||||
<footer className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
|
||||
{
|
||||
isBacker ? (
|
||||
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
|
||||
@@ -28,6 +28,6 @@ export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isB
|
||||
</>
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,35 +4,74 @@ import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Settings } from '../../models/Settings';
|
||||
import { Status } from '../../models/Status';
|
||||
import { Step } from './Step';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { Framework } from '../../../models';
|
||||
import {ChevronDownIcon} from '@heroicons/react/outline';
|
||||
import { ContentFolder, Framework } from '../../../models';
|
||||
import {CheckCircleIcon, ChevronDownIcon} from '@heroicons/react/outline';
|
||||
import {CheckCircleIcon as CheckCircleIconSolid} from '@heroicons/react/solid';
|
||||
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const Folder = ({ wsFolder, folder, folders, addFolder }: { wsFolder: string, folder: string, folders: ContentFolder[], addFolder: (folder: string) => void}) => {
|
||||
|
||||
const isAdded = useMemo(() => folders.find(f => f.path.toLowerCase() === join(wsFolder, folder).toLowerCase()), [folder, folders, wsFolder]);
|
||||
|
||||
return (
|
||||
<div className={`text-sm flex items-center ${isAdded ? "text-teal-800" : "text-vulcan-300 dark:text-whisper-800" }`}>
|
||||
<button onClick={() => addFolder(folder)} className='mr-2 hover:text-teal-500' title={`Add as a content folder to Front Matter`}>
|
||||
{ isAdded ? <CheckCircleIconSolid className={`h-4 w-4`} /> : <CheckCircleIcon className={`h-4 w-4`} /> }
|
||||
</button>
|
||||
<span>{folder}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
const [framework, setFramework] = useState<string | null>(null);
|
||||
const [taxImported, setTaxImported] = useState<boolean>(false);
|
||||
|
||||
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
|
||||
const setFrameworkAndSendMessage = (framework: string) => {
|
||||
setFramework(framework);
|
||||
Messenger.send(DashboardMessage.setFramework, framework);
|
||||
};
|
||||
|
||||
const addFolder = (folder: string) => {
|
||||
Messenger.send(DashboardMessage.addFolder, folder);
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
const crntState: any = Messenger.getState() || {};
|
||||
|
||||
Messenger.setState({
|
||||
...crntState,
|
||||
isWelcomeConfiguring: false
|
||||
});
|
||||
|
||||
Messenger.send(DashboardMessage.reload);
|
||||
};
|
||||
|
||||
const importTaxonomy = () => {
|
||||
Messenger.send(DashboardMessage.importTaxonomy);
|
||||
setTaxImported(true);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: `welcome-init`,
|
||||
name: 'Initialize project',
|
||||
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
|
||||
status: settings.initialized ? Status.Completed : Status.NotStarted,
|
||||
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
|
||||
},
|
||||
{
|
||||
id: `welcome-framework`,
|
||||
name: 'Framework presets',
|
||||
description: (
|
||||
<div>
|
||||
@@ -76,15 +115,50 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
onClick: undefined
|
||||
},
|
||||
{
|
||||
name: 'Register content folders (manual action)',
|
||||
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
|
||||
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
id: `welcome-content-folders`,
|
||||
name: 'Register content folder(s)',
|
||||
description: (
|
||||
<>
|
||||
<p>Add one of the folders we found in your project as a content folder. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</p>
|
||||
|
||||
{
|
||||
settings?.dashboardState?.welcome?.contentFolders?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="text-sm">
|
||||
Folders containing content:
|
||||
</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{settings?.dashboardState?.welcome?.contentFolders?.map((folder) => (
|
||||
<Folder
|
||||
key={folder}
|
||||
folder={folder}
|
||||
addFolder={addFolder}
|
||||
wsFolder={settings.wsFolder}
|
||||
folders={settings.contentFolders} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<p className='mt-4 text-vulcan-300 dark:text-gray-400'><b>IMPORTANT</b>: You can perform this action by <b>right-clicking on the folder in the explorer view</b>, and selecting <b>register folder</b>.</p>
|
||||
</>
|
||||
),
|
||||
status: settings.contentFolders && settings.contentFolders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
id: `welcome-import`,
|
||||
name: 'Import all tags and categories (optional)',
|
||||
description: <>Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?</>,
|
||||
status: taxImported ? Status.Completed : Status.NotStarted,
|
||||
onClick: settings.contentFolders && settings.contentFolders.length > 0 ? importTaxonomy : undefined
|
||||
},
|
||||
{
|
||||
id: `welcome-show-dashboard`,
|
||||
name: 'Show the dashboard',
|
||||
description: <>Once both actions are completed, click on this action to load the dashboard.</>,
|
||||
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
|
||||
description: <>Once all actions are completed, the dashboard can be loaded.</>,
|
||||
status: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? reload : undefined
|
||||
}
|
||||
];
|
||||
|
||||
@@ -98,7 +172,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
<nav aria-label="Progress">
|
||||
<ol role="list">
|
||||
{steps.map((step, stepIdx) => (
|
||||
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
|
||||
<li key={step.id} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`} data-test={step.id}>
|
||||
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />
|
||||
</li>
|
||||
))}
|
||||
|
||||
103
src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx
Normal file
103
src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ArrowCircleUpIcon, ArrowUpIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { MergeIcon } from '../../../components/icons/MergeIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
|
||||
export interface ITaxonomyActionsProps {
|
||||
field: string | null;
|
||||
value: string;
|
||||
unmapped?: boolean;
|
||||
}
|
||||
|
||||
export const TaxonomyActions: React.FunctionComponent<ITaxonomyActionsProps> = ({field, value, unmapped}: React.PropsWithChildren<ITaxonomyActionsProps>) => {
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.editTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onAdd = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.addToTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onMerge = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.mergeTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onMove = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.moveTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.deleteTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
return (
|
||||
<div className={`space-x-2`}>
|
||||
{
|
||||
unmapped && (
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Add ${value} to taxonomy settings`}
|
||||
onClick={onAdd}>
|
||||
<PlusIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Add to settings</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Edit ${value}`}
|
||||
onClick={onEdit}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Edit</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Merge ${value}`}
|
||||
onClick={onMerge}>
|
||||
<MergeIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Merge</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Move to another taxonomy type`}
|
||||
onClick={onMove}>
|
||||
<ArrowCircleUpIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Move to another taxonomy type</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Delete ${value}`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Page } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '../..';
|
||||
|
||||
export interface ITaxonomyLookupProps {
|
||||
taxonomy: string | null;
|
||||
value: string;
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const TaxonomyLookup: React.FunctionComponent<ITaxonomyLookupProps> = ({ taxonomy, value, pages }: React.PropsWithChildren<ITaxonomyLookupProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const total: number | undefined = useMemo(() => {
|
||||
if (!taxonomy || !value || !pages || !settings?.contentTypes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return pages.filter(page => {
|
||||
if (taxonomy === "tags") {
|
||||
return (page.fmTags || []).includes(value);
|
||||
} else if (taxonomy === "categories") {
|
||||
return (page.fmCategories || []).includes(value);
|
||||
}
|
||||
|
||||
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
|
||||
|
||||
if (!contentType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let fieldName = getTaxonomyField(taxonomy, contentType);
|
||||
|
||||
return fieldName && page[fieldName] ? page[fieldName].includes(value) : false;
|
||||
}).length;
|
||||
}, [taxonomy, value, pages, settings?.contentTypes]);
|
||||
|
||||
const onNavigate = useCallback(() => {
|
||||
if (total) {
|
||||
navigate(`${routePaths.contents}?taxonomy=${taxonomy}&value=${value}`);
|
||||
}
|
||||
}, [total, navigate]);
|
||||
|
||||
if (taxonomy === "tags" || taxonomy === "categories") {
|
||||
return (
|
||||
<button
|
||||
className={total ? `text-teal-900 hover:text-teal-600 font-bold` : ``}
|
||||
title={total ? `Show contents with ${value} in ${taxonomy}` : ``}
|
||||
onClick={onNavigate}>
|
||||
{total || `-`}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{total || `-`}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
184
src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx
Normal file
184
src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ExclamationIcon, PlusSmIcon, TagIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TaxonomyData } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Page } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
|
||||
import { TaxonomyActions } from './TaxonomyActions';
|
||||
import { TaxonomyLookup } from './TaxonomyLookup';
|
||||
|
||||
export interface ITaxonomyManagerProps {
|
||||
data: TaxonomyData | undefined;
|
||||
taxonomy: string | null;
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = ({ data, taxonomy, pages }: React.PropsWithChildren<ITaxonomyManagerProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const onCreate = () => {
|
||||
Messenger.send(DashboardMessage.createTaxonomy, {
|
||||
type: taxonomy
|
||||
});
|
||||
};
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (data && taxonomy) {
|
||||
let crntItems: string[] = [];
|
||||
|
||||
if (taxonomy === "tags" || taxonomy === "categories") {
|
||||
crntItems = data[taxonomy];
|
||||
} else {
|
||||
crntItems = data.customTaxonomy.find(c => c.id === taxonomy)?.options || [];
|
||||
}
|
||||
|
||||
// Alphabetically sort the items
|
||||
crntItems = Object.assign([], crntItems).sort((a: string, b: string) => {
|
||||
a = a || "";
|
||||
b = b || "";
|
||||
|
||||
if (a.toLowerCase() < b.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.toLowerCase() > b.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return crntItems;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [data, taxonomy]);
|
||||
|
||||
const unmappedItems = useMemo(() => {
|
||||
let unmapped: string[] = [];
|
||||
|
||||
if (!pages || !settings?.contentTypes || !taxonomy) {
|
||||
return unmapped;
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
let values: string[] = [];
|
||||
|
||||
if (taxonomy === "tags") {
|
||||
values = page.fmTags || [];
|
||||
} else if (taxonomy === "categories") {
|
||||
values = page.fmCategories || [];
|
||||
} else {
|
||||
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
|
||||
|
||||
if (!contentType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let fieldName = getTaxonomyField(taxonomy, contentType);
|
||||
|
||||
if (fieldName && page[fieldName]) {
|
||||
values = page[fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
if (!items.includes(value)) {
|
||||
unmapped.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(unmapped)];
|
||||
}, [items, taxonomy, pages, settings?.contentTypes]);
|
||||
|
||||
return (
|
||||
<div className={`py-6 px-4 flex flex-col h-full overflow-hidden`}>
|
||||
<div className={`flex w-full justify-between flex-shrink-0`}>
|
||||
<div>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900 first-letter:uppercase`}>{taxonomy}</h2>
|
||||
<p className={`mt-2 text-sm text-gray-500 dark:text-whisper-900 first-letter:uppercase`}>Create, edit, and manage the {taxonomy} of your site</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create a new ${taxonomy} value`}
|
||||
onClick={onCreate}>
|
||||
<PlusSmIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={`text-sm`}>Create a new {taxonomy} value</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pb-6 -mr-4 pr-4 flex flex-col flex-grow overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-vulcan-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Name</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Count</th>
|
||||
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-vulcan-300">
|
||||
{
|
||||
items && items.length > 0 ?
|
||||
items.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
<TagIcon className="inline-block h-4 w-4 mr-2" />
|
||||
<span>{item}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
<TaxonomyLookup
|
||||
taxonomy={taxonomy}
|
||||
value={item}
|
||||
pages={pages} />
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<TaxonomyActions
|
||||
field={taxonomy}
|
||||
value={item} />
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
!unmappedItems || unmappedItems.length === 0 && (
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200" colSpan={4}>No {taxonomy} found</td>
|
||||
</tr>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
unmappedItems && unmappedItems.length > 0 &&
|
||||
unmappedItems.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200" title='Missing in your settings'>
|
||||
<ExclamationIcon className="inline-block h-4 w-4 mr-2" />
|
||||
<span>{item}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
<TaxonomyLookup
|
||||
taxonomy={taxonomy}
|
||||
value={item}
|
||||
pages={pages} />
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<TaxonomyActions
|
||||
field={taxonomy}
|
||||
value={item}
|
||||
unmapped />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ChevronRightIcon, DownloadIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { TaxonomyData } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Page } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { NavigationBar, NavigationItem } from '../Layout';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
import { TaxonomyManager } from './TaxonomyManager';
|
||||
|
||||
export interface ITaxonomyViewProps {
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({ pages }: React.PropsWithChildren<ITaxonomyViewProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const [ taxonomySettings, setTaxonomySettings ] = useState<TaxonomyData>();
|
||||
const [ selectedTaxonomy, setSelectedTaxonomy ] = useState<string | null>(`tags`);
|
||||
|
||||
const onImport = () => {
|
||||
Messenger.send(DashboardMessage.importTaxonomy);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTaxonomySettings({
|
||||
tags: settings?.tags || [],
|
||||
categories: settings?.categories || [],
|
||||
customTaxonomy: settings?.customTaxonomy || [],
|
||||
});
|
||||
}, [settings?.tags, settings?.categories, settings?.customTaxonomy]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.send(DashboardMessage.sendTelemetry, {
|
||||
event: TelemetryEvent.webviewTaxonomyDashboard
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
contentClass={`relative w-full flex-grow flex flex-col mx-auto overflow-hidden`}>
|
||||
|
||||
<div className={`h-full w-full flex`}>
|
||||
<NavigationBar
|
||||
title='Select the taxonomy'
|
||||
bottom={(
|
||||
<button
|
||||
className={`-mb-4 text-xs opacity-80 flex items-center text-gray-500 dark:text-whisper-900 hover:text-gray-700 dark:hover:text-whisper-500`}
|
||||
title="Import taxonomy"
|
||||
onClick={onImport}>
|
||||
<DownloadIcon className={`w-5 mr-2`} />
|
||||
<span>Import taxonomy</span>
|
||||
</button>
|
||||
)}>
|
||||
<NavigationItem
|
||||
isSelected={selectedTaxonomy === "tags"}
|
||||
onClick={() => setSelectedTaxonomy(`tags`)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>Tags</span>
|
||||
</NavigationItem>
|
||||
|
||||
<NavigationItem
|
||||
isSelected={selectedTaxonomy === "categories"}
|
||||
onClick={() => setSelectedTaxonomy(`categories`)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>Categories</span>
|
||||
</NavigationItem>
|
||||
|
||||
{
|
||||
taxonomySettings?.customTaxonomy && taxonomySettings.customTaxonomy.map((taxonomy, index) => (
|
||||
<NavigationItem
|
||||
key={`${taxonomy.id}-${index}`}
|
||||
isSelected={selectedTaxonomy === taxonomy.id}
|
||||
onClick={() => setSelectedTaxonomy(taxonomy.id)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span className={`first-letter:uppercase`}>{taxonomy.id}</span>
|
||||
</NavigationItem>
|
||||
))
|
||||
}
|
||||
</NavigationBar>
|
||||
|
||||
<div className={`w-10/12 h-full overflow-hidden`}>
|
||||
<TaxonomyManager
|
||||
data={taxonomySettings}
|
||||
taxonomy={selectedTaxonomy}
|
||||
pages={pages} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
1
src/dashboardWebView/components/TaxonomyView/index.ts
Normal file
1
src/dashboardWebView/components/TaxonomyView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './TaxonomyView';
|
||||
18
src/dashboardWebView/components/UnknownView/UnknownView.tsx
Normal file
18
src/dashboardWebView/components/UnknownView/UnknownView.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { StopIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IUnknownViewProps {}
|
||||
|
||||
export const UnknownView: React.FunctionComponent<IUnknownViewProps> = (props: React.PropsWithChildren<IUnknownViewProps>) => {
|
||||
return (
|
||||
<div className={`w-full h-full flex items-center justify-center`}>
|
||||
<div className='flex flex-col items-center text-gray-500 dark:text-whisper-900'>
|
||||
<StopIcon className='w-32 h-32' />
|
||||
<p className='text-3xl mt-2'>View does not exist</p>
|
||||
<p className='text-xl mt-4'>
|
||||
You seem to have ended up on a view that doesn't exist. Please re-open the dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/dashboardWebView/components/UnknownView/index.ts
Normal file
1
src/dashboardWebView/components/UnknownView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './UnknownView';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user