mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-16 14:25:44 +02:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a12cf70a80 | |||
| 4b1d80f04b | |||
| a84fecaf96 | |||
| 5b9c279fa2 | |||
| f67be9efb9 | |||
| 4c50100230 | |||
| 82ace03692 | |||
| 576d07fdef | |||
| f6bc4fb630 | |||
| c35f4ab070 | |||
| 59cbc03b0c | |||
| 0ea06a841e | |||
| b54eb5a360 | |||
| 16b6fff6dc | |||
| 7a46729a46 | |||
| 32182c3df0 | |||
| 78587509b3 | |||
| e3bd7eebbe | |||
| f1a8e0d425 | |||
| 33e294d702 | |||
| e098442eaa | |||
| 1de14122c5 | |||
| c0838fffd4 | |||
| 082c25144f | |||
| d701651a05 | |||
| 5205b2d079 | |||
| e864d56081 | |||
| c6a4c239a0 | |||
| 42fbdf9708 | |||
| 8d53990aea | |||
| b9a0c656d3 | |||
| 8a8db67e82 | |||
| 0ac4571859 | |||
| a072957793 | |||
| fad5ad7243 | |||
| b248ee7184 | |||
| cf2d170d6f | |||
| 8d577ceb79 | |||
| 5748aa0540 | |||
| 4e850e5cb9 | |||
| f89d4fce3f | |||
| 1ecf75ae9c | |||
| 888e5c5229 | |||
| 45eb542619 | |||
| 5a565f1154 | |||
| 78002563be | |||
| be3071dc18 | |||
| 5c9d7eda17 | |||
| 9f3cfd9d3a | |||
| 0c6ae47a7b | |||
| 726a26850d | |||
| 5fbb05f083 | |||
| afca99b53a | |||
| a8d2c428bc | |||
| 5254f2b7f9 | |||
| 13a71cfd82 | |||
| 07d67bf881 | |||
| 27887bedef | |||
| 2b8f08c03c | |||
| cb2194bc48 | |||
| 46872f81ac | |||
| 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 |
@@ -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.
|
||||
Vendored
+39
@@ -10,4 +10,43 @@
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"eliostruyf.writingstyleguide.terms.isDisabled": true,
|
||||
"eliostruyf.writingstyleguide.biasFree.isDisabled": true,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -1,5 +1,96 @@
|
||||
# Change Log
|
||||
|
||||
## [8.2.0] - 2022-12-08 - [Release notes](https://beta.frontmatter.codes/updates/v8.2.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#362](https://github.com/estruyf/vscode-front-matter/issues/362): Support for conditional metadata
|
||||
- [#412](https://github.com/estruyf/vscode-front-matter/issues/412): Allow `frontmatter.json` to be split in multiple files
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#360](https://github.com/estruyf/vscode-front-matter/issues/360): Define which content types can be used on your page folders
|
||||
- [#406](https://github.com/estruyf/vscode-front-matter/issues/406): Added support for single data entries in the data dashboard
|
||||
- [#428](https://github.com/estruyf/vscode-front-matter/issues/428): Improved UX for inserting images to your content
|
||||
- [#430](https://github.com/estruyf/vscode-front-matter/issues/430): Support for HEXO its `post_asset_folder` setting (image location)
|
||||
- [#434](https://github.com/estruyf/vscode-front-matter/issues/434): Webview errors are logged in the extension output
|
||||
- [#440](https://github.com/estruyf/vscode-front-matter/issues/440): Type to search/filter in the snippets dashboard
|
||||
- [#447](https://github.com/estruyf/vscode-front-matter/issues/447): Allow to use placeholders on git commit messages
|
||||
- [#449](https://github.com/estruyf/vscode-front-matter/issues/449): Show `filename` if the `title` is not set
|
||||
- [#450](https://github.com/estruyf/vscode-front-matter/issues/450): Additional time placeholders added `{{hour12}}`, `{{hour24}}`, `{{ampm}}`, and `{{minute}}`
|
||||
- [#458](https://github.com/estruyf/vscode-front-matter/issues/458): Ability to configure the file prefix on folder level
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- [#431](https://github.com/estruyf/vscode-front-matter/issues/431): Performance improvements for the content dashboard
|
||||
- [#448](https://github.com/estruyf/vscode-front-matter/issues/448): Retrieving files fails when content folder name and workspace folder name are the same
|
||||
- [#455](https://github.com/estruyf/vscode-front-matter/issues/455): Show a description for the SEO section when title nor description is set
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Fix field error message color
|
||||
- [#433](https://github.com/estruyf/vscode-front-matter/issues/433): Fix issue with rendering an incorrect title value on the content dashboard
|
||||
- [#462](https://github.com/estruyf/vscode-front-matter/issues/462): Fix issue in script error notification
|
||||
- [#465](https://github.com/estruyf/vscode-front-matter/issues/465): Deleted content does not get added in git when syncing
|
||||
- [#471](https://github.com/estruyf/vscode-front-matter/issues/471): Fix typo on data dashboard
|
||||
|
||||
## [8.1.2] - 2022-10-06
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#435](https://github.com/estruyf/vscode-front-matter/issues/435): Fix required fields text color
|
||||
- [#436](https://github.com/estruyf/vscode-front-matter/issues/436): Fix inserting image/video snippets without defined fields
|
||||
|
||||
## [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
|
||||
|
||||
+8
-2
@@ -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!
|
||||
@@ -185,6 +191,6 @@ You can open showcase issues for the following things:
|
||||
|
||||
<p align="center">
|
||||
<a href="https://visitorbadge.io">
|
||||
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
|
||||
</a>
|
||||
<img src="https://api.visitorbadge.io/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
|
||||
</a>
|
||||
</p>
|
||||
@@ -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!
|
||||
@@ -184,6 +190,6 @@ You can open showcase issues for the following things:
|
||||
|
||||
<p align="center">
|
||||
<a href="https://visitorbadge.io">
|
||||
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
|
||||
<img src="https://api.visitorbadge.io/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
|
||||
</a>
|
||||
</p>
|
||||
+3
-237
@@ -41,7 +41,9 @@
|
||||
}
|
||||
|
||||
.collapsible__body,
|
||||
.ext_settings {
|
||||
.ext_settings,
|
||||
.git_actions,
|
||||
.initialize_actions {
|
||||
padding: 1rem 1.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -163,17 +165,6 @@
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.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 ul {
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
@@ -381,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;
|
||||
@@ -559,75 +394,6 @@ input:checked + .field__toggle__slider:before {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__multiple_images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.metadata_field__file__button,
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--vscode-button-background);
|
||||
padding: 1.5rem;
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button:hover,
|
||||
.metadata_field__preview_image__button:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button svg,
|
||||
.metadata_field__preview_image__button svg {
|
||||
color: var(--vscode-foreground);
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.metadata_field__file__button span,
|
||||
.metadata_field__preview_image__button span {
|
||||
color: var(--vscode-foreground);
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove:hover {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
/* File list */
|
||||
.file_list vscode-label {
|
||||
border-bottom: 1px solid var(--vscode-foreground);
|
||||
|
||||
@@ -68,11 +68,9 @@ describe("Initialization testing", 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;
|
||||
}
|
||||
|
||||
Generated
+408
-224
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "8.0.1",
|
||||
"version": "8.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "8.0.1",
|
||||
"version": "8.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.7"
|
||||
@@ -19,6 +19,7 @@
|
||||
"@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",
|
||||
@@ -56,6 +57,7 @@
|
||||
"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",
|
||||
@@ -81,6 +83,7 @@
|
||||
"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",
|
||||
@@ -106,12 +109,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client": {
|
||||
@@ -124,38 +128,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
||||
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
|
||||
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.10.4"
|
||||
"@babel/highlight": "^7.18.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
|
||||
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
|
||||
"dev": true
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
|
||||
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
|
||||
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
|
||||
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bendera/vscode-webview-elements": {
|
||||
@@ -217,6 +233,79 @@
|
||||
"integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
|
||||
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@kwsites/file-exists": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@kwsites/promise-deferred": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@microsoft/fast-element": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.7.0.tgz",
|
||||
@@ -449,9 +538,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz",
|
||||
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==",
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -1536,9 +1625,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -1760,26 +1849,31 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.16.8",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz",
|
||||
"integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz",
|
||||
"integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001251",
|
||||
"colorette": "^1.3.0",
|
||||
"electron-to-chromium": "^1.3.811",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.75"
|
||||
"caniuse-lite": "^1.0.30001370",
|
||||
"electron-to-chromium": "^1.4.202",
|
||||
"node-releases": "^2.0.6",
|
||||
"update-browserslist-db": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
@@ -1982,14 +2076,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001251",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
|
||||
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
|
||||
"version": "1.0.30001375",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz",
|
||||
"integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
}
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.3.6",
|
||||
@@ -3252,9 +3352,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.3.814",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.814.tgz",
|
||||
"integrity": "sha512-0mH03cyjh6OzMlmjauGg0TLd87ErIJqWiYxMcOLKf5w6p0YEOl7DJAj7BDlXEFmCguY5CQaKVOiMjAMODO2XDw==",
|
||||
"version": "1.4.215",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz",
|
||||
"integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -3718,31 +3818,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/picomatch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -4149,9 +4224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
@@ -5223,9 +5298,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.4.5",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.5.tgz",
|
||||
"integrity": "sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg==",
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -5315,6 +5390,12 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
@@ -6212,16 +6293,16 @@
|
||||
]
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
|
||||
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.0.5"
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
@@ -6312,9 +6393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
@@ -6662,9 +6743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
@@ -6692,9 +6773,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "1.1.75",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz",
|
||||
"integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
|
||||
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/normalize-package-data": {
|
||||
@@ -7300,10 +7381,16 @@
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -7998,9 +8085,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-quill": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/quill": "^1.3.10",
|
||||
@@ -8008,8 +8095,8 @@
|
||||
"quill": "^1.3.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16 || ^17",
|
||||
"react-dom": "^16 || ^17"
|
||||
"react": "^16 || ^17 || ^18",
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
@@ -8800,6 +8887,21 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-git": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.10.0.tgz",
|
||||
"integrity": "sha512-2w35xrS5rVtAW0g67LqtxCZN5cdddz/woQRfS0OJXaljXEoTychZ4jnE+CQgra/wX4ZvHeiChTUMenCwfIYEYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@kwsites/file-exists": "^1.1.1",
|
||||
"@kwsites/promise-deferred": "^1.1.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
@@ -9395,9 +9497,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
|
||||
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "^2.20.0",
|
||||
@@ -9446,13 +9548,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz",
|
||||
"integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
@@ -9460,23 +9563,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"acorn": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/thunky": {
|
||||
@@ -9911,6 +9997,32 @@
|
||||
"mkdirp": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
|
||||
"integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"escalade": "^3.1.1",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist-lint": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
|
||||
@@ -10927,12 +11039,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"@actions/http-client": {
|
||||
@@ -10945,35 +11058,35 @@
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
||||
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
|
||||
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.10.4"
|
||||
"@babel/highlight": "^7.18.6"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
|
||||
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
|
||||
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
|
||||
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
|
||||
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
@@ -11023,6 +11136,70 @@
|
||||
"integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/source-map": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
|
||||
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@kwsites/file-exists": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"@kwsites/promise-deferred": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/fast-element": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.7.0.tgz",
|
||||
@@ -11233,9 +11410,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz",
|
||||
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==",
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
|
||||
"dev": true
|
||||
},
|
||||
"@sentry/browser": {
|
||||
@@ -12180,9 +12357,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -12361,16 +12538,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.16.8",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz",
|
||||
"integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz",
|
||||
"integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001251",
|
||||
"colorette": "^1.3.0",
|
||||
"electron-to-chromium": "^1.3.811",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.75"
|
||||
"caniuse-lite": "^1.0.30001370",
|
||||
"electron-to-chromium": "^1.4.202",
|
||||
"node-releases": "^2.0.6",
|
||||
"update-browserslist-db": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
@@ -12522,9 +12698,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001251",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
|
||||
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
|
||||
"version": "1.0.30001375",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz",
|
||||
"integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==",
|
||||
"dev": true
|
||||
},
|
||||
"chai": {
|
||||
@@ -13496,9 +13672,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.814",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.814.tgz",
|
||||
"integrity": "sha512-0mH03cyjh6OzMlmjauGg0TLd87ErIJqWiYxMcOLKf5w6p0YEOl7DJAj7BDlXEFmCguY5CQaKVOiMjAMODO2XDw==",
|
||||
"version": "1.4.215",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz",
|
||||
"integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
@@ -13868,24 +14044,6 @@
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
}
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
@@ -14192,9 +14350,9 @@
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
|
||||
"dev": true
|
||||
},
|
||||
"gray-matter": {
|
||||
@@ -14963,9 +15121,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"jest-worker": {
|
||||
"version": "27.4.5",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.5.tgz",
|
||||
"integrity": "sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg==",
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
@@ -15039,6 +15197,12 @@
|
||||
"minimist": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
@@ -15647,13 +15811,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
|
||||
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.0.5"
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
@@ -15719,9 +15883,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
@@ -15990,9 +16154,9 @@
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"dev": true
|
||||
},
|
||||
"node-json-db": {
|
||||
@@ -16013,9 +16177,9 @@
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.75",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz",
|
||||
"integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
|
||||
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-package-data": {
|
||||
@@ -16465,10 +16629,16 @@
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"pidtree": {
|
||||
@@ -16982,9 +17152,9 @@
|
||||
}
|
||||
},
|
||||
"react-quill": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/quill": "^1.3.10",
|
||||
@@ -17608,6 +17778,17 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"simple-git": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.10.0.tgz",
|
||||
"integrity": "sha512-2w35xrS5rVtAW0g67LqtxCZN5cdddz/woQRfS0OJXaljXEoTychZ4jnE+CQgra/wX4ZvHeiChTUMenCwfIYEYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@kwsites/file-exists": "^1.1.1",
|
||||
"@kwsites/promise-deferred": "^1.1.1",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
@@ -18068,9 +18249,9 @@
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
|
||||
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "^2.20.0",
|
||||
@@ -18092,22 +18273,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz",
|
||||
"integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18452,6 +18626,16 @@
|
||||
"mkdirp": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"update-browserslist-db": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
|
||||
"integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escalade": "^3.1.1",
|
||||
"picocolors": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
|
||||
|
||||
+270
-63
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"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...",
|
||||
"displayName": "Front Matter CMS",
|
||||
"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, Docusaurus, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "8.0.1",
|
||||
"version": "8.2.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"sponsor": {
|
||||
"url": "https://github.com/sponsors/estruyf"
|
||||
},
|
||||
"qna": "https://github.com/estruyf/vscode-front-matter/discussions",
|
||||
"engines": {
|
||||
"vscode": "^1.63.0"
|
||||
},
|
||||
@@ -59,7 +60,7 @@
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "frontmatter-explorer",
|
||||
"title": "FrontMatter",
|
||||
"title": "Front Matter",
|
||||
"icon": "assets/frontmatter-short-min.svg"
|
||||
}
|
||||
]
|
||||
@@ -68,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"
|
||||
}
|
||||
]
|
||||
@@ -81,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": {
|
||||
@@ -167,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": [],
|
||||
@@ -194,6 +206,17 @@
|
||||
],
|
||||
"default": null,
|
||||
"description": "Defines a custom preview path for the folder."
|
||||
},
|
||||
"filePrefix": {
|
||||
"type": [ "null", "string" ],
|
||||
"description": "Defines a prefix for the file name."
|
||||
},
|
||||
"contentTypes": {
|
||||
"type": "array",
|
||||
"description": "Defines which content types can be used for the current location. If not defined, all content types will be available.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -218,12 +241,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"
|
||||
@@ -253,6 +282,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"description": "The snippet title.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "The snippet description.",
|
||||
"type": "string"
|
||||
@@ -311,7 +344,8 @@
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"date"
|
||||
"date",
|
||||
"number"
|
||||
],
|
||||
"description": "Type of the field value"
|
||||
}
|
||||
@@ -351,6 +385,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."
|
||||
@@ -390,6 +428,7 @@
|
||||
"description": "The type for which the script will be used."
|
||||
},
|
||||
"command": {
|
||||
"$id": "#scriptCommand",
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -407,6 +446,11 @@
|
||||
],
|
||||
"description": "The type of script you want to execute.",
|
||||
"default": "node"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "Hide the action from the UI",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -417,6 +461,12 @@
|
||||
},
|
||||
"scope": "Custom scripts"
|
||||
},
|
||||
"frontMatter.dashboard.content.pagination": {
|
||||
"type": ["boolean", "number"],
|
||||
"default": true,
|
||||
"markdownDescription": "Specify if you want to enable/disable pagination for your content. You can define your page number up to 52. Default items per page is `16`. Disabling the pagination can be done by setting it to `false`. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.dashboard.content.cardTags": {
|
||||
"type": "string",
|
||||
"default": "tags",
|
||||
@@ -515,6 +565,11 @@
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"description": "If you are using data types, you can specify your type ID."
|
||||
},
|
||||
"singleEntry": {
|
||||
"type": "boolean",
|
||||
"description": "If you want to use a single entry for your data file.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -566,6 +621,11 @@
|
||||
"type": "string",
|
||||
"default": "content",
|
||||
"description": "If you are using data types, you can specify your type ID."
|
||||
},
|
||||
"singleEntry": {
|
||||
"type": "boolean",
|
||||
"description": "If you want to use a single entry for your data files in the folder.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -630,6 +690,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",
|
||||
@@ -667,7 +737,8 @@
|
||||
"panel.otherActions",
|
||||
"dashboard.snippets.view",
|
||||
"dashboard.snippets.manage",
|
||||
"dashboard.data.view"
|
||||
"dashboard.data.view",
|
||||
"dashboard.taxonomy.view"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -697,6 +768,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": "",
|
||||
@@ -819,7 +898,9 @@
|
||||
"block",
|
||||
"list",
|
||||
"dataFile",
|
||||
"slug"
|
||||
"slug",
|
||||
"divider",
|
||||
"heading"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -831,6 +912,10 @@
|
||||
"type": "string",
|
||||
"description": "Title to show in the UI"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description to show in the UI"
|
||||
},
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -893,7 +978,20 @@
|
||||
"taxonomyId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The ID of your taxonomy field"
|
||||
"description": "The ID of your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
"const": "tags"
|
||||
},
|
||||
{
|
||||
"const": "categories"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"fileExtensions": {
|
||||
"type": "array",
|
||||
@@ -961,6 +1059,55 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Specify if the field is editable"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the field is required"
|
||||
},
|
||||
"when": {
|
||||
"type": "object",
|
||||
"description": "Specify the conditions to show the field",
|
||||
"properties": {
|
||||
"fieldRef": {
|
||||
"type": "string",
|
||||
"description": "The field ID to use"
|
||||
},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"description": "The operator to use",
|
||||
"enum": [
|
||||
"eq",
|
||||
"neq",
|
||||
"contains",
|
||||
"notContains",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"exlusiveMinimum",
|
||||
"exclusiveMaximum"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"array"
|
||||
],
|
||||
"description": "The value to compare"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Specify if the comparison is case sensitive. Default: true"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -969,6 +1116,20 @@
|
||||
"name"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "divider"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
@@ -1088,6 +1249,11 @@
|
||||
"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,
|
||||
@@ -1151,11 +1317,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"
|
||||
}
|
||||
@@ -1297,6 +1476,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",
|
||||
@@ -1308,40 +1493,39 @@
|
||||
"default": "yyyy-MM-dd",
|
||||
"markdownDescription": "Specify the prefix you want to add for your new article filenames. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.prefix)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.templates.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.enabled)",
|
||||
"scope": "Templates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "frontMatter.config.reload",
|
||||
"title": "Reload config",
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.authenticate",
|
||||
"title": "Authenticate",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.generate",
|
||||
"title": "Generate content type from current file",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.addMissingFields",
|
||||
"title": "Add missing fields from front matter to content type",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.setContentType",
|
||||
"title": "Set the content type to use for the current file",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.blockquote",
|
||||
"title": "Blockquote",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/blockquote-light.svg",
|
||||
"dark": "assets/icons/blockquote-dark.svg"
|
||||
@@ -1350,7 +1534,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"
|
||||
@@ -1359,7 +1543,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"
|
||||
@@ -1368,7 +1552,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"
|
||||
@@ -1377,16 +1561,22 @@
|
||||
{
|
||||
"command": "frontMatter.markup.codeblock",
|
||||
"title": "Codeblock",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/codeblock-light.svg",
|
||||
"dark": "assets/icons/codeblock-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.hyperlink",
|
||||
"title": "Hyperlink",
|
||||
"category": "Front Matter",
|
||||
"icon": "$(link)"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"title": "Collapse sections",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/close-light.svg",
|
||||
"dark": "assets/icons/close-dark.svg"
|
||||
@@ -1395,37 +1585,37 @@
|
||||
{
|
||||
"command": "frontMatter.initTemplate",
|
||||
"title": "Initialize the template folder",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"title": "Create template from current file",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createCategory",
|
||||
"title": "Create category",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createContent",
|
||||
"title": "Create new content from defined content type or template",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTag",
|
||||
"title": "Create tag",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.diagnostics",
|
||||
"title": "Diagnostic logging",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.exportTaxonomy",
|
||||
"title": "Export all tags & categories to your settings",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
@@ -1442,12 +1632,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"
|
||||
@@ -1456,17 +1646,17 @@
|
||||
{
|
||||
"command": "frontMatter.init",
|
||||
"title": "Initialize project",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertCategories",
|
||||
"title": "Insert categories",
|
||||
"category": "Front matter"
|
||||
"category": "Front Matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertMedia",
|
||||
"title": "Insert media into your content",
|
||||
"category": "Front matter",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/media-dark.svg",
|
||||
"light": "/assets/icons/media-light.svg"
|
||||
@@ -1475,7 +1665,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"
|
||||
@@ -1484,12 +1674,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"
|
||||
@@ -1498,7 +1688,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"
|
||||
@@ -1507,7 +1697,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"
|
||||
@@ -1516,7 +1706,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"
|
||||
@@ -1525,7 +1715,7 @@
|
||||
{
|
||||
"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"
|
||||
@@ -1533,8 +1723,8 @@
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.taxonomy",
|
||||
"title": "Open taxonomy dashboard",
|
||||
"category": "Front matter",
|
||||
"title": "Open taxonomies dashboard",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/frontmatter-small-dark.svg",
|
||||
"light": "/assets/icons/frontmatter-small-light.svg"
|
||||
@@ -1543,7 +1733,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"
|
||||
@@ -1552,7 +1742,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"
|
||||
@@ -1561,27 +1751,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"
|
||||
@@ -1590,22 +1780,32 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.cache.clear",
|
||||
"title": "Clear cache",
|
||||
"category": "Front Matter"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -1626,7 +1826,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"
|
||||
},
|
||||
@@ -1724,6 +1924,10 @@
|
||||
"command": "frontMatter.dashboard.snippets",
|
||||
"when": "frontMatter:dashboard:snippets:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.git.sync",
|
||||
"when": "frontMatter:git:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
@@ -1938,6 +2142,7 @@
|
||||
"@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",
|
||||
@@ -1975,6 +2180,7 @@
|
||||
"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",
|
||||
@@ -2000,6 +2206,7 @@
|
||||
"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",
|
||||
|
||||
@@ -8,14 +8,14 @@ const version = packageJson.version.split('.');
|
||||
packageJson.version = `${version[0]}.${version[1]}.${process.argv[process.argv.length-1].substr(0, 7)}`;
|
||||
packageJson.preview = true;
|
||||
packageJson.name = "vscode-front-matter-beta";
|
||||
packageJson.displayName = `${packageJson.displayName} BETA`;
|
||||
packageJson.displayName = `${packageJson.displayName} (BETA)`;
|
||||
packageJson.description = `BETA Version of Front Matter. ${packageJson.description}`;
|
||||
packageJson.icon = "assets/frontmatter-beta.png";
|
||||
packageJson.homepage = "https://beta.frontmatter.codes";
|
||||
|
||||
console.log(packageJson.version);
|
||||
|
||||
core.summary.addHeading(`Version info`).addDetails(`${packageJson.version}`);
|
||||
core.summary.addHeading(`Version info`).addRaw(`Version: ${packageJson.version}`).write();
|
||||
|
||||
const scripts = packageJson.scripts;
|
||||
for (const key in scripts) {
|
||||
|
||||
+22
-5
@@ -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';
|
||||
@@ -197,7 +198,7 @@ export class Article {
|
||||
Telemetry.send(TelemetryEvent.generateSlug);
|
||||
|
||||
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
|
||||
const filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
let filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!editor) {
|
||||
@@ -209,6 +210,12 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the file prefix from the folder
|
||||
const filePrefixOnFolder = Folders.getFilePrefixBeFilePath(editor.document.uri.fsPath);
|
||||
if (typeof filePrefixOnFolder !== "undefined") {
|
||||
filePrefix = filePrefixOnFolder;
|
||||
}
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const titleField = "title";
|
||||
const articleTitle: string = article.data[titleField];
|
||||
@@ -225,8 +232,8 @@ export class Article {
|
||||
}
|
||||
|
||||
// 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}}}`);
|
||||
@@ -323,6 +330,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));
|
||||
}
|
||||
@@ -355,6 +370,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",
|
||||
@@ -362,7 +378,8 @@ export class Article {
|
||||
pageBundle: !!contentType.pageBundle,
|
||||
filePath: editor.document.uri.fsPath,
|
||||
fieldName: basename(editor.document.uri.fsPath),
|
||||
position
|
||||
position,
|
||||
selection: selectionText
|
||||
}
|
||||
} as DashboardData);
|
||||
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { commands } from "vscode";
|
||||
import { COMMAND_NAME, ExtensionState } from "../constants";
|
||||
import { Extension, Notifications } from "../helpers";
|
||||
|
||||
export class Cache {
|
||||
|
||||
public static async registerCommands() {
|
||||
const ext = Extension.getInstance();
|
||||
const subscriptions = ext.subscriptions;
|
||||
|
||||
subscriptions.push(
|
||||
commands.registerCommand(COMMAND_NAME.clearCache, Cache.clear)
|
||||
);
|
||||
}
|
||||
|
||||
private static async clear() {
|
||||
const ext = Extension.getInstance();
|
||||
|
||||
await ext.setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace");
|
||||
await ext.setState(ExtensionState.Dashboard.Pages.Index, undefined, "workspace");
|
||||
|
||||
Notifications.info("Cache cleared");
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export class Content {
|
||||
} as QuickPickItem];
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
title: "Create content",
|
||||
placeHolder: `Select how you want to create your new content`,
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
|
||||
@@ -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, TaxonomyListener } from '../listeners/dashboard';
|
||||
import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener, LogListener } 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;
|
||||
@@ -130,8 +130,8 @@ export class Dashboard {
|
||||
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, false);
|
||||
});
|
||||
|
||||
SettingsHelper.onConfigChange((global?: any) => {
|
||||
SettingsListener.getSettings();
|
||||
SettingsHelper.onConfigChange(() => {
|
||||
SettingsListener.getSettings(true);
|
||||
});
|
||||
|
||||
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
|
||||
@@ -146,7 +146,9 @@ export class Dashboard {
|
||||
TelemetryListener.process(msg);
|
||||
SnippetListener.process(msg);
|
||||
ModeListener.process(msg);
|
||||
GitListener.process(msg);
|
||||
TaxonomyListener.process(msg);
|
||||
LogListener.process(msg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ViewColumn, workspace } from "vscode";
|
||||
import ContentProvider from "../providers/ContentProvider";
|
||||
import { join } from "path";
|
||||
import { ContentFolder } from "../models";
|
||||
import { Settings } from "../helpers/SettingsHelper";
|
||||
|
||||
|
||||
export class Diagnostics {
|
||||
@@ -38,6 +39,12 @@ ${all}
|
||||
# Folders to search files
|
||||
|
||||
${folderData.join("\n")}
|
||||
|
||||
# Complete frontmatter.json config
|
||||
|
||||
\`\`\`json
|
||||
${JSON.stringify(Settings.globalConfig, null, 2)}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
ContentProvider.show(logging, `${projectName} diagnostics`, "markdown", ViewColumn.One);
|
||||
|
||||
+72
-22
@@ -1,3 +1,4 @@
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
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";
|
||||
@@ -7,7 +8,7 @@ import uniqBy = require("lodash.uniqby");
|
||||
import { Template } from "./Template";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Logger, Settings } from "../helpers";
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { existsSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
@@ -16,6 +17,8 @@ import { MediaListener, PagesListener, SettingsListener } from '../listeners/das
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
import { glob } from 'glob';
|
||||
import { mkdirAsync } from '../utils/mkdirAsync';
|
||||
import { existsAsync } from '../utils';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
@@ -41,7 +44,12 @@ export class Folders {
|
||||
startPath += "/";
|
||||
}
|
||||
|
||||
if (startPath.includes(STATIC_FOLDER_PLACEHOLDER.hexo.placeholder)) {
|
||||
startPath = startPath.replace(STATIC_FOLDER_PLACEHOLDER.hexo.placeholder, STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -53,22 +61,17 @@ export class Folders {
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = folderName.split("/").filter(f => f);
|
||||
let parentFolders: string[] = [];
|
||||
await Folders.createFolder(join(parseWinPath(wsFolder?.fsPath || ""), folderName));
|
||||
}
|
||||
|
||||
for (const folder of folders) {
|
||||
const folderPath = join(parseWinPath(wsFolder?.fsPath || ""), parentFolders.join("/"), folder);
|
||||
|
||||
parentFolders.push(folder);
|
||||
|
||||
if (!existsSync(folderPath)) {
|
||||
mkdirSync(folderPath);
|
||||
}
|
||||
public static async createFolder(folderPath: string) {
|
||||
if (!(await existsAsync(folderPath))) {
|
||||
await mkdirAsync(folderPath, { recursive: true });
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
MediaHelpers.resetMedia();
|
||||
MediaListener.sendMediaFiles(0, folderName);
|
||||
MediaListener.sendMediaFiles(0, folderPath);
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.addMediaFolder);
|
||||
@@ -78,7 +81,7 @@ export class Folders {
|
||||
* Create content in a registered folder
|
||||
* @returns
|
||||
*/
|
||||
public static async create() {
|
||||
public static async create() {
|
||||
const selectedFolder = await Questions.SelectContentFolder();
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
@@ -96,7 +99,7 @@ export class Folders {
|
||||
|
||||
/**
|
||||
* Register the new folder path
|
||||
* @param folder
|
||||
* @param folderInfo
|
||||
*/
|
||||
public static async register(folderInfo: { title: string, path: Uri } | Uri) {
|
||||
let folderName = folderInfo instanceof Uri ? undefined : folderInfo.title;
|
||||
@@ -115,7 +118,8 @@ export class Folders {
|
||||
}
|
||||
|
||||
if (!folderName) {
|
||||
folderName = await window.showInputBox({
|
||||
folderName = await window.showInputBox({
|
||||
title: `Register folder`,
|
||||
prompt: `Which name would you like to specify for this folder?`,
|
||||
placeHolder: `Folder name`,
|
||||
value: basename(folder.fsPath),
|
||||
@@ -135,7 +139,7 @@ export class Folders {
|
||||
|
||||
Telemetry.send(TelemetryEvent.registerFolder);
|
||||
|
||||
SettingsListener.getSettings();
|
||||
SettingsListener.getSettings(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,9 +212,9 @@ export class Folders {
|
||||
if (!projectFolder) {
|
||||
window.showWorkspaceFolderPick({
|
||||
placeHolder: `Please select the main workspace folder for Front Matter to use.`
|
||||
}).then(selectedFolder => {
|
||||
}).then(async (selectedFolder) => {
|
||||
if (selectedFolder) {
|
||||
Settings.createGlobalFile(selectedFolder.uri);
|
||||
await Settings.createGlobalFile(selectedFolder.uri);
|
||||
// Full reload to make sure the whole extension is reloaded correctly
|
||||
commands.executeCommand(`workbench.action.reloadWindow`);
|
||||
}
|
||||
@@ -240,13 +244,14 @@ export class Folders {
|
||||
public static async getInfo(limit?: number): Promise<FolderInfo[] | null> {
|
||||
const supportedFiles = Settings.get<string[]>(SETTING_CONTENT_SUPPORTED_FILETYPES);
|
||||
const folders = Folders.get();
|
||||
const wsFolder = parseWinPath(Folders.getWorkspaceFolder()?.fsPath || "");
|
||||
|
||||
if (folders && folders.length > 0) {
|
||||
let folderInfo: FolderInfo[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
const projectName = Folders.getProjectFolderName();
|
||||
let projectStart = folder.path.split(projectName).pop();
|
||||
let projectStart = parseWinPath(folder.path).replace(wsFolder, "");
|
||||
|
||||
if (projectStart) {
|
||||
projectStart = projectStart.replace(/\\/g, '/');
|
||||
@@ -366,7 +371,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,7 +384,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,6 +427,51 @@ export class Folders {
|
||||
return uniqueFolders.map(folder => relative(wsFolder?.path || "", folder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file prefix for the given folder path
|
||||
* @param folderPath
|
||||
* @returns
|
||||
*/
|
||||
public static getFilePrefixByFolderPath(folderPath: string) {
|
||||
const folders = Folders.get();
|
||||
const pageFolder = folders.find(f => parseWinPath(f.path) === parseWinPath(folderPath));
|
||||
|
||||
if (pageFolder && typeof pageFolder.filePrefix !== "undefined") {
|
||||
return pageFolder.filePrefix;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the file prefix for the given file path
|
||||
* @param filePath
|
||||
* @returns
|
||||
*/
|
||||
public static getFilePrefixBeFilePath(filePath: string) {
|
||||
const folders = Folders.get();
|
||||
if (folders.length > 0) {
|
||||
filePath = parseWinPath(filePath);
|
||||
|
||||
let selectedFolder: ContentFolder | null = null;
|
||||
for (const folder of folders) {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
if (filePath.startsWith(folderPath)) {
|
||||
if (!selectedFolder || selectedFolder.path.length < folderPath.length) {
|
||||
selectedFolder = folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFolder && typeof selectedFolder.filePrefix !== "undefined") {
|
||||
return selectedFolder.filePrefix;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all content folders
|
||||
* @param pattern
|
||||
|
||||
+13
-4
@@ -15,7 +15,7 @@ import { Folders } from './Folders';
|
||||
|
||||
export class Preview {
|
||||
|
||||
/**
|
||||
/**
|
||||
* Init the preview
|
||||
*/
|
||||
public static async init() {
|
||||
@@ -47,8 +47,9 @@ export class Preview {
|
||||
|
||||
let selectedFolder: ContentFolder | null = null;
|
||||
for (const folder of foldersWithPath) {
|
||||
if (filePath.startsWith(folder.path)) {
|
||||
if (!selectedFolder || selectedFolder.path.length < folder.path.length) {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
if (filePath.startsWith(folderPath)) {
|
||||
if (!selectedFolder || selectedFolder.path.length < folderPath.length) {
|
||||
selectedFolder = folder;
|
||||
}
|
||||
}
|
||||
@@ -81,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
|
||||
|
||||
@@ -2,13 +2,13 @@ import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType';
|
||||
import { Telemetry } from './../helpers/Telemetry';
|
||||
import { workspace, Uri } from "vscode";
|
||||
import { join } from "path";
|
||||
import * as fs from "fs";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Template } from "./Template";
|
||||
import { Folders } from "./Folders";
|
||||
import { FrameworkDetector, Logger, Settings } from "../helpers";
|
||||
import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
|
||||
import { SettingsListener } from '../listeners/dashboard';
|
||||
import { existsAsync, writeFileAsync } from '../utils';
|
||||
|
||||
export class Project {
|
||||
|
||||
@@ -34,10 +34,10 @@ categories: []
|
||||
*/
|
||||
public static async init(sampleTemplate?: boolean) {
|
||||
try {
|
||||
Settings.createTeamSettings();
|
||||
await Settings.createTeamSettings();
|
||||
|
||||
// Add the default content type
|
||||
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true);
|
||||
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, [DEFAULT_CONTENT_TYPE], true);
|
||||
|
||||
if (sampleTemplate !== undefined) {
|
||||
await Project.createSampleTemplate();
|
||||
@@ -49,13 +49,13 @@ categories: []
|
||||
|
||||
// Check if you can find the framework
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const framework = FrameworkDetector.get(wsFolder?.fsPath || "");
|
||||
const framework = await FrameworkDetector.get(wsFolder?.fsPath || "");
|
||||
|
||||
if (framework) {
|
||||
SettingsListener.setFramework(framework.name);
|
||||
await SettingsListener.setFramework(framework.name);
|
||||
}
|
||||
|
||||
SettingsListener.getSettings();
|
||||
SettingsListener.getSettings(true);
|
||||
} catch (err: any) {
|
||||
Logger.error(`Project::init: ${err?.message || err}`);
|
||||
Notifications.error(`Sorry, something went wrong - ${err?.message || err}`);
|
||||
@@ -79,12 +79,12 @@ categories: []
|
||||
|
||||
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
|
||||
|
||||
if (!fs.existsSync(templatePath.fsPath)) {
|
||||
if (!(await existsAsync(templatePath.fsPath))) {
|
||||
await workspace.fs.createDirectory(templatePath);
|
||||
}
|
||||
|
||||
if (sampleTemplate) {
|
||||
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
await writeFileAsync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
Notifications.info("Sample template created.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class Settings {
|
||||
* @param type
|
||||
*/
|
||||
public static async create(type: TaxonomyType) {
|
||||
const newOption = await vscode.window.showInputBox({
|
||||
const newOption = await vscode.window.showInputBox({
|
||||
prompt: `Insert the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} that you want to add to your configuration.`,
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
|
||||
ignoreFocusOut: true
|
||||
@@ -151,7 +151,8 @@ export class Settings {
|
||||
const taxType = await vscode.window.showQuickPick([
|
||||
"Tag",
|
||||
"Category"
|
||||
], {
|
||||
], {
|
||||
title: `Remap`,
|
||||
placeHolder: `What do you want to remap?`,
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
-44
@@ -1,55 +1,20 @@
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEvent } from '../constants';
|
||||
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';
|
||||
import { extname } from 'path';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
import { writeFileAsync, copyFileAsync } from '../utils';
|
||||
|
||||
export class Template {
|
||||
|
||||
/**
|
||||
* Check if the template folder is available
|
||||
*/
|
||||
public static async init() {
|
||||
const isInitialized = await Template.isInitialized();
|
||||
await vscode.commands.executeCommand('setContext', CONTEXT.canInit, !isInitialized);
|
||||
|
||||
if (isInitialized) {
|
||||
await vscode.commands.executeCommand('setContext', CONTEXT.initialized, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the project is already initialized
|
||||
*/
|
||||
public static async isInitialized() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const folder = Template.getSettings();
|
||||
|
||||
if (!folder || !wsFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const templatePath = vscode.Uri.file(path.join(wsFolder.fsPath, folder));
|
||||
|
||||
try {
|
||||
await vscode.workspace.fs.stat(templatePath);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a template
|
||||
*/
|
||||
@@ -62,7 +27,8 @@ export class Template {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
const clonedArticle = Object.assign({}, article);
|
||||
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
title: `Template title`,
|
||||
prompt: `What name would you like to give your template?`,
|
||||
placeHolder: `article`,
|
||||
ignoreFocusOut: true
|
||||
@@ -76,6 +42,7 @@ export class Template {
|
||||
const keepContents = await vscode.window.showQuickPick(
|
||||
["yes", "no"],
|
||||
{
|
||||
title: `Keep contents`,
|
||||
canPickMany: false,
|
||||
placeHolder: `Do you want to keep the contents for the template?`,
|
||||
ignoreFocusOut: true
|
||||
@@ -93,7 +60,7 @@ export class Template {
|
||||
let fileContents = ArticleHelper.stringifyFrontMatter(keepContents === "no" ? "" : clonedArticle.content, clonedArticle.data);
|
||||
|
||||
const templateFile = path.join(templatePath.fsPath, `${titleValue}.${fileType}`);
|
||||
fs.writeFileSync(templateFile, fileContents, { encoding: "utf-8" });
|
||||
await writeFileAsync(templateFile, fileContents, { encoding: "utf-8" });
|
||||
|
||||
Notifications.info(`Template created and is now available in your ${folder} folder.`);
|
||||
}
|
||||
@@ -132,6 +99,7 @@ export class Template {
|
||||
}
|
||||
|
||||
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
|
||||
title: `Select a template`,
|
||||
placeHolder: `Select the content template to use`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
@@ -152,34 +120,34 @@ export class Template {
|
||||
return;
|
||||
}
|
||||
|
||||
const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath);
|
||||
const templateData = await ArticleHelper.getFrontMatterByPath(template.fsPath);
|
||||
let contentType: IContentType | undefined;
|
||||
if (templateData && templateData.data && templateData.data.type) {
|
||||
contentType = contentTypes?.find(t => t.name === templateData.data.type);
|
||||
}
|
||||
|
||||
const fileExtension = extname(template.fsPath).replace(".", "");
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension);
|
||||
let newFilePath: string | undefined = await ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the new file creation
|
||||
fs.copyFileSync(template.fsPath, newFilePath);
|
||||
await copyFileAsync(template.fsPath, newFilePath);
|
||||
|
||||
// Update the properties inside the template
|
||||
let frontMatter = ArticleHelper.getFrontMatterByPath(newFilePath);
|
||||
let frontMatter = await ArticleHelper.getFrontMatterByPath(newFilePath);
|
||||
if (!frontMatter) {
|
||||
Notifications.warning(`Something failed when retrieving the newly created file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frontMatter.data) {
|
||||
frontMatter.data = ArticleHelper.updatePlaceholders(frontMatter.data, titleValue);
|
||||
frontMatter.data = await ArticleHelper.updatePlaceholders(frontMatter.data, titleValue, newFilePath);
|
||||
|
||||
frontMatter = Article.updateDate(frontMatter);
|
||||
|
||||
fs.writeFileSync(newFilePath, ArticleHelper.stringifyFrontMatter(frontMatter.content, frontMatter.data), { encoding: "utf8" });
|
||||
await writeFileAsync(newFilePath, ArticleHelper.stringifyFrontMatter(frontMatter.content, frontMatter.data), { encoding: "utf8" });
|
||||
|
||||
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(newFilePath));
|
||||
}
|
||||
|
||||
+62
-4
@@ -1,4 +1,4 @@
|
||||
import { commands, window, Selection, QuickPickItem } from "vscode";
|
||||
import { commands, window, Selection, QuickPickItem, TextEditor } from "vscode";
|
||||
import { COMMAND_NAME, CONTEXT, SETTING_CONTENT_WYSIWYG } from "../constants";
|
||||
import { Settings } from "../helpers";
|
||||
|
||||
@@ -12,7 +12,8 @@ enum MarkupType {
|
||||
heading,
|
||||
unorderedList,
|
||||
orderedList,
|
||||
taskList
|
||||
taskList,
|
||||
hyperlink,
|
||||
}
|
||||
|
||||
export class Wysiwyg {
|
||||
@@ -46,6 +47,9 @@ export class Wysiwyg {
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.orderedlist, () => this.addMarkup(MarkupType.orderedList)));
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.taskList, () => this.addMarkup(MarkupType.taskList)));
|
||||
|
||||
// Other markup
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.hyperlink, () => this.addMarkup(MarkupType.hyperlink)));
|
||||
|
||||
// Options
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.options, async () => {
|
||||
const qpItems: QuickPickItem[] = [
|
||||
@@ -55,9 +59,11 @@ export class Wysiwyg {
|
||||
{ label: "$(code) Code", detail: "Add inline code snippet", alwaysShow: true },
|
||||
{ label: "$(symbol-namespace) Code block", detail: "Add a code block", alwaysShow: true },
|
||||
{ label: "$(quote) Blockquote", detail: "Add a blockquote", alwaysShow: true },
|
||||
{ label: "$(symbol-text) Strikethrough", detail: "Add a strikethrough", alwaysShow: true },
|
||||
]
|
||||
|
||||
const option = await window.showQuickPick([ ...qpItems ], {
|
||||
const option = await window.showQuickPick([ ...qpItems ], {
|
||||
title: "WYSIWYG Options",
|
||||
placeHolder: "Which type of markup would you like to insert?",
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
@@ -76,6 +82,8 @@ export class Wysiwyg {
|
||||
await this.addMarkup(MarkupType.codeblock);
|
||||
} else if (option.label === qpItems[5].label) {
|
||||
await this.addMarkup(MarkupType.blockquote);
|
||||
} else if (option.label === qpItems[6].label) {
|
||||
await this.addMarkup(MarkupType.strikethrough);
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -95,6 +103,10 @@ export class Wysiwyg {
|
||||
const selection = editor.selection;
|
||||
const hasTextSelection = !selection.isEmpty;
|
||||
|
||||
if (type === MarkupType.hyperlink) {
|
||||
return this.addHyperlink(editor, selection);
|
||||
}
|
||||
|
||||
const markers = this.getMarkers(type);
|
||||
if (!markers) {
|
||||
return;
|
||||
@@ -129,6 +141,51 @@ export class Wysiwyg {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hyperlink to the content
|
||||
* @returns void
|
||||
*/
|
||||
private static async addHyperlink(editor: TextEditor, selection: Selection) {
|
||||
const hasTextSelection = !selection.isEmpty;
|
||||
const linkText = hasTextSelection ? editor.document.getText(selection) : "";
|
||||
|
||||
const link = await window.showInputBox({
|
||||
title: "WYSIWYG Hyperlink",
|
||||
placeHolder: "Enter the URL",
|
||||
prompt: "Enter the URL",
|
||||
value: linkText,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
const text = await window.showInputBox({
|
||||
title: "WYSIWYG Text",
|
||||
prompt: "Enter the text for the hyperlink",
|
||||
placeHolder: "Enter the text for the hyperlink",
|
||||
value: linkText,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (link) {
|
||||
const txt = `[${text || link}](${link})`;
|
||||
|
||||
if (hasTextSelection) {
|
||||
editor.edit(builder => {
|
||||
builder.replace(selection, txt);
|
||||
});
|
||||
} else {
|
||||
const crntSelection = selection.active;
|
||||
const markerLength = txt.length;
|
||||
const newPosition = crntSelection.with(crntSelection.line, crntSelection.character + markerLength);
|
||||
|
||||
await editor.edit(builder => {
|
||||
builder.insert(newPosition, txt);
|
||||
});
|
||||
|
||||
editor.selection = new Selection(newPosition, newPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the text will be wrapped
|
||||
* @param type
|
||||
@@ -159,7 +216,8 @@ export class Wysiwyg {
|
||||
"Heading 4",
|
||||
"Heading 5",
|
||||
"Heading 6"
|
||||
], {
|
||||
], {
|
||||
title: "Heading Level",
|
||||
canPickMany: false,
|
||||
placeHolder: "Which heading level do you want to insert?",
|
||||
ignoreFocusOut: true
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
export * from './Article';
|
||||
export * from './Backers';
|
||||
export * from './Cache';
|
||||
export * from './Content';
|
||||
export * from './Dashboard';
|
||||
export * from './Diagnostics';
|
||||
export * from './Folders';
|
||||
export * from './Preview';
|
||||
export * from './Project';
|
||||
export * from './Settings';
|
||||
export * from './StatusListener';
|
||||
export * from './Template';
|
||||
export * from './Wysiwyg';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const DefaultFieldValues = {
|
||||
faultyCustomPlaceholder: "<failed to process>"
|
||||
}
|
||||
@@ -56,10 +56,23 @@ 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"),
|
||||
|
||||
// Cache
|
||||
clearCache: getCommandName("cache.clear"),
|
||||
};
|
||||
@@ -85,5 +85,17 @@ export const FrameworkDetectors = [{
|
||||
commands: {
|
||||
start: "npx @11ty/eleventy --serve"
|
||||
}
|
||||
},
|
||||
{
|
||||
framework: {
|
||||
name: "hexo",
|
||||
dist: "public",
|
||||
build: "npx hexo-cli generate"
|
||||
},
|
||||
requiredFiles: ["_config.js"],
|
||||
requiredDependencies: ["hexo"],
|
||||
commands: {
|
||||
start: "npx hexo-cli server"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -3,8 +3,11 @@
|
||||
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";
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const NOTIFICATION_TYPE = {
|
||||
requiredFieldValidation: "requiredFieldValidation",
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const STATIC_FOLDER_PLACEHOLDER = {
|
||||
hexo: {
|
||||
postsFolder: "source/_posts",
|
||||
placeholder: "hexo:post_asset_folder",
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export const TelemetryEvent = {
|
||||
uploadMedia: 'uploadMedia',
|
||||
refreshMedia: 'refreshMedia',
|
||||
deleteMedia: 'deleteMedia',
|
||||
insertContentSnippet: 'insertContentSnippet',
|
||||
insertMediaToContent: 'insertMediaToContent',
|
||||
insertFileToContent: 'insertFileToContent',
|
||||
updateMediaMetadata: 'updateMediaMetadata',
|
||||
@@ -43,4 +44,7 @@ export const TelemetryEvent = {
|
||||
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",
|
||||
@@ -13,4 +11,6 @@ export const CONTEXT = {
|
||||
|
||||
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,7 +10,9 @@ export * from './GeneralCommands';
|
||||
export * from './Links';
|
||||
export * from './LocalStore';
|
||||
export * from './Navigation';
|
||||
export * from './NotificationType';
|
||||
export * from './PreviewCommands';
|
||||
export * from './StaticFolderPlaceholder';
|
||||
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";
|
||||
|
||||
@@ -59,10 +60,14 @@ export const SETTING_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
|
||||
export const SETTING_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
|
||||
export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
|
||||
|
||||
export const SETTING_CONTENT_HIDE_FRONTMATTER = "content.hideFm";
|
||||
export const SETTING_CONTENT_HIDE_FRONTMATTER_MESSAGE = "content.hideFmMessage";
|
||||
|
||||
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
|
||||
|
||||
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
|
||||
export const SETTING_DASHBOARD_CONTENT_PAGINATION = "dashboard.content.pagination";
|
||||
|
||||
export const SETTING_DATA_FILES = "data.files";
|
||||
export const SETTING_DATA_FOLDERS = "data.folders";
|
||||
@@ -75,6 +80,9 @@ export const SETTING_FRAMEWORK_START = "framework.startCommand";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
export const SETTING_GIT_ENABLED = "git.enabled";
|
||||
export const SETTING_GIT_COMMIT_MSG = "git.commitMessage";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum DashboardMessage {
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
insertFile = 'insertFile',
|
||||
createHexoAssetFolder = 'createHexoAssetFolder',
|
||||
|
||||
// Data dashboard
|
||||
getDataEntries = 'getDataEntries',
|
||||
@@ -57,4 +58,5 @@ export enum DashboardMessage {
|
||||
setState = 'setState',
|
||||
runCustomScript = 'runCustomScript',
|
||||
sendTelemetry = 'sendTelemetry',
|
||||
logError = 'logError',
|
||||
}
|
||||
@@ -16,6 +16,9 @@ import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '..';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { UnknownView } from './UnknownView';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { ErrorView } from './ErrorView';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
|
||||
export interface IAppProps {
|
||||
showWelcome: boolean;
|
||||
@@ -68,23 +71,32 @@ export const App: React.FunctionComponent<IAppProps> = ({showWelcome}: React.Pro
|
||||
}
|
||||
|
||||
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 />} />
|
||||
}
|
||||
<ErrorBoundary
|
||||
fallback={(<ErrorView />)}
|
||||
onError={(error: Error, componentStack: string, eventId: string) => {
|
||||
Messenger.send(DashboardMessage.logError, `Event ID: ${eventId}
|
||||
Message: ${error.message}
|
||||
|
||||
{
|
||||
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
|
||||
}
|
||||
Stack: ${componentStack}`);
|
||||
}}>
|
||||
<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 />} />
|
||||
}
|
||||
|
||||
<Route path={`*`} element={<UnknownView />} />
|
||||
</Routes>
|
||||
</main>
|
||||
{
|
||||
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
|
||||
}
|
||||
|
||||
<Route path={`*`} element={<UnknownView />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -12,11 +12,10 @@ export interface IChoiceButtonProps {
|
||||
onClick: () => void;
|
||||
}[];
|
||||
disabled?: boolean;
|
||||
isTemplatesEnabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, isTemplatesEnabled, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
|
||||
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
|
||||
return (
|
||||
<span className="relative z-50 inline-flex shadow-sm rounded-md">
|
||||
<button
|
||||
@@ -29,7 +28,7 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
|
||||
</button>
|
||||
|
||||
{
|
||||
isTemplatesEnabled && (
|
||||
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"
|
||||
@@ -38,7 +37,7 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
|
||||
<MenuItems widthClass={`w-56`}>
|
||||
<MenuItems widthClass={`w-56`} disablePopper>
|
||||
<div className="py-1">
|
||||
{choices.map((choice, idx) => (
|
||||
<MenuItem
|
||||
|
||||
@@ -6,17 +6,27 @@ import { CustomScript, ScriptType } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface IContentActionsProps {
|
||||
title: string;
|
||||
path: string;
|
||||
scripts: CustomScript[] | undefined;
|
||||
listView?: boolean;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen }: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({ title, path, scripts, onOpen, listView }: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
const [ showDeletionAlert, setShowDeletionAlert ] = React.useState(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<any>(null);
|
||||
const [popperElement, setPopperElement] = useState<any>(null);
|
||||
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
|
||||
placement: listView ? 'right-start' : 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
})
|
||||
|
||||
const onView = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
onOpen();
|
||||
@@ -40,7 +50,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
}, [path]);
|
||||
|
||||
const customScriptActions = React.useMemo(() => {
|
||||
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
|
||||
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk && !script.hidden).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
|
||||
@@ -50,37 +60,45 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`group-scope absolute top-6 right-0 flex flex-col space-y-4`}>
|
||||
<div className="flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full p-2 -mt-4">
|
||||
<div className={`${listView ? '' : 'group-scope absolute top-6 right-0'} flex flex-col space-y-4`}>
|
||||
<div className={`flex items-center border border-transparent group-scope-hover:bg-gray-200 dark:group-scope-hover:bg-vulcan-200 group-scope-hover:border-gray-100 dark:group-scope-hover:border-vulcan-50 rounded-full ${listView ? '' : 'p-2 -mt-4'}`}>
|
||||
|
||||
<Menu as="div" className="relative z-10 flex text-left">
|
||||
<div className='hidden group-scope-hover:flex'>
|
||||
<QuickAction
|
||||
title={`View content`}
|
||||
onClick={onView}>
|
||||
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={`Delete content`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
<Menu as="div" className={`relative flex text-left ${listView ? '' : 'z-10'}`}>
|
||||
{
|
||||
!listView && (
|
||||
<div className='hidden group-scope-hover:flex'>
|
||||
<QuickAction
|
||||
title={`View content`}
|
||||
onClick={onView}>
|
||||
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={`Delete content`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div ref={setReferenceElement} className={`flex`}>
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
</div>
|
||||
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
<div className='menu_items__wrapper z-20' ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<MenuItems updatePopper={forceUpdate || undefined} widthClass='w-44' marginTopClass={listView ? '' : ''}>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
|
||||
onClick={(value, e) => onView(e)} />
|
||||
|
||||
<MenuItems widthClass='w-44' marginTopClass='mt-6'>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
|
||||
onClick={(value, e) => onView(e)} />
|
||||
{ customScriptActions }
|
||||
|
||||
{ customScriptActions }
|
||||
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={(value, e) => onDelete(e)} />
|
||||
</MenuItems>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={(value, e) => onDelete(e)} />
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,22 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const draftField = useMemo(() => settings?.draftField, [settings]);
|
||||
|
||||
const escapedTitle = useMemo(() => {
|
||||
if (title && typeof title !== 'string') {
|
||||
return '<invalid title>';
|
||||
}
|
||||
|
||||
return title;
|
||||
}, [title]);
|
||||
|
||||
const escapedDescription = useMemo(() => {
|
||||
if (description && typeof description !== 'string') {
|
||||
return '<invalid description>';
|
||||
}
|
||||
|
||||
return description;
|
||||
}, [description]);
|
||||
|
||||
const openFile = () => {
|
||||
Messenger.send(DashboardMessage.openFile, fmFilePath);
|
||||
@@ -52,20 +68,19 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
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" />
|
||||
<img src={`${pageData[PREVIEW_IMAGE_FIELD]}`} alt={escapedTitle} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
) : (
|
||||
<div className={`flex items-center justify-center bg-whisper-500 dark:bg-vulcan-200 dark:group-hover:bg-vulcan-100`}>
|
||||
<MarkdownIcon className={`h-32 text-vulcan-100 dark:text-whisper-100`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="relative p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
@@ -80,35 +95,48 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
onOpen={openFile} />
|
||||
</div>
|
||||
|
||||
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
|
||||
<button onClick={openFile} className={`text-left block`}><h2 className="mt-2 mb-2 font-bold">{escapedTitle}</h2></button>
|
||||
|
||||
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p>
|
||||
<button onClick={openFile} className={`text-left block`}><p className="text-xs text-vulcan-200 dark:text-whisper-800">{escapedDescription}</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: ${escapedTitle}`}
|
||||
onClick={openFile}>
|
||||
{escapedTitle}
|
||||
</button>
|
||||
|
||||
<ContentActions
|
||||
title={escapedTitle}
|
||||
path={fmFilePath}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
listView />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DateField value={date} />
|
||||
@@ -116,7 +144,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
<div className="col-span-2">
|
||||
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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 { Item } from './Item';
|
||||
import { List } from './List';
|
||||
import usePagination from '../../hooks/usePagination';
|
||||
|
||||
export interface IOverviewProps {
|
||||
pages: Page[];
|
||||
@@ -18,6 +20,24 @@ export interface IOverviewProps {
|
||||
|
||||
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settings}: React.PropsWithChildren<IOverviewProps>) => {
|
||||
const grouping = useRecoilValue(GroupingSelector);
|
||||
const page = useRecoilValue(PageAtom);
|
||||
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
|
||||
|
||||
const pagedPages = useMemo(() => {
|
||||
if (pageSetNr) {
|
||||
return pages.slice(page * pageSetNr, ((page + 1) * pageSetNr));
|
||||
}
|
||||
|
||||
return pages;
|
||||
}, [pages, page, pageSetNr]);
|
||||
|
||||
const groupName = useCallback((groupId, groupedPages) => {
|
||||
if (grouping === GroupOption.Draft) {
|
||||
return `${groupId} (${groupedPages[groupId].length})`;
|
||||
}
|
||||
|
||||
return `${GroupOption[grouping]}: ${groupId} (${groupedPages[groupId].length})`;
|
||||
}, [grouping])
|
||||
|
||||
if (!pages || !pages.length) {
|
||||
return (
|
||||
@@ -58,7 +78,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 +100,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>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Header } from '../Header';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { DataForm } from './DataForm';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { DataFile } from '../../../models/DataFile';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
@@ -27,7 +27,7 @@ export interface IDataViewProps {}
|
||||
export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.PropsWithChildren<IDataViewProps>) => {
|
||||
const [ selectedData, setSelectedData ] = useState<DataFile | null>(null);
|
||||
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
|
||||
const [ dataEntries, setDataEntries ] = useState<any[] | null>(null);
|
||||
const [ dataEntries, setDataEntries ] = useState<any | any[] | null>(null);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const setSchema = (dataFile: DataFile) => {
|
||||
@@ -57,6 +57,12 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
|
||||
|
||||
const onSubmit = useCallback((data: any) => {
|
||||
if (selectedData?.singleEntry) {
|
||||
// Needs to add a single entry
|
||||
updateData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataClone: any[] = Object.assign([], dataEntries);
|
||||
if (selectedIndex !== null && selectedIndex !== undefined) {
|
||||
dataClone[selectedIndex] = data;
|
||||
@@ -102,6 +108,14 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
});
|
||||
}, [selectedData]);
|
||||
|
||||
const dataEntry = useMemo(() => {
|
||||
if (selectedData?.singleEntry) {
|
||||
return dataEntries || {};
|
||||
}
|
||||
|
||||
return (dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null;
|
||||
}, [selectedData, , dataEntries, selectedIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
@@ -171,49 +185,53 @@ 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>
|
||||
{
|
||||
!selectedData.singleEntry && (
|
||||
<div className={`w-1/3 py-6 px-4 flex-1 border-r border-gray-200 dark:border-vulcan-300 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Your {selectedData?.title?.toLowerCase() || ""} data items</h2>
|
||||
|
||||
<div className='py-4'>
|
||||
{
|
||||
(dataEntries && dataEntries.length > 0) ? (
|
||||
<>
|
||||
<Container onSortEnd={onSortEnd} useDragHandle>
|
||||
{
|
||||
(dataEntries || []).map((dataEntry, idx) => (
|
||||
<SortableItem
|
||||
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
|
||||
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
|
||||
index={idx}
|
||||
crntIndex={idx}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
|
||||
onDeleteItem={deleteItem}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Container>
|
||||
<Button
|
||||
className='mt-4'
|
||||
onClick={() => setSelectedIndex(null)}>
|
||||
Add a new entry
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={`flex flex-col items-center justify-center`}>
|
||||
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-2/3 py-6 px-4 overflow-auto`}>
|
||||
<div className='py-4'>
|
||||
{
|
||||
(dataEntries && dataEntries.length > 0) ? (
|
||||
<>
|
||||
<Container onSortEnd={onSortEnd} useDragHandle>
|
||||
{
|
||||
(dataEntries as any[] || []).map((dataEntry, idx) => (
|
||||
<SortableItem
|
||||
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
|
||||
value={dataEntry[selectedData.labelField] || `Entry ${idx+1}`}
|
||||
index={idx}
|
||||
crntIndex={idx}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
|
||||
onDeleteItem={deleteItem}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Container>
|
||||
<Button
|
||||
className='mt-4'
|
||||
onClick={() => setSelectedIndex(null)}>
|
||||
Add a new entry
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={`flex flex-col items-center justify-center`}>
|
||||
<p className={`text-gray-500 dark:text-whisper-900`}>No {selectedData.title.toLowerCase()} data entries found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={`${selectedData.singleEntry ? "w-full" : "w-2/3"} py-6 px-4 overflow-auto`}>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>Create or modify your {selectedData.title.toLowerCase()} data</h2>
|
||||
{
|
||||
selectedData ? (
|
||||
<DataForm
|
||||
schema={selectedData?.schema}
|
||||
model={(dataEntries && selectedIndex !== null && selectedIndex !== undefined) ? dataEntries[selectedIndex] : null}
|
||||
model={dataEntry}
|
||||
onSubmit={onSubmit}
|
||||
onClear={() => setSelectedIndex(null)} />
|
||||
) : (
|
||||
@@ -234,7 +252,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
<DatabaseIcon className='w-32 h-32' />
|
||||
<p className='text-3xl mt-2'>No data files found</p>
|
||||
<p className='text-xl mt-4'>
|
||||
<a className={`text-teal-700 hover:text-teal-900`} href={`https://frontmatter.codes/docs/dashboard#data-files-view`} title={`Read read more to get started using data files`}>Read read more to get started using data files</a></p>
|
||||
<a className={`text-teal-700 hover:text-teal-900`} href={`https://frontmatter.codes/docs/dashboard#data-files-view`} title={`Read more to get started using data files`}>Read more to get started using data files</a></p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ExclamationIcon } from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IErrorViewProps {}
|
||||
|
||||
export const ErrorView: React.FunctionComponent<IErrorViewProps> = (props: React.PropsWithChildren<IErrorViewProps>) => {
|
||||
return (
|
||||
<main className={`h-full w-full flex flex-col justify-center items-center space-y-2`}>
|
||||
<ExclamationIcon className="w-24 h-24 text-red-500" />
|
||||
<p className='text-xl'>Sorry, something went wrong.</p>
|
||||
<p className='text-base'>Please close the dashboard and try again.</p>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { SearchIcon, XCircleIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IFilterInputProps {
|
||||
placeholder: string;
|
||||
value: string;
|
||||
isReady: boolean;
|
||||
autoFocus: boolean;
|
||||
onReset?: () => void;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const FilterInput: React.FunctionComponent<IFilterInputProps> = ({ placeholder, value, isReady, autoFocus, onReset, onChange}: React.PropsWithChildren<IFilterInputProps>) => {
|
||||
return (
|
||||
<div className="flex space-x-4 flex-1">
|
||||
<div className="min-w-0">
|
||||
<label htmlFor="search" className="sr-only">Search</label>
|
||||
<div className="relative flex justify-center">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none appearance-none disabled:opacity-50`}
|
||||
placeholder={placeholder || "Search"}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value)}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
|
||||
{
|
||||
value && onReset && (
|
||||
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||
<XCircleIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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, 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';
|
||||
@@ -21,7 +21,12 @@ import { CustomScript } from '../../../models';
|
||||
import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '../..';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { SyncButton } from './SyncButton';
|
||||
import { Pagination } from './Pagination';
|
||||
import { GroupOption } from '../../constants/GroupOption';
|
||||
import usePagination from '../../hooks/usePagination';
|
||||
import { PaginationStatus } from './PaginationStatus';
|
||||
|
||||
export interface IHeaderProps {
|
||||
header?: React.ReactNode;
|
||||
@@ -34,12 +39,14 @@ export interface IHeaderProps {
|
||||
folders?: string[];
|
||||
}
|
||||
|
||||
export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPages, folders, settings }: React.PropsWithChildren<IHeaderProps>) => {
|
||||
export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPages, settings }: React.PropsWithChildren<IHeaderProps>) => {
|
||||
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
|
||||
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
|
||||
const grouping = useRecoilValue(GroupingSelector);
|
||||
const resetSorting = useResetRecoilState(SortingAtom);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
|
||||
|
||||
const createContent = () => {
|
||||
Messenger.send(DashboardMessage.createContent);
|
||||
@@ -72,6 +79,38 @@ 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);
|
||||
@@ -108,33 +147,13 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
|
||||
|
||||
<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}
|
||||
isTemplatesEnabled={settings?.dashboardState?.contents?.templatesEnabled || undefined}
|
||||
choices={choiceOptions}
|
||||
onClick={createContent}
|
||||
disabled={!settings?.initialized} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,6 +181,16 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
|
||||
|
||||
<Sorting view={NavigationType.Contents} />
|
||||
</div>
|
||||
|
||||
{
|
||||
(pageSetNr > 0) && (totalPages || 0) > pageSetNr && (!grouping || grouping === GroupOption.none) && (
|
||||
<div className={`px-4 flex justify-between py-2 border-b border-gray-300 dark:border-vulcan-100`}>
|
||||
<PaginationStatus totalPages={totalPages || 0} />
|
||||
|
||||
<Pagination totalPages={totalPages || 0} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { LIMIT } from '../../hooks/useMedia';
|
||||
import { MediaTotalSelector, PageAtom } from '../../state';
|
||||
import usePagination from '../../hooks/usePagination';
|
||||
import { MediaTotalSelector, PageAtom, SettingsAtom } 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 Pagination: React.FunctionComponent<IPaginationProps> = ({ totalPages }: React.PropsWithChildren<IPaginationProps>) => {
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const { pageSetNr, totalPagesNr } = usePagination(settings?.dashboardState.contents.pagination, totalPages, totalMedia);
|
||||
|
||||
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
|
||||
|
||||
const getButtons = (): number[] => {
|
||||
const getButtons = useCallback((): number[] => {
|
||||
const maxButtons = 5;
|
||||
const buttons: number[] = [];
|
||||
const start = page - maxButtons;
|
||||
const end = page + maxButtons;
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i >= 0 && i <= totalPages) {
|
||||
if (i >= 0 && i <= totalPagesNr) {
|
||||
buttons.push(i);
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
}, [page, totalPagesNr]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [pageSetNr]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
|
||||
@@ -54,19 +65,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 >= totalPagesNr}
|
||||
onClick={() => setPage(page + 1)} />
|
||||
|
||||
<PaginationButton
|
||||
title="Last"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(totalPages)} />
|
||||
disabled={page >= totalPagesNr}
|
||||
onClick={() => setPage(totalPagesNr)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MediaTotalSelector, PageAtom } from '../../state';
|
||||
import { LIMIT } from '../../hooks/useMedia';
|
||||
import usePagination from '../../hooks/usePagination';
|
||||
import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state';
|
||||
|
||||
export interface IPaginationStatusProps {}
|
||||
export interface IPaginationStatusProps {
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
|
||||
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = ({ totalPages }: React.PropsWithChildren<IPaginationStatusProps>) => {
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const page = useRecoilValue(PageAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const { pageSetNr, totalItems } = usePagination(settings?.dashboardState.contents.pagination, totalPages || 0, totalMedia);
|
||||
|
||||
const getTotalPage = () => {
|
||||
const mediaItems = ((page + 1) * LIMIT);
|
||||
if (totalMedia < mediaItems) {
|
||||
return totalMedia;
|
||||
const totelItemsOnPage = useMemo(() => {
|
||||
const items = ((page + 1) * pageSetNr);
|
||||
if (totalItems < items) {
|
||||
return totalItems;
|
||||
}
|
||||
return mediaItems;
|
||||
};
|
||||
return totalItems;
|
||||
}, [page, totalMedia, pageSetNr]);
|
||||
|
||||
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{' '}
|
||||
<span className="font-medium">{totalMedia}</span> results
|
||||
Showing <span className="font-medium">{(page * pageSetNr) + 1}</span> to <span className="font-medium">{totelItemsOnPage}</span> of{' '}
|
||||
<span className="font-medium">{totalItems}</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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const Tabs: React.FunctionComponent<ITabsProps> = ({ onNavigate }: React.
|
||||
<Tab
|
||||
navigationType={NavigationType.Taxonomy}
|
||||
onNavigate={onNavigate}>
|
||||
<TagIcon className={`h-6 w-auto mr-2`} /><span>Taxonomy</span>
|
||||
<TagIcon className={`h-6 w-auto mr-2`} /><span>Taxonomies</span>
|
||||
</Tab>
|
||||
</li>
|
||||
</FeatureFlag>
|
||||
|
||||
@@ -2,16 +2,34 @@ import * as React from 'react';
|
||||
import {FolderAddIcon, LightningBoltIcon} from '@heroicons/react/outline';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SelectedMediaFolderAtom, SettingsSelector } from '../../state';
|
||||
import { AllContentFoldersAtom, AllStaticFoldersAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ChoiceButton } from '../ChoiceButton';
|
||||
import { CustomScript, ScriptType } from '../../../models';
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from '../../../constants';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { extname } from 'path';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
|
||||
export interface IFolderCreationProps {}
|
||||
|
||||
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const allStaticFolders = useRecoilValue(AllStaticFoldersAtom);
|
||||
const allContentFolders = useRecoilValue(AllContentFoldersAtom);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
const hexoAssetFolderPath = useMemo(() => {
|
||||
const path = viewData?.data?.filePath?.replace(extname(viewData.data.filePath), '');
|
||||
return parseWinPath(path);
|
||||
}, [viewData?.data?.filePath]);
|
||||
|
||||
const onAssetFolderCreation = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.createHexoAssetFolder, {
|
||||
hexoAssetFolderPath
|
||||
});
|
||||
}, [hexoAssetFolderPath]);
|
||||
|
||||
const onFolderCreation = () => {
|
||||
Messenger.send(DashboardMessage.createMediaFolder, {
|
||||
@@ -23,10 +41,34 @@ 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 isHexoPostAssetsEnabled = useMemo(() => {
|
||||
if (allStaticFolders && allContentFolders && settings?.staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder && hexoAssetFolderPath) {
|
||||
return ![...allStaticFolders, ...allContentFolders].some(f => f.startsWith(hexoAssetFolderPath));
|
||||
}
|
||||
return false;
|
||||
}, [settings?.staticFolder, allStaticFolders, allContentFolders, hexoAssetFolderPath]);
|
||||
|
||||
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder && !script.hidden);
|
||||
|
||||
const renderPostAssetsButton = useMemo(() => {
|
||||
if (isHexoPostAssetsEnabled) {
|
||||
return (
|
||||
<button
|
||||
className={`mr-2 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 post asset folder`}
|
||||
onClick={onAssetFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create post asset folder</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [isHexoPostAssetsEnabled]);
|
||||
|
||||
if (scripts.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-1 justify-end">
|
||||
{ renderPostAssetsButton }
|
||||
<ChoiceButton
|
||||
title={`Create new folder`}
|
||||
choices={scripts.map(s => ({
|
||||
@@ -42,6 +84,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 justify-end">
|
||||
{ renderPostAssetsButton }
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Menu } from '@headlessui/react';
|
||||
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TerminalIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
|
||||
import { basename, dirname } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { CustomScript } from '../../../helpers/CustomScript';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
@@ -18,6 +18,8 @@ import { QuickAction } from '../Menu/QuickAction';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { InfoDialog } from '../Modals/InfoDialog';
|
||||
import { DetailsSlideOver } from './DetailsSlideOver';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { MediaSnippetForm } from './MediaSnippetForm';
|
||||
|
||||
export interface IItemProps {
|
||||
media: MediaInfo;
|
||||
@@ -25,17 +27,31 @@ export interface IItemProps {
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWithChildren<IItemProps>) => {
|
||||
const [ , setLightbox ] = useRecoilState(LightboxAtom);
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
const [ showForm, setShowForm ] = React.useState(false);
|
||||
const [ showSnippetSelection, setShowSnippetSelection ] = React.useState(false);
|
||||
const [ showDetails, setShowDetails ] = React.useState(false);
|
||||
const [ caption, setCaption ] = React.useState(media.caption);
|
||||
const [ alt, setAlt ] = React.useState(media.alt);
|
||||
const [ filename, setFilename ] = React.useState<string | null>(null);
|
||||
const [ showAlert, setShowAlert ] = useState(false);
|
||||
const [ showForm, setShowForm ] = useState(false);
|
||||
const [ showSnippetSelection, setShowSnippetSelection ] = useState(false);
|
||||
const [ snippet, setSnippet ] = useState<Snippet | undefined>(undefined);
|
||||
const [ showDetails, setShowDetails ] = useState(false);
|
||||
const [ showSnippetFormDialog, setShowSnippetFormDialog ] = useState(false);
|
||||
const [ mediaData, setMediaData ] = useState<any | undefined>(undefined);
|
||||
const [ caption, setCaption ] = useState(media.caption);
|
||||
const [ alt, setAlt ] = useState(media.alt);
|
||||
const [ filename, setFilename ] = useState<string | null>(null);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
const hasViewData = useMemo(() => {
|
||||
return viewData?.data?.filePath !== undefined;
|
||||
}, [viewData]);
|
||||
|
||||
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 [];
|
||||
@@ -45,6 +61,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
return keys.filter(key => (settings.snippets || {})[key].isMediaSnippet).map(key => ({ title: key, ...(settings.snippets || {})[key]}));
|
||||
}, [settings]);
|
||||
|
||||
const showMediaSnippet = useMemo(() => {
|
||||
return viewData?.data?.position && mediaSnippets.length > 0;
|
||||
}, [viewData, mediaSnippets]);
|
||||
|
||||
const getFolder = () => {
|
||||
if (settings?.wsFolder && media.fsPath) {
|
||||
let relPath = media.fsPath.split(settings.wsFolder).pop();
|
||||
@@ -128,13 +148,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}
|
||||
}, [mediaSnippets]);
|
||||
|
||||
/**
|
||||
* Process the snippet
|
||||
*/
|
||||
const processSnippet = useCallback((snippet: Snippet) => {
|
||||
setShowSnippetSelection(false);
|
||||
|
||||
const relPath = getRelPath();
|
||||
|
||||
const fieldData = {
|
||||
mediaUrl: parseWinPath(relPath) || "",
|
||||
mediaUrl: (parseWinPath(relPath) || "").replace(/ /g, "%20"),
|
||||
alt: alt || "",
|
||||
caption: caption || "",
|
||||
title: media.title || "",
|
||||
@@ -143,7 +166,24 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
mediaHeight: media?.dimensions?.height?.toString() || "",
|
||||
};
|
||||
|
||||
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
|
||||
if (!snippet.fields || snippet.fields.length === 0) {
|
||||
setShowSnippetFormDialog(false);
|
||||
setMediaData(undefined);
|
||||
|
||||
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
|
||||
insertMediaSnippetToArticle(output);
|
||||
} else {
|
||||
setSnippet(snippet);
|
||||
setShowSnippetFormDialog(true);
|
||||
setMediaData(fieldData);
|
||||
}
|
||||
}, [alt, caption, media, settings, viewData, mediaSnippets]);
|
||||
|
||||
/**
|
||||
* Insert the media snippet
|
||||
*/
|
||||
const insertMediaSnippetToArticle = useCallback((output: string) => {
|
||||
const relPath = getRelPath();
|
||||
|
||||
Messenger.send(DashboardMessage.insertMedia, {
|
||||
relPath: parseWinPath(relPath) || "",
|
||||
@@ -152,7 +192,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
position: viewData?.data?.position || null,
|
||||
snippet: output
|
||||
});
|
||||
}, [alt, caption, media, settings, viewData, mediaSnippets]);
|
||||
}, [viewData]);
|
||||
|
||||
const deleteMedia = () => {
|
||||
setShowAlert(true);
|
||||
@@ -225,7 +265,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
};
|
||||
|
||||
const customScriptActions = () => {
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile && !script.hidden).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
|
||||
@@ -259,7 +299,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const extension = path.split('.').pop();
|
||||
|
||||
let icon = <DocumentIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
|
||||
console.log(media);
|
||||
|
||||
if (isImageFile) {
|
||||
return <PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
@@ -293,6 +332,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
return null;
|
||||
}, [media]);
|
||||
|
||||
const clearFormData = () => {
|
||||
setShowSnippetFormDialog(false);
|
||||
setSnippet(undefined);
|
||||
setMediaData(undefined);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (media.alt !== alt) {
|
||||
setAlt(media.alt);
|
||||
@@ -312,10 +357,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}
|
||||
}, [media.fsPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasViewData) {
|
||||
clearFormData();
|
||||
}
|
||||
}, [viewData, hasViewData]);
|
||||
|
||||
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 h-48 ${isImageFile ? "cursor-pointer" : "cursor-default"}`} onClick={openLightbox}>
|
||||
<button className={`group-scope 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={hasViewData ? undefined : openLightbox}>
|
||||
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
|
||||
{
|
||||
renderMediaIcon
|
||||
@@ -324,6 +375,32 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
|
||||
{ renderMedia }
|
||||
</div>
|
||||
{
|
||||
hasViewData && (
|
||||
<div className={`hidden group-scope-hover:flex absolute top-0 right-0 bottom-0 left-0 items-center bg-vulcan-500 bg-opacity-70 justify-center`}>
|
||||
<div className={`h-full ${showMediaSnippet ? 'w-1/3' : 'w-full'} flex items-center justify-center`}>
|
||||
<button
|
||||
title='Insert image'
|
||||
className={`text-gray-300 hover:text-teal-600 h-1/3`}
|
||||
onClick={insertToArticle}>
|
||||
<PlusIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && (
|
||||
<div className={`h-full w-1/3 flex items-center justify-center`}>
|
||||
<button
|
||||
title='Insert snippet'
|
||||
className={`text-gray-300 hover:text-teal-600 h-1/3`}
|
||||
onClick={insertSnippet}>
|
||||
<CodeIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
<div className={`relative py-4 pl-4 pr-12`}>
|
||||
<div className={`group-scope absolute top-4 right-4 flex flex-col space-y-4`}>
|
||||
@@ -381,59 +458,63 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</QuickAction>
|
||||
</div>
|
||||
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
<div ref={setReferenceElement} className={`flex`}>
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
</div>
|
||||
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={(
|
||||
<div className='flex items-center'>
|
||||
<PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span>
|
||||
</div>
|
||||
)}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
<div className='menu_items__wrapper z-20' ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={(
|
||||
<div className='flex items-center'>
|
||||
<PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span>
|
||||
</div>
|
||||
)}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
|
||||
onClick={insertToArticle} />
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image</span></div>}
|
||||
onClick={insertToArticle} />
|
||||
|
||||
{
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
|
||||
<MenuItem
|
||||
key={idx}
|
||||
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
|
||||
onClick={() => processSnippet(snippet)} />
|
||||
))
|
||||
}
|
||||
{
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
|
||||
<MenuItem
|
||||
key={idx}
|
||||
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
|
||||
onClick={() => processSnippet(snippet)} />
|
||||
))
|
||||
}
|
||||
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
title={(
|
||||
<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() }
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
title={(
|
||||
<div className='flex items-center'>
|
||||
<ClipboardIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Copy media path</span>
|
||||
</div>
|
||||
)}
|
||||
onClick={copyToClipboard} />
|
||||
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
)
|
||||
}
|
||||
{ customScriptActions() }
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
|
||||
onClick={revealMedia} />
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
<MenuItem
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -529,6 +610,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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,15 +9,16 @@ import { Item } from './Item';
|
||||
import { Lightbox } from './Lightbox';
|
||||
import { List } from './List';
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { FolderItem } from './FolderItem';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { STATIC_FOLDER_PLACEHOLDER, TelemetryEvent } from '../../../constants';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { basename, extname, join } from 'path';
|
||||
import { MediaInfo } from '../../../models';
|
||||
|
||||
export interface IMediaProps {}
|
||||
|
||||
@@ -29,9 +30,20 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
const folders = useRecoilValue(MediaFoldersAtom);
|
||||
const loading = useRecoilValue(LoadingAtom);
|
||||
|
||||
const contentFolders = React.useMemo(() => {
|
||||
// Check if content allows page bundle
|
||||
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
|
||||
const currentStaticFolder = useMemo(() => {
|
||||
if (settings?.staticFolder) {
|
||||
let staticFolderPath = join('/', settings?.staticFolder || '', '/');
|
||||
if (settings?.staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
staticFolderPath = join('/', STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder, '/');
|
||||
}
|
||||
return staticFolderPath;
|
||||
}
|
||||
return;
|
||||
}, [settings?.staticFolder])
|
||||
|
||||
const contentFolders = useMemo(() => {
|
||||
// Check if content allows page bundle or if Hexo post assets are enabled
|
||||
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle && settings?.staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -46,17 +58,26 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
}
|
||||
|
||||
return groupedFolders;
|
||||
}, [folders, viewData, settings?.contentFolders]);
|
||||
}, [folders, viewData, settings?.contentFolders, settings?.staticFolder]);
|
||||
|
||||
const publicFolders = React.useMemo(() => {
|
||||
return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
}, [folders, viewData, settings?.staticFolder]);
|
||||
const publicFolders = useMemo(() => {
|
||||
if (currentStaticFolder && settings?.staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
return folders.filter(f => parseWinPath(f).includes(currentStaticFolder));
|
||||
}
|
||||
|
||||
const allMedia = React.useMemo(() => {
|
||||
let mediaFiles = media;
|
||||
return undefined;
|
||||
}, [folders, viewData, currentStaticFolder, settings?.staticFolder]);
|
||||
|
||||
const allMedia = useMemo(() => {
|
||||
let mediaFiles: MediaInfo[] = Object.assign([], media);
|
||||
// Check if content allows page bundle
|
||||
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
|
||||
mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
if (currentStaticFolder && viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
|
||||
mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(currentStaticFolder));
|
||||
}
|
||||
|
||||
// Filter if Hexo post folder
|
||||
if (currentStaticFolder && settings?.staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
mediaFiles = mediaFiles.filter(m => parseWinPath(m.fsPath).includes(currentStaticFolder));
|
||||
}
|
||||
|
||||
if (viewData && viewData.data && viewData.data.type === "file" && viewData.data.fileExtensions && viewData.data.fileExtensions.length > 0) {
|
||||
@@ -70,7 +91,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
}
|
||||
|
||||
return mediaFiles;
|
||||
}, [media, viewData, settings?.staticFolder]);
|
||||
}, [media, viewData, currentStaticFolder, settings?.staticFolder]);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
acceptedFiles.forEach((file) => {
|
||||
@@ -120,7 +141,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
|
||||
<UploadIcon className={`h-32`} />
|
||||
<p className={`text-xl max-w-md text-center`}>
|
||||
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
|
||||
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${currentStaticFolder || "public"} folder.`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
@@ -132,7 +153,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
|
||||
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & drop new files.</p>
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & drop new files by holding your [shift] key.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -147,7 +168,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
<List gap={0}>
|
||||
{
|
||||
group.folders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
<FolderItem key={folder} folder={folder} staticFolder={currentStaticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
@@ -160,13 +181,13 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
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>)
|
||||
contentFolders && contentFolders.length > 0 && (<h2 className='text-lg mb-8'>Public folder{currentStaticFolder && (<span>: <b>{currentStaticFolder}</b></span>)}</h2>)
|
||||
}
|
||||
|
||||
<List gap={0}>
|
||||
{
|
||||
publicFolders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
<FolderItem key={folder} folder={folder} staticFolder={currentStaticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
|
||||
@@ -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-20 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} ${ disablePopper ? "origin-top-right absolute right-0 z-20" : ""} rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { CodeIcon, DocumentTextIcon, DotsHorizontalIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { CodeIcon, DocumentTextIcon, DotsHorizontalIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -7,7 +7,6 @@ import { FeatureFlag } from '../../../components/features/FeatureFlag';
|
||||
import { FEATURE_FLAG } from '../../../constants';
|
||||
import { SnippetParser } from '../../../helpers/SnippetParser';
|
||||
import { Snippet, Snippets } from '../../../models';
|
||||
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { QuickAction } from '../Menu';
|
||||
@@ -17,11 +16,11 @@ import { NewForm } from './NewForm';
|
||||
import SnippetForm, { SnippetFormHandle } from './SnippetForm';
|
||||
|
||||
export interface IItemProps {
|
||||
title: string;
|
||||
snippetKey: string;
|
||||
snippet: Snippet;
|
||||
}
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: React.PropsWithChildren<IItemProps>) => {
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ snippetKey, snippet }: React.PropsWithChildren<IItemProps>) => {
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
@@ -51,13 +50,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
setMediaSnippet(false);
|
||||
};
|
||||
|
||||
const showFile = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.openFile, snippet.sourcePath);
|
||||
}, [ snippet ]);
|
||||
|
||||
const onOpenEdit = useCallback(() => {
|
||||
setSnippetTitle(title);
|
||||
setSnippetTitle(snippet.title || snippetKey);
|
||||
setSnippetDescription(snippet.description);
|
||||
setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`));
|
||||
setShowEditDialog(true);
|
||||
setMediaSnippet(!!snippet.isMediaSnippet);
|
||||
}, [snippet]);
|
||||
}, [snippet, snippetKey]);
|
||||
|
||||
const onSnippetUpdate = useCallback(() => {
|
||||
if (!snippetTitle || !snippetOriginalBody) {
|
||||
@@ -65,10 +68,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
return;
|
||||
}
|
||||
|
||||
const snippets: Snippets = Object.assign({}, settings?.snippets || {});
|
||||
let snippets: Snippets = Object.assign({}, settings?.snippets || {});
|
||||
const snippetLines = snippetOriginalBody.split("\n");
|
||||
|
||||
const crntSnippet = Object.assign({}, snippets[title]);
|
||||
const crntSnippet = Object.assign({}, snippets[snippetKey]);
|
||||
|
||||
const fields = SnippetParser.getFields(snippetLines, crntSnippet.fields || [], crntSnippet?.openingTags, crntSnippet?.closingTags);
|
||||
|
||||
@@ -84,27 +87,33 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
snippetContents.isMediaSnippet = true;
|
||||
}
|
||||
|
||||
// Check if new or update
|
||||
if (title === snippetTitle) {
|
||||
snippets[title] = snippetContents;
|
||||
// Check if there is a title set in the snippet
|
||||
if (snippet.title) {
|
||||
snippetContents.title = snippetTitle;
|
||||
snippets[snippetKey] = snippetContents;
|
||||
} else {
|
||||
delete snippets[title];
|
||||
snippets[snippetTitle] = snippetContents;
|
||||
// Check if new or update
|
||||
if (snippetKey === snippetTitle) {
|
||||
snippets[snippetKey] = snippetContents;
|
||||
} else {
|
||||
delete snippets[snippetKey];
|
||||
snippets[snippetTitle] = snippetContents;
|
||||
}
|
||||
}
|
||||
|
||||
Messenger.send(DashboardMessage.updateSnippet, { snippets });
|
||||
|
||||
reset();
|
||||
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]);
|
||||
}, [settings?.snippets, snippetKey, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
const snippets = Object.assign({}, settings?.snippets || {});
|
||||
delete snippets[title];
|
||||
delete snippets[snippetKey];
|
||||
|
||||
Messenger.send(DashboardMessage.updateSnippet, { snippets });
|
||||
|
||||
setShowAlert(false);
|
||||
}, [settings?.snippets, title]);
|
||||
}, [settings?.snippets, snippetKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -116,7 +125,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
<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}
|
||||
{snippet.title || snippetKey}
|
||||
</h2>
|
||||
|
||||
<FeatureFlag
|
||||
@@ -161,17 +170,29 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
)
|
||||
}
|
||||
|
||||
<QuickAction
|
||||
title={`Edit snippet`}
|
||||
onClick={onOpenEdit}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
{
|
||||
!snippet.sourcePath ? (
|
||||
<>
|
||||
<QuickAction
|
||||
title={`Edit snippet`}
|
||||
onClick={onOpenEdit}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={`Delete snippet`}
|
||||
onClick={() => setShowAlert(true)}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
<QuickAction
|
||||
title={`Delete snippet`}
|
||||
onClick={() => setShowAlert(true)}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</>
|
||||
) : (
|
||||
<QuickAction
|
||||
title={`View snippet file`}
|
||||
onClick={showFile}>
|
||||
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,8 +204,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
{
|
||||
showInsertDialog && (
|
||||
<FormDialog
|
||||
title={`Insert snippet: ${title}`}
|
||||
description={`Insert the ${title.toLowerCase()} snippet into the current article`}
|
||||
title={`Insert snippet: ${(snippet.title || snippetKey)}`}
|
||||
description={`Insert the ${(snippet.title || snippetKey).toLowerCase()} snippet into the current article`}
|
||||
isSaveDisabled={!insertToContent}
|
||||
trigger={insertToArticle}
|
||||
dismiss={() => setShowInsertDialog(false)}
|
||||
@@ -203,8 +224,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
{
|
||||
showEditDialog && (
|
||||
<FormDialog
|
||||
title={`Edit snippet: ${title}`}
|
||||
description={`Edit the ${title.toLowerCase()} snippet`}
|
||||
title={`Edit snippet: ${(snippet.title || snippetKey)}`}
|
||||
description={`Edit the ${(snippet.title || snippetKey).toLowerCase()} snippet`}
|
||||
isSaveDisabled={!snippetTitle || !snippetOriginalBody}
|
||||
trigger={onSnippetUpdate}
|
||||
dismiss={reset}
|
||||
@@ -228,8 +249,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
{
|
||||
showAlert && (
|
||||
<Alert
|
||||
title={`Delete snippet: ${title}`}
|
||||
description={`Are you sure you want to delete the ${title.toLowerCase()} snippet?`}
|
||||
title={`Delete snippet: ${(snippet.title || snippetKey)}`}
|
||||
description={`Are you sure you want to delete the ${(snippet.title || snippetKey).toLowerCase()} snippet?`}
|
||||
okBtnText={`Delete`}
|
||||
cancelBtnText={`Cancel`}
|
||||
dismiss={() => setShowAlert(false)}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TelemetryEvent } from '../../../constants/TelemetryEvent';
|
||||
import { SnippetParser } from '../../../helpers/SnippetParser';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { FilterInput } from '../Header/FilterInput';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { FormDialog } from '../Modals/FormDialog';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
@@ -26,9 +27,20 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
const [ snippetBody, setSnippetBody ] = useState<string>('');
|
||||
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
|
||||
const [ mediaSnippet, setMediaSnippet ] = useState(false);
|
||||
const [ snippetFilter, setSnippetFilter ] = useState<string>('');
|
||||
|
||||
const snippets = settings?.snippets || {};
|
||||
const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]);
|
||||
const snippetKeys = useMemo(() => {
|
||||
const allSnippetKeys = Object.keys(snippets).sort((a, b) => a.localeCompare(b));
|
||||
return allSnippetKeys.filter((key) => {
|
||||
const value = snippetFilter.toLowerCase();
|
||||
const keyValue = key.toLowerCase();
|
||||
const descriptionValue = snippets[key].description?.toLowerCase() || '';
|
||||
|
||||
// Contains in key or description, values included in key are ranked higher (sort and fuzzy search)
|
||||
return keyValue.includes(value) || descriptionValue.includes(value);
|
||||
});
|
||||
}, [settings?.snippets, snippetFilter]);
|
||||
|
||||
const onSnippetAdd = useCallback(() => {
|
||||
if (!snippetTitle || !snippetBody) {
|
||||
@@ -70,6 +82,15 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
|
||||
aria-label="snippets header"
|
||||
>
|
||||
<FilterInput
|
||||
placeholder='Search'
|
||||
isReady={true}
|
||||
autoFocus={true}
|
||||
value={snippetFilter}
|
||||
onChange={(value: string) => setSnippetFilter(value)}
|
||||
onReset={() => setSnippetFilter('')}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 justify-end">
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
@@ -99,7 +120,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
snippetKeys.map((snippetKey: any, index: number) => (
|
||||
<Item
|
||||
key={index}
|
||||
title={snippetKey}
|
||||
snippetKey={snippetKey}
|
||||
snippet={snippets[snippetKey]} />
|
||||
))
|
||||
}
|
||||
|
||||
@@ -163,10 +163,10 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings.crntFramework) {
|
||||
setFramework(settings.crntFramework);
|
||||
if (settings.crntFramework || settings.framework?.name) {
|
||||
setFramework(settings.crntFramework || settings.framework?.name || null);
|
||||
}
|
||||
}, [settings.crntFramework]);
|
||||
}, [settings.crntFramework, settings.framework]);
|
||||
|
||||
return (
|
||||
<nav aria-label="Progress">
|
||||
|
||||
@@ -41,7 +41,6 @@ export const TaxonomyLookup: React.FunctionComponent<ITaxonomyLookupProps> = ({
|
||||
}).length;
|
||||
}, [taxonomy, value, pages, settings?.contentTypes]);
|
||||
|
||||
|
||||
const onNavigate = useCallback(() => {
|
||||
if (total) {
|
||||
navigate(`${routePaths.contents}?taxonomy=${taxonomy}&value=${value}`);
|
||||
|
||||
@@ -38,6 +38,9 @@ export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = (
|
||||
|
||||
// Alphabetically sort the items
|
||||
crntItems = Object.assign([], crntItems).sort((a: string, b: string) => {
|
||||
a = a || "";
|
||||
b = b || "";
|
||||
|
||||
if (a.toLowerCase() < b.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaInfo, MediaPaths } from '../../models';
|
||||
import { DashboardCommand } from '../DashboardCommand';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SearchSelector, SelectedMediaFolderAtom } from '../state';
|
||||
import { AllContentFoldersAtom, AllStaticFoldersAtom, LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../state';
|
||||
import Fuse from 'fuse.js';
|
||||
import usePagination from './usePagination';
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
|
||||
keys: [
|
||||
@@ -18,21 +19,23 @@ const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
|
||||
includeScore: true
|
||||
};
|
||||
|
||||
export const LIMIT = 16;
|
||||
|
||||
export default function useMedia() {
|
||||
const [ media, setMedia ] = useState<MediaInfo[]>([]);
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const page = useRecoilValue(PageAtom);
|
||||
const [ searchedMedia, setSearchedMedia ] = useState<MediaInfo[]>([]);
|
||||
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
|
||||
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
|
||||
const [ , setAllContentFolders ] = useRecoilState(AllContentFoldersAtom);
|
||||
const [ , setAllStaticFolders ] = useRecoilState(AllStaticFoldersAtom);
|
||||
const [ , setLoading ] = useRecoilState(LoadingAtom);
|
||||
const search = useRecoilValue(SearchAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
|
||||
|
||||
const getMedia = useCallback(() => {
|
||||
return searchedMedia.slice(page * LIMIT, ((page + 1) * LIMIT));
|
||||
}, [searchedMedia, page]);
|
||||
return searchedMedia.slice(page * pageSetNr, ((page + 1) * pageSetNr));
|
||||
}, [searchedMedia, page, pageSetNr]);
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
|
||||
if (message.data.command === DashboardCommand.media) {
|
||||
@@ -43,6 +46,8 @@ export default function useMedia() {
|
||||
setFolders(data.folders);
|
||||
setSelectedFolder(data.selectedFolder);
|
||||
setSearchedMedia(data.media);
|
||||
setAllContentFolders(data.allContentFolders);
|
||||
setAllStaticFolders(data.allStaticfolders);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,8 +63,9 @@ export default function useMedia() {
|
||||
return;
|
||||
}
|
||||
|
||||
setTotal(media.length);
|
||||
setSearchedMedia(media);
|
||||
}, [search]);
|
||||
}, [search, media]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen<MediaPaths>(messageListener);
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function useMessages() {
|
||||
const [, setView] = useRecoilState(DashboardViewAtom);
|
||||
const [, setSearchReady] = useRecoilState(SearchReadyAtom);
|
||||
|
||||
Messenger.listen((event: MessageEvent<EventData<any>>) => {
|
||||
const messageListener = (event: MessageEvent<EventData<any>>) => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.command) {
|
||||
@@ -53,14 +53,20 @@ export default function useMessages() {
|
||||
setMode(message.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
setLoading(true);
|
||||
Messenger.send(DashboardMessage.getViewType);
|
||||
Messenger.send(DashboardMessage.getTheme);
|
||||
Messenger.send(DashboardMessage.getData);
|
||||
Messenger.send(DashboardMessage.getMode);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, ['']);
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [ pageItems, setPageItems ] = useState<Page[]>([]);
|
||||
const [ sortedPages, setSortedPages ] = useState<Page[]>([]);
|
||||
const [ sorting, setSorting ] = useRecoilState(SortingAtom);
|
||||
const [ tabInfo , setTabInfo ] = useRecoilState(TabInfoAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
@@ -22,8 +23,10 @@ export default function usePages(pages: Page[]) {
|
||||
const tag = useRecoilValue(TagSelector);
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
|
||||
const processPages = useCallback((searchedPages: Page[]) => {
|
||||
const draftField = settings?.draftField;
|
||||
/**
|
||||
* Process all the pages by applying the sorting, filtering and searching.
|
||||
*/
|
||||
const processPages = useCallback((searchedPages: Page[], fullProcess: boolean = true) => {
|
||||
const framework = settings?.crntFramework;
|
||||
|
||||
// Filter the pages
|
||||
@@ -67,6 +70,8 @@ export default function usePages(pages: Page[]) {
|
||||
pagesSorted = pagesSorted.sort(Sorting.alphabetically(name));
|
||||
} else if (type === SortType.date) {
|
||||
pagesSorted = pagesSorted.sort(Sorting.date(name));
|
||||
} else if (type === SortType.number) {
|
||||
pagesSorted = pagesSorted.sort(Sorting.number(name));
|
||||
}
|
||||
|
||||
if (order === SortOrder.desc) {
|
||||
@@ -91,40 +96,52 @@ export default function usePages(pages: Page[]) {
|
||||
pagesSorted = pagesSorted.filter(page => page.fmCategories && page.fmCategories.includes(category));
|
||||
}
|
||||
|
||||
setSortedPages(pagesSorted);
|
||||
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
|
||||
|
||||
|
||||
/**
|
||||
* Process the pages when the tab changes
|
||||
*/
|
||||
const processByTab = useCallback((pages: Page[]) => {
|
||||
const draftField = settings?.draftField;
|
||||
|
||||
let crntPages: Page[] = Object.assign([], pages);
|
||||
|
||||
// Process the tab data
|
||||
const draftTypes = Object.assign({}, tabInfo);
|
||||
draftTypes[Tab.All] = pagesSorted.length;
|
||||
draftTypes[Tab.All] = crntPages.length;
|
||||
|
||||
// Filter by draft status
|
||||
if (draftField && draftField.type === 'choice') {
|
||||
const draftChoices = settings?.draftField?.choices;
|
||||
for (const choice of (draftChoices || [])) {
|
||||
if (choice) {
|
||||
draftTypes[choice] = pagesSorted.filter(page => page.fmDraft === choice).length;
|
||||
draftTypes[choice] = crntPages.filter(page => page.fmDraft === choice).length;
|
||||
}
|
||||
}
|
||||
|
||||
if (tab !== Tab.All) {
|
||||
pagesSorted = pagesSorted.filter(page => page.fmDraft === tab);
|
||||
crntPages = crntPages.filter(page => page.fmDraft === tab);
|
||||
} else {
|
||||
pagesSorted = pagesSorted;
|
||||
crntPages = crntPages;
|
||||
}
|
||||
} else {
|
||||
// Draft field is a boolean field
|
||||
const draftFieldName = draftField?.name || "draft";
|
||||
|
||||
const drafts = pagesSorted.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
|
||||
const published = pagesSorted.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
|
||||
const drafts = crntPages.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
|
||||
const published = crntPages.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
|
||||
|
||||
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
|
||||
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
|
||||
|
||||
if (tab === Tab.Published) {
|
||||
pagesSorted = draftField?.invert ? drafts : published;
|
||||
crntPages = draftField?.invert ? drafts : published;
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesSorted = draftField?.invert ? published : drafts;
|
||||
crntPages = draftField?.invert ? published : drafts;
|
||||
} else {
|
||||
pagesSorted = pagesSorted;
|
||||
crntPages = crntPages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +149,14 @@ export default function usePages(pages: Page[]) {
|
||||
setTabInfo(draftTypes);
|
||||
|
||||
// Set the pages
|
||||
setPageItems(pagesSorted);
|
||||
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
|
||||
|
||||
setPageItems(crntPages);
|
||||
}, [ tab, tabInfo, settings ]);
|
||||
|
||||
|
||||
/**
|
||||
* Search listener for filtered pages
|
||||
* @param message
|
||||
*/
|
||||
const searchListener = (message: MessageEvent<EventData<any>>) => {
|
||||
switch (message.data.command) {
|
||||
case DashboardMessage.searchPages:
|
||||
@@ -144,6 +165,7 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let usedSorting = sorting;
|
||||
|
||||
@@ -158,15 +180,19 @@ export default function usePages(pages: Page[]) {
|
||||
// Check if search needs to be performed
|
||||
let searchedPages = pages;
|
||||
if (search) {
|
||||
// const fuse = new Fuse(pages, fuseOptions);
|
||||
// const results = fuse.search(search);
|
||||
// searchedPages = results.map(page => page.item);
|
||||
|
||||
Messenger.send(DashboardMessage.searchPages, { query: search });
|
||||
} else {
|
||||
processPages(searchedPages);
|
||||
}
|
||||
}, [ settings?.draftField, pages, sorting, search, tab, tag, category, folder ]);
|
||||
}, [ settings?.draftField, pages, sorting, search, tag, category, folder ]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedPages.length > 0) {
|
||||
processByTab(sortedPages);
|
||||
}
|
||||
}, [sortedPages, tab])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(searchListener);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { routePaths } from '..';
|
||||
|
||||
export const PAGE_LIMIT = 16;
|
||||
|
||||
export default function usePagination(value: number | boolean | null | undefined, totalPages?: number, totalMedia?: number) {
|
||||
const location = useLocation();
|
||||
|
||||
const pagination = useMemo(() => {
|
||||
if (location.pathname === routePaths.contents) {
|
||||
if (typeof value === 'number') {
|
||||
const pageNr = value > 0 ? value : 0;
|
||||
if (pageNr > 52) {
|
||||
return 52;
|
||||
}
|
||||
return pageNr;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? PAGE_LIMIT : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return PAGE_LIMIT;
|
||||
}, [value, location.pathname]);
|
||||
|
||||
|
||||
const totalPagesNr: number = useMemo(() => {
|
||||
if (location.pathname === routePaths.contents) {
|
||||
if (totalPages) {
|
||||
return Math.ceil((totalPages || 0) / pagination) - 1
|
||||
}
|
||||
} else {
|
||||
if (totalMedia) {
|
||||
return Math.ceil(totalMedia / pagination) - 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}, [location.pathname, totalPages, totalMedia, pagination]);
|
||||
|
||||
/**
|
||||
* The total items (pages or media)
|
||||
*/
|
||||
const totalItems: number = useMemo(() => {
|
||||
if (location.pathname === routePaths.contents) {
|
||||
if (totalPages) {
|
||||
return totalPages;
|
||||
}
|
||||
} else {
|
||||
if (totalMedia) {
|
||||
return totalMedia;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}, [location.pathname, totalPages, totalMedia, pagination]);
|
||||
|
||||
|
||||
return {
|
||||
pageSetNr: pagination,
|
||||
totalPagesNr,
|
||||
totalItems
|
||||
};
|
||||
}
|
||||
@@ -40,7 +40,10 @@ if (elm) {
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || "",
|
||||
environment: environment || "",
|
||||
ignoreErrors: ['ResizeObserver loop limit exceeded']
|
||||
ignoreErrors: [
|
||||
'ResizeObserver loop limit exceeded',
|
||||
"Cannot read properties of undefined (reading 'unobserve')"
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Uri } from "vscode";
|
||||
|
||||
export interface Page {
|
||||
// Properties for caching
|
||||
fmCachePath: string;
|
||||
fmCacheModifiedTime: number;
|
||||
|
||||
// Front matter fields
|
||||
fmFolder: string;
|
||||
fmFilePath: string;
|
||||
fmFileName: string;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { DataType } from './../../models/DataType';
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType, CustomScript, CustomTaxonomy, DraftField, Framework, Snippets, SortingSetting } from '../../models';
|
||||
import { ContentType, CustomScript, CustomTaxonomy, DraftField, Framework, GitSettings, Snippets, SortingSetting } from '../../models';
|
||||
import { SortingOption } from './SortingOption';
|
||||
import { DashboardViewType } from '.';
|
||||
import { DataFile } from '../../models/DataFile';
|
||||
|
||||
export interface Settings {
|
||||
git: GitSettings;
|
||||
beta: boolean;
|
||||
initialized: boolean;
|
||||
wsFolder: string;
|
||||
@@ -43,6 +44,7 @@ export interface ContentsViewState {
|
||||
defaultSorting: string | null | undefined;
|
||||
tags: string | null | undefined;
|
||||
templatesEnabled: boolean | null | undefined;
|
||||
pagination: boolean | number | null | undefined;
|
||||
}
|
||||
|
||||
export interface MediaViewState extends ContentsViewState {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const AllContentFoldersAtom = atom<string[] | undefined>({
|
||||
key: 'AllContentFoldersAtom',
|
||||
default: undefined
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const AllStaticFoldersAtom = atom<string[] | undefined>({
|
||||
key: 'AllStaticFoldersAtom',
|
||||
default: undefined
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './AllContentFoldersAtom';
|
||||
export * from './AllStaticFoldersAtom';
|
||||
export * from './CategoryAtom';
|
||||
export * from './DashboardViewAtom';
|
||||
export * from './FolderAtom';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TagType } from '../panelWebView/TagType';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
import { ModeListener } from '../listeners/general';
|
||||
import { GitListener, ModeListener } from '../listeners/general';
|
||||
|
||||
export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
public static readonly viewType = "frontMatter.explorer";
|
||||
@@ -82,6 +82,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
SettingsListener.process(msg);
|
||||
TaxonomyListener.process(msg);
|
||||
ModeListener.process(msg);
|
||||
GitListener.process(msg);
|
||||
});
|
||||
|
||||
webviewView.onDidChangeVisibility(() => {
|
||||
@@ -99,7 +100,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
}
|
||||
}, this);
|
||||
|
||||
Settings.onConfigChange((global?: any) => {
|
||||
Settings.onConfigChange(() => {
|
||||
SettingsListener.getSettings();
|
||||
});
|
||||
}
|
||||
|
||||
+29
-39
@@ -1,29 +1,19 @@
|
||||
import { GitListener } from './listeners/general/GitListener';
|
||||
import * as vscode from 'vscode';
|
||||
import { Telemetry } from './helpers/Telemetry';
|
||||
import { ContentType } from './helpers/ContentType';
|
||||
import { Dashboard } from './commands/Dashboard';
|
||||
import { Article, Settings, StatusListener } from './commands';
|
||||
import { Folders } from './commands/Folders';
|
||||
import { Preview } from './commands/Preview';
|
||||
import { Project } from './commands/Project';
|
||||
import { Template } from './commands/Template';
|
||||
import { COMMAND_NAME, TelemetryEvent } from './constants';
|
||||
import { TaxonomyType } from './models';
|
||||
import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider';
|
||||
import { TagType } from './panelWebView/TagType';
|
||||
import { ExplorerView } from './explorerView/ExplorerView';
|
||||
import { Extension } from './helpers/Extension';
|
||||
import { DashboardData } from './models/DashboardData';
|
||||
import { Logger, Settings as SettingsHelper } from './helpers';
|
||||
import { Content } from './commands/Content';
|
||||
import { DashboardSettings, debounceCallback, Logger, Settings as SettingsHelper } from './helpers';
|
||||
import ContentProvider from './providers/ContentProvider';
|
||||
import { Wysiwyg } from './commands/Wysiwyg';
|
||||
import { Diagnostics } from './commands/Diagnostics';
|
||||
import { PagesListener } from './listeners/dashboard';
|
||||
import { Backers } from './commands/Backers';
|
||||
import { DataListener, SettingsListener } from './listeners/panel';
|
||||
import { NavigationType } from './dashboardWebView/models';
|
||||
import { ModeSwitch } from './services/ModeSwitch';
|
||||
import { PagesParser } from './services/PagesParser';
|
||||
import { ContentType, Telemetry, Extension } from './helpers';
|
||||
import { TaxonomyType, DashboardData } from './models';
|
||||
import { Backers, Diagnostics, Wysiwyg, Content, Cache, Template, Project, Preview, Folders, Dashboard, Article, Settings, StatusListener } from './commands';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
let statusDebouncer: { (fnc: any, time: number): void; };
|
||||
@@ -34,13 +24,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const { subscriptions, extensionUri, extensionPath } = context;
|
||||
|
||||
const extension = Extension.getInstance(context);
|
||||
Backers.init(context);
|
||||
Backers.init(context).then(() => {});
|
||||
|
||||
if (!extension.checkIfExtensionCanRun()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
SettingsHelper.init();
|
||||
await SettingsHelper.init();
|
||||
extension.migrateSettings();
|
||||
|
||||
SettingsHelper.checkToPromote();
|
||||
@@ -181,14 +171,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
);
|
||||
|
||||
// Initialize command
|
||||
Template.init();
|
||||
const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
|
||||
await Project.init();
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
|
||||
await Project.init();
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Settings promotion command
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, SettingsHelper.promote));
|
||||
@@ -199,13 +190,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
// Things to do when configuration changes
|
||||
SettingsHelper.onConfigChange((global?: any) => {
|
||||
Template.init();
|
||||
SettingsHelper.onConfigChange(() => {
|
||||
Preview.init();
|
||||
GitListener.init();
|
||||
|
||||
SettingsListener.getSettings();
|
||||
DataListener.getFoldersAndFiles();
|
||||
MarkdownFoldingProvider.triggerHighlighting();
|
||||
MarkdownFoldingProvider.triggerHighlighting(true);
|
||||
ModeSwitch.register();
|
||||
});
|
||||
|
||||
@@ -261,6 +252,16 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Diagnostics
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.diagnostics, Diagnostics.show));
|
||||
|
||||
// Git
|
||||
GitListener.init();
|
||||
|
||||
// Once everything is registered, the page parsing can start in the background
|
||||
DashboardSettings.get();
|
||||
PagesParser.start();
|
||||
|
||||
// Cache commands
|
||||
Cache.registerCommands();
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(
|
||||
insertTags,
|
||||
@@ -280,7 +281,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
createContent,
|
||||
createByContentType,
|
||||
createByTemplate,
|
||||
projectInit,
|
||||
collapseAll,
|
||||
createFolder
|
||||
);
|
||||
@@ -297,14 +297,4 @@ const handleAutoDateUpdate = (e: vscode.TextDocumentWillSaveEvent) => {
|
||||
const triggerShowDraftStatus = (location: string) => {
|
||||
Logger.info(`Triggering draft status update: ${location}`);
|
||||
statusDebouncer(() => { StatusListener.verify(frontMatterStatusBar, collection); }, 1000);
|
||||
};
|
||||
|
||||
const debounceCallback = () => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return (fnc: any, time: number) => {
|
||||
const functionCall = (...args: any[]) => fnc.apply(args);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(functionCall, time) as any;
|
||||
};
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as jsoncParser from 'jsonc-parser';
|
||||
import { CustomPlaceholder } from './../models/CustomPlaceholder';
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider';
|
||||
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from "fs";
|
||||
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD } from '../constants';
|
||||
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD, DefaultFieldValues } from '../constants';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
|
||||
import { Extension, Logger, Settings, SlugHelper } from '.';
|
||||
@@ -13,7 +14,6 @@ import { Article } from '../commands';
|
||||
import { join } from 'path';
|
||||
import { EditorHelper } from '@estruyf/vscode';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { ContentType } from '../models';
|
||||
import { DateHelper } from './DateHelper';
|
||||
import { DiagnosticSeverity, Position, window, Range } from 'vscode';
|
||||
@@ -22,6 +22,10 @@ import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { Link, Parent } from 'mdast-util-from-markdown/lib';
|
||||
import { Content } from 'mdast';
|
||||
import { processKnownPlaceholders } from './PlaceholderHelper';
|
||||
import { CustomScript } from './CustomScript';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { existsAsync, readFileAsync } from '../utils';
|
||||
import { mkdirAsync } from '../utils/mkdirAsync';
|
||||
|
||||
export class ArticleHelper {
|
||||
private static notifiedFiles: string[] = [];
|
||||
@@ -66,8 +70,8 @@ export class ArticleHelper {
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static getFrontMatterByPath(filePath: string) {
|
||||
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
|
||||
public static async getFrontMatterByPath(filePath: string) {
|
||||
const file = await readFileAsync(filePath, { encoding: "utf-8" });
|
||||
return ArticleHelper.parseFile(file, filePath);
|
||||
}
|
||||
|
||||
@@ -324,36 +328,49 @@ export class ArticleHelper {
|
||||
* @param titleValue
|
||||
* @returns The new file path
|
||||
*/
|
||||
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined {
|
||||
public static async createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): Promise<string | undefined> {
|
||||
FrontMatterParser.currentContent = null;
|
||||
|
||||
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
let prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
const filePrefixOnFolder = Folders.getFilePrefixByFolderPath(folderPath);
|
||||
if (typeof filePrefixOnFolder !== "undefined") {
|
||||
prefix = filePrefixOnFolder;
|
||||
}
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
prefix = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}`;
|
||||
}
|
||||
|
||||
// Name of the file or folder to create
|
||||
const sanitizedName = ArticleHelper.sanitize(titleValue);
|
||||
let sanitizedName = ArticleHelper.sanitize(titleValue);
|
||||
let newFilePath: string | undefined;
|
||||
|
||||
// Create a folder with the `index.md` file
|
||||
if (contentType?.pageBundle) {
|
||||
if (prefix && typeof prefix === "string") {
|
||||
sanitizedName = `${prefix}-${sanitizedName}`;
|
||||
}
|
||||
|
||||
const newFolder = join(folderPath, sanitizedName);
|
||||
if (existsSync(newFolder)) {
|
||||
if (await existsAsync(newFolder)) {
|
||||
Notifications.error(`A page bundle with the name ${sanitizedName} already exists in ${folderPath}`);
|
||||
return;
|
||||
} else {
|
||||
mkdirSync(newFolder);
|
||||
await mkdirAsync(newFolder);
|
||||
newFilePath = join(newFolder, `index.${fileExtension || contentType.fileType || fileType}`);
|
||||
}
|
||||
} else {
|
||||
let newFileName = `${sanitizedName}.${fileExtension || contentType?.fileType || fileType}`;
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;
|
||||
newFileName = `${prefix}-${newFileName}`;
|
||||
}
|
||||
|
||||
newFilePath = join(folderPath, newFileName);
|
||||
|
||||
if (existsSync(newFilePath)) {
|
||||
if (await existsAsync(newFilePath)) {
|
||||
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
|
||||
return;
|
||||
}
|
||||
@@ -368,7 +385,7 @@ export class ArticleHelper {
|
||||
* @param title
|
||||
* @returns
|
||||
*/
|
||||
public static updatePlaceholders(data: any, title: string) {
|
||||
public static async updatePlaceholders(data: any, title: string, filePath: string) {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
const fmData = Object.assign({}, data);
|
||||
|
||||
@@ -384,7 +401,7 @@ export class ArticleHelper {
|
||||
}
|
||||
|
||||
fmData[fieldName] = processKnownPlaceholders(fmData[fieldName], title, dateFormat);
|
||||
fmData[fieldName] = this.processCustomPlaceholders(fmData[fieldName], title);
|
||||
fmData[fieldName] = await this.processCustomPlaceholders(fmData[fieldName], title, filePath);
|
||||
}
|
||||
|
||||
return fmData;
|
||||
@@ -396,16 +413,52 @@ export class ArticleHelper {
|
||||
* @param title
|
||||
* @returns
|
||||
*/
|
||||
public static processCustomPlaceholders(value: string, title: string) {
|
||||
public static async processCustomPlaceholders(value: string, title: string | undefined, filePath: string | undefined) {
|
||||
if (value && typeof value === "string") {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS);
|
||||
const placeholders = Settings.get<CustomPlaceholder[]>(SETTING_CONTENT_PLACEHOLDERS);
|
||||
if (placeholders && placeholders.length > 0) {
|
||||
for (const placeholder of placeholders) {
|
||||
if (value.includes(`{{${placeholder.id}}}`)) {
|
||||
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
|
||||
const updatedValue = processKnownPlaceholders(placeholder.value, title, dateFormat);
|
||||
value = value.replace(regex, updatedValue);
|
||||
|
||||
try {
|
||||
let placeHolderValue = placeholder.value || "";
|
||||
if (placeholder.script) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const script = { title: placeholder.id, script: placeholder.script, command: placeholder.command };
|
||||
let output: string | any = await CustomScript.executeScript(script, wsFolder?.fsPath || "", `'${wsFolder?.fsPath}' '${filePath}' '${title}'`);
|
||||
|
||||
if (output) {
|
||||
// Check if the output needs to be parsed
|
||||
if (output.includes("{") && output.includes("}")) {
|
||||
try {
|
||||
output = jsoncParser.parse(output);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
} else {
|
||||
output = output.split("\n");
|
||||
}
|
||||
|
||||
placeHolderValue = output;
|
||||
}
|
||||
}
|
||||
|
||||
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
|
||||
const updatedValue = processKnownPlaceholders(placeHolderValue, title, dateFormat);
|
||||
|
||||
if (value === `{{${placeholder.id}}}`) {
|
||||
value = updatedValue;
|
||||
} else {
|
||||
value = value.replace(regex, updatedValue);
|
||||
}
|
||||
} catch (e) {
|
||||
Notifications.error(`Error while processing the ${placeholder.id} placeholder`);
|
||||
Logger.error((e as Error).message);
|
||||
|
||||
value = DefaultFieldValues.faultyCustomPlaceholder;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+145
-66
@@ -1,18 +1,18 @@
|
||||
import { ModeListener } from './../listeners/general/ModeListener';
|
||||
import { PagesListener } from './../listeners/dashboard';
|
||||
import { ArticleHelper, Settings } from ".";
|
||||
import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants";
|
||||
import { ContentType as IContentType, DraftField, Field, FieldGroup, FieldType } from '../models';
|
||||
import { Uri, commands, window } from 'vscode';
|
||||
import { ArticleHelper, CustomScript, Settings } from ".";
|
||||
import { DefaultFieldValues, FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants";
|
||||
import { ContentType as IContentType, DraftField, Field, FieldGroup, FieldType, ScriptType } from '../models';
|
||||
import { Uri, commands, window, ProgressLocation, workspace } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Questions } from "./Questions";
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
|
||||
import { Telemetry } from './Telemetry';
|
||||
import { processKnownPlaceholders } from './PlaceholderHelper';
|
||||
import { basename } from 'path';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { existsAsync, writeFileAsync } from '../utils';
|
||||
|
||||
export class ContentType {
|
||||
|
||||
@@ -48,7 +48,7 @@ export class ContentType {
|
||||
fieldValue = data[draftSetting.name];
|
||||
}
|
||||
|
||||
if (draftSetting && fieldValue) {
|
||||
if (draftSetting && fieldValue !== null) {
|
||||
if (draftSetting.type === "boolean") {
|
||||
return fieldValue ? "Draft" : "Published";
|
||||
} else {
|
||||
@@ -64,11 +64,6 @@ export class ContentType {
|
||||
* @returns
|
||||
*/
|
||||
public static async createContent() {
|
||||
const selectedContentType = await Questions.SelectContentType();
|
||||
if (!selectedContentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFolder = await Questions.SelectContentFolder();
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
@@ -76,10 +71,19 @@ export class ContentType {
|
||||
|
||||
const contentTypes = ContentType.getAll();
|
||||
const folders = Folders.get();
|
||||
const folder = folders.find(f => f.title === selectedFolder);
|
||||
|
||||
const location = folders.find(f => f.title === selectedFolder);
|
||||
if (contentTypes && location) {
|
||||
const folderPath = Folders.getFolderPath(Uri.file(location.path));
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedContentType = await Questions.SelectContentType(folder.contentTypes || []);
|
||||
if (!selectedContentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentTypes && folder) {
|
||||
const folderPath = Folders.getFolderPath(Uri.file(folder.path));
|
||||
const contentType = contentTypes.find(ct => ct.name === selectedContentType);
|
||||
if (folderPath && contentType) {
|
||||
ContentType.create(contentType, folderPath);
|
||||
@@ -116,9 +120,9 @@ export class ContentType {
|
||||
}
|
||||
|
||||
const override = await window.showQuickPick(["Yes", "No"], {
|
||||
title: "Override default content type",
|
||||
placeHolder: "Do you want to override the default content type?",
|
||||
ignoreFocusOut: true,
|
||||
title: "Override default content type"
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
const overrideBool = override === "Yes";
|
||||
|
||||
@@ -127,10 +131,10 @@ export class ContentType {
|
||||
// Ask for the new content type name
|
||||
if (!overrideBool) {
|
||||
contentTypeName = await window.showInputBox({
|
||||
ignoreFocusOut: true,
|
||||
title: "Generate Content Type",
|
||||
placeHolder: "Enter the name of the content type to generate",
|
||||
prompt: "Enter the name of the content type to generate",
|
||||
title: "Generate Content Type",
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (value: string) => {
|
||||
if (!value) {
|
||||
return "Please enter a name for the content type";
|
||||
@@ -156,9 +160,9 @@ export class ContentType {
|
||||
const fileName = filePath ? basename(filePath) : undefined;
|
||||
if (fileName?.startsWith(`index.`)) {
|
||||
const pageBundleAnswer = await window.showQuickPick(["Yes", "No"], {
|
||||
title: "Use as page bundle",
|
||||
placeHolder: "Do you want to use this content type as a page bundle?",
|
||||
ignoreFocusOut: true,
|
||||
title: "Use as page bundle"
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
pageBundle = pageBundleAnswer === "Yes";
|
||||
}
|
||||
@@ -194,12 +198,12 @@ export class ContentType {
|
||||
contentTypes.push(newContentType);
|
||||
}
|
||||
|
||||
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
|
||||
const configPath = Settings.projectConfigPath;
|
||||
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
|
||||
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && await existsAsync(configPath) ? `Open settings` : undefined);
|
||||
|
||||
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
|
||||
if (notificationAction === "Open settings" && configPath && await existsAsync(configPath)) {
|
||||
commands.executeCommand('vscode.open', Uri.file(configPath));
|
||||
}
|
||||
}
|
||||
@@ -228,12 +232,12 @@ export class ContentType {
|
||||
const index = contentTypes.findIndex(ct => ct.name === contentType.name);
|
||||
contentTypes[index].fields = updatedFields;
|
||||
|
||||
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
|
||||
const configPath = Settings.projectConfigPath;
|
||||
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
|
||||
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && await existsAsync(configPath) ? `Open settings` : undefined);
|
||||
|
||||
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
|
||||
if (notificationAction === "Open settings" && configPath && await existsAsync(configPath)) {
|
||||
commands.executeCommand('vscode.open', Uri.file(configPath));
|
||||
}
|
||||
}
|
||||
@@ -278,7 +282,7 @@ export class ContentType {
|
||||
* @param parents
|
||||
* @returns
|
||||
*/
|
||||
public static getFieldValue(data: any, parents: string[]): string[] {
|
||||
public static getFieldValue(data: any, parents: string[]): string | string[] {
|
||||
let fieldValue = [];
|
||||
let crntPageData = data;
|
||||
|
||||
@@ -375,6 +379,51 @@ export class ContentType {
|
||||
return parents;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the required fields
|
||||
*/
|
||||
public static findEmptyRequiredFields(article: ParsedFrontMatter): Field[][] | undefined {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (!contentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allRequiredFields = ContentType.findRequiredFieldsDeep(contentType.fields);
|
||||
|
||||
let emptyFields: Field[][] = [];
|
||||
|
||||
for (const fields of allRequiredFields) {
|
||||
const fieldValue = this.getFieldValue(article.data, fields.map(f => f.name));
|
||||
if ((fieldValue === null || fieldValue === undefined || fieldValue === "") || fieldValue.length === 0 || fieldValue === DefaultFieldValues.faultyCustomPlaceholder) {
|
||||
emptyFields.push(fields);
|
||||
}
|
||||
}
|
||||
|
||||
return emptyFields || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the required fields in the content type
|
||||
* @param fields
|
||||
* @param parents
|
||||
* @returns
|
||||
*/
|
||||
private static findRequiredFieldsDeep(fields: Field[], parents: Field[][] = [], parentFields: Field[] = []): Field[][] {
|
||||
for (const field of fields) {
|
||||
if (field.required) {
|
||||
parents.push([...parentFields, field]);
|
||||
}
|
||||
|
||||
if (field.type === "fields" && field.fields) {
|
||||
this.findRequiredFieldsDeep(field.fields, parents, [...parentFields, field]);
|
||||
}
|
||||
}
|
||||
|
||||
return parents;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Look for the preview image in the block field
|
||||
* @param field
|
||||
@@ -499,53 +548,72 @@ export class ContentType {
|
||||
* @returns
|
||||
*/
|
||||
private static async create(contentType: IContentType, folderPath: string) {
|
||||
const titleValue = await Questions.ContentTitle();
|
||||
if (!titleValue) {
|
||||
return;
|
||||
}
|
||||
window.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: "Front Matter: Creating content...",
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
const titleValue = await Questions.ContentTitle();
|
||||
if (!titleValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let templatePath = contentType.template;
|
||||
let templateData: ParsedFrontMatter | null = null;
|
||||
if (templatePath) {
|
||||
templatePath = Folders.getAbsFilePath(templatePath);
|
||||
templateData = ArticleHelper.getFrontMatterByPath(templatePath);
|
||||
}
|
||||
let templatePath = contentType.template;
|
||||
let templateData: ParsedFrontMatter | null = null;
|
||||
if (templatePath) {
|
||||
templatePath = Folders.getAbsFilePath(templatePath);
|
||||
templateData = await ArticleHelper.getFrontMatterByPath(templatePath);
|
||||
}
|
||||
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
let newFilePath: string | undefined = await ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentType.name === "default") {
|
||||
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
|
||||
if (crntFramework?.toLowerCase() === "jekyll") {
|
||||
const idx = contentType.fields.findIndex(f => f.name === "draft");
|
||||
if (idx > -1) {
|
||||
contentType.fields.splice(idx, 1);
|
||||
if (contentType.name === "default") {
|
||||
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
|
||||
if (crntFramework?.toLowerCase() === "jekyll") {
|
||||
const idx = contentType.fields.findIndex(f => f.name === "draft");
|
||||
if (idx > -1) {
|
||||
contentType.fields.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data: any = this.processFields(contentType, titleValue, templateData?.data || {});
|
||||
let data: any = await this.processFields(contentType, titleValue, templateData?.data || {}, newFilePath);
|
||||
|
||||
data = ArticleHelper.updateDates(Object.assign({}, data));
|
||||
data = ArticleHelper.updateDates(Object.assign({}, data));
|
||||
|
||||
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
|
||||
data['type'] = contentType.name;
|
||||
}
|
||||
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
|
||||
data['type'] = contentType.name;
|
||||
}
|
||||
|
||||
const content = ArticleHelper.stringifyFrontMatter(templateData?.content || ``, data);
|
||||
const content = ArticleHelper.stringifyFrontMatter(templateData?.content || ``, data);
|
||||
|
||||
writeFileSync(newFilePath, content, { encoding: "utf8" });
|
||||
await writeFileAsync(newFilePath, content, { encoding: "utf8" });
|
||||
|
||||
await commands.executeCommand('vscode.open', Uri.file(newFilePath));
|
||||
// Check if the content type has a post script to execute
|
||||
if (contentType.postScript) {
|
||||
const scripts = await CustomScript.getScripts();
|
||||
const script = scripts.find(s => s.id === contentType.postScript);
|
||||
|
||||
if (script && (script.type === ScriptType.Content || !script?.type)) {
|
||||
await CustomScript.run(script, newFilePath);
|
||||
|
||||
Notifications.info(`Your new content has been created.`);
|
||||
const doc = await workspace.openTextDocument(Uri.file(newFilePath));
|
||||
await doc.save();
|
||||
}
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.createContentFromContentType);
|
||||
await commands.executeCommand('vscode.open', Uri.file(newFilePath));
|
||||
|
||||
// Trigger a refresh for the dashboard
|
||||
PagesListener.refresh();
|
||||
Notifications.info(`Your new content has been created.`);
|
||||
|
||||
Telemetry.send(TelemetryEvent.createContentFromContentType);
|
||||
|
||||
// Trigger a refresh for the dashboard
|
||||
PagesListener.refresh();
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,29 +621,40 @@ export class ContentType {
|
||||
* @param contentType
|
||||
* @param data
|
||||
*/
|
||||
private static processFields(obj: IContentType | Field, titleValue: string, data: any) {
|
||||
private static async processFields(obj: IContentType | Field, titleValue: string, data: any, filePath: string, isRoot: boolean = true): Promise<any> {
|
||||
|
||||
if (obj.fields) {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
|
||||
for (const field of obj.fields) {
|
||||
if (field.name === "title") {
|
||||
if (field.default) {
|
||||
data[field.name] = processKnownPlaceholders(field.default, titleValue, dateFormat);
|
||||
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
|
||||
} else {
|
||||
data[field.name] = await ArticleHelper.processCustomPlaceholders(data[field.name], titleValue, filePath);
|
||||
} else if (isRoot) {
|
||||
data[field.name] = titleValue;
|
||||
} else {
|
||||
data[field.name] = ""
|
||||
}
|
||||
} else {
|
||||
if (field.type === "fields") {
|
||||
data[field.name] = this.processFields(field, titleValue, {});
|
||||
data[field.name] = await this.processFields(field, titleValue, {}, filePath, false);
|
||||
} else {
|
||||
const defaultValue = field.default;
|
||||
|
||||
if (typeof defaultValue === "string") {
|
||||
data[field.name] = processKnownPlaceholders(defaultValue, titleValue, dateFormat);
|
||||
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
|
||||
data[field.name] = await ArticleHelper.processCustomPlaceholders(data[field.name], titleValue, filePath);
|
||||
} else if (typeof defaultValue !== "undefined") {
|
||||
data[field.name] = defaultValue;
|
||||
} else {
|
||||
data[field.name] = typeof defaultValue !== "undefined" ? defaultValue : "";
|
||||
const draftField = ContentType.getDraftField();
|
||||
|
||||
if (field.type === "draft" && (draftField?.type === "boolean" || draftField?.type === undefined)) {
|
||||
data[field.name] = true;
|
||||
} else {
|
||||
data[field.name] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-17
@@ -1,3 +1,4 @@
|
||||
import { Settings } from './SettingsHelper';
|
||||
import { CommandType } from './../models/PanelSettings';
|
||||
import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings';
|
||||
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
|
||||
@@ -12,9 +13,25 @@ import { Dashboard } from '../commands/Dashboard';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { TelemetryEvent } from '../constants/TelemetryEvent';
|
||||
import { SETTING_CUSTOM_SCRIPTS } from '../constants';
|
||||
import { existsAsync } from '../utils';
|
||||
|
||||
export class CustomScript {
|
||||
|
||||
/**
|
||||
* Retrieve all scripts
|
||||
* @returns
|
||||
*/
|
||||
public static async getScripts(): Promise<ICustomScript[]> {
|
||||
const scripts = Settings.get<ICustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [];
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a script
|
||||
* @param script
|
||||
* @param path
|
||||
*/
|
||||
public static async run(script: ICustomScript, path: string | null = null): Promise<void> {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
@@ -24,19 +41,19 @@ export class CustomScript {
|
||||
if (script.type === ScriptType.MediaFile || script.type === ScriptType.MediaFolder) {
|
||||
Telemetry.send(TelemetryEvent.runMediaScript);
|
||||
|
||||
CustomScript.runMediaScript(wsPath, path, script);
|
||||
await CustomScript.runMediaScript(wsPath, path, script);
|
||||
} else {
|
||||
Telemetry.send(TelemetryEvent.runCustomScript);
|
||||
|
||||
if (script.bulk) {
|
||||
// Run script on all files
|
||||
CustomScript.bulkRun(wsPath, script);
|
||||
await CustomScript.bulkRun(wsPath, script);
|
||||
} else if (path) {
|
||||
// Run script for provided path
|
||||
CustomScript.singleRun(wsPath, script, path);
|
||||
await CustomScript.singleRun(wsPath, script, path);
|
||||
} else {
|
||||
// Run script on current file.
|
||||
CustomScript.singleRun(wsPath, script);
|
||||
await CustomScript.singleRun(wsPath, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,17 +77,17 @@ export class CustomScript {
|
||||
articlePath = editor.document.uri.fsPath;
|
||||
article = ArticleHelper.getFrontMatter(editor);
|
||||
} else {
|
||||
article = ArticleHelper.getFrontMatterByPath(path);
|
||||
article = await ArticleHelper.getFrontMatterByPath(path);
|
||||
}
|
||||
|
||||
if (articlePath && article) {
|
||||
window.withProgress({
|
||||
return window.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: `Executing: ${script.title}`,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
const output = await CustomScript.runScript(wsPath, article, articlePath as string, script);
|
||||
CustomScript.showOutput(output, script, articlePath);
|
||||
await CustomScript.showOutput(output, script, articlePath);
|
||||
});
|
||||
} else {
|
||||
Notifications.warning(`${script.title}: Article couldn't be retrieved.`);
|
||||
@@ -102,7 +119,7 @@ export class CustomScript {
|
||||
if (folder.lastModified.length > 0) {
|
||||
for await (const file of folder.lastModified) {
|
||||
try {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
const article = await ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
if (article) {
|
||||
const crntOutput = await CustomScript.runScript(wsPath, article, file.filePath, script);
|
||||
if (crntOutput) {
|
||||
@@ -116,7 +133,7 @@ export class CustomScript {
|
||||
}
|
||||
}
|
||||
|
||||
CustomScript.showOutput(output.join(`\n`), script);
|
||||
await CustomScript.showOutput(output.join(`\n`), script);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,7 +159,7 @@ export class CustomScript {
|
||||
try {
|
||||
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${path}"`);
|
||||
|
||||
CustomScript.showOutput(output, script);
|
||||
await CustomScript.showOutput(output, script);
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.mediaUpdate
|
||||
@@ -178,7 +195,11 @@ export class CustomScript {
|
||||
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${contentPath}" ${articleData}`);
|
||||
return output;
|
||||
} catch (e) {
|
||||
Notifications.error(`${script.title}: ${(e as Error).message}`);
|
||||
if (typeof e === "string") {
|
||||
Notifications.error(`${script.title}: ${e}`);
|
||||
} else {
|
||||
Notifications.error(`${script.title}: ${(e as Error).message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -188,7 +209,7 @@ export class CustomScript {
|
||||
* @param output
|
||||
* @param script
|
||||
*/
|
||||
private static showOutput(output: string | null, script: ICustomScript, articlePath?: string | null): void {
|
||||
private static async showOutput(output: string | null, script: ICustomScript, articlePath?: string | null): Promise<void> {
|
||||
if (output) {
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
@@ -203,7 +224,7 @@ export class CustomScript {
|
||||
articlePath = editor.document.uri.fsPath;
|
||||
article = ArticleHelper.getFrontMatter(editor);
|
||||
} else {
|
||||
article = ArticleHelper.getFrontMatterByPath(articlePath);
|
||||
article = await ArticleHelper.getFrontMatterByPath(articlePath);
|
||||
}
|
||||
|
||||
if (article && article.data) {
|
||||
@@ -212,9 +233,9 @@ export class CustomScript {
|
||||
}
|
||||
|
||||
if (articlePath) {
|
||||
ArticleHelper.updateByPath(articlePath, article);
|
||||
await ArticleHelper.updateByPath(articlePath, article);
|
||||
} else if (editor) {
|
||||
ArticleHelper.update(editor, article);
|
||||
await ArticleHelper.update(editor, article);
|
||||
} else {
|
||||
throw new Error(`Couldn't update article.`);
|
||||
}
|
||||
@@ -246,8 +267,8 @@ export class CustomScript {
|
||||
* @param args
|
||||
* @returns
|
||||
*/
|
||||
private static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
public static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
// Check the command to use
|
||||
let command = script.nodeBin || "node";
|
||||
@@ -256,12 +277,24 @@ export class CustomScript {
|
||||
}
|
||||
|
||||
const scriptPath = join(wsPath, script.script);
|
||||
|
||||
if (!await existsAsync(scriptPath)) {
|
||||
reject(new Error(`Script not found: ${scriptPath}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const fullScript = `${command} ${scriptPath} ${args}`;
|
||||
Logger.info(`Executing: ${fullScript}`);
|
||||
|
||||
exec(fullScript, (error, stdout) => {
|
||||
if (error) {
|
||||
reject(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout && stdout.endsWith(`\n`)) {
|
||||
// Remove empty line at the end of the string
|
||||
stdout = stdout.slice(0, -1);
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GitListener } from './../listeners/general/GitListener';
|
||||
import { basename, join } from "path";
|
||||
import { workspace } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Project } from "../commands/Project";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TEMPLATES_ENABLED } from "../constants";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TEMPLATES_ENABLED, SETTING_GIT_ENABLED, SETTING_DASHBOARD_CONTENT_PAGINATION } from "../constants";
|
||||
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
|
||||
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
|
||||
import { DataFile } from "../models/DataFile";
|
||||
@@ -11,16 +12,32 @@ import { DataType } from "../models/DataType";
|
||||
import { Extension } from "./Extension";
|
||||
import { FrameworkDetector } from "./FrameworkDetector";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
|
||||
|
||||
export class DashboardSettings {
|
||||
private static cachedSettings: ISettings | undefined = undefined;
|
||||
|
||||
public static async get() {
|
||||
public static async get(clear: boolean = false) {
|
||||
if (!this.cachedSettings || clear) {
|
||||
this.cachedSettings = await this.getSettings();
|
||||
}
|
||||
|
||||
return this.cachedSettings;
|
||||
}
|
||||
|
||||
public static async getSettings() {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = Project.isInitialized();
|
||||
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
|
||||
const pagination = Settings.get<boolean | number>(SETTING_DASHBOARD_CONTENT_PAGINATION)
|
||||
|
||||
return {
|
||||
const settings = {
|
||||
git: {
|
||||
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
|
||||
actions: gitActions || false
|
||||
},
|
||||
beta: ext.isBetaVersion(),
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: Folders.getStaticFolderRelativePath(),
|
||||
@@ -36,7 +53,7 @@ export class DashboardSettings {
|
||||
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
|
||||
contentFolders: Folders.get(),
|
||||
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
framework: (!isInitialized && wsFolder) ? await FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []),
|
||||
date: {
|
||||
format: Settings.get<string>(SETTING_DATE_FORMAT) || ""
|
||||
@@ -46,7 +63,8 @@ export class DashboardSettings {
|
||||
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
|
||||
defaultSorting: Settings.get<string>(SETTING_CONTENT_SORTING_DEFAULT),
|
||||
tags: Settings.get<string>(SETTING_DASHBOARD_CONTENT_TAGS),
|
||||
templatesEnabled: Settings.get<boolean>(SETTING_TEMPLATES_ENABLED)
|
||||
templatesEnabled: Settings.get<boolean>(SETTING_TEMPLATES_ENABLED),
|
||||
pagination: pagination !== undefined ? pagination : true
|
||||
},
|
||||
media: {
|
||||
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
|
||||
@@ -62,7 +80,9 @@ export class DashboardSettings {
|
||||
dataTypes: Settings.get<DataType[]>(SETTING_DATA_TYPES),
|
||||
snippets: Settings.get<Snippets>(SETTING_CONTENT_SNIPPETS),
|
||||
isBacker: await ext.getState<boolean | undefined>(CONTEXT.backer, 'global')
|
||||
} as ISettings
|
||||
} as ISettings;
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +90,7 @@ export class DashboardSettings {
|
||||
* @returns
|
||||
*/
|
||||
private static async getDataFiles(): Promise<DataFile[]> {
|
||||
const wsPath = Folders.getWorkspaceFolder()?.fsPath;
|
||||
const wsPath = parseWinPath(Folders.getWorkspaceFolder()?.fsPath);
|
||||
const files = Settings.get<DataFile[]>(SETTING_DATA_FILES);
|
||||
const folders = Settings.get<DataFolder[]>(SETTING_DATA_FOLDERS);
|
||||
|
||||
@@ -86,14 +106,14 @@ export class DashboardSettings {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dataFolderPath = join(folderPath.replace((wsPath || ''), ''));
|
||||
let dataFolderPath = parseWinPath(join(folderPath.replace((wsPath || ''), '')));
|
||||
if (dataFolderPath.startsWith('/')) {
|
||||
dataFolderPath = dataFolderPath.substring(1);
|
||||
}
|
||||
|
||||
const dataJsonFiles = await workspace.findFiles(join(dataFolderPath, '*.json'));
|
||||
const dataYmlFiles = await workspace.findFiles(join(dataFolderPath, '*.yml'));
|
||||
const dataYamlFiles = await workspace.findFiles(join(dataFolderPath, '*.yaml'));
|
||||
const dataJsonFiles = await workspace.findFiles(parseWinPath(join(dataFolderPath, '*.json')));
|
||||
const dataYmlFiles = await workspace.findFiles(parseWinPath(join(dataFolderPath, '*.yml')));
|
||||
const dataYamlFiles = await workspace.findFiles(parseWinPath(join(dataFolderPath, '*.yaml')));
|
||||
|
||||
const dataFiles = [...dataJsonFiles, ...dataYmlFiles, ...dataYamlFiles];
|
||||
for (let dataFile of dataFiles) {
|
||||
@@ -104,7 +124,8 @@ export class DashboardSettings {
|
||||
fileType: dataFile.fsPath.endsWith('.json') ? 'json' : 'yaml',
|
||||
labelField: folder.labelField,
|
||||
schema: folder.schema,
|
||||
type: folder.type
|
||||
type: folder.type,
|
||||
singleEntry: typeof folder.singleEntry === 'boolean' ? folder.singleEntry : false,
|
||||
} as DataFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { DataFile } from "../models";
|
||||
import * as yaml from 'js-yaml';
|
||||
@@ -7,6 +6,7 @@ import { Notifications } from "./Notifications";
|
||||
import { commands } from "vscode";
|
||||
import { COMMAND_NAME, SETTING_DATA_FILES } from "../constants";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
import { existsAsync, readFileAsync } from "../utils";
|
||||
|
||||
|
||||
export class DataFileHelper {
|
||||
@@ -16,10 +16,10 @@ export class DataFileHelper {
|
||||
* @param filePath
|
||||
* @returns
|
||||
*/
|
||||
public static get(filePath: string) {
|
||||
public static async get(filePath: string) {
|
||||
const absPath = Folders.getAbsFilePath(filePath);
|
||||
if (existsSync(absPath)) {
|
||||
return readFileSync(absPath, 'utf8');
|
||||
if (await existsAsync(absPath)) {
|
||||
return await readFileAsync(absPath, 'utf8');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -53,7 +53,7 @@ export class DataFileHelper {
|
||||
public static async process(data: DataFile) {
|
||||
try {
|
||||
const { file, fileType } = data;
|
||||
const dataFile = DataFileHelper.get(file);
|
||||
const dataFile = await DataFileHelper.get(file);
|
||||
|
||||
if (fileType === "yaml") {
|
||||
return yaml.safeLoad(dataFile || "");
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const debounceCallback = () => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return (fnc: any, time: number) => {
|
||||
const functionCall = (...args: any[]) => fnc.apply(args);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(functionCall, time) as any;
|
||||
};
|
||||
};
|
||||
@@ -104,10 +104,20 @@ export class Extension {
|
||||
return this.ctx.extensionMode === ExtensionMode.Production;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the diagnostic collection for the extension
|
||||
*/
|
||||
public get diagnosticCollection(): DiagnosticCollection {
|
||||
return this._collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extension subscriptions
|
||||
*/
|
||||
public get subscriptions() {
|
||||
return this.ctx.subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current version information for the extension
|
||||
*/
|
||||
@@ -144,7 +154,7 @@ export class Extension {
|
||||
|
||||
// Create team settings
|
||||
if (Settings.hasSettings()) {
|
||||
Settings.createTeamSettings();
|
||||
await Settings.createTeamSettings();
|
||||
}
|
||||
|
||||
const hideDateDeprecation = await Extension.getInstance().getState<boolean>(ExtensionState.Updates.v7_0_0.dateFields, "workspace");
|
||||
@@ -222,7 +232,7 @@ export class Extension {
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
Settings.update(SETTING_TEMPLATES_ENABLED, answer?.toLocaleLowerCase() === "yes", true);
|
||||
await Settings.update(SETTING_TEMPLATES_ENABLED, answer?.toLocaleLowerCase() === "yes", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +270,10 @@ export class Extension {
|
||||
return true;
|
||||
}
|
||||
|
||||
public asAbsolutePath(path: string) {
|
||||
return this.ctx.asAbsolutePath(path);
|
||||
}
|
||||
|
||||
public get packageJson() {
|
||||
const frontMatter = extensions.getExtension(this.isBetaVersion() ? EXTENSION_BETA_ID : EXTENSION_ID)!;
|
||||
return frontMatter.packageJSON;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
import * as jsoncParser from 'jsonc-parser';
|
||||
import jsyaml = require("js-yaml");
|
||||
import { join, resolve } from "path";
|
||||
import { commands, Uri } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { COMMAND_NAME } from "../constants";
|
||||
import { COMMAND_NAME, SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID, STATIC_FOLDER_PLACEHOLDER } from "../constants";
|
||||
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
|
||||
import { Framework } from "../models";
|
||||
import { Logger } from "./Logger";
|
||||
import { existsAsync, readFileAsync } from '../utils';
|
||||
import { Settings } from '.';
|
||||
import { parse } from 'path';
|
||||
|
||||
export class FrameworkDetector {
|
||||
|
||||
@@ -18,7 +22,7 @@ export class FrameworkDetector {
|
||||
return FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
}
|
||||
|
||||
private static check(folder: string) {
|
||||
private static async check(folder: string) {
|
||||
let dependencies = null;
|
||||
let devDependencies = null;
|
||||
let gemContent = null;
|
||||
@@ -26,10 +30,10 @@ export class FrameworkDetector {
|
||||
// Try fetching the package JSON file
|
||||
try {
|
||||
const pkgFile = join(folder, 'package.json');
|
||||
if (existsSync(pkgFile)) {
|
||||
let packageJson: any = readFileSync(pkgFile, "utf8");
|
||||
if (await existsAsync(pkgFile)) {
|
||||
let packageJson: any = await readFileAsync(pkgFile, "utf8");
|
||||
if (packageJson) {
|
||||
packageJson = typeof packageJson === "string" ? JSON.parse(packageJson) : packageJson;
|
||||
packageJson = typeof packageJson === "string" ? jsoncParser.parse(packageJson) : packageJson;
|
||||
|
||||
dependencies = packageJson.dependencies || null;
|
||||
devDependencies = packageJson.devDependencies || null;
|
||||
@@ -42,8 +46,8 @@ export class FrameworkDetector {
|
||||
// Try fetching the Gemfile
|
||||
try {
|
||||
const gemFile = join(folder, 'Gemfile');
|
||||
if (existsSync(gemFile)) {
|
||||
gemContent = readFileSync(gemFile, "utf8");
|
||||
if (await existsAsync(gemFile)) {
|
||||
gemContent = await readFileAsync(gemFile, "utf8");
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
@@ -69,7 +73,7 @@ export class FrameworkDetector {
|
||||
|
||||
// Verify by files
|
||||
for (const filename of detector.requiredFiles ?? []) {
|
||||
const fileExists = existsSync(resolve(folder, filename));
|
||||
const fileExists = await existsAsync(resolve(folder, filename));
|
||||
if (fileExists) {
|
||||
return detector.framework;
|
||||
}
|
||||
@@ -80,21 +84,87 @@ export class FrameworkDetector {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static checkDefaultSettings(framework: Framework) {
|
||||
public static async checkDefaultSettings(framework: Framework) {
|
||||
if (framework.name.toLowerCase() === "jekyll") {
|
||||
FrameworkDetector.jekyll();
|
||||
await FrameworkDetector.jekyll();
|
||||
} else if (framework.name.toLowerCase() === "hexo") {
|
||||
await FrameworkDetector.hexo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes for the current framework that need to be applied
|
||||
* @param relAssetPath
|
||||
* @param filePath
|
||||
*/
|
||||
public static relAssetPathUpdate(relAssetPath: string, filePath: string): string {
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
const frameworkId = Settings.get(SETTING_FRAMEWORK_ID);
|
||||
|
||||
private static jekyll() {
|
||||
// Support for HEXO post asset folders
|
||||
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
relAssetPath = relAssetPath.replace(STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder, "");
|
||||
|
||||
// Filename without the extension
|
||||
const fileParsing = parse(filePath);
|
||||
const name = fileParsing.name;
|
||||
relAssetPath = relAssetPath.replace(name, "");
|
||||
relAssetPath = join(relAssetPath);
|
||||
|
||||
// Remove remove the slash at the beginning
|
||||
relAssetPath = parseWinPath(relAssetPath);
|
||||
if (relAssetPath.startsWith("/")) {
|
||||
relAssetPath = relAssetPath.substring(1);
|
||||
}
|
||||
}
|
||||
// Support for HEXO image folder
|
||||
else if (frameworkId === "hexo") {
|
||||
relAssetPath = parseWinPath(relAssetPath);
|
||||
if (relAssetPath.startsWith("/")) {
|
||||
relAssetPath = relAssetPath.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
return parseWinPath(relAssetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default settings for Hexo
|
||||
*/
|
||||
private static async hexo() {
|
||||
try {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const hexoConfig = join(wsFolder?.fsPath || "", '_config.yml');
|
||||
let assetFoler = "source/images";
|
||||
|
||||
if (await existsAsync(hexoConfig)) {
|
||||
const content = await readFileAsync(hexoConfig, "utf8");
|
||||
// Convert YAML to JSON
|
||||
const config = jsyaml.safeLoad(content);
|
||||
|
||||
// Check if post assets are used: https://hexo.io/docs/asset-folders.html#Post-Asset-Folder
|
||||
if (config.post_asset_folder) {
|
||||
assetFoler = STATIC_FOLDER_PLACEHOLDER.hexo.placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
await Settings.update(SETTING_CONTENT_STATIC_FOLDER, assetFoler, true);
|
||||
} catch (e) {
|
||||
Logger.error(`Something failed while processing your Hexo configuration. ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default settings for Jekyll
|
||||
*/
|
||||
private static async jekyll() {
|
||||
try {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const jekyllConfig = join(wsFolder?.fsPath || "", '_config.yml');
|
||||
let collectionDir = "";
|
||||
|
||||
if (existsSync(jekyllConfig)) {
|
||||
const content = readFileSync(jekyllConfig, "utf8");
|
||||
if (await existsAsync(jekyllConfig)) {
|
||||
const content = await readFileAsync(jekyllConfig, "utf8");
|
||||
// Convert YAML to JSON
|
||||
const config = jsyaml.safeLoad(content);
|
||||
|
||||
@@ -106,7 +176,7 @@ export class FrameworkDetector {
|
||||
const draftsPath = join(wsFolder?.fsPath || "", collectionDir, "_drafts");
|
||||
const postsPath = join(wsFolder?.fsPath || "", collectionDir, "_posts");
|
||||
|
||||
if (existsSync(draftsPath)) {
|
||||
if (await existsAsync(draftsPath)) {
|
||||
const folderUri = Uri.file(draftsPath);
|
||||
commands.executeCommand(COMMAND_NAME.registerFolder, {
|
||||
title: "drafts",
|
||||
@@ -114,7 +184,7 @@ export class FrameworkDetector {
|
||||
});
|
||||
}
|
||||
|
||||
if (existsSync(postsPath)) {
|
||||
if (await existsAsync(postsPath)) {
|
||||
const folderUri = Uri.file(postsPath);
|
||||
commands.executeCommand(COMMAND_NAME.registerFolder, {
|
||||
title: "posts",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
import { ExplorerView } from './../explorerView/ExplorerView';
|
||||
import { Uri, window } from 'vscode';
|
||||
import { dirname, join } from "path";
|
||||
import { dirname, extname, join } from "path";
|
||||
import { Field } from '../models';
|
||||
import { existsSync } from 'fs';
|
||||
import { Folders } from '../commands/Folders';
|
||||
@@ -49,7 +50,21 @@ export class ImageHelper {
|
||||
*/
|
||||
public static relToAbs(filePath: string, value: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
let staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor) {
|
||||
const document = editor.document;
|
||||
const filePath = parseWinPath(document.fileName);
|
||||
const pathWithoutExtension = filePath.replace(extname(filePath), '');
|
||||
const assetFilePath = join(pathWithoutExtension, value);
|
||||
|
||||
if (existsSync(assetFilePath)) {
|
||||
return Uri.file(assetFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
|
||||
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
|
||||
|
||||
+55
-32
@@ -1,18 +1,20 @@
|
||||
import { decodeBase64, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
import { decodeBase64, Extension, FrameworkDetector, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
|
||||
import { Dashboard } from "../commands/Dashboard";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
import { SortingOption } from "../dashboardWebView/models";
|
||||
import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models";
|
||||
import { basename, extname, join, parse, dirname, relative } from "path";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, workspace, window, Position } from "vscode";
|
||||
import { basename, join, parse, dirname, relative } from "path";
|
||||
import { statSync } from "fs";
|
||||
import { Uri, workspace, window, Position } from "vscode";
|
||||
import imageSize from "image-size";
|
||||
import { EditorHelper } from "@estruyf/vscode";
|
||||
import { SortOption } from "../dashboardWebView/constants/SortOption";
|
||||
import { DataListener, MediaListener } from "../listeners/panel";
|
||||
import { ArticleHelper } from "./ArticleHelper";
|
||||
import { lookup } from "mime-types";
|
||||
import { existsAsync, readdirAsync, unlinkAsync, writeFileAsync } from "../utils";
|
||||
|
||||
|
||||
export class MediaHelpers {
|
||||
@@ -48,7 +50,7 @@ export class MediaHelpers {
|
||||
if (viewData?.data?.filePath && (viewData?.data?.filePath.endsWith('index.md') || viewData?.data?.filePath.endsWith('index.mdx'))) {
|
||||
const folderPath = parse(viewData.data.filePath).dir;
|
||||
selectedFolder = folderPath;
|
||||
} else if (stateValue && existsSync(stateValue)) {
|
||||
} else if (stateValue && await existsAsync(stateValue)) {
|
||||
selectedFolder = stateValue;
|
||||
}
|
||||
}
|
||||
@@ -77,11 +79,17 @@ export class MediaHelpers {
|
||||
|
||||
allMedia = [...media];
|
||||
} else {
|
||||
if (staticFolder) {
|
||||
if (staticFolder && staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
const folderSearch = join(staticFolder || "", '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
|
||||
|
||||
allMedia = [...media];
|
||||
} else if (staticFolder && staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
const folderSearch = join(STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder, '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
|
||||
|
||||
allMedia = [...media];
|
||||
}
|
||||
|
||||
@@ -149,31 +157,40 @@ export class MediaHelpers {
|
||||
let allContentFolders: string[] = [];
|
||||
let allFolders: string[] = [];
|
||||
|
||||
let foldersFromSelection: string[] = [];
|
||||
|
||||
if (selectedFolder) {
|
||||
if (existsSync(selectedFolder)) {
|
||||
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
|
||||
if (await existsAsync(selectedFolder)) {
|
||||
foldersFromSelection = (await readdirAsync(selectedFolder, { withFileTypes: true })).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
|
||||
}
|
||||
} else {
|
||||
if (pageBundleContentTypes.length > 0) {
|
||||
for (const contentFolder of contentFolders) {
|
||||
const contentPath = contentFolder.path;
|
||||
if (contentPath && existsSync(contentPath)) {
|
||||
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
|
||||
allContentFolders = [...allContentFolders, ...subFolders];
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve all the content folders
|
||||
if (pageBundleContentTypes.length > 0) {
|
||||
for (const contentFolder of contentFolders) {
|
||||
const contentPath = contentFolder.path;
|
||||
if (contentPath && await existsAsync(contentPath)) {
|
||||
const subFolders = (await readdirAsync(contentPath, { withFileTypes: true })).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
|
||||
allContentFolders = [...allContentFolders, ...subFolders];
|
||||
}
|
||||
}
|
||||
|
||||
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
|
||||
if (staticPath && existsSync(staticPath)) {
|
||||
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve all the static folders
|
||||
let staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
|
||||
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
|
||||
staticPath = join(parseWinPath(wsFolder?.fsPath || ""), STATIC_FOLDER_PLACEHOLDER.hexo.postsFolder);
|
||||
}
|
||||
|
||||
if (staticPath && await existsAsync(staticPath)) {
|
||||
allFolders = (await readdirAsync(staticPath, { withFileTypes: true })).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
|
||||
}
|
||||
|
||||
// Store the last opened folder
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
|
||||
|
||||
let sortedFolders = [...allContentFolders, ...allFolders];
|
||||
let sortedFolders = selectedFolder ? foldersFromSelection : [...allContentFolders, ...allFolders];
|
||||
|
||||
sortedFolders = sortedFolders.sort((a, b) => {
|
||||
if (a.toLowerCase() < b.toLowerCase()) {
|
||||
return -1;
|
||||
@@ -192,7 +209,9 @@ export class MediaHelpers {
|
||||
media: files,
|
||||
total: total,
|
||||
folders: sortedFolders,
|
||||
selectedFolder
|
||||
selectedFolder,
|
||||
allContentFolders,
|
||||
allStaticfolders: allFolders,
|
||||
} as MediaPaths
|
||||
}
|
||||
|
||||
@@ -218,11 +237,11 @@ export class MediaHelpers {
|
||||
absFolderPath = folder;
|
||||
}
|
||||
|
||||
if (!existsSync(absFolderPath)) {
|
||||
if (!(await existsAsync(absFolderPath))) {
|
||||
absFolderPath = join(wsPath, folder || "");
|
||||
}
|
||||
|
||||
if (!existsSync(absFolderPath)) {
|
||||
if (!(await existsAsync(absFolderPath))) {
|
||||
Notifications.error(`We couldn't find your selected folder.`);
|
||||
return;
|
||||
}
|
||||
@@ -231,7 +250,7 @@ export class MediaHelpers {
|
||||
const imgData = decodeBase64(contents);
|
||||
|
||||
if (imgData) {
|
||||
writeFileSync(staticPath, imgData.data);
|
||||
await writeFileAsync(staticPath, imgData.data);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
|
||||
|
||||
return true;
|
||||
@@ -255,7 +274,7 @@ export class MediaHelpers {
|
||||
}
|
||||
|
||||
try {
|
||||
unlinkSync(file);
|
||||
await unlinkAsync(file);
|
||||
|
||||
MediaHelpers.media = [];
|
||||
return true;
|
||||
@@ -275,6 +294,10 @@ export class MediaHelpers {
|
||||
Dashboard.resetViewData();
|
||||
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const filePath = data.file;
|
||||
let relPath = data.relPath;
|
||||
@@ -303,7 +326,7 @@ export class MediaHelpers {
|
||||
|
||||
// Snippets are already parsed, so update the URL of the image
|
||||
if (data.snippet) {
|
||||
data.snippet = data.snippet.replace(data.relPath, relPath);
|
||||
data.snippet = data.snippet.replace(data.relPath, FrameworkDetector.relAssetPathUpdate(relPath, editor.document.fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +347,7 @@ export class MediaHelpers {
|
||||
|
||||
const caption = isFile ? `${data.title || ""}` : `${data.alt || data.caption || ""}`;
|
||||
|
||||
const snippet = data.snippet || `${isFile ? "" : "!"}[${caption}](${relPath})`;
|
||||
const snippet = data.snippet || `${isFile ? "" : "!"}[${caption}](${FrameworkDetector.relAssetPathUpdate(relPath, editor.document.fileName).replace(/ /g, "%20")})`;
|
||||
if (selection !== undefined) {
|
||||
builder.replace(selection, snippet);
|
||||
} else {
|
||||
@@ -338,7 +361,7 @@ export class MediaHelpers {
|
||||
|
||||
DataListener.updateMetadata({
|
||||
field: data.fieldName,
|
||||
value: relPath,
|
||||
value: FrameworkDetector.relAssetPathUpdate(relPath, editor.document.fileName),
|
||||
parents: data.parents,
|
||||
blockData: data.blockData
|
||||
});
|
||||
@@ -350,14 +373,14 @@ export class MediaHelpers {
|
||||
* Update the metadata of a media file
|
||||
* @param data
|
||||
*/
|
||||
public static updateMetadata(data: any) {
|
||||
public static async updateMetadata(data: any) {
|
||||
const { file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; } = data;
|
||||
|
||||
const mediaLib = MediaLibrary.getInstance();
|
||||
mediaLib.set(file, metadata);
|
||||
|
||||
// Check if filename needs to be updated
|
||||
mediaLib.updateFilename(file, filename);
|
||||
await mediaLib.updateFilename(file, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,10 +3,10 @@ import { workspace } from 'vscode';
|
||||
import { JsonDB } from 'node-json-db/dist/JsonDB';
|
||||
import { basename, dirname, join, parse } from 'path';
|
||||
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders';
|
||||
import { existsSync, renameSync } from 'fs';
|
||||
import { Notifications } from './Notifications';
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
import { LocalStore } from '../constants';
|
||||
import { existsAsync, renameAsync } from '../utils';
|
||||
|
||||
interface MediaRecord {
|
||||
description: string;
|
||||
@@ -75,7 +75,7 @@ export class MediaLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
public updateFilename(filePath: string, filename: string) {
|
||||
public async updateFilename(filePath: string, filename: string) {
|
||||
const name = basename(filePath);
|
||||
|
||||
if (name !== filename && filename) {
|
||||
@@ -84,10 +84,10 @@ export class MediaLibrary {
|
||||
const newFileInfo = parse(filename);
|
||||
const newPath = join(dirname(filePath), `${newFileInfo.name}${oldFileInfo.ext}`);
|
||||
|
||||
if (existsSync(newPath)) {
|
||||
if (await existsAsync(newPath)) {
|
||||
Notifications.warning(`The name "${filename}" already exists at the file location.`);
|
||||
} else {
|
||||
renameSync(filePath, newPath);
|
||||
await renameAsync(filePath, newPath);
|
||||
this.rename(filePath, newPath);
|
||||
MediaHelpers.resetMedia();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { SETTING_GLOBAL_NOTIFICATIONS_DISABLED } from './../constants/settings';
|
||||
import { window } from "vscode";
|
||||
import { EXTENSION_NAME, SETTING_GLOBAL_NOTIFICATIONS } from "../constants";
|
||||
import { Logger } from "./Logger";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
|
||||
type NotificationType = "INFO" | "WARNING" | "ERROR" | "ERROR_ONCE";
|
||||
|
||||
export class Notifications {
|
||||
private static notifications: string[] = [];
|
||||
|
||||
/**
|
||||
* Show a notification to the user
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static info(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO");
|
||||
|
||||
@@ -17,6 +25,12 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning notification to the user
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static warning(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "WARNING");
|
||||
|
||||
@@ -27,6 +41,12 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error notification to the user
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static error(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "ERROR");
|
||||
|
||||
@@ -37,6 +57,12 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error notification to the user only once
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static async errorShowOnce(message: string, ...items: any): Promise<string | undefined> {
|
||||
if (this.notifications.includes(message)) {
|
||||
return;
|
||||
@@ -47,7 +73,40 @@ export class Notifications {
|
||||
return this.error(message, ...items);
|
||||
}
|
||||
|
||||
private static shouldShow(level: "INFO" | "WARNING" | "ERROR"): boolean {
|
||||
/**
|
||||
* Show the notification if not disabled
|
||||
* @param type
|
||||
* @param notificationType
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static async showIfNotDisabled(type: string, notificationType: NotificationType, message: string, ...items: any): Promise<string | undefined> {
|
||||
const disabledTypes = Settings.get<string[]>(SETTING_GLOBAL_NOTIFICATIONS_DISABLED);
|
||||
|
||||
if (disabledTypes && disabledTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case "WARNING":
|
||||
return await Notifications.warning(message, ...items);
|
||||
case "ERROR":
|
||||
return await Notifications.error(message, ...items);
|
||||
case "ERROR_ONCE":
|
||||
return await Notifications.errorShowOnce(message, ...items);
|
||||
case "INFO":
|
||||
default:
|
||||
return await Notifications.info(message, ...items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the notification should be shown
|
||||
* @param level
|
||||
* @returns
|
||||
*/
|
||||
private static shouldShow(level: NotificationType): boolean {
|
||||
let levels = Settings.get<string[]>(SETTING_GLOBAL_NOTIFICATIONS);
|
||||
|
||||
if (!levels) {
|
||||
|
||||
@@ -2,14 +2,21 @@ import { workspace } from "vscode"
|
||||
import { Extension, Settings } from "."
|
||||
import { Dashboard } from "../commands/Dashboard"
|
||||
import { Preview } from "../commands/Preview"
|
||||
import { Template } from "../commands/Template"
|
||||
import { CONTEXT, DefaultFields, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS } from "../constants"
|
||||
import { Project } from "../commands/Project"
|
||||
import { CONTEXT, DefaultFields, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS, SETTING_GIT_ENABLED } from "../constants"
|
||||
import { GitListener } from "../listeners/general"
|
||||
import { CustomScript, DataType, DraftField, FieldGroup, PanelSettings as IPanelSettings, ScriptType } from "../models"
|
||||
|
||||
export class PanelSettings {
|
||||
|
||||
public static async get(): Promise<IPanelSettings> {
|
||||
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
|
||||
|
||||
return {
|
||||
git: {
|
||||
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
|
||||
actions: gitActions || false
|
||||
},
|
||||
seo: {
|
||||
title: Settings.get(SETTING_SEO_TITLE_LENGTH) as number || -1,
|
||||
slug: Settings.get(SETTING_SEO_SLUG_LENGTH) as number || -1,
|
||||
@@ -29,8 +36,8 @@ export class PanelSettings {
|
||||
categories: Settings.get(SETTING_TAXONOMY_CATEGORIES, true) || [],
|
||||
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
|
||||
freeform: Settings.get(SETTING_PANEL_FREEFORM),
|
||||
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type === ScriptType.Content || !s.type),
|
||||
isInitialized: await Template.isInitialized(),
|
||||
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => (s.type === ScriptType.Content || !s.type) && !s.hidden),
|
||||
isInitialized: Project.isInitialized(),
|
||||
modifiedDateUpdate: Settings.get(SETTING_AUTO_UPDATE_DATE) || false,
|
||||
writingSettingsEnabled: this.isWritingSettingsEnabled() || false,
|
||||
fmHighlighting: Settings.get(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT),
|
||||
|
||||
@@ -8,16 +8,16 @@ import { SlugHelper } from "./SlugHelper";
|
||||
* @param title
|
||||
* @returns
|
||||
*/
|
||||
export const processKnownPlaceholders = (value: string, title: string, dateFormat: string) => {
|
||||
export const processKnownPlaceholders = (value: string, title: string | undefined, dateFormat: string) => {
|
||||
if (value && typeof value === "string") {
|
||||
if (value.includes("{{title}}")) {
|
||||
const regex = new RegExp("{{title}}", "g");
|
||||
value = value.replace(regex, title);
|
||||
value = value.replace(regex, title || "");
|
||||
}
|
||||
|
||||
if (value.includes("{{slug}}")) {
|
||||
const regex = new RegExp("{{slug}}", "g");
|
||||
value = value.replace(regex, SlugHelper.createSlug(title) || "");
|
||||
value = value.replace(regex, SlugHelper.createSlug(title || "") || "");
|
||||
}
|
||||
|
||||
if (value.includes("{{now}}")) {
|
||||
@@ -26,7 +26,7 @@ export const processKnownPlaceholders = (value: string, title: string, dateForma
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
value = value.replace(regex, format(new Date(), DateHelper.formatUpdate(dateFormat) as string));
|
||||
} else {
|
||||
return (new Date()).toISOString();
|
||||
value = value.replace(regex, (new Date()).toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,26 @@ export const processKnownPlaceholders = (value: string, title: string, dateForma
|
||||
const regex = new RegExp("{{day}}", "g");
|
||||
value = value.replace(regex, format(new Date(), "dd"));
|
||||
}
|
||||
|
||||
if (value.includes("{{hour12}}")) {
|
||||
const regex = new RegExp("{{hour12}}", "g");
|
||||
value = value.replace(regex, format(new Date(), "hh"));
|
||||
}
|
||||
|
||||
if (value.includes("{{hour24}}")) {
|
||||
const regex = new RegExp("{{hour24}}", "g");
|
||||
value = value.replace(regex, format(new Date(), "HH"));
|
||||
}
|
||||
|
||||
if (value.includes("{{ampm}}")) {
|
||||
const regex = new RegExp("{{ampm}}", "g");
|
||||
value = value.replace(regex, format(new Date(), "aaa"));
|
||||
}
|
||||
|
||||
if (value.includes("{{minute}}")) {
|
||||
const regex = new RegExp("{{minute}}", "g");
|
||||
value = value.replace(regex, format(new Date(), "mm"));
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
@@ -11,7 +11,11 @@ export class Questions {
|
||||
* @returns
|
||||
*/
|
||||
public static async yesOrNo(placeholder: string) {
|
||||
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: true });
|
||||
const answer = await window.showQuickPick(["yes", "no"], {
|
||||
placeHolder: placeholder,
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
return answer === "yes";
|
||||
}
|
||||
|
||||
@@ -21,7 +25,8 @@ export class Questions {
|
||||
* @returns
|
||||
*/
|
||||
public static async ContentTitle(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const title = await window.showInputBox({
|
||||
const title = await window.showInputBox({
|
||||
title: "Title",
|
||||
prompt: `What would you like to use as a title for the content to create?`,
|
||||
placeHolder: `Content title`,
|
||||
ignoreFocusOut: true
|
||||
@@ -41,16 +46,20 @@ export class Questions {
|
||||
* @returns
|
||||
*/
|
||||
public static async SelectContentFolder(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const folders = Folders.get();
|
||||
let folders = Folders.get();
|
||||
|
||||
let selectedFolder: string | undefined;
|
||||
if (folders.length > 1) {
|
||||
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
|
||||
title: `Select a folder`,
|
||||
placeHolder: `Select where you want to create your content`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
} else {
|
||||
} else if (folders.length === 1) {
|
||||
selectedFolder = folders[0].title;
|
||||
} else {
|
||||
Notifications.warning(`No page folders were configures.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedFolder && showWarning) {
|
||||
@@ -63,16 +72,22 @@ export class Questions {
|
||||
|
||||
/**
|
||||
* Select the content type to create new content
|
||||
* @param allowedCts Allowed content types for the folder
|
||||
* @param showWarning
|
||||
* @returns
|
||||
*/
|
||||
public static async SelectContentType(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const contentTypes = ContentType.getAll();
|
||||
public static async SelectContentType(allowedCts: string[], showWarning: boolean = true): Promise<string | undefined> {
|
||||
let contentTypes = ContentType.getAll();
|
||||
if (!contentTypes || contentTypes.length === 0) {
|
||||
Notifications.warning("No content types found. Please create a content type first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow content types that are allowed for the folder
|
||||
if (allowedCts && allowedCts.length > 0) {
|
||||
contentTypes = contentTypes.filter(ct => allowedCts.find(allowedCt => allowedCt === ct.name));
|
||||
}
|
||||
|
||||
if (contentTypes.length === 1) {
|
||||
return contentTypes[0].name;
|
||||
}
|
||||
@@ -82,6 +97,7 @@ export class Questions {
|
||||
}));
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
title: `Content type`,
|
||||
placeHolder: `Select the content type to create your new content`,
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
|
||||
+285
-53
@@ -1,37 +1,44 @@
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
import { Telemetry } from './Telemetry';
|
||||
import { Notifications } from './Notifications';
|
||||
import { commands, Uri, workspace, window } from 'vscode';
|
||||
import * as vscode from 'vscode';
|
||||
import { ContentType, CustomTaxonomy, TaxonomyType } from '../models';
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent } from '../constants';
|
||||
import { ContentFolder, ContentType, CustomPlaceholder, CustomTaxonomy, DataFile, DataFolder, DataType, TaxonomyType } from '../models';
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent, COMMAND_NAME, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_SNIPPETS, SETTING_CONTENT_PLACEHOLDERS, SETTING_CUSTOM_SCRIPTS, SETTING_DATA_FILES, SETTING_DATA_TYPES, SETTING_DATA_FOLDERS } from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename } from 'path';
|
||||
import { existsSync, readFileSync, watch, writeFileSync } from 'fs';
|
||||
import { join, basename, dirname, parse } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { Extension } from './Extension';
|
||||
import { debounceCallback } from './DebounceCallback';
|
||||
import { Logger } from './Logger';
|
||||
import * as jsoncParser from 'jsonc-parser';
|
||||
import { existsAsync, readFileAsync, writeFileAsync } from '../utils';
|
||||
|
||||
export class Settings {
|
||||
public static globalFile = "frontmatter.json";
|
||||
public static globalConfigFolder = ".frontmatter/config";
|
||||
public static globalConfig: any;
|
||||
private static config: vscode.WorkspaceConfiguration;
|
||||
private static globalConfig: any;
|
||||
private static isInitialized: boolean = false;
|
||||
private static listeners: any[] = [];
|
||||
private static fileCreationWatcher: vscode.FileSystemWatcher | undefined;
|
||||
private static readConfigPromise: Promise<void> | undefined = undefined;
|
||||
|
||||
public static init() {
|
||||
const fmConfig = Settings.projectConfigPath;
|
||||
if (fmConfig && existsSync(fmConfig)) {
|
||||
const localConfig = readFileSync(fmConfig, 'utf8');
|
||||
Settings.globalConfig = JSON.parse(localConfig);
|
||||
commands.executeCommand('setContext', CONTEXT.isEnabled, true);
|
||||
} else {
|
||||
Settings.globalConfig = undefined;
|
||||
public static async init() {
|
||||
await Settings.readConfig();
|
||||
|
||||
Settings.listeners = [];
|
||||
|
||||
if (!Settings.isInitialized) {
|
||||
Settings.isInitialized = true;
|
||||
|
||||
commands.registerCommand(COMMAND_NAME.reloadConfig, Settings.rebindWatchers)
|
||||
}
|
||||
|
||||
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
|
||||
Settings.onConfigChange((global?: any) => {
|
||||
if (global) {
|
||||
Settings.globalConfig = Object.assign({}, global);
|
||||
} else {
|
||||
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
}
|
||||
Settings.onConfigChange(async () => {
|
||||
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,15 +68,28 @@ export class Settings {
|
||||
*/
|
||||
public static onConfigChange(callback: (global?: any) => void) {
|
||||
const projectConfig = Settings.projectConfigPath;
|
||||
const configDebouncer = debounceCallback();
|
||||
|
||||
workspace.onDidChangeConfiguration(() => {
|
||||
callback();
|
||||
});
|
||||
|
||||
// Keep track of the listeners
|
||||
Settings.listeners.push(callback);
|
||||
|
||||
if (projectConfig && !existsSync(projectConfig)) {
|
||||
// No config file, no need to watch
|
||||
Settings.createFileCreationWatcher();
|
||||
return;
|
||||
}
|
||||
|
||||
// Background listener for when it is not a user interaction
|
||||
if (projectConfig && existsSync(projectConfig)) {
|
||||
watch(projectConfig, () => {
|
||||
callback();
|
||||
let watcher = workspace.createFileSystemWatcher(projectConfig, true, false, true);
|
||||
watcher.onDidChange(async (uri: Uri) => {
|
||||
Logger.info(`Config change detected - ${projectConfig} changed`);
|
||||
configDebouncer(() => callback(), 200);
|
||||
// callback()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,18 +97,28 @@ export class Settings {
|
||||
const filename = e.uri.fsPath;
|
||||
|
||||
if (Settings.checkProjectConfig(filename)) {
|
||||
const file = await workspace.openTextDocument(e.uri);
|
||||
if (file) {
|
||||
const fileContents = file.getText();
|
||||
const json = JSON.parse(fileContents);
|
||||
callback(json);
|
||||
Logger.info(`Config change detected - ${projectConfig} saved`);
|
||||
|
||||
Logger.info(`Reloading config...`);
|
||||
if (Settings.readConfigPromise === undefined) {
|
||||
Settings.readConfigPromise = Settings.readConfig();
|
||||
}
|
||||
await Settings.readConfigPromise;
|
||||
|
||||
Logger.info(`Reloaded config...`);
|
||||
configDebouncer(() => callback(), 200);
|
||||
}
|
||||
});
|
||||
|
||||
workspace.onDidDeleteFiles((e) => {
|
||||
workspace.onDidDeleteFiles(async (e) => {
|
||||
const needCallback = e?.files.find(f => Settings.checkProjectConfig(f.fsPath));
|
||||
if (needCallback) {
|
||||
Logger.info(`Reloading config...`);
|
||||
if (Settings.readConfigPromise === undefined) {
|
||||
Settings.readConfigPromise = Settings.readConfig();
|
||||
}
|
||||
await Settings.readConfigPromise;
|
||||
|
||||
callback();
|
||||
}
|
||||
});
|
||||
@@ -113,7 +143,11 @@ export class Settings {
|
||||
/**
|
||||
* Retrieve a setting from global and local config
|
||||
*/
|
||||
public static get<T>(name: string, merging: boolean = false): T | undefined{
|
||||
public static get<T>(name: string, merging: boolean = false): T | undefined {
|
||||
if (!Settings.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configInpection = Settings.config.inspect<T>(name);
|
||||
|
||||
let setting = undefined;
|
||||
@@ -148,17 +182,22 @@ export class Settings {
|
||||
const fmConfig = Settings.projectConfigPath;
|
||||
|
||||
if (updateGlobal) {
|
||||
if (fmConfig && existsSync(fmConfig)) {
|
||||
const localConfig = readFileSync(fmConfig, 'utf8');
|
||||
Settings.globalConfig = JSON.parse(localConfig);
|
||||
if (fmConfig && await existsAsync(fmConfig)) {
|
||||
const localConfig = await readFileAsync(fmConfig, 'utf8');
|
||||
Settings.globalConfig = jsoncParser.parse(localConfig);
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value;
|
||||
writeFileSync(fmConfig, JSON.stringify(Settings.globalConfig, null, 2), 'utf8');
|
||||
|
||||
const content = JSON.stringify(Settings.globalConfig, null, 2);
|
||||
await writeFileAsync(fmConfig, content, 'utf8');
|
||||
|
||||
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
|
||||
if (workspaceSettingValue) {
|
||||
await Settings.update(name, undefined);
|
||||
}
|
||||
|
||||
// Make sure to reload the whole config + all the data files
|
||||
await Settings.readConfig();
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -182,24 +221,24 @@ export class Settings {
|
||||
/**
|
||||
* Create team settings
|
||||
*/
|
||||
public static createTeamSettings() {
|
||||
public static async createTeamSettings() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
this.createGlobalFile(wsFolder);
|
||||
await this.createGlobalFile(wsFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the frontmatter.json file
|
||||
* @param wsFolder
|
||||
*/
|
||||
public static createGlobalFile(wsFolder: Uri | undefined | null) {
|
||||
public static async createGlobalFile(wsFolder: Uri | undefined | null) {
|
||||
const initialConfig = {
|
||||
"$schema": `https://${Extension.getInstance().isBetaVersion() ? `beta.` : ``}frontmatter.codes/frontmatter.schema.json`
|
||||
};
|
||||
|
||||
if (wsFolder) {
|
||||
const configPath = join(wsFolder.fsPath, Settings.globalFile);
|
||||
if (!existsSync(configPath)) {
|
||||
writeFileSync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
|
||||
if (!(await existsAsync(configPath))) {
|
||||
await writeFileAsync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,22 +385,6 @@ export class Settings {
|
||||
return hasSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if its the project config
|
||||
* @param filePath
|
||||
* @returns
|
||||
*/
|
||||
private static checkProjectConfig(filePath: string) {
|
||||
const fmConfig = Settings.projectConfigPath;
|
||||
if (fmConfig && existsSync(fmConfig)) {
|
||||
return filePath &&
|
||||
basename(filePath).toLowerCase() === Settings.globalFile.toLowerCase() &&
|
||||
fmConfig.toLowerCase() === filePath.toLowerCase();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project config path
|
||||
* @returns
|
||||
@@ -374,4 +397,213 @@ export class Settings {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if its the project config
|
||||
* @param filePath
|
||||
* @returns
|
||||
*/
|
||||
private static checkProjectConfig(filePath: string) {
|
||||
const fmConfig = Settings.projectConfigPath;
|
||||
filePath = parseWinPath(filePath);
|
||||
|
||||
if (filePath.includes(Settings.globalConfigFolder)) {
|
||||
return true;
|
||||
} else if (fmConfig && existsSync(fmConfig)) {
|
||||
return filePath &&
|
||||
basename(filePath).toLowerCase() === Settings.globalFile.toLowerCase() &&
|
||||
fmConfig.toLowerCase() === filePath.toLowerCase();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the global config file
|
||||
*/
|
||||
private static async readConfig() {
|
||||
try {
|
||||
const fmConfig = Settings.projectConfigPath;
|
||||
if (fmConfig && await existsAsync(fmConfig)) {
|
||||
const localConfig = await readFileAsync(fmConfig, 'utf8');
|
||||
Settings.globalConfig = jsoncParser.parse(localConfig);
|
||||
commands.executeCommand('setContext', CONTEXT.isEnabled, true);
|
||||
} else {
|
||||
Settings.globalConfig = undefined;
|
||||
}
|
||||
|
||||
// Read the files from the config folder
|
||||
let configFiles = await workspace.findFiles(`**/${Settings.globalConfigFolder}/**/*.json`);
|
||||
if (configFiles.length === 0) {
|
||||
Logger.info(`No ".frontmatter/config" config files found.`);
|
||||
}
|
||||
|
||||
// Sort the files by fsPath
|
||||
configFiles = configFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath));
|
||||
|
||||
for await (const configFile of configFiles) {
|
||||
await Settings.processConfigFile(configFile);
|
||||
}
|
||||
} catch (e) {
|
||||
Settings.globalConfig = undefined;
|
||||
Notifications.error(`Error reading "frontmatter.json" config file. Check [output window](command:${COMMAND_NAME.showOutputChannel}) for more details.`);
|
||||
Logger.error((e as Error).message);
|
||||
}
|
||||
|
||||
Settings.readConfigPromise = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the config file
|
||||
* @param configFile
|
||||
* @returns
|
||||
*/
|
||||
private static async processConfigFile(configFile: Uri) {
|
||||
try {
|
||||
const config = await workspace.fs.readFile(configFile);
|
||||
const configJson = jsoncParser.parse(config.toString());
|
||||
|
||||
const filePath = parseWinPath(configFile.fsPath);
|
||||
const configFilePath = filePath.split(Settings.globalConfigFolder).pop();
|
||||
if (!configFilePath) {
|
||||
return;
|
||||
}
|
||||
Logger.info(`Processing "${configFilePath}" config file.`);
|
||||
|
||||
// Get the path without the filename
|
||||
const configFolder = parseWinPath(dirname(configFilePath));
|
||||
let relSettingName = configFolder.split('/').join('.');
|
||||
if (relSettingName.startsWith('.')) {
|
||||
relSettingName = relSettingName.substring(1);
|
||||
}
|
||||
relSettingName = relSettingName.toLowerCase();
|
||||
|
||||
if (!Settings.globalConfig) {
|
||||
Settings.globalConfig = {};
|
||||
}
|
||||
|
||||
// Array settings
|
||||
if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CUSTOM_SCRIPTS)) {
|
||||
const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] || [];
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] = [...crntValue, configJson];
|
||||
}
|
||||
// Content types
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CONTENT_TYPES, "name", configJson);
|
||||
}
|
||||
// Data files
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FILES)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FILES, "id", configJson);
|
||||
}
|
||||
// Data folders
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FOLDERS)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FOLDERS, "id", configJson);
|
||||
}
|
||||
// Data types
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_TYPES)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_DATA_TYPES, "id", configJson);
|
||||
}
|
||||
// Page folders
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PAGE_FOLDERS)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PAGE_FOLDERS, "path", configJson);
|
||||
}
|
||||
// Placeholders
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PLACEHOLDERS)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PLACEHOLDERS, "id", configJson);
|
||||
}
|
||||
// Object settings
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SNIPPETS)) {
|
||||
Settings.updateGlobalConfigObjectByNameSetting(SETTING_CONTENT_SNIPPETS, configFilePath, configJson, filePath);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(`Error reading config file: ${configFile.fsPath}`);
|
||||
Logger.error((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the setting name is equal or starts with the reference setting name
|
||||
* @param value
|
||||
* @param startsWith
|
||||
* @returns
|
||||
*/
|
||||
private static isEqualOrStartsWith(value: string, startsWith: string) {
|
||||
value = value.toLowerCase();
|
||||
startsWith = startsWith.toLowerCase();
|
||||
|
||||
return value === startsWith || value.startsWith(`${startsWith}.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an array setting in the global config
|
||||
* @param settingName
|
||||
* @param fieldName
|
||||
* @param configJson
|
||||
*/
|
||||
private static updateGlobalConfigArraySetting<T>(settingName: string, fieldName: string, configJson: any): void {
|
||||
const crntValue: T[] = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || [];
|
||||
|
||||
// Check if folder is already added
|
||||
const itemIdx = crntValue.findIndex((item: any) => item[fieldName] === configJson[fieldName]);
|
||||
if (itemIdx === -1) {
|
||||
crntValue.push(configJson);
|
||||
}
|
||||
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] = [...crntValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an object by the file name in the global config
|
||||
* @param settingName
|
||||
* @param fileNamepath
|
||||
* @param configJson
|
||||
*/
|
||||
private static updateGlobalConfigObjectByNameSetting<T>(settingName: string, fileNamepath: string, configJson: any, absPath: string): void {
|
||||
const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || {};
|
||||
|
||||
// Filename is the key
|
||||
const fileName = parse(fileNamepath).name;
|
||||
|
||||
configJson = {
|
||||
...configJson,
|
||||
sourcePath: absPath
|
||||
};
|
||||
|
||||
if (!crntValue[fileName]) {
|
||||
crntValue[fileName] = configJson;
|
||||
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] = { ...crntValue, ...{ [fileName]: configJson } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file creation watcher
|
||||
*/
|
||||
private static createFileCreationWatcher() {
|
||||
const ext = Extension.getInstance();
|
||||
|
||||
if (!Settings.fileCreationWatcher) {
|
||||
Settings.fileCreationWatcher = workspace.createFileSystemWatcher(`**/*.json`, false, true, true);
|
||||
Settings.fileCreationWatcher.onDidCreate(uri => {
|
||||
if (parseWinPath(uri.fsPath) === parseWinPath(Settings.projectConfigPath)) {
|
||||
Settings.rebindWatchers();
|
||||
// Stop listening to file creation events
|
||||
Settings.fileCreationWatcher?.dispose();
|
||||
Settings.fileCreationWatcher = undefined;
|
||||
}
|
||||
}, null, ext.subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebind the configuration watchers
|
||||
*/
|
||||
private static rebindWatchers() {
|
||||
Logger.info(`Rebinding ${this.listeners.length} listeners`);
|
||||
|
||||
this.listeners.forEach(l => {
|
||||
Settings.onConfigChange(l);
|
||||
l();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import { CustomTaxonomy, TaxonomyType, ContentType as IContentType } from "../mo
|
||||
import { FilesHelper } from "./FilesHelper";
|
||||
import { ProgressLocation, window } from "vscode";
|
||||
import { parseWinPath } from "./parseWinPath";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { FrontMatterParser } from "../parsers";
|
||||
import { DumpOptions } from "js-yaml";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { ArticleHelper } from './ArticleHelper';
|
||||
import { ContentType } from './ContentType';
|
||||
import { readFileAsync, writeFileAsync } from '../utils';
|
||||
|
||||
|
||||
export class TaxonomyHelper {
|
||||
@@ -116,7 +116,7 @@ export class TaxonomyHelper {
|
||||
const options = this.getTaxonomyOptions(taxonomyType);
|
||||
|
||||
const newOption = await window.showInputBox({
|
||||
title: `Create a new ${taxonomyType} value`,
|
||||
title: `Create a new ${type} value`,
|
||||
placeHolder: `The value you want to add`,
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (text) => {
|
||||
@@ -186,7 +186,7 @@ export class TaxonomyHelper {
|
||||
for (const file of allFiles) {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
|
||||
const mdFile = readFileSync(parseWinPath(file.fsPath), { encoding: "utf8" });
|
||||
const mdFile = await readFileAsync(parseWinPath(file.fsPath), { encoding: "utf8" });
|
||||
|
||||
if (mdFile) {
|
||||
try {
|
||||
@@ -197,7 +197,10 @@ export class TaxonomyHelper {
|
||||
|
||||
if (fieldNames.length > 0 && article && article.data) {
|
||||
const { data } = article;
|
||||
let taxonomies: string[] = ContentType.getFieldValue(data, fieldNames);
|
||||
let taxonomies: string| string[] = ContentType.getFieldValue(data, fieldNames);
|
||||
if (typeof taxonomies === "string") {
|
||||
taxonomies = taxonomies.split(`,`);
|
||||
}
|
||||
|
||||
if (taxonomies && taxonomies.length > 0) {
|
||||
const idx = taxonomies.findIndex(o => o === oldValue);
|
||||
@@ -214,7 +217,7 @@ export class TaxonomyHelper {
|
||||
|
||||
const spaces = window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
writeFileSync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
|
||||
await writeFileAsync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any), { encoding: "utf8" });
|
||||
}
|
||||
@@ -288,7 +291,7 @@ export class TaxonomyHelper {
|
||||
for (const file of allFiles) {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
|
||||
const mdFile = readFileSync(parseWinPath(file.fsPath), { encoding: "utf8" });
|
||||
const mdFile = await readFileAsync(parseWinPath(file.fsPath), { encoding: "utf8" });
|
||||
|
||||
if (mdFile) {
|
||||
try {
|
||||
@@ -300,8 +303,15 @@ export class TaxonomyHelper {
|
||||
|
||||
if (oldFieldNames.length > 0 && newFieldNames.length > 0 && article && article.data) {
|
||||
const { data } = article;
|
||||
let oldTaxonomies: string[] = ContentType.getFieldValue(data, oldFieldNames) || [];
|
||||
let newTaxonomies: string[] = ContentType.getFieldValue(data, newFieldNames) || [];
|
||||
let oldTaxonomies: string | string[] = ContentType.getFieldValue(data, oldFieldNames) || [];
|
||||
let newTaxonomies: string | string[] = ContentType.getFieldValue(data, newFieldNames) || [];
|
||||
|
||||
if (typeof oldTaxonomies === "string") {
|
||||
oldTaxonomies = oldTaxonomies.split(",");
|
||||
}
|
||||
if (typeof newTaxonomies === "string") {
|
||||
newTaxonomies = newTaxonomies.split(",");
|
||||
}
|
||||
|
||||
if (oldTaxonomies && oldTaxonomies.length > 0) {
|
||||
const idx = oldTaxonomies.findIndex(o => o === value);
|
||||
@@ -314,7 +324,7 @@ export class TaxonomyHelper {
|
||||
|
||||
const spaces = window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
writeFileSync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
|
||||
await writeFileAsync(parseWinPath(file.fsPath), FrontMatterParser.toFile(article.content, article.data, mdFile, {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './CustomScript';
|
||||
export * from './DashboardSettings';
|
||||
export * from './DataFileHelper';
|
||||
export * from './DateHelper';
|
||||
export * from './DebounceCallback';
|
||||
export * from './Extension';
|
||||
export * from './FilesHelper';
|
||||
export * from './FrameworkDetector';
|
||||
|
||||
@@ -4,10 +4,11 @@ import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
|
||||
import { Folders } from '../../commands/Folders';
|
||||
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { DataFileHelper } from '../../helpers';
|
||||
import { existsAsync, readFileAsync, writeFileAsync } from '../../utils';
|
||||
import { mkdirAsync } from '../../utils/mkdirAsync';
|
||||
|
||||
|
||||
export class DataListener extends BaseListener {
|
||||
@@ -35,28 +36,28 @@ export class DataListener extends BaseListener {
|
||||
* Process the data update
|
||||
* @param msgData
|
||||
*/
|
||||
private static processDataUpdate(msgData: any) {
|
||||
const { file, fileType, entries } = msgData as { file: string, fileType: string, entries: any[] };
|
||||
private static async processDataUpdate(msgData: any) {
|
||||
const { file, fileType, entries } = msgData as { file: string, fileType: string, entries: unknown | unknown[] };
|
||||
|
||||
const absPath = Folders.getAbsFilePath(file);
|
||||
if (!existsSync(absPath)) {
|
||||
if (!await existsAsync(absPath)) {
|
||||
const dirPath = dirname(absPath);
|
||||
if (!existsSync(dirPath)) {
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
if (!await existsAsync(dirPath)) {
|
||||
await mkdirAsync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const fileContent = readFileSync(absPath, 'utf8');
|
||||
const fileContent = await readFileAsync(absPath, 'utf8');
|
||||
// check if file content ends with newline
|
||||
const newFileContent = fileContent.endsWith('\n');
|
||||
const insertFinalNewLine = newFileContent || workspace.getConfiguration().get('files.insertFinalNewline');
|
||||
|
||||
if (fileType === 'yaml') {
|
||||
const yamlData = yaml.safeDump(entries);
|
||||
writeFileSync(absPath, insertFinalNewLine ? `${yamlData}\n` : yamlData, 'utf8');
|
||||
await writeFileAsync(absPath, insertFinalNewLine ? `${yamlData}\n` : yamlData, 'utf8');
|
||||
} else {
|
||||
const jsonData = JSON.stringify(entries, null, 2);
|
||||
writeFileSync(absPath, insertFinalNewLine ? `${jsonData}\n` : jsonData, 'utf8');
|
||||
await writeFileAsync(absPath, insertFinalNewLine ? `${jsonData}\n` : jsonData, 'utf8');
|
||||
}
|
||||
|
||||
this.processDataFile(msgData);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { Logger } from "../../helpers";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
|
||||
|
||||
export class LogListener extends BaseListener {
|
||||
|
||||
/**
|
||||
* Process the messages for the dashboard views
|
||||
* @param msg
|
||||
*/
|
||||
public static process(msg: { command: DashboardMessage, data: any }) {
|
||||
super.process(msg);
|
||||
|
||||
switch(msg.command) {
|
||||
case DashboardMessage.logError:
|
||||
Logger.error(msg.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { SortingOption } from '../../dashboardWebView/models';
|
||||
import { commands, env, Uri } from 'vscode';
|
||||
import { COMMAND_NAME, TelemetryEvent } from '../../constants';
|
||||
import * as os from 'os';
|
||||
import { Folders } from '../../commands';
|
||||
|
||||
|
||||
export class MediaListener extends BaseListener {
|
||||
@@ -51,6 +52,11 @@ export class MediaListener extends BaseListener {
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
|
||||
break;
|
||||
case DashboardMessage.createHexoAssetFolder:
|
||||
if (msg?.data.hexoAssetFolderPath) {
|
||||
Folders.createFolder(msg?.data.hexoAssetFolderPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,11 +119,11 @@ export class MediaListener extends BaseListener {
|
||||
* Update media metadata
|
||||
* @param data
|
||||
*/
|
||||
private static update(data: any) {
|
||||
private static async update(data: any) {
|
||||
try {
|
||||
const { page, folder } = data;
|
||||
|
||||
MediaHelpers.updateMetadata(data);
|
||||
await MediaHelpers.updateMetadata(data);
|
||||
|
||||
this.sendMediaFiles(page || 0, folder || "");
|
||||
} catch {}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from './../../constants/ContentType';
|
||||
import { isValidFile } from '../../helpers/isValidFile';
|
||||
import { existsSync, unlinkSync } from "fs";
|
||||
import { basename, dirname, join } from "path";
|
||||
import { basename } from "path";
|
||||
import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode";
|
||||
import { Dashboard } from "../../commands/Dashboard";
|
||||
import { Folders } from "../../commands/Folders";
|
||||
import { COMMAND_NAME, DefaultFields, ExtensionState, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants";
|
||||
import { COMMAND_NAME, ExtensionState } from "../../constants";
|
||||
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { Page } from "../../dashboardWebView/models";
|
||||
import { ArticleHelper, Extension, Logger, Settings } from "../../helpers";
|
||||
import { ContentType } from "../../helpers/ContentType";
|
||||
import { DateHelper } from "../../helpers/DateHelper";
|
||||
import { Notifications } from "../../helpers/Notifications";
|
||||
import { ArticleHelper, Extension, Logger } from "../../helpers";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { DataListener } from '../panel';
|
||||
import Fuse from 'fuse.js';
|
||||
import { PagesParser } from '../../services/PagesParser';
|
||||
import { unlinkAsync } from "../../utils";
|
||||
|
||||
|
||||
export class PagesListener extends BaseListener {
|
||||
@@ -110,7 +106,7 @@ export class PagesListener extends BaseListener {
|
||||
|
||||
Logger.info(`Deleting file: ${path}`)
|
||||
|
||||
unlinkSync(path);
|
||||
await unlinkAsync(path);
|
||||
|
||||
this.lastPages = this.lastPages.filter(p => p.fmFilePath !== path);
|
||||
this.sendPageData(this.lastPages);
|
||||
@@ -132,7 +128,7 @@ export class PagesListener extends BaseListener {
|
||||
if (pageIdx !== -1) {
|
||||
const stats = await workspace.fs.stat(file);
|
||||
const crntPage = this.lastPages[pageIdx];
|
||||
const updatedPage = this.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder);
|
||||
const updatedPage = await PagesParser.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder);
|
||||
if (updatedPage) {
|
||||
this.lastPages[pageIdx] = updatedPage;
|
||||
this.sendPageData(this.lastPages);
|
||||
@@ -156,43 +152,18 @@ export class PagesListener extends BaseListener {
|
||||
if (cachedPages) {
|
||||
this.sendPageData(cachedPages);
|
||||
}
|
||||
} else {
|
||||
PagesParser.reset();
|
||||
}
|
||||
|
||||
// Update the dashboard with the fresh data
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Page[] = [];
|
||||
PagesParser.getPages(async (pages: Page[]) => {
|
||||
this.lastPages = pages;
|
||||
this.sendPageData(pages);
|
||||
|
||||
if (folderInfo) {
|
||||
for (const folder of folderInfo) {
|
||||
for (const file of folder.lastModified) {
|
||||
if (isValidFile(file.fileName)) {
|
||||
try {
|
||||
const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title);
|
||||
|
||||
if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) {
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
if ((error as Error)?.message.toLowerCase() === "webview is disposed") {
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`);
|
||||
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPages = pages;
|
||||
this.sendPageData(pages);
|
||||
|
||||
this.sendMsg(DashboardCommand.searchReady, true);
|
||||
|
||||
await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace");
|
||||
await this.createSearchIndex(pages);
|
||||
this.sendMsg(DashboardCommand.searchReady, true);
|
||||
|
||||
await this.createSearchIndex(pages);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,134 +216,4 @@ export class PagesListener extends BaseListener {
|
||||
public static refresh() {
|
||||
this.getPagesData(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the page content
|
||||
* @param filePath
|
||||
* @param fileMtime
|
||||
* @param fileName
|
||||
* @param folderTitle
|
||||
* @returns
|
||||
*/
|
||||
private static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined {
|
||||
const article = ArticleHelper.getFrontMatterByPath(filePath);
|
||||
|
||||
if (article?.data.title) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
|
||||
|
||||
const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
|
||||
const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined;
|
||||
|
||||
const modifiedField = ArticleHelper.getModifiedDateField(article) || null;
|
||||
const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined;
|
||||
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
const page: Page = {
|
||||
...article.data,
|
||||
// FrontMatter properties
|
||||
fmFolder: folderTitle,
|
||||
fmFilePath: filePath,
|
||||
fmFileName: fileName,
|
||||
fmDraft: ContentType.getDraftStatus(article?.data),
|
||||
fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime,
|
||||
fmPublished: dateFieldValue ? dateFieldValue.getTime() : null,
|
||||
fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null,
|
||||
fmPreviewImage: "",
|
||||
fmTags: [],
|
||||
fmCategories: [],
|
||||
fmContentType: DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmBody: article?.content || "",
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
date: article?.data[dateField] || "",
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (contentType) {
|
||||
page.fmContentType = contentType.name;
|
||||
}
|
||||
|
||||
let previewFieldParents = ContentType.findPreviewField(contentType.fields);
|
||||
if (previewFieldParents.length === 0) {
|
||||
const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview");
|
||||
if (previewField) {
|
||||
previewFieldParents = ["preview"];
|
||||
}
|
||||
}
|
||||
|
||||
let tagParents = ContentType.findFieldByType(contentType.fields, "tags");
|
||||
page.fmTags = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]);
|
||||
|
||||
let categoryParents = ContentType.findFieldByType(contentType.fields, "categories");
|
||||
page.fmCategories = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]);
|
||||
|
||||
// Check if parent fields were retrieved, if not there was no image present
|
||||
if (previewFieldParents.length > 0) {
|
||||
let fieldValue = null;
|
||||
let crntPageData = article?.data;
|
||||
|
||||
for (let i = 0; i < previewFieldParents.length; i++) {
|
||||
const previewField = previewFieldParents[i];
|
||||
|
||||
if (i === previewFieldParents.length - 1) {
|
||||
fieldValue = crntPageData[previewField];
|
||||
} else {
|
||||
if (!crntPageData[previewField]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
crntPageData = crntPageData[previewField];
|
||||
|
||||
// Check for preview image in block data
|
||||
if (crntPageData instanceof Array && crntPageData.length > 0) {
|
||||
// Get the first field block that contains the next field data
|
||||
const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]);
|
||||
if (fieldData) {
|
||||
crntPageData = fieldData;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldValue && wsFolder) {
|
||||
if (fieldValue && Array.isArray(fieldValue)) {
|
||||
if (fieldValue.length > 0) {
|
||||
fieldValue = fieldValue[0];
|
||||
} else {
|
||||
fieldValue = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Revalidate as the array could have been empty
|
||||
if (fieldValue) {
|
||||
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
|
||||
const contentFolderPath = join(dirname(filePath), fieldValue);
|
||||
|
||||
let previewUri = null;
|
||||
if (existsSync(staticPath)) {
|
||||
previewUri = Uri.file(staticPath);
|
||||
} else if (existsSync(contentFolderPath)) {
|
||||
previewUri = Uri.file(contentFolderPath);
|
||||
}
|
||||
|
||||
if (previewUri) {
|
||||
const preview = Dashboard.getWebview()?.asWebviewUri(previewUri);
|
||||
page["fmPreviewImage"] = preview?.toString() || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -42,15 +42,15 @@ export class SettingsListener extends BaseListener {
|
||||
private static async update(data: { name: string, value: any }) {
|
||||
if (data.name) {
|
||||
await Settings.update(data.name, data.value);
|
||||
this.getSettings();
|
||||
this.getSettings(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the settings for the dashboard
|
||||
*/
|
||||
public static async getSettings() {
|
||||
const settings = await DashboardSettings.get();
|
||||
public static async getSettings(clear: boolean = false) {
|
||||
const settings = await DashboardSettings.get(clear);
|
||||
|
||||
this.sendMsg(DashboardCommand.settings, settings);
|
||||
}
|
||||
@@ -59,22 +59,24 @@ export class SettingsListener extends BaseListener {
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
*/
|
||||
public static setFramework(frameworkId: string | null) {
|
||||
Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
|
||||
public static async setFramework(frameworkId: string | null) {
|
||||
await Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
|
||||
|
||||
if (frameworkId) {
|
||||
const allFrameworks = FrameworkDetector.getAll();
|
||||
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
|
||||
if (framework) {
|
||||
Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
if (framework.static) {
|
||||
await Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
}
|
||||
|
||||
FrameworkDetector.checkDefaultSettings(framework);
|
||||
await FrameworkDetector.checkDefaultSettings(framework);
|
||||
} else {
|
||||
Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
|
||||
await Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsListener.getSettings();
|
||||
SettingsListener.getSettings(true);
|
||||
}
|
||||
|
||||
private static addFolder(folder: string) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user