mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e695bad1c6 | ||
|
|
fe31081907 | ||
|
|
248ccb3718 | ||
|
|
2260174ec2 | ||
|
|
cd3a867422 | ||
|
|
05a63dd110 | ||
|
|
cfc0c3d5a1 | ||
|
|
d6dbca25ce | ||
|
|
d21ad14e89 | ||
|
|
8d00726322 | ||
|
|
af11c304d3 | ||
|
|
223276f6af | ||
|
|
a6fdfe0dfa | ||
|
|
6154164b1c | ||
|
|
1cdb6c56a5 | ||
|
|
c63310a2db | ||
|
|
9f681a7459 | ||
|
|
2610032a38 | ||
|
|
d11e8112e0 | ||
|
|
df5e346cf1 | ||
|
|
9892d14a62 | ||
|
|
8c61f79885 | ||
|
|
ffa6638d3d | ||
|
|
f9ef12bd3a | ||
|
|
b79216f2b5 | ||
|
|
2597a63718 | ||
|
|
126a21a6b5 | ||
|
|
3abfbd5302 | ||
|
|
efdbce2d08 | ||
|
|
09eea16d60 | ||
|
|
71697a09b6 | ||
|
|
3583a2b962 | ||
|
|
2825d5ddd8 | ||
|
|
2e66174c4a | ||
|
|
e78069ad17 | ||
|
|
4c97993c5f | ||
|
|
a452173d9a | ||
|
|
60a38be923 | ||
|
|
6c3d286282 | ||
|
|
32c7bbd3f9 | ||
|
|
426dbc2e46 | ||
|
|
9882dea960 | ||
|
|
eb9a05e90c | ||
|
|
4e59e736ed | ||
|
|
9f91ebf289 | ||
|
|
b80de402bd | ||
|
|
e3c535276c | ||
|
|
add22b0bd0 | ||
|
|
48f855144e | ||
|
|
67291f0cbe | ||
|
|
45b302c698 | ||
|
|
f897edab5f | ||
|
|
dffb9f3dd8 | ||
|
|
573e1966ae | ||
|
|
d161aa98a0 | ||
|
|
f10d93c22e | ||
|
|
17a98fba68 | ||
|
|
dee28397cb | ||
|
|
c4055eb37c | ||
|
|
91049bebd9 | ||
|
|
9f6c35b9ec | ||
|
|
f0ed7c0b39 | ||
|
|
e64c4fc0f8 | ||
|
|
cd19cec4f7 | ||
|
|
070fc53685 | ||
|
|
b232c55843 | ||
|
|
55a14b3fbe | ||
|
|
60fde1711e | ||
|
|
31ca9d4e8b | ||
|
|
bd47a09d1e | ||
|
|
744322a398 | ||
|
|
70de0e3ebd | ||
|
|
935ef83c4f | ||
|
|
93370095e9 | ||
|
|
e8e9a5a5d3 | ||
|
|
5e23d4446b | ||
|
|
6ef1ba5b57 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'Issue: '
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'Enhancement: '
|
||||
labels: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# PR Details
|
||||
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!--- This project only accepts pull requests related to open issues -->
|
||||
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
|
||||
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
|
||||
<!--- Please link to the issue here: -->
|
||||
|
||||
## Motivation and Context
|
||||
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
|
||||
## How Has This Been Tested
|
||||
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
<!--- see how your change affects other areas of the code, etc. -->
|
||||
|
||||
## Types of changes
|
||||
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] Docs change / refactoring / dependency upgrade
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
27
.github/workflows/project-labelling.yml
vendored
Normal file
27
.github/workflows/project-labelling.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Project labelling
|
||||
|
||||
on:
|
||||
project_card:
|
||||
types: [created, moved, deleted]
|
||||
|
||||
jobs:
|
||||
automate-issues-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch project data
|
||||
run: |
|
||||
echo 'PROJECT_DATA<<EOF' >> $GITHUB_ENV
|
||||
curl --request GET --url '${{ github.event.project_card.project_url }}' --header 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
|
||||
- name: Add the project label
|
||||
uses: andymckay/labeler@master
|
||||
if: ${{ contains(github.event.action, 'created') || contains(github.event.action, 'moved') }}
|
||||
with:
|
||||
add-labels: "Project: ${{ fromJSON(env.PROJECT_DATA).name }}"
|
||||
|
||||
- name: Remove the project label
|
||||
uses: andymckay/labeler@master
|
||||
if: ${{ contains(github.event.action, 'deleted') }}
|
||||
with:
|
||||
remove-labels: "Project: ${{ fromJSON(env.PROJECT_DATA).name }}"
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -2,6 +2,7 @@
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"eliostruyf.vscode-typescript-exportallmodules"
|
||||
]
|
||||
}
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,10 +1,70 @@
|
||||
# Change Log
|
||||
|
||||
## [7.3.2] - 2022-06-01
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#346](https://github.com/estruyf/vscode-front-matter/issues/346): Fix media dashboard refresh action
|
||||
|
||||
|
||||
## [7.3.1] - 2022-05-26
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#343](https://github.com/estruyf/vscode-front-matter/issues/343): Fix in the schema for the `frontMatter.taxonomy.fieldGroups` setting
|
||||
|
||||
## [7.3.0] - 2022-05-25 - [Release notes](https://beta.frontmatter.codes/updates/v7.3.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- JSON schema enhancements for working with data files
|
||||
- [#330](https://github.com/estruyf/vscode-front-matter/issues/330): Allow custom scripts to easily update front matter
|
||||
- [#331](https://github.com/estruyf/vscode-front-matter/issues/331): Added functionality to run other type of scripts
|
||||
- [#332](https://github.com/estruyf/vscode-front-matter/issues/332): New `dataFile` field which allows you to create data file references
|
||||
- [#333](https://github.com/estruyf/vscode-front-matter/issues/333): Automatically mark Jekyll posts in `_drafts` folder as draft
|
||||
- [#335](https://github.com/estruyf/vscode-front-matter/issues/335): Merge media snippets with content snippets to allow you to define multiple media snippets and use these in your content
|
||||
- [#336](https://github.com/estruyf/vscode-front-matter/issues/336): Support added for inverting the draft field so that SSGs/authors can use a published field instead
|
||||
- [#337](https://github.com/estruyf/vscode-front-matter/issues/337): Allow multiple front matter types to be used
|
||||
- [#338](https://github.com/estruyf/vscode-front-matter/issues/338): Ability to disable the templates functionality (default is disabled)
|
||||
- [#340](https://github.com/estruyf/vscode-front-matter/issues/340): Show an error message when there is a content folder registered that does not exist in the project
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#334](https://github.com/estruyf/vscode-front-matter/issues/334): Fix for locked content folders retrieval
|
||||
- [#339](https://github.com/estruyf/vscode-front-matter/issues/339): Fix for content folders without a title
|
||||
|
||||
|
||||
## [7.2.0] - 2022-05-02 - [Release notes](https://beta.frontmatter.codes/updates/v7.2.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- New tag design for the tags, category, and taxonomy fields
|
||||
- [#263](https://github.com/estruyf/vscode-front-matter/issues/263): WYSIWYG string field option
|
||||
- [#308](https://github.com/estruyf/vscode-front-matter/issues/308): New `File` field
|
||||
- [#314](https://github.com/estruyf/vscode-front-matter/issues/314): New preview actions to open the page in the browser and refresh the preview
|
||||
- [#322](https://github.com/estruyf/vscode-front-matter/issues/322): Show parent folder name when file is an index page (`index.md` / `_index.md`)
|
||||
- [#323](https://github.com/estruyf/vscode-front-matter/issues/323): Added 11ty, jekyll, and docusaurus to the framework selection list
|
||||
- [#325](https://github.com/estruyf/vscode-front-matter/issues/325): Better welcome experience that allows you to add content folders straight from the welcome view
|
||||
- [#326](https://github.com/estruyf/vscode-front-matter/issues/326): Content type actions to create, update, or set according to the current file
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- [#316](https://github.com/estruyf/vscode-front-matter/issues/316): Suppress file parsing errors when closing the dashboard
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Updated JSON schema link to supported version by VS Code (draft-07)
|
||||
- Hide the view mode action from the Front Matter panel if no custom modes are defined
|
||||
- Fix in decode base64 uploaded video files
|
||||
- Fix for a lightbox on other types of documents (pdf, etc.)
|
||||
- Fix for hiding the image preview on slide-over for none image documents
|
||||
- [#324](https://github.com/estruyf/vscode-front-matter/issues/324): Fix for the framework selection on the welcome screen
|
||||
|
||||
## [7.1.2] - 2022-04-11
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#316](https://github.com/estruyf/vscode-front-matter/issues/316): Fix draft tab navigation
|
||||
- [#315](https://github.com/estruyf/vscode-front-matter/issues/315): Fix draft tab navigation
|
||||
|
||||
## [7.1.1] - 2022-04-08
|
||||
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
elio@struyfconsulting.be.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
59
CONTRIBUTING.md
Normal file
59
CONTRIBUTING.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Contributing to Front Matter
|
||||
|
||||
First of all, it is amazing you want to contribute to Front Matter 💚.
|
||||
|
||||
There are various ways in how you can contribute to the project, it can be as simple from opening a bug report to implementing fixes or features.
|
||||
|
||||
## How you can help us
|
||||
|
||||
- Testing out the extension and providing feedback
|
||||
- Reporting issues and bugs
|
||||
- Suggesting new features
|
||||
- Fixing an issue
|
||||
- Updating documentation
|
||||
- UI improvements
|
||||
- Tutorials
|
||||
- etc.
|
||||
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following projects:
|
||||
|
||||
- [Extension](https://github.com/estruyf/vscode-front-matter)
|
||||
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
- [Sample Projects](https://github.com/FrontMatter/project-samples)
|
||||
|
||||
## How to get started
|
||||
|
||||
- Start by forking this project;
|
||||
- Clone your fork to your local machine;
|
||||
- Run `npm i`;
|
||||
- Open the project in VS Code;
|
||||
- To start developing, run `npm run dev:ext` and press `f5` to start the debugging session.
|
||||
|
||||
### Tips
|
||||
|
||||
- Ensure that the main branch on your fork is in sync with the original **vscode-front-matter** repository
|
||||
|
||||
```bash
|
||||
# assuming you are in the folder of your locally cloned fork....
|
||||
git checkout main
|
||||
|
||||
# assuming you have a remote named `upstream` pointing to the official **vscode-front-matter** repo
|
||||
git fetch upstream
|
||||
|
||||
# update your local main to be a mirror of what's in the main repo
|
||||
git pull --rebase upstream main
|
||||
```
|
||||
|
||||
- Create a feature branch in your fork. In case you get stuck, or have issues with merging your PR, this will allow you to have a clean main branch that you can use for contributing other changes.
|
||||
|
||||
```bash
|
||||
git checkout -b issue/<id>
|
||||
```
|
||||
|
||||
## Pull request
|
||||
|
||||
Once you are done with implementing the fix or feature. Please create a PR to our `dev` branch.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
@@ -221,60 +221,6 @@
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.article__tags__items__item {
|
||||
display: inline-flex;
|
||||
margin-bottom: .5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.article__tags__items__item {
|
||||
display: inline-block;
|
||||
margin-bottom: .5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.article__tags__items__item_add,
|
||||
.article__tags__items__item_delete {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.article__tags__items__item svg {
|
||||
display: inline;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.article__tags__items__item_delete span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.article__tags__items__pill_notexists {
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
padding-left: .5rem;
|
||||
}
|
||||
|
||||
.article__tags__items__pill_notexists:hover {
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
|
||||
filter: contrast(60%);
|
||||
}
|
||||
|
||||
.article__tags__items__item_add {
|
||||
color: var(--vscode-inputValidation-infoForeground);
|
||||
background-color: var(--vscode-inputValidation-infoBackground);
|
||||
border-right: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
}
|
||||
|
||||
.article__tags__items__item_add:hover {
|
||||
color: var(--vscode-inputValidation-infoForeground);
|
||||
background-color: var(--vscode-inputValidation-infoBackground);
|
||||
border-right: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
|
||||
filter: contrast(60%);
|
||||
}
|
||||
|
||||
.article__actions > * + *,
|
||||
.other_actions > * + *,
|
||||
.base__actions > * + *,
|
||||
@@ -355,6 +301,11 @@
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.ext_link_block a:hover,
|
||||
.ext_link_block button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.table__cell {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -639,6 +590,7 @@ input:checked + .field__toggle__slider:before {
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.metadata_field__file__button,
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--vscode-button-background);
|
||||
@@ -646,11 +598,13 @@ input:checked + .field__toggle__slider:before {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button:hover,
|
||||
.metadata_field__preview_image__button:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button svg,
|
||||
.metadata_field__preview_image__button svg {
|
||||
color: var(--vscode-foreground);
|
||||
display: block;
|
||||
@@ -659,6 +613,7 @@ input:checked + .field__toggle__slider:before {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.metadata_field__file__button span,
|
||||
.metadata_field__preview_image__button span {
|
||||
color: var(--vscode-foreground);
|
||||
display: inline-block;
|
||||
@@ -773,8 +728,12 @@ input:checked + .field__toggle__slider:before {
|
||||
}
|
||||
|
||||
/* Timepicker */
|
||||
.react-datepicker button {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-datepicker button:hover {
|
||||
background-color: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.react-datepicker__triangle {
|
||||
|
||||
@@ -50,6 +50,28 @@
|
||||
],
|
||||
"openingTags": "{{",
|
||||
"closingTags": "}}"
|
||||
},
|
||||
"Issue link": {
|
||||
"description": "Link to a GitHub issue",
|
||||
"body": "- [#{{id}}](https://github.com/estruyf/vscode-front-matter/issues/{{id}}): {{title}}",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "Issue ID",
|
||||
"type": "string",
|
||||
"single": true,
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "Title",
|
||||
"type": "string",
|
||||
"single": true,
|
||||
"default": ""
|
||||
}
|
||||
],
|
||||
"openingTags": "{{",
|
||||
"closingTags": "}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
111
package-lock.json
generated
111
package-lock.json
generated
@@ -1,9 +1,27 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "7.1.2",
|
||||
"version": "7.3.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tunnel": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
||||
@@ -642,6 +660,15 @@
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/quill": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
|
||||
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"parchment": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||
@@ -1432,6 +1459,12 @@
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
|
||||
"dev": true
|
||||
},
|
||||
"clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
@@ -2286,6 +2319,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"dev": true
|
||||
},
|
||||
"extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
@@ -2301,6 +2340,12 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-diff": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
|
||||
@@ -2574,12 +2619,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"gray-matter": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
|
||||
"integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"js-yaml": "^3.11.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"kind-of": "^6.0.2",
|
||||
"section-matter": "^1.0.0",
|
||||
"strip-bom-string": "^1.0.0"
|
||||
@@ -4224,6 +4269,12 @@
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"parchment": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
|
||||
"dev": true
|
||||
},
|
||||
"parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -4606,6 +4657,39 @@
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true
|
||||
},
|
||||
"quill": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
|
||||
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clone": "^2.1.1",
|
||||
"deep-equal": "^1.0.1",
|
||||
"eventemitter3": "^2.0.3",
|
||||
"extend": "^3.0.2",
|
||||
"parchment": "^1.1.4",
|
||||
"quill-delta": "^3.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||
"integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"quill-delta": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"deep-equal": "^1.0.1",
|
||||
"extend": "^3.0.2",
|
||||
"fast-diff": "1.1.2"
|
||||
}
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -4707,6 +4791,17 @@
|
||||
"warning": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"react-quill": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/quill": "^1.3.10",
|
||||
"lodash": "^4.17.4",
|
||||
"quill": "^1.3.7"
|
||||
}
|
||||
},
|
||||
"react-sortable-hoc": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
|
||||
@@ -5734,6 +5829,12 @@
|
||||
"tslib": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"dev": true
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
||||
201
package.json
201
package.json
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter",
|
||||
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "7.1.2",
|
||||
"version": "7.3.2",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -137,6 +137,10 @@
|
||||
"type": "string",
|
||||
"description": "Name of the field to use"
|
||||
},
|
||||
"invert": {
|
||||
"type": "boolean",
|
||||
"description": "By default the draft field is set to true when the content is a draft. Set this to true to set it to false."
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "List of choices for the field",
|
||||
@@ -225,8 +229,7 @@
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"body",
|
||||
"fields"
|
||||
"body"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
@@ -255,6 +258,11 @@
|
||||
"description": "The snippet closing tags.",
|
||||
"type": "string",
|
||||
"default": "]]"
|
||||
},
|
||||
"isMediaSnippet": {
|
||||
"description": "Specify if the snippet is to be used for media files.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -342,7 +350,7 @@
|
||||
},
|
||||
"nodeBin": {
|
||||
"type": "string",
|
||||
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use."
|
||||
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use. (deprecated: use the command property instead)"
|
||||
},
|
||||
"bulk": {
|
||||
"type": "boolean",
|
||||
@@ -369,6 +377,25 @@
|
||||
"mediaFile"
|
||||
],
|
||||
"description": "The type for which the script will be used."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"node",
|
||||
"bash",
|
||||
"powershell",
|
||||
"python",
|
||||
"python3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "The type of script you want to execute.",
|
||||
"default": "node"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -389,6 +416,7 @@
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
|
||||
"deprecationMessage": "This setting is deprecated and will be removed in the next major version. Please define your media snippet in the `frontMatter.content.snippet` setting.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Use the `{mediaUrl}`, `{caption}`, `{alt}`, `{filename}`, `{mediaHeight}`, and `{mediaWidth}` placeholders in your snippet to automatically insert the media information."
|
||||
@@ -426,7 +454,8 @@
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to load. Only JSON or YAML files are supported."
|
||||
"description": "Path to the file to load. Only JSON or YAML files are supported.",
|
||||
"default": "[[workspace]]/"
|
||||
},
|
||||
"fileType": {
|
||||
"type": "string",
|
||||
@@ -438,10 +467,38 @@
|
||||
"description": "Defines how you want to parse the file. JSON is the default."
|
||||
},
|
||||
"schema": {
|
||||
"$id": "#dataFileSchema",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "The JSON schema for your data which will be used to render the data form.",
|
||||
"additionalProperties": true
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"type",
|
||||
"properties"
|
||||
],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title of the form."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Defines the type of the form. Default is 'object'.",
|
||||
"default": "object"
|
||||
},
|
||||
"required": {
|
||||
"type": "array",
|
||||
"description": "Defines the required fields for the form.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"description": "Defines the fields of the form.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
@@ -488,13 +545,11 @@
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the folder to load files."
|
||||
"description": "Path to the folder to load files.",
|
||||
"default": "[[workspace]]/"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "The JSON schema for your data which will be used to render the data form.",
|
||||
"additionalProperties": true
|
||||
"$ref": "#dataFileSchema"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
@@ -535,10 +590,7 @@
|
||||
"description": "Your unique ID you want to use for your data type."
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "The JSON schema for your data which will be used to render the data form.",
|
||||
"additionalProperties": true
|
||||
"$ref": "#dataFileSchema"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -598,6 +650,7 @@
|
||||
"panel.globalSettings",
|
||||
"panel.seo",
|
||||
"panel.actions",
|
||||
"panel.contentType",
|
||||
"panel.metadata",
|
||||
"panel.recentlyModified",
|
||||
"panel.otherActions",
|
||||
@@ -744,6 +797,7 @@
|
||||
"datetime",
|
||||
"boolean",
|
||||
"image",
|
||||
"file",
|
||||
"choice",
|
||||
"taxonomy",
|
||||
"tags",
|
||||
@@ -751,7 +805,8 @@
|
||||
"draft",
|
||||
"fields",
|
||||
"json",
|
||||
"block"
|
||||
"block",
|
||||
"dataFile"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -802,6 +857,11 @@
|
||||
"default": false,
|
||||
"description": "Is a single line field"
|
||||
},
|
||||
"wysiwyg": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Is a WYSIWYG field (HTML output)"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -822,6 +882,13 @@
|
||||
"default": "",
|
||||
"description": "The ID of your taxonomy field"
|
||||
},
|
||||
"fileExtensions": {
|
||||
"type": "array",
|
||||
"description": "Specify the file extensions to allow for the file picker",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"$ref": "#contenttypefield"
|
||||
},
|
||||
@@ -861,6 +928,21 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the field is the modified date field"
|
||||
},
|
||||
"dataFileId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Specify the ID of the data file to use for this field"
|
||||
},
|
||||
"dataFileKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Specify the key of the data file to use for this field"
|
||||
},
|
||||
"dataFileValue": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Specify the property name that will be used to show the value for the field"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -869,6 +951,35 @@
|
||||
"name"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "dataFile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"dataFileId",
|
||||
"dataFileKey"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"fileExtensions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
@@ -1063,7 +1174,7 @@
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"id",
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
@@ -1170,6 +1281,12 @@
|
||||
"default": "yyyy-MM-dd",
|
||||
"markdownDescription": "Specify the prefix you want to add for your new article filenames. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.prefix)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.templates.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.enabled)",
|
||||
"scope": "Templates"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1179,6 +1296,21 @@
|
||||
"title": "Authenticate",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.generate",
|
||||
"title": "Generate content type from current file",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.addMissingFields",
|
||||
"title": "Add missing fields from front matter to content type",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.setContentType",
|
||||
"title": "Set the content type to use for the current file",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.blockquote",
|
||||
"title": "Blockquote",
|
||||
@@ -1233,9 +1365,14 @@
|
||||
"dark": "assets/icons/close-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.initTemplate",
|
||||
"title": "Initialize the template folder",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"title": "Create a template from current file",
|
||||
"title": "Create template from current file",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
@@ -1300,8 +1437,8 @@
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"title": "Insert image into your content",
|
||||
"command": "frontMatter.insertMedia",
|
||||
"title": "Insert media into your content",
|
||||
"category": "Front matter",
|
||||
"icon": {
|
||||
"dark": "/assets/icons/media-dark.svg",
|
||||
@@ -1463,7 +1600,7 @@
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"command": "frontMatter.insertMedia",
|
||||
"group": "navigation@-128",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
@@ -1620,7 +1757,7 @@
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"command": "frontMatter.insertMedia",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
@@ -1654,6 +1791,18 @@
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.generate",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.addMissingFields",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.contenttype.setContentType",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
@@ -1665,7 +1814,7 @@
|
||||
{
|
||||
"command": "frontMatter.mode.switch",
|
||||
"group": "navigation@1",
|
||||
"when": "view == frontMatter.explorer"
|
||||
"when": "view == frontMatter.explorer && frontMatter:has:modes == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
@@ -1744,9 +1893,10 @@
|
||||
"start:site": "cd ./docs && npm run dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "^1.8.2",
|
||||
"@bendera/vscode-webview-elements": "0.6.2",
|
||||
"@estruyf/vscode": "0.0.3",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@headlessui/react": "1.5.0",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
@@ -1780,7 +1930,7 @@
|
||||
"downshift": "6.0.6",
|
||||
"fuse.js": "6.5.3",
|
||||
"glob": "7.1.6",
|
||||
"gray-matter": "4.0.2",
|
||||
"gray-matter": "4.0.3",
|
||||
"html-loader": "1.3.2",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"image-size": "^1.0.0",
|
||||
@@ -1802,6 +1952,7 @@
|
||||
"react-datepicker": "4.2.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-dropzone": "^11.3.4",
|
||||
"react-quill": "^2.0.0-beta.4",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-toastify": "^8.1.0",
|
||||
"recoil": "^0.4.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const core = require('@actions/core');
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
const version = packageJson.version.split('.');
|
||||
@@ -14,7 +15,24 @@ packageJson.homepage = "https://beta.frontmatter.codes";
|
||||
|
||||
console.log(packageJson.version);
|
||||
|
||||
core.summary.addHeading(`Version info`).addDetails(`${packageJson.version}`);
|
||||
|
||||
const scripts = packageJson.scripts;
|
||||
for (const key in scripts) {
|
||||
if (key.startsWith(`prod:`)) {
|
||||
scripts[key] = scripts[key].replace("production", "development");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(packageJson.scripts, null, 2));
|
||||
|
||||
fs.writeFileSync(path.join(path.resolve('.'), 'package.json'), JSON.stringify(packageJson, null, 2));
|
||||
|
||||
let readme = fs.readFileSync(path.join(__dirname, '../README.beta.md'), 'utf8');
|
||||
fs.writeFileSync(path.join(__dirname, '../README.md'), readme);
|
||||
fs.writeFileSync(path.join(__dirname, '../README.md'), readme);
|
||||
|
||||
// Update the .vscodeignore file
|
||||
const ignoreFilePath = path.join(path.resolve('.'), '.vscodeignore');
|
||||
let vscodeignore = fs.readFileSync(ignoreFilePath, 'utf8');
|
||||
vscodeignore = vscodeignore.replace(`**/*.map`, '');
|
||||
fs.writeFileSync(ignoreFilePath, vscodeignore);
|
||||
@@ -30,7 +30,7 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Article.getCurrent();
|
||||
const article = ArticleHelper.getCurrent();
|
||||
|
||||
if (!article) {
|
||||
return;
|
||||
@@ -69,7 +69,8 @@ export class Article {
|
||||
|
||||
const selectedOptions = await vscode.window.showQuickPick(options, {
|
||||
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
|
||||
canPickMany: true
|
||||
canPickMany: true,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (selectedOptions) {
|
||||
@@ -324,7 +325,7 @@ export class Article {
|
||||
/**
|
||||
* Insert an image from the media dashboard into the article
|
||||
*/
|
||||
public static async insertImage() {
|
||||
public static async insertMedia() {
|
||||
let editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
@@ -375,23 +376,6 @@ export class Article {
|
||||
} as DashboardData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current article
|
||||
*/
|
||||
private static getCurrent(): ParsedFrontMatter | undefined {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the article date and return it
|
||||
* @param article
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { commands, QuickPickItem, window } from 'vscode';
|
||||
import { COMMAND_NAME } from '../constants';
|
||||
import { COMMAND_NAME, SETTING_TEMPLATES_ENABLED } from '../constants';
|
||||
import { Settings } from '../helpers';
|
||||
|
||||
export class Content {
|
||||
|
||||
public static async create() {
|
||||
const templatesEnabled = await Settings.get(SETTING_TEMPLATES_ENABLED);
|
||||
if (!templatesEnabled) {
|
||||
commands.executeCommand(COMMAND_NAME.createByContentType);
|
||||
}
|
||||
|
||||
const options: QuickPickItem[] = [{
|
||||
label: "Create content by content type",
|
||||
@@ -15,7 +20,8 @@ export class Content {
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
placeHolder: `Select how you want to create your new content`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (selectedOption) {
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import { SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_STATIC_FOLDER, SETTING_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants';
|
||||
import { commands, Uri, workspace, window } from "vscode";
|
||||
import { basename, dirname, join, sep } from "path";
|
||||
import { basename, dirname, join, relative, sep } from "path";
|
||||
import { ContentFolder, FileInfo, FolderInfo } from "../models";
|
||||
import uniqBy = require("lodash.uniqby");
|
||||
import { Template } from "./Template";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Settings } from "../helpers";
|
||||
import { Logger, Settings } from "../helpers";
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { MediaHelpers } from '../helpers/MediaHelpers';
|
||||
import { MediaListener, PagesListener } from '../listeners/dashboard';
|
||||
import { MediaListener, PagesListener, SettingsListener } from '../listeners/dashboard';
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
import { Telemetry } from '../helpers/Telemetry';
|
||||
import { glob } from 'glob';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
@@ -97,7 +98,10 @@ export class Folders {
|
||||
* Register the new folder path
|
||||
* @param folder
|
||||
*/
|
||||
public static async register(folder: Uri) {
|
||||
public static async register(folderInfo: { title: string, path: Uri } | Uri) {
|
||||
let folderName = folderInfo instanceof Uri ? undefined : folderInfo.title;
|
||||
const folder = folderInfo instanceof Uri ? folderInfo : folderInfo.path;
|
||||
|
||||
if (folder && folder.fsPath) {
|
||||
const wslPath = folder.fsPath.replace(/\//g, '\\');
|
||||
|
||||
@@ -110,11 +114,14 @@ export class Folders {
|
||||
return;
|
||||
}
|
||||
|
||||
const folderName = await window.showInputBox({
|
||||
prompt: `Which name would you like to specify for this folder?`,
|
||||
placeHolder: `Folder name`,
|
||||
value: basename(folder.fsPath)
|
||||
});
|
||||
if (!folderName) {
|
||||
folderName = await window.showInputBox({
|
||||
prompt: `Which name would you like to specify for this folder?`,
|
||||
placeHolder: `Folder name`,
|
||||
value: basename(folder.fsPath),
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
}
|
||||
|
||||
folders.push({
|
||||
title: folderName,
|
||||
@@ -127,6 +134,8 @@ export class Folders {
|
||||
Notifications.info(`Folder registered`);
|
||||
|
||||
Telemetry.send(TelemetryEvent.registerFolder);
|
||||
|
||||
SettingsListener.getSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,11 +293,30 @@ export class Folders {
|
||||
public static get(): ContentFolder[] {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
|
||||
|
||||
const contentFolders = folders.map(folder => {
|
||||
if (!folder.title) {
|
||||
folder.title = basename(folder.path);
|
||||
}
|
||||
|
||||
let folderPath = Folders.absWsFolder(folder, wsFolder);
|
||||
if (!existsSync(folderPath)) {
|
||||
Notifications.errorShowOnce(`Folder "${folder.title} (${folder.path})" does not exist. Please remove it from the settings.`, "Remove folder").then(answer => {
|
||||
if (answer === "Remove folder") {
|
||||
let folders = Folders.get();
|
||||
Folders.update(folders.filter(f => f.path !== folder.path));
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...folder,
|
||||
path: folderPath
|
||||
}
|
||||
})
|
||||
|
||||
return folders.map(folder => ({
|
||||
...folder,
|
||||
path: Folders.absWsFolder(folder, wsFolder)
|
||||
}));
|
||||
return contentFolders.filter(folder => folder !== null) as ContentFolder[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,4 +375,46 @@ export class Folders {
|
||||
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the content folders
|
||||
*/
|
||||
public static async getContentFolders() {
|
||||
// Find folders that contain files
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const supportedFiles = Settings.get<string[]>(SETTING_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES;
|
||||
const patterns = supportedFiles.map(fileType => `${join(parseWinPath(wsFolder?.fsPath || ""), "**", `*${fileType.startsWith('.') ? '' : '.'}${fileType}`)}`);
|
||||
let folders: string[] = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
folders = [...folders, ...(await this.findFolders(pattern))];
|
||||
} catch (e) {
|
||||
Logger.error(`Something went wrong while searching for folders with pattern "${pattern}": ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out the workspace folder
|
||||
if (wsFolder) {
|
||||
folders = folders.filter(folder => folder !== wsFolder.fsPath);
|
||||
}
|
||||
|
||||
const uniqueFolders = [...new Set(folders)];
|
||||
return uniqueFolders.map(folder => relative(wsFolder?.path || "", folder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all content folders
|
||||
* @param pattern
|
||||
* @returns
|
||||
*/
|
||||
private static findFolders(pattern: string): Promise<string[]> {
|
||||
return new Promise(resolve => {
|
||||
glob(pattern, { ignore: "**/node_modules/**" }, (err, files) => {
|
||||
const allFolders = files.map(file => dirname(file));
|
||||
const uniqueFolders = [...new Set(allFolders)];
|
||||
resolve(uniqueFolders);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Telemetry } from './../helpers/Telemetry';
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent } from './../constants';
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT, TelemetryEvent, PreviewCommands } from './../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { join } from "path";
|
||||
import { commands, env, Uri, ViewColumn, window } from "vscode";
|
||||
import { Settings } from '../helpers';
|
||||
import { Extension, Settings } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { Article } from '.';
|
||||
import { urlJoin } from 'url-join-ts';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
|
||||
|
||||
export class Preview {
|
||||
@@ -81,59 +82,62 @@ export class Preview {
|
||||
|
||||
const cspSource = webView.webview.cspSource;
|
||||
|
||||
webView.webview.html = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; frame-src ${localhostUrl} ${cspSource} http: https:; img-src ${localhostUrl} ${cspSource} http: https:; script-src ${localhostUrl} ${cspSource} 'unsafe-inline'; style-src ${localhostUrl} ${cspSource} 'self' 'unsafe-inline' http: https:;"
|
||||
/>
|
||||
<style>
|
||||
html,body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: white;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
webView.webview.onDidReceiveMessage(message => {
|
||||
switch (message.command) {
|
||||
case PreviewCommands.toVSCode.open:
|
||||
if (message.data) {
|
||||
commands.executeCommand('vscode.open', message.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
border: 0;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.slug {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-bottom: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
const dashboardFile = "dashboardWebView.js";
|
||||
const localPort = `9000`;
|
||||
const localServerUrl = `localhost:${localPort}`;
|
||||
|
||||
input {
|
||||
color: var(--vscode-editor-foreground);
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: none;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slug">
|
||||
<input type="text" value="${urlJoin(localhostUrl.toString(), slug || '')}" disabled />
|
||||
</div>
|
||||
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
|
||||
</body>
|
||||
</html>`;
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const ext = Extension.getInstance();
|
||||
const isProd = ext.isProductionMode;
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
const extensionUri = ext.extensionPath;
|
||||
|
||||
const csp = [
|
||||
`default-src 'none';`,
|
||||
`img-src ${localhostUrl} ${cspSource} http: https:;`,
|
||||
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`} 'unsafe-eval'`,
|
||||
`style-src ${cspSource} 'self' 'unsafe-inline' http: https:`,
|
||||
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
|
||||
`frame-src ${localhostUrl} ${cspSource} http: https:;`,
|
||||
];
|
||||
|
||||
let scriptUri = "";
|
||||
if (isProd) {
|
||||
scriptUri = webView.webview.asWebviewUri(Uri.joinPath(extensionUri, 'dist', dashboardFile)).toString();
|
||||
} else {
|
||||
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
|
||||
}
|
||||
|
||||
webView.webview.html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Front Matter Preview</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden">
|
||||
<div id="app" data-type="preview" data-url="${urlJoin(localhostUrl.toString(), slug || '')}" data-isProd="${isProd}" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;"></div>
|
||||
|
||||
<script ${isProd ? `nonce="${nonce}"` : ""} src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
Telemetry.send(TelemetryEvent.openPreview);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as fs from "fs";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { Template } from "./Template";
|
||||
import { Folders } from "./Folders";
|
||||
import { Logger, Settings } from "../helpers";
|
||||
import { FrameworkDetector, Logger, Settings } from "../helpers";
|
||||
import { SETTING_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants";
|
||||
import { SettingsListener } from '../listeners/dashboard';
|
||||
|
||||
@@ -24,33 +24,32 @@ categories: []
|
||||
---
|
||||
`;
|
||||
|
||||
public static isInitialized() {
|
||||
return Settings.hasProjectFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new "Project" instance.
|
||||
*/
|
||||
public static async init(sampleTemplate: boolean = true) {
|
||||
public static async init(sampleTemplate?: boolean) {
|
||||
try {
|
||||
Settings.createTeamSettings();
|
||||
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
const folder = Template.getSettings();
|
||||
const templatePath = Project.templatePath();
|
||||
|
||||
if (!folder || !templatePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
|
||||
|
||||
if (!fs.existsSync(templatePath.fsPath)) {
|
||||
await workspace.fs.createDirectory(templatePath);
|
||||
}
|
||||
|
||||
if (sampleTemplate) {
|
||||
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
if (sampleTemplate !== undefined) {
|
||||
await Project.createSampleTemplate();
|
||||
} else {
|
||||
Notifications.info("Project initialized successfully.");
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.initialization);
|
||||
|
||||
Telemetry.send(TelemetryEvent.initialization)
|
||||
// Check if you can find the framework
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const framework = FrameworkDetector.get(wsFolder?.fsPath || "");
|
||||
|
||||
if (framework) {
|
||||
SettingsListener.setFramework(framework.name);
|
||||
}
|
||||
|
||||
SettingsListener.getSettings();
|
||||
} catch (err: any) {
|
||||
@@ -59,6 +58,33 @@ categories: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the templates folder + sample if needed
|
||||
* @param sampleTemplate
|
||||
* @returns
|
||||
*/
|
||||
public static async createSampleTemplate(sampleTemplate?: boolean) {
|
||||
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
|
||||
|
||||
const folder = Template.getSettings();
|
||||
const templatePath = Project.templatePath();
|
||||
|
||||
if (!folder || !templatePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
|
||||
|
||||
if (!fs.existsSync(templatePath.fsPath)) {
|
||||
await workspace.fs.createDirectory(templatePath);
|
||||
}
|
||||
|
||||
if (sampleTemplate) {
|
||||
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
|
||||
Notifications.info("Sample template created.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template path for the current project
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,8 @@ export class Settings {
|
||||
public static async create(type: TaxonomyType) {
|
||||
const newOption = await vscode.window.showInputBox({
|
||||
prompt: `Insert the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} that you want to add to your configuration.`,
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (newOption) {
|
||||
@@ -36,7 +37,11 @@ export class Settings {
|
||||
await SettingsHelper.updateTaxonomy(type, options);
|
||||
|
||||
// Ask if the new term needs to be added to the page
|
||||
const addToPage = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?` });
|
||||
const addToPage = await vscode.window.showQuickPick(["yes", "no"], {
|
||||
canPickMany: false,
|
||||
placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (addToPage && addToPage === "yes") {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
@@ -149,7 +154,8 @@ export class Settings {
|
||||
"Category"
|
||||
], {
|
||||
placeHolder: `What do you want to remap?`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (!taxType) {
|
||||
return;
|
||||
@@ -165,7 +171,8 @@ export class Settings {
|
||||
|
||||
const selectedOption = await vscode.window.showQuickPick(options, {
|
||||
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!selectedOption) {
|
||||
@@ -174,11 +181,16 @@ export class Settings {
|
||||
|
||||
const newOptionValue = await vscode.window.showInputBox({
|
||||
prompt: `Specify the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} with which you want to remap "${selectedOption}". Leave the input <blank> if you want to remove the ${type === TaxonomyType.Tag ? "tag" : "category"} from all articles.`,
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
|
||||
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!newOptionValue) {
|
||||
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?` });
|
||||
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], {
|
||||
canPickMany: false,
|
||||
placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (deleteAnswer === "no") {
|
||||
return;
|
||||
}
|
||||
@@ -226,7 +238,7 @@ export class Settings {
|
||||
data[matterProp] = [...new Set(taxonomies)].sort();
|
||||
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, {
|
||||
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, mdFile, {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ export class Template {
|
||||
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
prompt: `What name would you like to give your template?`,
|
||||
placeHolder: `article`
|
||||
placeHolder: `article`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!titleValue) {
|
||||
@@ -77,6 +78,7 @@ export class Template {
|
||||
{
|
||||
canPickMany: false,
|
||||
placeHolder: `Do you want to keep the contents for the template?`,
|
||||
ignoreFocusOut: true
|
||||
}
|
||||
);
|
||||
|
||||
@@ -98,11 +100,24 @@ export class Template {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all templates
|
||||
*/
|
||||
public static async getTemplates() {
|
||||
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
|
||||
|
||||
if (!folder) {
|
||||
Notifications.warning(`No templates found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
return await vscode.workspace.findFiles(`${folder}/**/*`, "**/node_modules/**,**/archetypes/**");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a template
|
||||
*/
|
||||
public static async create(folderPath: string) {
|
||||
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
|
||||
const contentTypes = ContentType.getAll();
|
||||
|
||||
if (!folderPath) {
|
||||
@@ -110,19 +125,15 @@ export class Template {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!folder) {
|
||||
Notifications.warning(`No templates found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const templates = await vscode.workspace.findFiles(`${folder}/**/*`, "**/node_modules/**,**/archetypes/**");
|
||||
const templates = await Template.getTemplates();
|
||||
if (!templates || templates.length === 0) {
|
||||
Notifications.warning(`No templates found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
|
||||
placeHolder: `Select the content template to use`
|
||||
placeHolder: `Select the content template to use`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (!selectedTemplate) {
|
||||
Notifications.warning(`No template selected.`);
|
||||
|
||||
@@ -59,8 +59,8 @@ export class Wysiwyg {
|
||||
|
||||
const option = await window.showQuickPick([ ...qpItems ], {
|
||||
placeHolder: "Which type of markup would you like to insert?",
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: false,
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (option) {
|
||||
@@ -161,8 +161,8 @@ export class Wysiwyg {
|
||||
"Heading 6"
|
||||
], {
|
||||
canPickMany: false,
|
||||
placeHolder: "Which heading level do you want to insert?",
|
||||
ignoreFocusOut: false
|
||||
placeHolder: "Which heading level do you want to insert?",
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (headingLvl) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const COMMAND_NAME = {
|
||||
createByContentType: getCommandName("createByContentType"),
|
||||
createByTemplate: getCommandName("createByTemplate"),
|
||||
createTemplate: getCommandName("createTemplate"),
|
||||
initTemplate: getCommandName("initTemplate"),
|
||||
collapseSections: getCommandName("collapseSections"),
|
||||
preview: getCommandName("preview"),
|
||||
dashboard: getCommandName("dashboard"),
|
||||
@@ -37,8 +38,10 @@ export const COMMAND_NAME = {
|
||||
diagnostics: getCommandName("diagnostics"),
|
||||
modeSwitch: getCommandName("mode.switch"),
|
||||
|
||||
showOutputChannel: getCommandName("showOutputChannel"),
|
||||
|
||||
// Insert dashboards
|
||||
insertImage: getCommandName("insertImage"),
|
||||
insertMedia: getCommandName("insertMedia"),
|
||||
insertSnippet: getCommandName("insertSnippet"),
|
||||
|
||||
// WYSIWYG
|
||||
@@ -53,4 +56,9 @@ export const COMMAND_NAME = {
|
||||
orderedlist: getCommandName("markup.orderedlist"),
|
||||
taskList: getCommandName("markup.tasklist"),
|
||||
options: getCommandName("markup.options"),
|
||||
|
||||
// Content types
|
||||
generateContentType: getCommandName("contenttype.generate"),
|
||||
addMissingFields: getCommandName("contenttype.addMissingFields"),
|
||||
setContentType: getCommandName("contenttype.setContentType"),
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export const FEATURE_FLAG = {
|
||||
metadata: "panel.metadata",
|
||||
recentlyModified: "panel.recentlyModified",
|
||||
otherActions: "panel.otherActions",
|
||||
contentType: "panel.contentType",
|
||||
},
|
||||
dashboard: {
|
||||
snippets: {
|
||||
|
||||
@@ -1,33 +1,89 @@
|
||||
export const FrameworkDetectors = [
|
||||
{
|
||||
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
|
||||
"requiredFiles": ["gatsby-config.js"],
|
||||
"requiredDependencies": ["gatsby"],
|
||||
"commands": {
|
||||
"start": "npx gatsby develop"
|
||||
export const FrameworkDetectors = [{
|
||||
framework: {
|
||||
name: "gatsby",
|
||||
dist: "public",
|
||||
static: "static",
|
||||
build: "gatsby build"
|
||||
},
|
||||
requiredFiles: ["gatsby-config.js"],
|
||||
requiredDependencies: ["gatsby"],
|
||||
commands: {
|
||||
start: "npx gatsby develop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
|
||||
"requiredFiles": ["config.toml", "config.yaml", "config.yml"],
|
||||
"commands": {
|
||||
"start": "hugo server -D"
|
||||
framework: {
|
||||
name: "hugo",
|
||||
dist: "public",
|
||||
static: "static",
|
||||
build: "hugo"
|
||||
},
|
||||
requiredFiles: ["config.toml", "config.yaml", "config.yml"],
|
||||
commands: {
|
||||
start: "hugo server -D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
|
||||
"requiredFiles": ["next.config.js"],
|
||||
"requiredDependencies": ["next"],
|
||||
"commands": {
|
||||
"start": "npx next dev"
|
||||
framework: {
|
||||
name: "next",
|
||||
dist: ".next",
|
||||
static: "public",
|
||||
build: "next build"
|
||||
},
|
||||
requiredFiles: ["next.config.js"],
|
||||
requiredDependencies: ["next"],
|
||||
commands: {
|
||||
start: "npx next dev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
|
||||
"requiredFiles": ["nuxt.config.js"],
|
||||
"requiredDependencies": ["nuxt"],
|
||||
"commands": {
|
||||
"start": "npx nuxt"
|
||||
framework: {
|
||||
name: "nuxt",
|
||||
dist: "dist",
|
||||
static: "static",
|
||||
build: "nuxt"
|
||||
},
|
||||
requiredFiles: ["nuxt.config.js"],
|
||||
requiredDependencies: ["nuxt"],
|
||||
commands: {
|
||||
start: "npx nuxt"
|
||||
}
|
||||
},
|
||||
{
|
||||
framework: {
|
||||
name: "jekyll",
|
||||
dist: "_site",
|
||||
static: "assets",
|
||||
build: "bundle exec jekyll build"
|
||||
},
|
||||
requiredFiles: ["Gemfile"],
|
||||
requiredDependencies: ["jekyll"],
|
||||
commands: {
|
||||
start: "bundle exec jekyll serve --livereload"
|
||||
}
|
||||
},
|
||||
{
|
||||
framework: {
|
||||
name: "docusaurus",
|
||||
dist: "build",
|
||||
static: "static",
|
||||
build: "npx docusaurus build"
|
||||
},
|
||||
requiredFiles: ["docusaurus.config.js"],
|
||||
requiredDependencies: ["@docusaurus/core"],
|
||||
commands: {
|
||||
start: "npx docusaurus start"
|
||||
}
|
||||
},
|
||||
{
|
||||
framework: {
|
||||
name: "11ty",
|
||||
dist: "_site",
|
||||
build: "npx @11ty/eleventy"
|
||||
},
|
||||
requiredDependencies: ["@11ty/eleventy"],
|
||||
commands: {
|
||||
start: "npx @11ty/eleventy --serve"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1,5 +1,10 @@
|
||||
|
||||
|
||||
export enum GeneralCommands{
|
||||
setMode = "setMode"
|
||||
export const GeneralCommands = {
|
||||
toWebview: {
|
||||
setMode: "setMode",
|
||||
},
|
||||
toVSCode: {
|
||||
openLink: "openLink",
|
||||
}
|
||||
};
|
||||
8
src/constants/PreviewCommands.ts
Normal file
8
src/constants/PreviewCommands.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const PreviewCommands = {
|
||||
toVSCode: {
|
||||
open: `preview.open`
|
||||
},
|
||||
fromVSCode: {}
|
||||
};
|
||||
@@ -22,9 +22,15 @@ export const TelemetryEvent = {
|
||||
refreshMedia: 'refreshMedia',
|
||||
deleteMedia: 'deleteMedia',
|
||||
insertMediaToContent: 'insertMediaToContent',
|
||||
insertFileToContent: 'insertFileToContent',
|
||||
updateMediaMetadata: 'updateMediaMetadata',
|
||||
openExplorerView: 'openExplorerView',
|
||||
|
||||
// Content types
|
||||
generateContentType: 'generateContentType',
|
||||
addMissingFields: 'addMissingFields',
|
||||
setContentType: 'setContentType',
|
||||
|
||||
// Custom scripts
|
||||
runCustomScript: 'runCustomScript',
|
||||
runMediaScript: 'runMediaScript',
|
||||
|
||||
@@ -9,6 +9,8 @@ export const CONTEXT = {
|
||||
backer: "frontMatter:backers:supporter",
|
||||
isValidFile: "frontMatter:file:isValid",
|
||||
|
||||
hasViewModes: "frontMatter:has:modes",
|
||||
|
||||
isSnippetsDashboardEnabled: "frontMatter:dashboard:snippets:enabled",
|
||||
isDataDashboardEnabled: "frontMatter:dashboard:data:enabled",
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export * from './GeneralCommands';
|
||||
export * from './Links';
|
||||
export * from './LocalStore';
|
||||
export * from './Navigation';
|
||||
export * from './PreviewCommands';
|
||||
export * from './TelemetryEvent';
|
||||
export * from './charCode';
|
||||
export * from './charMap';
|
||||
|
||||
@@ -32,6 +32,7 @@ export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
|
||||
|
||||
export const SETTING_TEMPLATES_FOLDER = "templates.folder";
|
||||
export const SETTING_TEMPLATES_PREFIX = "templates.prefix";
|
||||
export const SETTING_TEMPLATES_ENABLED = "templates.enabled";
|
||||
|
||||
export const SETTING_TELEMETRY_DISABLE = "telemetry.disable";
|
||||
|
||||
@@ -61,7 +62,6 @@ export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
|
||||
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
|
||||
|
||||
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
|
||||
|
||||
export const SETTING_DATA_FILES = "data.files";
|
||||
@@ -89,3 +89,8 @@ export const SETTING_DATE_FIELD = "taxonomy.dateField";
|
||||
* Use the `isModifiedDate` property on the content type datetime field instead
|
||||
*/
|
||||
export const SETTING_MODIFIED_FIELD = "taxonomy.modifiedField";
|
||||
/**
|
||||
* @deprecated
|
||||
* Use the `frontMatter.content.snippets` setting instead
|
||||
*/
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
@@ -5,6 +5,11 @@ export enum DashboardMessage {
|
||||
getMode = 'getMode',
|
||||
showWarning = 'showWarning',
|
||||
|
||||
// Welcome view
|
||||
initializeProject = 'initializeProject',
|
||||
setFramework = 'setFramework',
|
||||
addFolder = 'addFolder',
|
||||
|
||||
// Content dashboard
|
||||
getData = 'getData',
|
||||
createContent = 'createContent',
|
||||
@@ -22,9 +27,10 @@ export enum DashboardMessage {
|
||||
uploadMedia = 'uploadMedia',
|
||||
deleteMedia = 'deleteMedia',
|
||||
revealMedia = 'revealMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
insertMedia = 'insertMedia',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
insertFile = 'insertFile',
|
||||
|
||||
// Data dashboard
|
||||
getDataEntries = 'getDataEntries',
|
||||
@@ -38,8 +44,6 @@ export enum DashboardMessage {
|
||||
// Other
|
||||
getTheme = 'getTheme',
|
||||
updateSetting = 'updateSetting',
|
||||
initializeProject = 'initializeProject',
|
||||
setFramework = 'setFramework',
|
||||
setState = 'setState',
|
||||
runCustomScript = 'runCustomScript',
|
||||
sendTelemetry = 'sendTelemetry',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { EyeIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { EyeIcon, TerminalIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { CustomScript, ScriptType } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
@@ -43,7 +43,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={script.title}
|
||||
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
|
||||
onClick={(value, e) => runCustomScript(e, script)} />
|
||||
))
|
||||
}, [scripts]);
|
||||
@@ -70,15 +70,15 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
|
||||
<MenuItems widthClass='w-40' marginTopClass='mt-6'>
|
||||
<MenuItems widthClass='w-44' marginTopClass='mt-6'>
|
||||
<MenuItem
|
||||
title={`View`}
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
|
||||
onClick={(value, e) => onView(e)} />
|
||||
|
||||
{ customScriptActions }
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={(value, e) => onDelete(e)} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
|
||||
@@ -9,14 +9,16 @@ import { Status } from '../Status';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardViewType } from '../../models';
|
||||
import { ContentActions } from './ContentActions';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface IItemProps extends Page {}
|
||||
|
||||
const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const draftField = useMemo(() => settings?.draftField, [settings]);
|
||||
|
||||
const openFile = () => {
|
||||
Messenger.send(DashboardMessage.openFile, fmFilePath);
|
||||
@@ -51,7 +53,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
|
||||
<div className="relative p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
<Status draft={draft} />
|
||||
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
|
||||
|
||||
<DateField className={`mr-4`} value={date} />
|
||||
|
||||
@@ -96,7 +98,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Status draft={draft} />
|
||||
{ draftField && draftField.name && <Status draft={pageData[draftField.name]} /> }
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settin
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
|
||||
{
|
||||
settings && settings?.folders?.length > 0 ? (
|
||||
settings && settings?.contentFolders?.length > 0 ? (
|
||||
<p className={`text-xl font-medium`}>No Markdown to show</p>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DataView } from './DataView';
|
||||
import { Snippets } from './SnippetsView/Snippets';
|
||||
import { FeatureFlag } from '../../components/features/FeatureFlag';
|
||||
import { FEATURE_FLAG } from '../../constants';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
|
||||
export interface IDashboardProps {
|
||||
showWelcome: boolean;
|
||||
@@ -23,15 +24,17 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
useDarkMode();
|
||||
|
||||
const viewState: any = Messenger.getState() || {};
|
||||
|
||||
if (!settings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (showWelcome) {
|
||||
if (showWelcome || viewState.isWelcomeConfiguring) {
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
if (!settings.initialized || settings.folders?.length === 0) {
|
||||
if (!settings.initialized || settings.contentFolders?.length === 0) {
|
||||
return <WelcomeScreen settings={settings} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
(dataFiles && dataFiles.length > 0) && (
|
||||
dataFiles.map((dataFile) => (
|
||||
dataFiles.map((dataFile, idx) => (
|
||||
<button
|
||||
key={dataFile.id}
|
||||
key={`${dataFile.id}-${idx}`}
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
onClick={() => setSchema(dataFile)}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PencilAltIcon, XIcon } from '@heroicons/react/outline';
|
||||
import { format } from 'date-fns';
|
||||
import { basename } from 'path';
|
||||
import * as React from 'react';
|
||||
import { Fragment, useMemo } from 'react';
|
||||
import { Fragment, useCallback, useMemo } from 'react';
|
||||
import { DateHelper } from '../../../helpers/DateHelper';
|
||||
import { MediaInfo } from '../../../models';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
@@ -18,14 +18,16 @@ export interface IDetailsSlideOverProps {
|
||||
folder: string;
|
||||
media: MediaInfo;
|
||||
showForm: boolean;
|
||||
isImageFile: boolean;
|
||||
onEdit: () => void;
|
||||
onEditClose: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> = ({ imgSrc, size, dimensions, media, folder, showForm, onEdit, onEditClose, onDismiss }: React.PropsWithChildren<IDetailsSlideOverProps>) => {
|
||||
export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> = ({ imgSrc, size, dimensions, media, folder, showForm, onEdit, onEditClose, onDismiss, isImageFile }: React.PropsWithChildren<IDetailsSlideOverProps>) => {
|
||||
const [ filename, setFilename ] = React.useState<string>(media.filename);
|
||||
const [ caption, setCaption ] = React.useState<string | undefined>(media.caption);
|
||||
const [ title, setTitle ] = React.useState<string | undefined>(media.title);
|
||||
const [ alt, setAlt ] = React.useState(media.alt);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const page = useRecoilValue(PageSelector);
|
||||
@@ -37,20 +39,22 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
|
||||
const extension = fileInfo?.pop();
|
||||
const name = fileInfo?.join('.');
|
||||
|
||||
const onSubmitMetadata = () => {
|
||||
const onSubmitMetadata = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.updateMediaMetadata, {
|
||||
file: media.fsPath,
|
||||
filename,
|
||||
caption,
|
||||
alt,
|
||||
title,
|
||||
folder: selectedFolder,
|
||||
page
|
||||
});
|
||||
|
||||
onEditClose();
|
||||
};
|
||||
}, [media, filename, caption, alt, title, selectedFolder, page])
|
||||
|
||||
React.useEffect(() => {
|
||||
setTitle(media.title);
|
||||
setAlt(media.alt);
|
||||
setCaption(media.caption);
|
||||
setFilename(media.filename);
|
||||
@@ -93,9 +97,13 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
|
||||
<div className="relative mt-6 flex-1 px-4 sm:px-6">
|
||||
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
|
||||
<div>
|
||||
<div className="block w-full aspect-w-10 aspect-h-7 overflow-hidden border-gray-200 dark:border-vulcan-200 border">
|
||||
<img src={imgSrc} alt={media.filename} className="object-cover" />
|
||||
</div>
|
||||
{
|
||||
isImageFile && (
|
||||
<div className="block w-full aspect-w-10 aspect-h-7 overflow-hidden border-gray-200 dark:border-vulcan-200 border">
|
||||
<img src={imgSrc} alt={media.filename} className="object-cover" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="mt-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium text-vulcan-300 dark:text-whisper-500"><span className="sr-only">Details for </span>{media.filename}</h2>
|
||||
@@ -128,31 +136,51 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Caption
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
rows={3}
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={caption || ''}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Alt tag value
|
||||
Title
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={alt || ''}
|
||||
onChange={(e) => setAlt(e.target.value)}
|
||||
value={title || ''}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
isImageFile && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Caption
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
rows={3}
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={caption || ''}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Alt tag value
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={alt || ''}
|
||||
onChange={(e) => setAlt(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
@@ -192,16 +220,27 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
|
||||
<dt className="text-vulcan-100 dark:text-whisper-900">Filename</dt>
|
||||
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.filename}</dd>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="py-3 flex justify-between text-sm font-medium">
|
||||
<dt className="text-vulcan-100 dark:text-whisper-900">Caption</dt>
|
||||
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.caption || ""}</dd>
|
||||
<dt className="text-vulcan-100 dark:text-whisper-900">Title</dt>
|
||||
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.title}</dd>
|
||||
</div>
|
||||
|
||||
<div className="py-3 flex justify-between text-sm font-medium">
|
||||
<dt className="text-vulcan-100 dark:text-whisper-900">Alternate text</dt>
|
||||
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.alt || ""}</dd>
|
||||
</div>
|
||||
{
|
||||
isImageFile && (
|
||||
<>
|
||||
<div className="py-3 flex justify-between text-sm font-medium">
|
||||
<dt className="text-vulcan-100 dark:text-whisper-900">Caption</dt>
|
||||
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.caption || ""}</dd>
|
||||
</div>
|
||||
|
||||
<div className="py-3 flex justify-between text-sm font-medium">
|
||||
<dt className="text-vulcan-100 dark:text-whisper-900">Alternate text</dt>
|
||||
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.alt || ""}</dd>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</dl>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
|
||||
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TerminalIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
|
||||
import { basename, dirname } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { CustomScript } from '../../../helpers/CustomScript';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { ScriptType } from '../../../models';
|
||||
import { SnippetParser } from '../../../helpers/SnippetParser';
|
||||
import { ScriptType, Snippet } from '../../../models';
|
||||
import { MediaInfo } from '../../../models/MediaPaths';
|
||||
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { MenuItem, MenuItems } from '../Menu';
|
||||
import { ActionMenuButton } from '../Menu/ActionMenuButton';
|
||||
import { QuickAction } from '../Menu/QuickAction';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { InfoDialog } from '../Modals/InfoDialog';
|
||||
import { DetailsSlideOver } from './DetailsSlideOver';
|
||||
|
||||
export interface IItemProps {
|
||||
@@ -26,6 +27,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const [ , setLightbox ] = useRecoilState(LightboxAtom);
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
const [ showForm, setShowForm ] = React.useState(false);
|
||||
const [ showSnippetSelection, setShowSnippetSelection ] = React.useState(false);
|
||||
const [ showDetails, setShowDetails ] = React.useState(false);
|
||||
const [ caption, setCaption ] = React.useState(media.caption);
|
||||
const [ alt, setAlt ] = React.useState(media.alt);
|
||||
@@ -34,6 +36,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
const mediaSnippets = useMemo(() => {
|
||||
if (!settings?.snippets) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keys = Object.keys(settings.snippets);
|
||||
return keys.filter(key => (settings.snippets || {})[key].isMediaSnippet).map(key => ({ title: key, ...(settings.snippets || {})[key]}));
|
||||
}, [settings]);
|
||||
|
||||
const getFolder = () => {
|
||||
if (settings?.wsFolder && media.fsPath) {
|
||||
let relPath = media.fsPath.split(settings.wsFolder).pop();
|
||||
@@ -74,39 +85,70 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
|
||||
const insertToArticle = () => {
|
||||
const relPath = getRelPath();
|
||||
Messenger.send(DashboardMessage.insertPreviewImage, {
|
||||
image: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
parents: viewData?.data?.parents,
|
||||
multiple: viewData?.data?.multiple,
|
||||
value: viewData?.data?.value,
|
||||
position: viewData?.data?.position || null,
|
||||
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
|
||||
alt: alt || "",
|
||||
caption: caption || ""
|
||||
});
|
||||
|
||||
if (viewData?.data?.type === "file") {
|
||||
Messenger.send(DashboardMessage.insertFile, {
|
||||
relPath: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
parents: viewData?.data?.parents,
|
||||
multiple: viewData?.data?.multiple,
|
||||
value: viewData?.data?.value,
|
||||
position: viewData?.data?.position || null,
|
||||
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
|
||||
title: media.title
|
||||
});
|
||||
} else {
|
||||
Messenger.send(DashboardMessage.insertMedia, {
|
||||
relPath: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
parents: viewData?.data?.parents,
|
||||
multiple: viewData?.data?.multiple,
|
||||
value: viewData?.data?.value,
|
||||
position: viewData?.data?.position || null,
|
||||
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
|
||||
alt: alt || "",
|
||||
caption: caption || "",
|
||||
title: media.title || ""
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const insertSnippet = () => {
|
||||
const insertSnippet = useCallback(() => {
|
||||
if (mediaSnippets.length === 1) {
|
||||
processSnippet(mediaSnippets[0]);
|
||||
} else {
|
||||
// Show dialog to select
|
||||
setShowSnippetSelection(true);
|
||||
}
|
||||
}, [mediaSnippets]);
|
||||
|
||||
const processSnippet = useCallback((snippet: Snippet) => {
|
||||
setShowSnippetSelection(false);
|
||||
|
||||
const relPath = getRelPath();
|
||||
let snippet = settings?.mediaSnippet.join("\n");
|
||||
|
||||
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
|
||||
snippet = snippet?.replace("{alt}", alt || "");
|
||||
snippet = snippet?.replace("{caption}", caption || "");
|
||||
snippet = snippet?.replace("{filename}", basename(relPath || ""));
|
||||
snippet = snippet?.replace("{mediaWidth}", media?.dimensions?.width?.toString() || "");
|
||||
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
|
||||
const fieldData = {
|
||||
mediaUrl: parseWinPath(relPath) || "",
|
||||
alt: alt || "",
|
||||
caption: caption || "",
|
||||
title: media.title || "",
|
||||
filename: basename(relPath || ""),
|
||||
mediaWidth: media?.dimensions?.width?.toString() || "",
|
||||
mediaHeight: media?.dimensions?.height?.toString() || "",
|
||||
};
|
||||
|
||||
Messenger.send(DashboardMessage.insertPreviewImage, {
|
||||
image: parseWinPath(relPath) || "",
|
||||
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
|
||||
|
||||
Messenger.send(DashboardMessage.insertMedia, {
|
||||
relPath: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
position: viewData?.data?.position || null,
|
||||
snippet
|
||||
snippet: output
|
||||
});
|
||||
};
|
||||
}, [alt, caption, media, settings, viewData, mediaSnippets]);
|
||||
|
||||
const deleteMedia = () => {
|
||||
setShowAlert(true);
|
||||
@@ -168,7 +210,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
};
|
||||
|
||||
const openLightbox = useCallback(() => {
|
||||
if (!isVideoFile() && !isAudioFile()) {
|
||||
if (isImageFile) {
|
||||
setLightbox(media.vsPath || "");
|
||||
}
|
||||
}, [media.vsPath]);
|
||||
@@ -182,26 +224,26 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={script.title}
|
||||
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
|
||||
onClick={() => runCustomScript(script)} />
|
||||
))
|
||||
}
|
||||
|
||||
const isVideoFile = useCallback(() => {
|
||||
const isVideoFile = useMemo(() => {
|
||||
if (media.mimeType?.startsWith("video/")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [media]);
|
||||
|
||||
const isAudioFile = useCallback(() => {
|
||||
const isAudioFile = useMemo(() => {
|
||||
if (media.mimeType?.startsWith("audio/")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [media]);
|
||||
|
||||
const isImageFile = useCallback(() => {
|
||||
const isImageFile = useMemo(() => {
|
||||
if (media.mimeType?.startsWith("image/")) {
|
||||
return true;
|
||||
}
|
||||
@@ -209,27 +251,37 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}, [media]);
|
||||
|
||||
const renderMediaIcon = useMemo(() => {
|
||||
if (isVideoFile()) {
|
||||
return <VideoCameraIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}
|
||||
|
||||
if (isAudioFile()) {
|
||||
return <MusicNoteIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}
|
||||
const path = media.fsPath;
|
||||
const extension = path.split('.').pop();
|
||||
|
||||
if (isImageFile()) {
|
||||
if (isImageFile) {
|
||||
return <PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}
|
||||
|
||||
return <DocumentIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}, [media]);
|
||||
let icon = <DocumentIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
|
||||
|
||||
if (isVideoFile) {
|
||||
icon = <VideoCameraIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}
|
||||
|
||||
if (isAudioFile) {
|
||||
icon = <MusicNoteIcon className={`h-4/6 text-gray-300 dark:text-vulcan-200`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center items-center'>
|
||||
{icon}
|
||||
<span className='text-2xl font-bold absolute top-0 right-0 bottom-0 left-0 flex justify-center items-center'>{extension}</span>
|
||||
</div>
|
||||
);
|
||||
}, [media, isImageFile, isVideoFile, isAudioFile]);
|
||||
|
||||
const renderMedia = useMemo(() => {
|
||||
if (isVideoFile() || isAudioFile()) {
|
||||
if (isVideoFile || isAudioFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isImageFile()) {
|
||||
if (isImageFile) {
|
||||
return <img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />;
|
||||
}
|
||||
|
||||
@@ -258,7 +310,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
return (
|
||||
<>
|
||||
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-200 dark:border-vulcan-50">
|
||||
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
|
||||
<button className={`relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImageFile ? "cursor-pointer" : "cursor-default"}`} onClick={openLightbox}>
|
||||
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
|
||||
{
|
||||
renderMediaIcon
|
||||
@@ -297,7 +349,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</QuickAction>
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && (
|
||||
<QuickAction
|
||||
title='Insert snippet'
|
||||
onClick={insertSnippet}>
|
||||
@@ -328,7 +380,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={`Edit metadata`}
|
||||
title={<div className='flex items-center'><PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span></div>}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
|
||||
@@ -336,15 +388,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={`Insert image markdown`}
|
||||
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
|
||||
onClick={insertToArticle} />
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
|
||||
<MenuItem
|
||||
title={`Insert snippet`}
|
||||
onClick={insertSnippet} />
|
||||
)
|
||||
key={idx}
|
||||
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
|
||||
onClick={() => processSnippet(snippet)} />
|
||||
))
|
||||
}
|
||||
|
||||
{ customScriptActions() }
|
||||
@@ -361,11 +414,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={`Reveal media`}
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
@@ -375,6 +428,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center break-all">
|
||||
{basename(parseWinPath(media.fsPath) || "")}
|
||||
</p>
|
||||
{
|
||||
!isImageFile && media.title && (
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
<b className={`mr-2`}>Title:</b>
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.title}</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
{
|
||||
media.caption && (
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
@@ -402,6 +463,32 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
showSnippetSelection && (
|
||||
<InfoDialog
|
||||
icon={<CodeIcon className="h-6 w-6" aria-hidden="true" />}
|
||||
title='Insert snippet'
|
||||
description='Select the media snippet to use for the current media file.'
|
||||
dismiss={() => setShowSnippetSelection(false)}>
|
||||
|
||||
<ul className='flex justify-center'>
|
||||
{
|
||||
mediaSnippets.map((snippet, idx) => (
|
||||
<li key={idx} className="inline-flex items-center pb-2 mr-2">
|
||||
<button
|
||||
className="w-full inline-flex justify-center border border-transparent shadow-sm px-4 py-2 bg-teal-600 text-base font-medium text-white hover:bg-teal-700 dark:hover:bg-teal-900 focus:outline-none sm:w-auto sm:text-sm disabled:opacity-30"
|
||||
onClick={() => processSnippet(snippet)}>
|
||||
{snippet.title}
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
</InfoDialog>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showDetails && (
|
||||
<DetailsSlideOver
|
||||
@@ -411,6 +498,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
folder={getFolder()}
|
||||
media={media}
|
||||
showForm={showForm}
|
||||
isImageFile={isImageFile}
|
||||
onEdit={() => setShowForm(true)}
|
||||
onEditClose={() => setShowForm(false)}
|
||||
onDismiss={() => { setShowDetails(false); setShowForm(false); }} />
|
||||
|
||||
@@ -17,7 +17,7 @@ import useMedia from '../../hooks/useMedia';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { join } from 'path';
|
||||
import { extname, join } from 'path';
|
||||
|
||||
export interface IMediaProps {}
|
||||
|
||||
@@ -40,12 +40,23 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
}, [folders, viewData, settings?.staticFolder]);
|
||||
|
||||
const allMedia = React.useMemo(() => {
|
||||
let mediaFiles = media;
|
||||
// Check if content allows page bundle
|
||||
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
|
||||
return media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
|
||||
}
|
||||
|
||||
return media;
|
||||
if (viewData && viewData.data && viewData.data.type === "file" && viewData.data.fileExtensions && viewData.data.fileExtensions.length > 0) {
|
||||
const supportedExtensions = viewData.data.fileExtensions;
|
||||
mediaFiles = mediaFiles.filter(m => {
|
||||
const ext = extname(m.fsPath);
|
||||
// Remove the dot from the extension
|
||||
const extWithoutDot = ext.substring(1);
|
||||
return supportedExtensions.includes(extWithoutDot);
|
||||
});
|
||||
}
|
||||
|
||||
return mediaFiles;
|
||||
}, [media, viewData, settings?.staticFolder]);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-10 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-20 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
72
src/dashboardWebView/components/Modals/InfoDialog.tsx
Normal file
72
src/dashboardWebView/components/Modals/InfoDialog.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
export interface IInfoDialogProps {
|
||||
icon?: JSX.Element;
|
||||
title: string;
|
||||
description: string;
|
||||
dismiss: () => void;
|
||||
}
|
||||
|
||||
export const InfoDialog: React.FunctionComponent<IInfoDialogProps> = ({dismiss, icon, title, description, children}: React.PropsWithChildren<IInfoDialogProps>) => {
|
||||
return (
|
||||
<Transition.Root show={true} as={Fragment}>
|
||||
<Dialog className="fixed z-10 inset-0 overflow-y-auto" onClose={dismiss}>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white dark:bg-vulcan-500 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 border-whisper-900">
|
||||
<div className="sm:flex sm:items-start">
|
||||
{
|
||||
icon && (
|
||||
<div className="mt-3 sm:mr-4 mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:mx-0 sm:h-10 sm:w-10 bg-gray-50 dark:bg-vulcan-400">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-vulcan-300 dark:text-whisper-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-vulcan-500 dark:text-whisper-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
69
src/dashboardWebView/components/Preview/Preview.tsx
Normal file
69
src/dashboardWebView/components/Preview/Preview.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ExternalLinkIcon, RefreshIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { PreviewCommands } from '../../../constants';
|
||||
|
||||
export interface IPreviewProps {
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export const Preview: React.FunctionComponent<IPreviewProps> = ({url}: React.PropsWithChildren<IPreviewProps>) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const onRefresh = () => {
|
||||
if (iframeRef.current?.src) {
|
||||
iframeRef.current.src = iframeRef.current.src;
|
||||
}
|
||||
};
|
||||
|
||||
const openInBrowser = () => {
|
||||
Messenger.send(PreviewCommands.toVSCode.open, url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full h-full bg-white'>
|
||||
<div
|
||||
className="slug fixed w-full top-0 flex items-center"
|
||||
style={{
|
||||
height: "30px",
|
||||
background: "var(--vscode-editor-background)",
|
||||
borderBottom: "1px solid var(--vscode-focusBorder)"
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={url || ""}
|
||||
className="w-full h-full border-0 bg-transparent text-xs py-1 px-2"
|
||||
style={{
|
||||
color: "var(--vscode-editor-foreground)"
|
||||
}}
|
||||
disabled />
|
||||
|
||||
<div
|
||||
className="actions absolute right-0 top-0 bottom-0 flex items-center space-x-2 px-2"
|
||||
style={{
|
||||
background: "var(--vscode-editor-background)",
|
||||
}}
|
||||
>
|
||||
<button title="Refresh" onClick={onRefresh}>
|
||||
<RefreshIcon className='w-4 h-4' aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button title="Open" onClick={openInBrowser} className="mr-2">
|
||||
<ExternalLinkIcon className='w-4 h-4' aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={url || ""}
|
||||
className={`w-full border-0`}
|
||||
style={{
|
||||
height: "calc(100% - 30px)",
|
||||
marginTop: "30px"
|
||||
}}></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/dashboardWebView/components/Preview/index.ts
Normal file
1
src/dashboardWebView/components/Preview/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Preview';
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { CodeIcon, DocumentTextIcon, DotsHorizontalIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlag } from '../../../components/features/FeatureFlag';
|
||||
import { FEATURE_FLAG } from '../../../constants';
|
||||
import { SnippetParser } from '../../../helpers/SnippetParser';
|
||||
import { Snippet, Snippets } from '../../../models';
|
||||
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { ModeAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { QuickAction } from '../Menu';
|
||||
@@ -31,9 +32,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
const [ snippetTitle, setSnippetTitle ] = useState<string>('');
|
||||
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
|
||||
const [ snippetOriginalBody, setSnippetOriginalBody ] = useState<string>('');
|
||||
const [ mediaSnippet, setMediaSnippet ] = useState<boolean>(false);
|
||||
|
||||
const formRef = useRef<SnippetFormHandle>(null);
|
||||
|
||||
const insertToContent = useMemo(() => viewData?.data?.filePath, [ viewData ]);
|
||||
|
||||
const insertToArticle = () => {
|
||||
formRef.current?.onSave();
|
||||
setShowInsertDialog(false);
|
||||
@@ -44,6 +48,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
setSnippetTitle('');
|
||||
setSnippetDescription('');
|
||||
setSnippetOriginalBody('');
|
||||
setMediaSnippet(false);
|
||||
};
|
||||
|
||||
const onOpenEdit = useCallback(() => {
|
||||
@@ -51,6 +56,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
setSnippetDescription(snippet.description);
|
||||
setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`));
|
||||
setShowEditDialog(true);
|
||||
setMediaSnippet(!!snippet.isMediaSnippet);
|
||||
}, [snippet]);
|
||||
|
||||
const onSnippetUpdate = useCallback(() => {
|
||||
@@ -68,11 +74,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
|
||||
const snippetContents: Snippet = {
|
||||
...crntSnippet,
|
||||
fields,
|
||||
description: snippetDescription || '',
|
||||
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
|
||||
};
|
||||
|
||||
if (!mediaSnippet) {
|
||||
snippetContents.fields = fields;
|
||||
} else {
|
||||
snippetContents.isMediaSnippet = true;
|
||||
}
|
||||
|
||||
// Check if new or update
|
||||
if (title === snippetTitle) {
|
||||
snippets[title] = snippetContents;
|
||||
@@ -84,7 +95,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
Messenger.send(DashboardMessage.updateSnippet, { snippets });
|
||||
|
||||
reset();
|
||||
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody]);
|
||||
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
const snippets = Object.assign({}, settings?.snippets || {});
|
||||
@@ -102,13 +113,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
<CodeIcon className='w-64 h-64 opacity-5 text-vulcan-200 dark:text-gray-400' />
|
||||
</div>
|
||||
|
||||
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
|
||||
<h2 className="mt-2 mb-2 font-bold flex items-center" title={snippet.isMediaSnippet ? "Media snippet" : "Content snippet"}>
|
||||
{ snippet.isMediaSnippet ? <PhotographIcon className='w-5 h-5 mr-1' aria-hidden={true} /> : <DocumentTextIcon className='w-5 h-5 mr-1' aria-hidden={true} /> }
|
||||
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<FeatureFlag
|
||||
features={mode?.features || []}
|
||||
flag={FEATURE_FLAG.dashboard.snippets.manage}
|
||||
alternative={(
|
||||
viewData?.data?.filePath ? (
|
||||
insertToContent ? (
|
||||
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
|
||||
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
|
||||
<div className='group-hover:hidden'>
|
||||
@@ -135,7 +150,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
|
||||
<div className='hidden group-hover:flex'>
|
||||
{
|
||||
viewData?.data?.filePath && (
|
||||
insertToContent && !snippet.isMediaSnippet && (
|
||||
<>
|
||||
<QuickAction
|
||||
title={`Insert snippet`}
|
||||
@@ -170,7 +185,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
<FormDialog
|
||||
title={`Insert snippet: ${title}`}
|
||||
description={`Insert the ${title.toLowerCase()} snippet into the current article`}
|
||||
isSaveDisabled={!viewData?.data?.filePath}
|
||||
isSaveDisabled={!insertToContent}
|
||||
trigger={insertToArticle}
|
||||
dismiss={() => setShowInsertDialog(false)}
|
||||
okBtnText='Insert'
|
||||
@@ -200,6 +215,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
title={snippetTitle}
|
||||
description={snippetDescription}
|
||||
body={snippetOriginalBody}
|
||||
isMediaSnippet={mediaSnippet}
|
||||
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
|
||||
onTitleUpdate={(value: string) => setSnippetTitle(value)}
|
||||
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
|
||||
onBodyUpdate={(value: string) => setSnippetOriginalBody(value)} />
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import * as React from 'react';
|
||||
import { GeneralCommands } from '../../../constants';
|
||||
|
||||
export interface INewFormProps {
|
||||
title: string;
|
||||
description: string;
|
||||
body: string;
|
||||
isMediaSnippet: boolean;
|
||||
|
||||
onMediaSnippetUpdate: (value: boolean) => void;
|
||||
onTitleUpdate: (value: string) => void;
|
||||
onDescriptionUpdate: (value: string) => void;
|
||||
onBodyUpdate: (value: string) => void;
|
||||
}
|
||||
|
||||
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
|
||||
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, isMediaSnippet, onMediaSnippetUpdate, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
|
||||
|
||||
const openLink = () => {
|
||||
Messenger.send(GeneralCommands.toVSCode.openLink, "https://frontmatter.codes/docs/markdown#placeholders");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
@@ -60,6 +68,36 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, descrip
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={`snippet`} className="block text-sm font-medium">
|
||||
Is a media snippet?
|
||||
</label>
|
||||
<div className="mt-1 relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="isMediaSnippet"
|
||||
aria-describedby="isMediaSnippet-description"
|
||||
name="isMediaSnippet"
|
||||
type="checkbox"
|
||||
checked={isMediaSnippet}
|
||||
onChange={(e) => onMediaSnippetUpdate(e.currentTarget.checked)}
|
||||
className="focus:ring-teal-500 h-4 w-4 text-teal-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="isMediaSnippet" className="font-medium text-vulcan-100 dark:text-whisper-900">
|
||||
Media snippet
|
||||
</label>
|
||||
<p id="isMediaSnippet-description" className="text-vulcan-300 dark:text-whisper-500">
|
||||
Use the current snippet for inserting media files into your content.
|
||||
</p>
|
||||
<p>
|
||||
Check our <button className='text-teal-700 hover:text-teal-500' onClick={openLink} title='media snippet placeholders'>media snippet placeholders</button> documentation to know which placeholders you can use.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -103,7 +103,7 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
|
||||
|
||||
return (
|
||||
<div>
|
||||
<pre className='border border-opacity-40 p-2 whitespace-pre-wrap break-words'>
|
||||
<pre className='border border-opacity-40 p-2 whitespace-pre-wrap break-words max-h-64 overflow-auto'>
|
||||
{snippetBody}
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
|
||||
<textarea
|
||||
name={field.name}
|
||||
value={field.value || ""}
|
||||
className="focus:outline-none block w-full sm:text-sm border-gray-300 text-vulcan-500"
|
||||
className="focus:outline-none block w-full sm:text-sm border-gray-300 text-vulcan-500 h-auto"
|
||||
onChange={(e) => onValueChange(field, e.currentTarget.value)}
|
||||
rows={4}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
|
||||
const [ snippetBody, setSnippetBody ] = useState<string>('');
|
||||
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
|
||||
const [ mediaSnippet, setMediaSnippet ] = useState(false);
|
||||
|
||||
const snippets = settings?.snippets || {};
|
||||
const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]);
|
||||
@@ -41,11 +42,12 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
title: snippetTitle,
|
||||
description: snippetDescription || '',
|
||||
body: snippetBody,
|
||||
fields
|
||||
fields,
|
||||
isMediaSnippet: mediaSnippet
|
||||
});
|
||||
|
||||
reset();
|
||||
}, [snippetTitle, snippetDescription, snippetBody]);
|
||||
}, [snippetTitle, snippetDescription, snippetBody, mediaSnippet]);
|
||||
|
||||
const reset = () => {
|
||||
setShowCreateDialog(false);
|
||||
@@ -127,6 +129,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
title={snippetTitle}
|
||||
description={snippetDescription}
|
||||
body={snippetBody}
|
||||
isMediaSnippet={mediaSnippet}
|
||||
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
|
||||
onTitleUpdate={(value: string) => setSnippetTitle(value)}
|
||||
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
|
||||
onBodyUpdate={(value: string) => setSnippetBody(value)} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsAtom } from '../state';
|
||||
|
||||
@@ -9,15 +10,29 @@ export interface IStatusProps {
|
||||
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
const draftField = useMemo(() => settings?.draftField, [settings]);
|
||||
|
||||
const draftValue = useMemo(() => {
|
||||
if (draftField && draftField.type === 'choice') {
|
||||
return draft;
|
||||
} else if (draftField && typeof draftField.invert !== 'undefined' && draftField.invert) {
|
||||
return !draft;
|
||||
} else {
|
||||
return draft;
|
||||
}
|
||||
}, [draftField, draft]);
|
||||
|
||||
if (settings?.draftField && settings.draftField.type === "choice") {
|
||||
if (draft) {
|
||||
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
|
||||
if (draftValue) {
|
||||
return <span className={`inline-block px-2 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draftValue}</span>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draftValue ? "bg-red-500" : "bg-teal-500"}`}>
|
||||
{draftValue ? "Draft" : "Published"}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ export const Step: React.FunctionComponent<IStepProps> = ({name, description, st
|
||||
|
||||
<span className="ml-4 min-w-0 flex flex-col">
|
||||
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
|
||||
<div className="text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
|
||||
<div className="mt-1 text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,17 +4,33 @@ import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Settings } from '../../models/Settings';
|
||||
import { Status } from '../../models/Status';
|
||||
import { Step } from './Step';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { Framework } from '../../../models';
|
||||
import {ChevronDownIcon} from '@heroicons/react/outline';
|
||||
import { ContentFolder, Framework } from '../../../models';
|
||||
import {CheckCircleIcon, ChevronDownIcon} from '@heroicons/react/outline';
|
||||
import {CheckCircleIcon as CheckCircleIconSolid} from '@heroicons/react/solid';
|
||||
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const Folder = ({ wsFolder, folder, folders, addFolder }: { wsFolder: string, folder: string, folders: ContentFolder[], addFolder: (folder: string) => void}) => {
|
||||
|
||||
const isAdded = useMemo(() => folders.find(f => f.path.toLowerCase() === join(wsFolder, folder).toLowerCase()), [folder, folders, wsFolder]);
|
||||
|
||||
return (
|
||||
<div className={`text-sm flex items-center ${isAdded ? "text-teal-800" : "text-vulcan-300 dark:text-whisper-800" }`}>
|
||||
<button onClick={() => addFolder(folder)} className='mr-2 hover:text-teal-500' title={`Add as a content folder to Front Matter`}>
|
||||
{ isAdded ? <CheckCircleIconSolid className={`h-4 w-4`} /> : <CheckCircleIcon className={`h-4 w-4`} /> }
|
||||
</button>
|
||||
<span>{folder}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
const [framework, setFramework] = useState<string | null>(null);
|
||||
|
||||
@@ -23,7 +39,21 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
const setFrameworkAndSendMessage = (framework: string) => {
|
||||
setFramework(framework);
|
||||
Messenger.send(DashboardMessage.setFramework, framework);
|
||||
}
|
||||
};
|
||||
|
||||
const addFolder = (folder: string) => {
|
||||
Messenger.send(DashboardMessage.addFolder, folder);
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
const crntState: any = Messenger.getState() || {};
|
||||
Messenger.setState({
|
||||
...crntState,
|
||||
isWelcomeConfiguring: false
|
||||
});
|
||||
|
||||
Messenger.send(DashboardMessage.reload);
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
@@ -76,15 +106,41 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
onClick: undefined
|
||||
},
|
||||
{
|
||||
name: 'Register content folders (manual action)',
|
||||
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
|
||||
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
name: 'Register content folder(s)',
|
||||
description: (
|
||||
<>
|
||||
<p>Add one of the folders we found in your project as a content folder. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</p>
|
||||
|
||||
{
|
||||
settings?.dashboardState?.welcome?.contentFolders?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="text-sm">
|
||||
Folders containing content:
|
||||
</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{settings?.dashboardState?.welcome?.contentFolders?.map((folder) => (
|
||||
<Folder
|
||||
key={folder}
|
||||
folder={folder}
|
||||
addFolder={addFolder}
|
||||
wsFolder={settings.wsFolder}
|
||||
folders={settings.contentFolders} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<p className='mt-4 text-vulcan-300 dark:text-gray-400'><b>IMPORTANT</b>: You can perform this action by <b>right-clicking on the folder in the explorer view</b>, and selecting <b>register folder</b>.</p>
|
||||
</>
|
||||
),
|
||||
status: settings.contentFolders && settings.contentFolders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
name: 'Show the dashboard',
|
||||
description: <>Once both actions are completed, click on this action to load the dashboard.</>,
|
||||
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
|
||||
description: <>Once all actions are completed, the dashboard can be loaded.</>,
|
||||
status: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.contentFolders && settings.contentFolders.length > 0) ? reload : undefined
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -15,18 +15,23 @@ export interface IWelcomeScreenProps {
|
||||
export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({settings}: React.PropsWithChildren<IWelcomeScreenProps>) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
Messenger.send(DashboardMessage.sendTelemetry, {
|
||||
event: TelemetryEvent.webviewWelcomeScreen
|
||||
});
|
||||
|
||||
const crntState: any = Messenger.getState() || {};
|
||||
Messenger.setState({
|
||||
...crntState,
|
||||
isWelcomeConfiguring: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
Messenger.send(DashboardMessage.reload)
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`h-full overflow-auto py-24`}>
|
||||
<div className={`h-full overflow-auto py-8`}>
|
||||
<main>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid grid-cols-12 gap-8">
|
||||
@@ -82,7 +87,7 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({set
|
||||
<StepsToGetStarted settings={settings} />
|
||||
|
||||
<p className="mt-5 text-sm text-vulcan-300 dark:text-whisper-700">
|
||||
Once you completed both actions, the dashboard will show its full potential. You can also use the extension from the <b>Front Matter</b> side panel. There you will find the actions you can perform specifically for your pages.
|
||||
You can also use the extension from the <b>Front Matter</b> side panel. There you will find the actions you can perform specifically for your pages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function useMessages() {
|
||||
case DashboardCommand.searchReady:
|
||||
setSearchReady(true);
|
||||
break;
|
||||
case GeneralCommands.setMode:
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.data);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Sorting } from '../../helpers/Sorting';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [ pageItems, setPageItems ] = useState<Page[]>([]);
|
||||
@@ -24,10 +25,27 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
const processPages = useCallback((searchedPages: Page[]) => {
|
||||
const draftField = settings?.draftField;
|
||||
const framework = settings?.crntFramework;
|
||||
|
||||
// Filter the pages
|
||||
let pagesToShow: Page[] = Object.assign([], searchedPages);
|
||||
|
||||
// Framework specific actions
|
||||
if (framework?.toLowerCase() === "jekyll") {
|
||||
pagesToShow = pagesToShow.map(page => {
|
||||
// https://jekyllrb.com/docs/posts/#drafts
|
||||
const filePath = parseWinPath(page.fmFilePath);
|
||||
page.draft = filePath.indexOf(`/_drafts/`) > -1;
|
||||
|
||||
// Published field: https://jekyllrb.com/docs/front-matter/#predefined-global-variables
|
||||
if (typeof page.published !== "undefined") {
|
||||
page.draft = !page.published;
|
||||
}
|
||||
|
||||
return page;
|
||||
});
|
||||
}
|
||||
|
||||
const draftTypes = Object.assign({}, tabInfo);
|
||||
draftTypes[Tab.All] = pagesToShow.length;
|
||||
|
||||
@@ -46,15 +64,19 @@ export default function usePages(pages: Page[]) {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
} else {
|
||||
// Draft field is a boolean field
|
||||
const draftFieldName = draftField?.name || "draft";
|
||||
|
||||
const drafts = pagesToShow.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
|
||||
const published = pagesToShow.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
|
||||
|
||||
draftTypes[Tab.Draft] = pagesToShow.filter(page => !!page[draftFieldName]).length;
|
||||
draftTypes[Tab.Published] = pagesToShow.filter(page => !page[draftFieldName]).length;
|
||||
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
|
||||
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
|
||||
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
|
||||
pagesToShow = draftField?.invert ? drafts : published;
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
|
||||
pagesToShow = draftField?.invert ? published : drafts;
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { SENTRY_LINK } from "../constants";
|
||||
import './styles.css';
|
||||
import { Preview } from "./components/Preview";
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
getState: () => T;
|
||||
@@ -19,6 +20,8 @@ if (elm) {
|
||||
const version = elm?.getAttribute("data-version");
|
||||
const environment = elm?.getAttribute("data-environment");
|
||||
const isProd = elm?.getAttribute("data-isProd");
|
||||
const type = elm?.getAttribute("data-type");
|
||||
const url = elm?.getAttribute("data-url");
|
||||
|
||||
if (isProd === "true") {
|
||||
Sentry.init({
|
||||
@@ -31,7 +34,11 @@ if (elm) {
|
||||
});
|
||||
}
|
||||
|
||||
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
|
||||
if (type === "preview") {
|
||||
render(<Preview url={url} />, elm);
|
||||
} else {
|
||||
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
|
||||
}
|
||||
}
|
||||
|
||||
// Webpack HMR
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface Page {
|
||||
title: string;
|
||||
slug: string;
|
||||
date: string | Date;
|
||||
draft: string;
|
||||
draft: boolean | string;
|
||||
description: string;
|
||||
|
||||
preview?: string;
|
||||
|
||||
@@ -10,14 +10,12 @@ export interface Settings {
|
||||
beta: boolean;
|
||||
initialized: boolean;
|
||||
wsFolder: string;
|
||||
staticFolder: string;
|
||||
folders: ContentFolder[];
|
||||
staticFolder: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
openOnStart: boolean | null;
|
||||
versionInfo: VersionInfo;
|
||||
pageViewType: DashboardViewType | undefined;
|
||||
mediaSnippet: string[];
|
||||
contentTypes: ContentType[];
|
||||
contentFolders: ContentFolder[];
|
||||
crntFramework: string;
|
||||
@@ -36,6 +34,7 @@ export interface Settings {
|
||||
export interface DashboardState {
|
||||
contents: ContentsViewState;
|
||||
media: MediaViewState;
|
||||
welcome: WelcomeViewState;
|
||||
}
|
||||
|
||||
export interface ContentsViewState {
|
||||
@@ -47,4 +46,8 @@ export interface ContentsViewState {
|
||||
export interface MediaViewState extends ContentsViewState {
|
||||
selectedFolder: string | null | undefined;
|
||||
mimeTypes: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export interface WelcomeViewState {
|
||||
contentFolders: string[];
|
||||
}
|
||||
@@ -136,6 +136,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
let createTemplate = vscode.commands.registerCommand(COMMAND_NAME.createTemplate, Template.generate);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.initTemplate, () => Project.createSampleTemplate(true))
|
||||
);
|
||||
|
||||
const toggleDraftCommand = COMMAND_NAME.toggleDraft;
|
||||
const toggleDraft = vscode.commands.registerCommand(toggleDraftCommand, async () => {
|
||||
@@ -154,6 +158,18 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create);
|
||||
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate)
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.addMissingFields, ContentType.addMissingFields)
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.setContentType, ContentType.setContentType)
|
||||
);
|
||||
|
||||
// Initialize command
|
||||
Template.init();
|
||||
const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
|
||||
@@ -193,7 +209,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => triggerShowDraftStatus(`onDidChangeActiveTextEditor`)));
|
||||
subscriptions.push(vscode.window.onDidChangeTextEditorSelection((e) => {
|
||||
if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) {
|
||||
triggerShowDraftStatus(`onDidChangeTextEditorSelection`);
|
||||
statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200);
|
||||
}
|
||||
}));
|
||||
subscriptions.push(vscode.workspace.onDidChangeTextDocument((TextDocumentChangeEvent) => {
|
||||
const filePath = TextDocumentChangeEvent.document.uri.fsPath;
|
||||
if (filePath && !filePath.toLowerCase().startsWith(`extension-output`)) {
|
||||
MarkdownFoldingProvider.triggerHighlighting();
|
||||
statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -211,7 +234,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
|
||||
|
||||
// Inserting an image in Markdown
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia));
|
||||
|
||||
// Inserting a snippet in Markdown
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider';
|
||||
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
|
||||
import * as vscode from 'vscode';
|
||||
@@ -44,6 +45,23 @@ export class ArticleHelper {
|
||||
return ArticleHelper.parseFile(fileContents, document.fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current article
|
||||
*/
|
||||
public static getCurrent(): ParsedFrontMatter | undefined {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
@@ -65,6 +83,23 @@ export class ArticleHelper {
|
||||
await editor.edit(builder => builder.replace(update.range, update.newText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the new information for the article path
|
||||
*
|
||||
* @param path
|
||||
* @param article
|
||||
*/
|
||||
public static async updateByPath(path: string, article: ParsedFrontMatter) {
|
||||
const file = await workspace.openTextDocument(Uri.parse(path));
|
||||
const editor = await window.showTextDocument(file);
|
||||
|
||||
if (file && editor) {
|
||||
const update = this.generateUpdate(file, article);
|
||||
|
||||
await editor.edit(builder => builder.replace(update.range, update.newText));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the update to be applied to the article.
|
||||
* @param article
|
||||
@@ -80,7 +115,7 @@ export class ArticleHelper {
|
||||
const lastLine = lines.pop();
|
||||
const endsWithNewLine = lastLine !== undefined && lastLine.trim() === "";
|
||||
|
||||
let newMarkdown = this.stringifyFrontMatter(article.content, Object.assign({}, article.data));
|
||||
let newMarkdown = this.stringifyFrontMatter(article.content, Object.assign({}, article.data), document?.getText());
|
||||
|
||||
// Logic to not include a new line at the end of the file
|
||||
if (!endsWithNewLine) {
|
||||
@@ -115,8 +150,9 @@ export class ArticleHelper {
|
||||
*
|
||||
* @param content
|
||||
* @param data
|
||||
* @param originalContent
|
||||
*/
|
||||
public static stringifyFrontMatter(content: string, data: any) {
|
||||
public static stringifyFrontMatter(content: string, data: any, originalContent?: string) {
|
||||
const indentArray = Settings.get(SETTING_INDENT_ARRAY) as boolean;
|
||||
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
|
||||
|
||||
@@ -130,7 +166,7 @@ export class ArticleHelper {
|
||||
}
|
||||
}
|
||||
|
||||
return FrontMatterParser.toFile(content, data, ({
|
||||
return FrontMatterParser.toFile(content, data, originalContent, ({
|
||||
noArrayIndent: !indentArray,
|
||||
skipInvalid: true,
|
||||
noCompatMode: true,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { ModeListener } from './../listeners/general/ModeListener';
|
||||
import { PagesListener } from './../listeners/dashboard';
|
||||
import { ArticleHelper, Settings } from ".";
|
||||
import { SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
|
||||
import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
|
||||
import { ContentType as IContentType, DraftField, Field } from '../models';
|
||||
import { Uri, commands } from 'vscode';
|
||||
import { Uri, commands, window } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Questions } from "./Questions";
|
||||
import { writeFileSync } from "fs";
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
|
||||
import { Telemetry } from './Telemetry';
|
||||
import { processKnownPlaceholders } from './PlaceholderHelper';
|
||||
import { basename } from 'path';
|
||||
|
||||
export class ContentType {
|
||||
|
||||
@@ -92,6 +94,262 @@ export class ContentType {
|
||||
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a content type
|
||||
*/
|
||||
public static async generate() {
|
||||
if (!(await ContentType.verify())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.generateContentType);
|
||||
|
||||
const content = ArticleHelper.getCurrent();
|
||||
|
||||
const editor = window.activeTextEditor;
|
||||
const filePath = editor?.document.uri.fsPath;
|
||||
|
||||
if (!content || !content.data) {
|
||||
Notifications.warning(`No front matter data found to generate a content type.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const override = await window.showQuickPick(["Yes", "No"], {
|
||||
placeHolder: "Do you want to override the default content type?",
|
||||
ignoreFocusOut: true,
|
||||
title: "Override default content type"
|
||||
});
|
||||
const overrideBool = override === "Yes";
|
||||
|
||||
let contentTypeName: string | undefined = `default`;
|
||||
|
||||
// Ask for the new content type name
|
||||
if (!overrideBool) {
|
||||
contentTypeName = await window.showInputBox({
|
||||
ignoreFocusOut: true,
|
||||
placeHolder: "Enter the name of the content type to generate",
|
||||
prompt: "Enter the name of the content type to generate",
|
||||
title: "Generate Content Type",
|
||||
validateInput: (value: string) => {
|
||||
if (!value) {
|
||||
return "Please enter a name for the content type";
|
||||
}
|
||||
|
||||
const contentTypes = ContentType.getAll();
|
||||
if (contentTypes && contentTypes.find(ct => ct.name.toLowerCase() === value.toLowerCase())) {
|
||||
return "A content type with this name already exists";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (!contentTypeName) {
|
||||
Notifications.warning(`You didn't specify a name for the content type.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ask if the content type needs to be used as a page bundle
|
||||
let pageBundle = false;
|
||||
const fileName = filePath ? basename(filePath) : undefined;
|
||||
if (fileName?.startsWith(`index.`)) {
|
||||
const pageBundleAnswer = await window.showQuickPick(["Yes", "No"], {
|
||||
placeHolder: "Do you want to use this content type as a page bundle?",
|
||||
ignoreFocusOut: true,
|
||||
title: "Use as page bundle"
|
||||
});
|
||||
pageBundle = pageBundleAnswer === "Yes";
|
||||
}
|
||||
|
||||
const fields = ContentType.generateFields(content.data);
|
||||
if (!overrideBool && !fields.some(f => f.name === "type")) {
|
||||
fields.push({
|
||||
name: "type",
|
||||
type: "string",
|
||||
default: contentTypeName,
|
||||
hidden: true
|
||||
} as Field);
|
||||
}
|
||||
|
||||
// Update the type field in the page
|
||||
if (!overrideBool && editor) {
|
||||
content.data["type"] = contentTypeName;
|
||||
ArticleHelper.update(editor, content);
|
||||
}
|
||||
|
||||
const newContentType: IContentType = {
|
||||
name: contentTypeName,
|
||||
pageBundle,
|
||||
fields
|
||||
};
|
||||
|
||||
const contentTypes = ContentType.getAll() || [];
|
||||
|
||||
if (overrideBool) {
|
||||
const index = contentTypes.findIndex(ct => ct.name === contentTypeName);
|
||||
contentTypes[index].fields = fields;
|
||||
} else {
|
||||
contentTypes.push(newContentType);
|
||||
}
|
||||
|
||||
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
|
||||
const configPath = Settings.projectConfigPath;
|
||||
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
|
||||
|
||||
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
|
||||
commands.executeCommand('vscode.open', Uri.file(configPath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing fields to the content type
|
||||
*/
|
||||
public static async addMissingFields() {
|
||||
if (!(await ContentType.verify())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.addMissingFields);
|
||||
|
||||
const content = ArticleHelper.getCurrent();
|
||||
|
||||
if (!content || !content.data) {
|
||||
Notifications.warning(`No front matter data found to add missing fields.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = ArticleHelper.getContentType(content?.data);
|
||||
const updatedFields = ContentType.generateFields(content.data, contentType.fields);
|
||||
|
||||
const contentTypes = ContentType.getAll() || [];
|
||||
const index = contentTypes.findIndex(ct => ct.name === contentType.name);
|
||||
contentTypes[index].fields = updatedFields;
|
||||
|
||||
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
|
||||
const configPath = Settings.projectConfigPath;
|
||||
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
|
||||
|
||||
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
|
||||
commands.executeCommand('vscode.open', Uri.file(configPath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content type to be used for the current file
|
||||
*/
|
||||
public static async setContentType() {
|
||||
if (!(await ContentType.verify())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.setContentType);
|
||||
|
||||
const content = ArticleHelper.getCurrent();
|
||||
const contentTypes = ContentType.getAll() || [];
|
||||
|
||||
if (!content || !content.data) {
|
||||
Notifications.warning(`No front matter data found to set the content type.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ctAnswer = await window.showQuickPick(contentTypes.map(ct => ct.name), {
|
||||
title: "Select the content type",
|
||||
ignoreFocusOut: true,
|
||||
placeHolder: "Which content type would you like to use?"
|
||||
});
|
||||
|
||||
if (!ctAnswer) {
|
||||
return;
|
||||
}
|
||||
|
||||
content.data.type = ctAnswer;
|
||||
|
||||
const editor = window.activeTextEditor;
|
||||
ArticleHelper.update(editor!, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the fields from the data
|
||||
* @param data
|
||||
* @param fields
|
||||
* @returns
|
||||
*/
|
||||
private static generateFields(data: any, fields: any[] = []) {
|
||||
for (const field in data) {
|
||||
const fieldData = data[field];
|
||||
|
||||
if (fields.some(f => f.name === field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "string") {
|
||||
if (field.toLowerCase() === "tag" || field.toLowerCase() === "tags") {
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "tags",
|
||||
} as Field);
|
||||
} else if (field.toLowerCase() === "category" || field.toLowerCase() === "categories") {
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "categories",
|
||||
} as Field);
|
||||
} else {
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "choice",
|
||||
choices: fieldData
|
||||
} as Field);
|
||||
}
|
||||
} else if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "object") {
|
||||
const newFields = ContentType.generateFields(fieldData);
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "block",
|
||||
fields: newFields
|
||||
} as Field);
|
||||
} else if (fieldData && fieldData instanceof Object) {
|
||||
const newFields = ContentType.generateFields(fieldData);
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "fields",
|
||||
fields: newFields
|
||||
} as Field);
|
||||
} else {
|
||||
if (!isNaN(new Date(fieldData).getDate())) {
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "datetime"
|
||||
} as Field);
|
||||
} else if (field.toLowerCase() === "draft") {
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: "draft"
|
||||
} as Field);
|
||||
} else if (field.toLowerCase() === "slug") {
|
||||
// Do nothing
|
||||
} else {
|
||||
fields.push({
|
||||
title: field,
|
||||
name: field,
|
||||
type: typeof fieldData
|
||||
} as Field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file with the specified content type
|
||||
* @param contentType
|
||||
@@ -109,6 +367,16 @@ export class ContentType {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentType.name === "default") {
|
||||
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
|
||||
if (crntFramework?.toLowerCase() === "jekyll") {
|
||||
const idx = contentType.fields.findIndex(f => f.name === "draft");
|
||||
if (idx > -1) {
|
||||
contentType.fields.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data: any = this.processFields(contentType, titleValue, {});
|
||||
|
||||
data = ArticleHelper.updateDates(Object.assign({}, data));
|
||||
@@ -167,4 +435,18 @@ export class ContentType {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the content type feature is enabled
|
||||
* @returns
|
||||
*/
|
||||
private static async verify() {
|
||||
const hasFeature = await ModeListener.hasFeature(FEATURE_FLAG.panel.contentType);
|
||||
if (!hasFeature) {
|
||||
Notifications.warning(`The content type actions are not available in this mode.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CommandType } from './../models/PanelSettings';
|
||||
import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings';
|
||||
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
|
||||
import { ArticleHelper, Telemetry } from '.';
|
||||
import { ArticleHelper, Logger, Telemetry } from '.';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { exec } from 'child_process';
|
||||
import * as os from 'os';
|
||||
@@ -41,6 +42,13 @@ export class CustomScript {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the script on the current file
|
||||
* @param wsPath
|
||||
* @param script
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
private static async singleRun(wsPath: string, script: ICustomScript, path: string | null = null): Promise<void> {
|
||||
let articlePath: string | null = path;
|
||||
let article: ParsedFrontMatter | null = null;
|
||||
@@ -62,13 +70,19 @@ export class CustomScript {
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
const output = await CustomScript.runScript(wsPath, article, articlePath as string, script);
|
||||
CustomScript.showOutput(output, script);
|
||||
CustomScript.showOutput(output, script, articlePath);
|
||||
});
|
||||
} else {
|
||||
Notifications.warning(`${script.title}: Article couldn't be retrieved.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the script on multiple files
|
||||
* @param wsPath
|
||||
* @param script
|
||||
* @returns
|
||||
*/
|
||||
private static async bulkRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const folders = await Folders.getInfo();
|
||||
|
||||
@@ -106,6 +120,13 @@ export class CustomScript {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a script for a media file
|
||||
* @param wsPath
|
||||
* @param path
|
||||
* @param script
|
||||
* @returns
|
||||
*/
|
||||
private static async runMediaScript(wsPath: string, path: string | null, script: ICustomScript): Promise<void> {
|
||||
if (!path) {
|
||||
Notifications.error(`${script.title}: There was no folder or media path specified.`);
|
||||
@@ -118,28 +139,34 @@ export class CustomScript {
|
||||
title: `Executing: ${script.title}`,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${path}"`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${script.title}: ${error.message}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
CustomScript.showOutput(stdout, script);
|
||||
try {
|
||||
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${path}"`);
|
||||
|
||||
CustomScript.showOutput(output, script);
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.mediaUpdate
|
||||
});
|
||||
|
||||
resolve();
|
||||
|
||||
return;
|
||||
});
|
||||
} catch (e) {
|
||||
Notifications.error(`${script.title}: ${(e as Error).message}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Script runner
|
||||
* @param wsPath
|
||||
* @param article
|
||||
* @param contentPath
|
||||
* @param script
|
||||
* @returns
|
||||
*/
|
||||
private static async runScript(wsPath: string, article: ParsedFrontMatter | null, contentPath: string, script: ICustomScript): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let articleData = "";
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
@@ -148,31 +175,97 @@ export class CustomScript {
|
||||
articleData = `'${articleData}'`;
|
||||
}
|
||||
|
||||
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => {
|
||||
const output = await CustomScript.executeScript(script, wsPath, `"${wsPath}" "${contentPath}" ${articleData}`);
|
||||
return output;
|
||||
} catch (e) {
|
||||
Notifications.error(`${script.title}: ${(e as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/process the output of the script
|
||||
* @param output
|
||||
* @param script
|
||||
*/
|
||||
private static showOutput(output: string | null, script: ICustomScript, articlePath?: string | null): void {
|
||||
if (output) {
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
|
||||
if (data.frontmatter) {
|
||||
let article = null;
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
if (!articlePath) {
|
||||
if (!editor) return;
|
||||
|
||||
articlePath = editor.document.uri.fsPath;
|
||||
article = ArticleHelper.getFrontMatter(editor);
|
||||
} else {
|
||||
article = ArticleHelper.getFrontMatterByPath(articlePath);
|
||||
}
|
||||
|
||||
if (article && article.data) {
|
||||
for (const key in data.frontmatter) {
|
||||
article.data[key] = data.frontmatter[key];
|
||||
}
|
||||
|
||||
if (articlePath) {
|
||||
ArticleHelper.updateByPath(articlePath, article);
|
||||
} else if (editor) {
|
||||
ArticleHelper.update(editor, article);
|
||||
} else {
|
||||
throw new Error(`Couldn't update article.`);
|
||||
}
|
||||
Notifications.info(`${script.title}: front matter updated.`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`No frontmatter found.`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (script.output === "editor") {
|
||||
ContentProvider.show(output, script.title, script.outputType || "text");
|
||||
} else {
|
||||
window.showInformationMessage(`${script.title}: ${output}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Notifications.info(`${script.title}: Executed your custom script.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute script
|
||||
* @param script
|
||||
* @param wsPath
|
||||
* @param args
|
||||
* @returns
|
||||
*/
|
||||
private static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Check the command to use
|
||||
let command = script.nodeBin || "node";
|
||||
if (script.command && script.command !== CommandType.Node) {
|
||||
command = script.command;
|
||||
}
|
||||
|
||||
const scriptPath = join(wsPath, script.script);
|
||||
const fullScript = `${command} ${scriptPath} ${args}`;
|
||||
Logger.info(`Executing: ${fullScript}`);
|
||||
|
||||
exec(fullScript, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${script.title}: ${error.message}`);
|
||||
resolve(null);
|
||||
return;
|
||||
reject(error.message);
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static showOutput(output: string | null, script: ICustomScript): void {
|
||||
if (output) {
|
||||
if (script.output === "editor") {
|
||||
ContentProvider.show(output, script.title, script.outputType || "text");
|
||||
} else {
|
||||
window.showInformationMessage(`${script.title}: ${output}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Notifications.info(`${script.title}: Executed your custom script.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { basename, join } from "path";
|
||||
import { workspace } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Project } from "../commands/Project";
|
||||
import { Template } from "../commands/Template";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
|
||||
import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
|
||||
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
|
||||
import { DataFile } from "../models/DataFile";
|
||||
import { DataFolder } from "../models/DataFolder";
|
||||
import { DataType } from "../models/DataType";
|
||||
@@ -18,20 +19,18 @@ export class DashboardSettings {
|
||||
public static async get() {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = await Template.isInitialized();
|
||||
const isInitialized = Project.isInitialized();
|
||||
|
||||
return {
|
||||
beta: ext.isBetaVersion(),
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: Settings.get<string>(SETTING_CONTENT_STATIC_FOLDER),
|
||||
folders: Folders.get(),
|
||||
initialized: isInitialized,
|
||||
tags: Settings.getTaxonomy(TaxonomyType.Tag),
|
||||
categories: Settings.getTaxonomy(TaxonomyType.Category),
|
||||
openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<DashboardViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
mediaSnippet: Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
|
||||
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
|
||||
@@ -53,6 +52,9 @@ export class DashboardSettings {
|
||||
defaultSorting: Settings.get<string>(SETTING_MEDIA_SORTING_DEFAULT),
|
||||
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace"),
|
||||
mimeTypes: Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES)
|
||||
},
|
||||
welcome: {
|
||||
contentFolders: await Folders.getContentFolders()
|
||||
}
|
||||
},
|
||||
dataFiles: await this.getDataFiles(),
|
||||
|
||||
74
src/helpers/DataFileHelper.ts
Normal file
74
src/helpers/DataFileHelper.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { DataFile } from "../models";
|
||||
import * as yaml from 'js-yaml';
|
||||
import { Logger } from "./Logger";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { commands } from "vscode";
|
||||
import { COMMAND_NAME, SETTING_DATA_FILES } from "../constants";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
|
||||
|
||||
export class DataFileHelper {
|
||||
|
||||
/**
|
||||
* Retrieve the file data
|
||||
* @param filePath
|
||||
* @returns
|
||||
*/
|
||||
public static get(filePath: string) {
|
||||
const absPath = Folders.getAbsFilePath(filePath);
|
||||
if (existsSync(absPath)) {
|
||||
return readFileSync(absPath, 'utf8');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get by the id of the data file
|
||||
* @param id
|
||||
*/
|
||||
public static getById(id: string) {
|
||||
const files = Settings.get<DataFile[]>(SETTING_DATA_FILES);
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files.find(f => f.id === id);
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
return DataFileHelper.process(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the data file
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public static async process(data: DataFile) {
|
||||
try {
|
||||
const { file, fileType } = data;
|
||||
const dataFile = DataFileHelper.get(file);
|
||||
|
||||
if (fileType === "yaml") {
|
||||
return yaml.safeLoad(dataFile || "");
|
||||
} else {
|
||||
return dataFile ? JSON.parse(dataFile) : undefined;
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger.error(`DataFileHelper::process: ${(ex as Error).message}`);
|
||||
const btnClick = await Notifications.error(`Something went wrong while processing the data file. Check your file and output log for more information.`, 'Open output');
|
||||
|
||||
if (btnClick && btnClick === 'Open output') {
|
||||
commands.executeCommand(COMMAND_NAME.showOutputChannel);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { basename } from "path";
|
||||
import { extensions, Uri, ExtensionContext, window, workspace, commands, ExtensionMode, DiagnosticCollection, languages } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { EXTENSION_NAME, GITHUB_LINK, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS } from "../constants";
|
||||
import { ContentFolder } from "../models";
|
||||
import { Template } from "../commands/Template";
|
||||
import { EXTENSION_NAME, GITHUB_LINK, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_CONTENT_SNIPPETS, SETTING_TEMPLATES_ENABLED } from "../constants";
|
||||
import { ContentFolder, Snippet } from "../models";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
|
||||
@@ -189,6 +190,41 @@ export class Extension {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (major <= 7 && minor < 3) {
|
||||
const mediaSnippet = Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET);
|
||||
if (mediaSnippet && mediaSnippet.length > 0) {
|
||||
let snippet = mediaSnippet.join(`\n`);
|
||||
|
||||
snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`);
|
||||
snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`);
|
||||
snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`);
|
||||
snippet = snippet.replace(`{caption}`, `[[&caption]]`);
|
||||
snippet = snippet.replace(`{alt}`, `[[alt]]`);
|
||||
snippet = snippet.replace(`{filename}`, `[[filename]]`);
|
||||
snippet = snippet.replace(`{title}`, `[[title]]`);
|
||||
|
||||
const snippets = Settings.get<Snippet[]>(SETTING_CONTENT_SNIPPETS) || {} as any;
|
||||
snippets[`Media snippet (migrated)`] = {
|
||||
body: snippet.split(`\n`),
|
||||
isMediaSnippet: true,
|
||||
description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting`
|
||||
}
|
||||
|
||||
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
|
||||
}
|
||||
|
||||
const templates = await Template.getTemplates();
|
||||
if (templates && templates.length > 0) {
|
||||
const answer = await window.showQuickPick(["Yes", "No"], {
|
||||
title: "Front Matter - Templates",
|
||||
placeHolder: "Do you want to keep on using the template functionality?",
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
Settings.update(SETTING_TEMPLATES_ENABLED, answer?.toLocaleLowerCase() === "yes", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async setState<T>(propKey: string, propValue: T, type: "workspace" | "global" = "global"): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { existsSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import jsyaml = require("js-yaml");
|
||||
import { join, resolve } from "path";
|
||||
import { commands, Uri } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { COMMAND_NAME } from "../constants";
|
||||
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
|
||||
import { Extension } from "./Extension";
|
||||
import { Framework } from "../models";
|
||||
import { Logger } from "./Logger";
|
||||
|
||||
export class FrameworkDetector {
|
||||
|
||||
@@ -14,18 +19,52 @@ export class FrameworkDetector {
|
||||
}
|
||||
|
||||
private static check(folder: string) {
|
||||
const { dependencies, devDependencies } = Extension.getInstance().packageJson;
|
||||
let dependencies = null;
|
||||
let devDependencies = null;
|
||||
let gemContent = null;
|
||||
|
||||
// Try fetching the package JSON file
|
||||
try {
|
||||
const pkgFile = join(folder, 'package.json');
|
||||
if (existsSync(pkgFile)) {
|
||||
let packageJson: any = readFileSync(pkgFile, "utf8");
|
||||
if (packageJson) {
|
||||
packageJson = typeof packageJson === "string" ? JSON.parse(packageJson) : packageJson;
|
||||
|
||||
dependencies = packageJson.dependencies || null;
|
||||
devDependencies = packageJson.devDependencies || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Try fetching the Gemfile
|
||||
try {
|
||||
const gemFile = join(folder, 'Gemfile');
|
||||
if (existsSync(gemFile)) {
|
||||
gemContent = readFileSync(gemFile, "utf8");
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
for (const detector of FrameworkDetectors) {
|
||||
if (detector && folder) {
|
||||
|
||||
// Verify by dependencies
|
||||
for (const dependency of detector.requiredDependencies ?? []) {
|
||||
// Checks for package.json dependencies
|
||||
const inDependencies = dependencies && dependencies[dependency]
|
||||
const inDevDependencies = devDependencies && devDependencies[dependency]
|
||||
if (inDependencies || inDevDependencies) {
|
||||
return detector.framework;
|
||||
}
|
||||
|
||||
// Checks for Gemfile
|
||||
if (gemContent && gemContent.includes(dependency)) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify by files
|
||||
@@ -40,4 +79,50 @@ export class FrameworkDetector {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static checkDefaultSettings(framework: Framework) {
|
||||
if (framework.name.toLowerCase() === "jekyll") {
|
||||
FrameworkDetector.jekyll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static jekyll() {
|
||||
try {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const jekyllConfig = join(wsFolder?.fsPath || "", '_config.yml');
|
||||
let collectionDir = "";
|
||||
|
||||
if (existsSync(jekyllConfig)) {
|
||||
const content = readFileSync(jekyllConfig, "utf8");
|
||||
// Convert YAML to JSON
|
||||
const config = jsyaml.safeLoad(content);
|
||||
|
||||
if (config.collections_dir) {
|
||||
collectionDir = config.collections_dir;
|
||||
}
|
||||
}
|
||||
|
||||
const draftsPath = join(wsFolder?.fsPath || "", collectionDir, "_drafts");
|
||||
const postsPath = join(wsFolder?.fsPath || "", collectionDir, "_posts");
|
||||
|
||||
if (existsSync(draftsPath)) {
|
||||
const folderUri = Uri.file(draftsPath);
|
||||
commands.executeCommand(COMMAND_NAME.registerFolder, {
|
||||
title: "drafts",
|
||||
path: folderUri
|
||||
});
|
||||
}
|
||||
|
||||
if (existsSync(postsPath)) {
|
||||
const folderUri = Uri.file(postsPath);
|
||||
commands.executeCommand(COMMAND_NAME.registerFolder, {
|
||||
title: "posts",
|
||||
path: folderUri
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(`Something failed while processing your Jekyll configuration. ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Extension } from './Extension';
|
||||
import { OutputChannel, window } from 'vscode';
|
||||
import { commands, OutputChannel, window } from 'vscode';
|
||||
import { format } from 'date-fns';
|
||||
import { COMMAND_NAME } from '../constants';
|
||||
|
||||
export class Logger {
|
||||
private static instance: Logger;
|
||||
private static channel: OutputChannel | null = null;
|
||||
public static channel: OutputChannel | null = null;
|
||||
|
||||
private constructor() {
|
||||
const displayName = Extension.getInstance().displayName;
|
||||
Logger.channel = window.createOutputChannel(displayName);
|
||||
commands.registerCommand(COMMAND_NAME.showOutputChannel, () => { Logger.channel?.show(); });
|
||||
}
|
||||
|
||||
public static getInstance(): Logger {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
|
||||
import { decodeBase64, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
|
||||
import { Dashboard } from "../commands/Dashboard";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
@@ -65,7 +65,7 @@ export class MediaHelpers {
|
||||
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
|
||||
}
|
||||
|
||||
if (relSelectedFolderPath.startsWith('/')) {
|
||||
if (relSelectedFolderPath && relSelectedFolderPath.startsWith('/')) {
|
||||
relSelectedFolderPath = relSelectedFolderPath.substring(1);
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export class MediaHelpers {
|
||||
}
|
||||
|
||||
const staticPath = join(absFolderPath, fileName);
|
||||
const imgData = decodeBase64Image(contents);
|
||||
const imgData = decodeBase64(contents);
|
||||
|
||||
if (imgData) {
|
||||
writeFileSync(staticPath, imgData.data);
|
||||
@@ -270,7 +270,7 @@ export class MediaHelpers {
|
||||
* @param data
|
||||
*/
|
||||
public static async insertMediaToMarkdown(data: any) {
|
||||
if (data?.file && data?.image) {
|
||||
if (data?.file && data?.relPath) {
|
||||
if (!data?.position) {
|
||||
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
|
||||
}
|
||||
@@ -281,12 +281,12 @@ export class MediaHelpers {
|
||||
const editor = window.activeTextEditor;
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const filePath = data.file;
|
||||
let imgPath = data.image;
|
||||
let relPath = data.relPath;
|
||||
|
||||
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
|
||||
const articleCt = article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
|
||||
|
||||
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
|
||||
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), relPath);
|
||||
const fileDir = parseWinPath(dirname(filePath));
|
||||
const imgDir = parseWinPath(dirname(absImgPath));
|
||||
const contentFolders = Folders.get();
|
||||
@@ -303,11 +303,11 @@ export class MediaHelpers {
|
||||
if (existsInContent) {
|
||||
const relImgPath = relative(fileDir, imgDir);
|
||||
|
||||
imgPath = join(relImgPath, basename(imgPath));
|
||||
relPath = join(relImgPath, basename(relPath));
|
||||
|
||||
// Snippets are already parsed, so update the URL of the image
|
||||
if (data.snippet) {
|
||||
data.snippet = data.snippet.replace(data.image, imgPath);
|
||||
data.snippet = data.snippet.replace(data.relPath, relPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,7 +319,16 @@ export class MediaHelpers {
|
||||
if (line) {
|
||||
const selection = editor?.selection;
|
||||
await editor?.edit(builder => {
|
||||
const snippet = data.snippet || ``;
|
||||
const mimeType = lookup(relPath)
|
||||
|
||||
let isFile = true;
|
||||
if (mimeType) {
|
||||
isFile = !mimeType.startsWith('image');
|
||||
}
|
||||
|
||||
const caption = isFile ? `${data.title || ""}` : `${data.alt || data.caption || ""}`;
|
||||
|
||||
const snippet = data.snippet || `${isFile ? "" : "!"}[${caption}](${relPath})`;
|
||||
if (selection !== undefined) {
|
||||
builder.replace(selection, snippet);
|
||||
} else {
|
||||
@@ -333,7 +342,7 @@ export class MediaHelpers {
|
||||
|
||||
DataListener.updateMetadata({
|
||||
field: data.fieldName,
|
||||
value: imgPath,
|
||||
value: relPath,
|
||||
parents: data.parents,
|
||||
blockData: data.blockData
|
||||
});
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
|
||||
import { CommandToCode } from "../panelWebView/CommandToCode";
|
||||
|
||||
|
||||
interface ClientVsCode<T> {
|
||||
getState: () => T;
|
||||
setState: (data: T) => void;
|
||||
postMessage: (msg: unknown) => void;
|
||||
}
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => ClientVsCode<T>;
|
||||
|
||||
export class MessageHelper {
|
||||
private static vscode: ClientVsCode<any>;
|
||||
|
||||
public static getVsCodeAPI() {
|
||||
if (!MessageHelper.vscode) {
|
||||
MessageHelper.vscode = acquireVsCodeApi();
|
||||
}
|
||||
return MessageHelper.vscode;
|
||||
}
|
||||
|
||||
public static sendMessage = (command: CommandToCode | DashboardMessage, data?: any) => {
|
||||
const vscode = MessageHelper.getVsCodeAPI();
|
||||
if (data) {
|
||||
vscode.postMessage({ command, data });
|
||||
} else {
|
||||
vscode.postMessage({ command });
|
||||
}
|
||||
}
|
||||
|
||||
public static getState = () => {
|
||||
const vscode = MessageHelper.getVsCodeAPI();
|
||||
return vscode.getState();
|
||||
}
|
||||
|
||||
public static setState = (data: any) => {
|
||||
const vscode = MessageHelper.getVsCodeAPI();
|
||||
vscode.setState({
|
||||
...data
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Settings } from "./SettingsHelper";
|
||||
|
||||
|
||||
export class Notifications {
|
||||
private static notifications: string[] = [];
|
||||
|
||||
public static info(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO");
|
||||
@@ -36,6 +37,16 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public static async errorShowOnce(message: string, ...items: any): Promise<string | undefined> {
|
||||
if (this.notifications.includes(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications.push(message);
|
||||
|
||||
return this.error(message, ...items);
|
||||
}
|
||||
|
||||
private static shouldShow(level: "INFO" | "WARNING" | "ERROR"): boolean {
|
||||
let levels = Settings.get<string[]>(SETTING_GLOBAL_NOTIFICATIONS);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export class Questions {
|
||||
* @returns
|
||||
*/
|
||||
public static async yesOrNo(placeholder: string) {
|
||||
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: false });
|
||||
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: true });
|
||||
return answer === "yes";
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ export class Questions {
|
||||
public static async ContentTitle(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const title = await window.showInputBox({
|
||||
prompt: `What would you like to use as a title for the content to create?`,
|
||||
placeHolder: `Content title`
|
||||
placeHolder: `Content title`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!title && showWarning) {
|
||||
@@ -82,7 +83,8 @@ export class Questions {
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
placeHolder: `Select the content type to create your new content`,
|
||||
canPickMany: false
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!selectedOption && showWarning) {
|
||||
|
||||
@@ -170,6 +170,15 @@ export class Settings {
|
||||
await Settings.config.update(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the project contains the frontmatter.json file
|
||||
*/
|
||||
public static hasProjectFile() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const configPath = join(wsFolder?.fsPath || "", Settings.globalFile);
|
||||
return existsSync(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create team settings
|
||||
*/
|
||||
@@ -328,7 +337,7 @@ export class Settings {
|
||||
* Get the project config path
|
||||
* @returns
|
||||
*/
|
||||
private static get projectConfigPath() {
|
||||
public static get projectConfigPath() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const fmConfig = join(wsFolder.fsPath, Settings.globalFile);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
export const decodeBase64Image = (dataString: string) => {
|
||||
const matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
||||
let response: any = {};
|
||||
export const decodeBase64 = (dataString: string) => {
|
||||
const dataParts = dataString.split(';base64,');
|
||||
|
||||
if (matches?.length !== 3) {
|
||||
if (dataParts?.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
response.type = matches[1];
|
||||
response.data = Buffer.from(matches[2], 'base64');
|
||||
const typePart = dataParts[0].split(':').pop() as string;
|
||||
const dataPart = dataParts.pop() as string;
|
||||
|
||||
let response: any = {};
|
||||
|
||||
response.type = typePart;
|
||||
response.data = Buffer.from(dataPart, 'base64');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './ArticleHelper';
|
||||
export * from './ContentType';
|
||||
export * from './CustomScript';
|
||||
export * from './DashboardSettings';
|
||||
export * from './DataFileHelper';
|
||||
export * from './DateHelper';
|
||||
export * from './Extension';
|
||||
export * from './FilesHelper';
|
||||
@@ -11,13 +12,15 @@ export * from './ImageHelper';
|
||||
export * from './Logger';
|
||||
export * from './MediaHelpers';
|
||||
export * from './MediaLibrary';
|
||||
export * from './MessageHelper';
|
||||
export * from './Notifications';
|
||||
export * from './PanelSettings';
|
||||
export * from './PlaceholderHelper';
|
||||
export * from './Questions';
|
||||
export * from './Sanitize';
|
||||
export * from './SeoHelper';
|
||||
export * from './SettingsHelper';
|
||||
export * from './SlugHelper';
|
||||
export * from './SnippetParser';
|
||||
export * from './Sorting';
|
||||
export * from './StringHelpers';
|
||||
export * from './Telemetry';
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function useContentType(settings: PanelSettings | Settings | unde
|
||||
|
||||
setContentType(ct || DEFAULT_CONTENT_TYPE)
|
||||
}
|
||||
}, [settings?.contentTypes, metadata?.data]);
|
||||
}, [settings?.contentTypes, metadata?.type]);
|
||||
|
||||
return contentType;
|
||||
}
|
||||
@@ -3,11 +3,10 @@ import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
|
||||
import { Folders } from '../../commands/Folders';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { Logger, Notifications } from '../../helpers';
|
||||
import { commands } from 'vscode';
|
||||
import { DataFileHelper } from '../../helpers';
|
||||
|
||||
|
||||
export class DataListener extends BaseListener {
|
||||
@@ -57,38 +56,7 @@ export class DataListener extends BaseListener {
|
||||
* @param msgData
|
||||
*/
|
||||
private static async processDataFile(msgData: DataFile) {
|
||||
try {
|
||||
const { file } = msgData;
|
||||
const dataFile = this.getDataFile(file);
|
||||
|
||||
if (msgData.fileType === "yaml") {
|
||||
const entries = yaml.safeLoad(dataFile || "");
|
||||
this.sendMsg(DashboardCommand.dataFileEntries, entries);
|
||||
} else {
|
||||
const jsonData = dataFile ? JSON.parse(dataFile) : [];
|
||||
this.sendMsg(DashboardCommand.dataFileEntries, jsonData);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger.error(`DataListener::processDataFile: ${(ex as Error).message}`);
|
||||
const btnClick = await Notifications.error(`Something went wrong while processing the data file. Check your file and output log for more information.`, 'Open output');
|
||||
|
||||
if (btnClick && btnClick === 'Open output') {
|
||||
commands.executeCommand(`workbench.panel.output.focus`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file data
|
||||
* @param file
|
||||
* @returns
|
||||
*/
|
||||
private static getDataFile(file: string) {
|
||||
const absPath = Folders.getAbsFilePath(file);
|
||||
if (existsSync(absPath)) {
|
||||
return readFileSync(absPath, 'utf8');
|
||||
}
|
||||
|
||||
return null;
|
||||
const entries = await DataFileHelper.process(msgData);
|
||||
this.sendMsg(DashboardCommand.dataFileEntries, entries);
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,14 @@ export class MediaListener extends BaseListener {
|
||||
case DashboardMessage.revealMedia:
|
||||
this.openFileInFinder(msg?.data?.file);
|
||||
break;
|
||||
case DashboardMessage.insertPreviewImage:
|
||||
case DashboardMessage.insertMedia:
|
||||
Telemetry.send(TelemetryEvent.insertMediaToContent);
|
||||
MediaHelpers.insertMediaToMarkdown(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.insertFile:
|
||||
Telemetry.send(TelemetryEvent.insertFileToContent);
|
||||
MediaHelpers.insertMediaToMarkdown(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.updateMediaMetadata:
|
||||
Telemetry.send(TelemetryEvent.updateMediaMetadata);
|
||||
this.update(msg.data);
|
||||
|
||||
@@ -174,6 +174,10 @@ export class PagesListener extends BaseListener {
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
if ((error as Error)?.message.toLowerCase() === "webview is disposed") {
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`);
|
||||
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID } from "../../constants";
|
||||
import { join } from "path";
|
||||
import { commands, Uri } from "vscode";
|
||||
import { Folders } from "../../commands/Folders";
|
||||
import { COMMAND_NAME, SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID } from "../../constants";
|
||||
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { DashboardSettings, Settings } from "../../helpers";
|
||||
@@ -26,6 +29,9 @@ export class SettingsListener extends BaseListener {
|
||||
case DashboardMessage.setFramework:
|
||||
this.setFramework(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.addFolder:
|
||||
this.addFolder(msg?.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +59,7 @@ export class SettingsListener extends BaseListener {
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
*/
|
||||
private static setFramework(frameworkId: string | null) {
|
||||
public static setFramework(frameworkId: string | null) {
|
||||
Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
|
||||
|
||||
if (frameworkId) {
|
||||
@@ -61,9 +67,21 @@ export class SettingsListener extends BaseListener {
|
||||
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
|
||||
if (framework) {
|
||||
Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
|
||||
FrameworkDetector.checkDefaultSettings(framework);
|
||||
} else {
|
||||
Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsListener.getSettings();
|
||||
}
|
||||
|
||||
private static addFolder(folder: string) {
|
||||
if (folder) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const folderUri = Uri.file(join(wsFolder?.fsPath || "", folder));
|
||||
commands.executeCommand(COMMAND_NAME.registerFolder, folderUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Dashboard } from "../../commands/Dashboard";
|
||||
import { SETTING_CONTENT_SNIPPETS } from "../../constants";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { Notifications, Settings } from "../../helpers";
|
||||
import { Snippet } from "../../models";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { SettingsListener } from "./SettingsListener";
|
||||
|
||||
@@ -27,7 +28,7 @@ export class SnippetListener extends BaseListener {
|
||||
}
|
||||
|
||||
private static async addSnippet(data: any) {
|
||||
const { title, description, body, fields } = data;
|
||||
const { title, description, body, fields, isMediaSnippet } = data;
|
||||
|
||||
if (!title || !body) {
|
||||
Notifications.warning("Snippet missing title or body");
|
||||
@@ -42,11 +43,18 @@ export class SnippetListener extends BaseListener {
|
||||
|
||||
const snippetLines = body.split("\n");
|
||||
|
||||
snippets[title] = {
|
||||
const snippetContent: any = {
|
||||
description,
|
||||
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines,
|
||||
fields: fields || []
|
||||
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
|
||||
};
|
||||
|
||||
if (isMediaSnippet) {
|
||||
snippetContent.isMediaSnippet = true;
|
||||
} else {
|
||||
snippetContent.fields = fields || []
|
||||
}
|
||||
|
||||
snippets[title] = snippetContent;
|
||||
|
||||
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
|
||||
SettingsListener.getSettings();
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { GeneralCommands } from './../../constants';
|
||||
import { GeneralCommands } from './../../constants/GeneralCommands';
|
||||
import { Dashboard } from "../../commands/Dashboard";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { ExplorerView } from "../../explorerView/ExplorerView";
|
||||
import { Extension } from "../../helpers";
|
||||
import { Logger } from "../../helpers/Logger";
|
||||
import { CommandToCode } from '../../panelWebView/CommandToCode';
|
||||
import { commands, Uri } from 'vscode';
|
||||
|
||||
|
||||
export abstract class BaseListener {
|
||||
|
||||
public static process(msg: { command: DashboardMessage, data: any }) {}
|
||||
public static process(msg: { command: DashboardMessage | CommandToCode | string , data: any }) {
|
||||
switch(msg.command) {
|
||||
case GeneralCommands.toVSCode.openLink:
|
||||
if (msg.data) {
|
||||
commands.executeCommand('vscode.open', Uri.parse(msg.data));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the webview
|
||||
* @param command
|
||||
* @param data
|
||||
*/
|
||||
public static sendMsg(command: GeneralCommands, data: any) {
|
||||
public static sendMsg(command: string, data: any) {
|
||||
Logger.info(`Sending message to webview (panel&dashboard): ${command}`);
|
||||
|
||||
const extPath = Extension.getInstance().extensionPath;
|
||||
|
||||
@@ -35,7 +35,7 @@ export class ModeListener extends BaseListener {
|
||||
const activeMode = ModeSwitch.getMode();
|
||||
if (activeMode) {
|
||||
const mode = modes.find(m => m.id === activeMode);
|
||||
this.sendMsg(GeneralCommands.setMode as any, mode);
|
||||
this.sendMsg(GeneralCommands.toWebview.setMode as any, mode);
|
||||
|
||||
// Check the commands that need to be enabled/disabled
|
||||
const snippetsView = mode?.features.find(f => f === FEATURE_FLAG.dashboard.snippets.view);
|
||||
@@ -44,13 +44,37 @@ export class ModeListener extends BaseListener {
|
||||
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, !!snippetsView);
|
||||
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, !!dataView);
|
||||
} else {
|
||||
this.sendMsg(GeneralCommands.setMode as any, undefined);
|
||||
this.sendMsg(GeneralCommands.toWebview.setMode as any, undefined);
|
||||
|
||||
// Enable dashboards
|
||||
await this.resetEnablement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the mode has the feature enabled
|
||||
* @param feature
|
||||
* @returns
|
||||
*/
|
||||
public static async hasFeature(feature: string) {
|
||||
const modes = Settings.get<Mode[]>(SETTING_GLOBAL_MODES);
|
||||
|
||||
if (!modes || modes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activeMode = ModeSwitch.getMode();
|
||||
if (activeMode) {
|
||||
const mode = modes.find(m => m.id === activeMode);
|
||||
return mode?.features.find(f => f === feature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the context
|
||||
*/
|
||||
public static async resetEnablement() {
|
||||
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, true);
|
||||
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DataFileHelper } from './../../helpers/DataFileHelper';
|
||||
import { BlockFieldData } from './../../models/BlockFieldData';
|
||||
import { ImageHelper } from './../../helpers/ImageHelper';
|
||||
import { Folders } from "../../commands/Folders";
|
||||
@@ -43,6 +44,18 @@ export class DataListener extends BaseListener {
|
||||
case CommandToCode.updatePlaceholder:
|
||||
this.updatePlaceholder(msg?.data?.field, msg?.data?.value, msg?.data?.title);
|
||||
break;
|
||||
case CommandToCode.generateContentType:
|
||||
commands.executeCommand(COMMAND_NAME.generateContentType);
|
||||
break;
|
||||
case CommandToCode.addMissingFields:
|
||||
commands.executeCommand(COMMAND_NAME.addMissingFields);
|
||||
break;
|
||||
case CommandToCode.setContentType:
|
||||
commands.executeCommand(COMMAND_NAME.setContentType);
|
||||
break;
|
||||
case CommandToCode.getDataEntries:
|
||||
this.getDataFileEntries(msg.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +108,7 @@ export class DataListener extends BaseListener {
|
||||
// Get the current content type
|
||||
const contentType = ArticleHelper.getContentType(updatedMetadata);
|
||||
if (contentType) {
|
||||
ImageHelper.processImageFields(updatedMetadata, contentType.fields)
|
||||
ImageHelper.processImageFields(updatedMetadata, contentType.fields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +160,14 @@ export class DataListener extends BaseListener {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const dateFields = contentType.fields.filter((f) => f.type === "datetime");
|
||||
const imageFields = contentType.fields.filter((f) => f.type === "image" && f.multiple);
|
||||
const fileFields = contentType.fields.filter((f) => f.type === "file" && f.multiple);
|
||||
|
||||
// Support multi-level fields
|
||||
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
|
||||
|
||||
const isDateField = dateFields.some(f => f.name === field);
|
||||
const isMultiImageField = imageFields.some(f => f.name === field);
|
||||
const isMultiFileField = fileFields.some(f => f.name === field);
|
||||
|
||||
if (isDateField) {
|
||||
for (const dateField of dateFields) {
|
||||
@@ -160,9 +175,11 @@ export class DataListener extends BaseListener {
|
||||
parentObj[field] = Article.formatDate(new Date(value));
|
||||
}
|
||||
}
|
||||
} else if (isMultiImageField) {
|
||||
for (const imageField of imageFields) {
|
||||
if (field === imageField.name) {
|
||||
} else if (isMultiImageField || isMultiFileField) {
|
||||
const fields = isMultiImageField ? imageFields : fileFields;
|
||||
|
||||
for (const crntField of fields) {
|
||||
if (field === crntField.name) {
|
||||
// If value is an array, it means it comes from the explorer view itself (deletion)
|
||||
if (Array.isArray(value)) {
|
||||
parentObj[field] = value || [];
|
||||
@@ -268,6 +285,17 @@ export class DataListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data entries from local data files
|
||||
* @param data
|
||||
*/
|
||||
private static async getDataFileEntries(data: any) {
|
||||
const entries = await DataFileHelper.getById(data);
|
||||
if (entries) {
|
||||
this.sendMsg(Command.dataFileEntries, entries);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a terminal and run the passed command
|
||||
* @param command
|
||||
|
||||
@@ -22,6 +22,9 @@ export class MediaListener extends BaseListener {
|
||||
case CommandToCode.selectImage:
|
||||
this.selectMedia(msg);
|
||||
break;
|
||||
case CommandToCode.selectFile:
|
||||
this.selectMedia(msg);
|
||||
break;
|
||||
case CommandToCode.getImageUrl:
|
||||
this.generateUrl(msg.data);
|
||||
break;
|
||||
|
||||
@@ -19,4 +19,8 @@ export interface ViewData {
|
||||
parents?: string[];
|
||||
multiple?: string[];
|
||||
value?: string;
|
||||
|
||||
// File fields
|
||||
type: "file" | "media";
|
||||
fileExtensions?: string[];
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export interface DraftField {
|
||||
name: string;
|
||||
type: "boolean" | "choice";
|
||||
choices?: string[];
|
||||
invert?: boolean;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export interface MediaInfo {
|
||||
fsPath: string;
|
||||
vsPath: string | undefined;
|
||||
dimensions?: ISizeCalculationResult | undefined;
|
||||
title?: string | undefined;
|
||||
caption?: string | undefined;
|
||||
alt?: string | undefined;
|
||||
mimeType?: string | undefined;
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface ContentType {
|
||||
pageBundle?: boolean;
|
||||
}
|
||||
|
||||
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block";
|
||||
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file" | "dataFile";
|
||||
|
||||
export interface Field {
|
||||
title?: string;
|
||||
@@ -56,6 +56,7 @@ export interface Field {
|
||||
type: FieldType;
|
||||
choices?: string[] | Choice[];
|
||||
single?: boolean;
|
||||
wysiwyg?: boolean;
|
||||
multiple?: boolean;
|
||||
isPreviewImage?: boolean;
|
||||
hidden?: boolean;
|
||||
@@ -65,10 +66,16 @@ export interface Field {
|
||||
fieldGroup?: string | string[];
|
||||
dataType?: string | string[];
|
||||
taxonomyLimit?: number;
|
||||
fileExtensions?: string[];
|
||||
|
||||
// Date fields
|
||||
isPublishDate?: boolean;
|
||||
isModifiedDate?: boolean;
|
||||
|
||||
// Data file
|
||||
dataFileId?: string;
|
||||
dataFileKey?: string;
|
||||
dataFileValue?: string;
|
||||
}
|
||||
|
||||
export interface DateInfo {
|
||||
@@ -109,6 +116,7 @@ export interface CustomScript {
|
||||
output?: "notification" | "editor";
|
||||
outputType?: string;
|
||||
type?: ScriptType;
|
||||
command?: CommandType;
|
||||
}
|
||||
|
||||
export interface PreviewSettings {
|
||||
@@ -125,4 +133,12 @@ export enum ScriptType {
|
||||
Content = "content",
|
||||
MediaFolder = "mediaFolder",
|
||||
MediaFile = "mediaFile"
|
||||
}
|
||||
|
||||
export enum CommandType {
|
||||
Node = "node",
|
||||
Shell = "shell",
|
||||
PowerShell = "powershell",
|
||||
Python = "python",
|
||||
Python3 = "python3"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface Snippet {
|
||||
fields: SnippetField[];
|
||||
openingTags?: string;
|
||||
closingTags?: string;
|
||||
isMediaSnippet?: boolean;
|
||||
}
|
||||
|
||||
export type SnippetSpecialPlaceholders = "FM_SELECTED_TEXT" | string;
|
||||
|
||||
@@ -9,4 +9,5 @@ export enum Command {
|
||||
mediaSelectionData = "mediaSelectionData",
|
||||
sendMediaUrl = "sendMediaUrl",
|
||||
updatePlaceholder = "updatePlaceholder",
|
||||
dataFileEntries = "dataFileEntries",
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export enum CommandToCode {
|
||||
updateMetadata = "update-metadata",
|
||||
openDashboard = "open-dashboard",
|
||||
selectImage = "select-image",
|
||||
selectFile = "select-file",
|
||||
updateCustomTaxonomy = "updateCustomTaxonomy",
|
||||
addToCustomTaxonomy = "addToCustomTaxonomy",
|
||||
frameworkCommand = "framework-command",
|
||||
@@ -33,4 +34,8 @@ export enum CommandToCode {
|
||||
getImageUrl = "get-image-url",
|
||||
updatePlaceholder = "update-placeholder",
|
||||
getMode = "get-mode",
|
||||
generateContentType = "generate-content-type",
|
||||
addMissingFields = "add-missing-fields",
|
||||
setContentType = "set-content-type",
|
||||
getDataEntries = "get-data-entries",
|
||||
}
|
||||
@@ -70,7 +70,8 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React
|
||||
settings={settings}
|
||||
metadata={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus} />
|
||||
unsetFocus={unsetFocus}
|
||||
features={mode?.features || []} />
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.recentlyModified}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { CustomScript, FolderInfo, Mode, PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { GlobalSettings } from './GlobalSettings';
|
||||
import { OtherActions } from './OtherActions';
|
||||
@@ -10,6 +9,7 @@ import { SponsorMsg } from './SponsorMsg';
|
||||
import { StartServerButton } from './StartServerButton';
|
||||
import { FeatureFlag } from '../../components/features/FeatureFlag';
|
||||
import { FEATURE_FLAG } from '../../constants/Features';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
|
||||
export interface IBaseViewProps {
|
||||
settings: PanelSettings | undefined;
|
||||
@@ -20,23 +20,23 @@ export interface IBaseViewProps {
|
||||
const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles, mode}: React.PropsWithChildren<IBaseViewProps>) => {
|
||||
|
||||
const openDashboard = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openDashboard);
|
||||
Messenger.send(CommandToCode.openDashboard);
|
||||
};
|
||||
|
||||
const initProject = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.initProject);
|
||||
Messenger.send(CommandToCode.initProject);
|
||||
};
|
||||
|
||||
const createContent = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.createContent);
|
||||
Messenger.send(CommandToCode.createContent);
|
||||
};
|
||||
|
||||
const openPreview = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openPreview);
|
||||
Messenger.send(CommandToCode.openPreview);
|
||||
};
|
||||
|
||||
const runBulkScript = (script: CustomScript) => {
|
||||
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title: script.title, script });
|
||||
Messenger.send(CommandToCode.runCustomScript, { title: script.title, script });
|
||||
};
|
||||
|
||||
const customActions: any[] = (settings?.scripts || []).filter(s => s.bulk && (s.type === "content" || !s.type));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { Command } from '../Command';
|
||||
import { VsCollapsible } from './VscodeComponents';
|
||||
|
||||
@@ -16,7 +16,7 @@ const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children,
|
||||
const collapseKey = `collapse-${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const prevState = MessageHelper.getState();
|
||||
const prevState: any = Messenger.getState();
|
||||
if (!prevState || !prevState[collapseKey] || prevState[collapseKey] === null || prevState[collapseKey] === 'true') {
|
||||
setIsOpen(true);
|
||||
updateStorage(true);
|
||||
@@ -32,8 +32,8 @@ const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children,
|
||||
}, ['']);
|
||||
|
||||
const updateStorage = (value: boolean) => {
|
||||
const prevState = MessageHelper.getState();
|
||||
MessageHelper.setState({
|
||||
const prevState: any = Messenger.getState();
|
||||
Messenger.setState({
|
||||
...prevState,
|
||||
[collapseKey]: value.toString()
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { VSCodeButton, VSCodeDivider } from '@vscode/webview-ui-toolkit/react';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Field } from '../../../models';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { IMetadata } from '../Metadata';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
|
||||
export interface IContentTypeValidatorProps {
|
||||
fields: Field[];
|
||||
metadata: IMetadata
|
||||
}
|
||||
|
||||
const fieldsToIgnore = [`filePath`, `articleDetails`, `slug`];
|
||||
|
||||
export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidatorProps> = ({ fields, metadata}: React.PropsWithChildren<IContentTypeValidatorProps>) => {
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
const metadataFields = Object.keys(metadata).filter(key => !fieldsToIgnore.includes(key));
|
||||
|
||||
for (const mField of metadataFields) {
|
||||
if (!fields.find(field => field.name === mField)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [fields, metadata]);
|
||||
|
||||
|
||||
const generateContentType = () => {
|
||||
Messenger.send(CommandToCode.generateContentType);
|
||||
};
|
||||
|
||||
const addMissingFields = () => {
|
||||
Messenger.send(CommandToCode.addMissingFields);
|
||||
};
|
||||
|
||||
const setContentType = () => {
|
||||
Messenger.send(CommandToCode.setContentType);
|
||||
};
|
||||
|
||||
|
||||
if (isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='hint'>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label metadata_field__alert`}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fillRule="evenodd" clipRule="evenodd" d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"/></svg>
|
||||
|
||||
<span>Content type</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
<p className='inline_hint'>We noticed field differences between the content type and the front matter data.</p>
|
||||
|
||||
<p className='inline_hint'>Would you like to create, update, or set the content type for this content?</p>
|
||||
|
||||
<div className='hint__buttons'>
|
||||
<VSCodeButton appearance={`secondary`} onClick={generateContentType}>Create content type</VSCodeButton>
|
||||
|
||||
<VSCodeButton appearance={`secondary`} onClick={addMissingFields}>Add missing fields</VSCodeButton>
|
||||
|
||||
<VSCodeButton appearance={`secondary`} onClick={setContentType}>Set content type</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<VSCodeDivider />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
|
||||
export interface ICustomScriptProps {
|
||||
title: string;
|
||||
@@ -11,7 +11,7 @@ export interface ICustomScriptProps {
|
||||
const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title, script}: React.PropsWithChildren<ICustomScriptProps>) => {
|
||||
|
||||
const runCustomScript = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title, script });
|
||||
Messenger.send(CommandToCode.runCustomScript, { title, script });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
181
src/panelWebView/components/Fields/DataFileField.tsx
Normal file
181
src/panelWebView/components/Fields/DataFileField.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { ChevronDownIcon, DatabaseIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Command } from '../../Command';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import Downshift from 'downshift';
|
||||
import { ChoiceButton } from './ChoiceButton';
|
||||
|
||||
export interface IDataFileFieldProps {
|
||||
label: string;
|
||||
dataFileId?: string;
|
||||
dataFileKey?: string;
|
||||
dataFileValue?: string;
|
||||
selected: string | string[];
|
||||
multiSelect?: boolean;
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ label, dataFileId, dataFileKey, dataFileValue, selected, multiSelect, onChange }: React.PropsWithChildren<IDataFileFieldProps>) => {
|
||||
const [ dataEntries, setDataEntries ] = useState<string[] | null>(null);
|
||||
const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>();
|
||||
const dsRef = React.useRef<Downshift<string> | null>(null);
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
||||
const { command, data } = message.data;
|
||||
|
||||
if (command === Command.dataFileEntries) {
|
||||
setDataEntries(data || null);
|
||||
}
|
||||
};
|
||||
|
||||
const onValueChange = useCallback((txtValue: string) => {
|
||||
if (multiSelect) {
|
||||
const newValue = [...(crntSelected || []) as string[], txtValue];
|
||||
setCrntSelected(newValue);
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setCrntSelected(txtValue);
|
||||
onChange(txtValue);
|
||||
}
|
||||
}, [crntSelected, multiSelect, onChange]);
|
||||
|
||||
const removeSelected = useCallback((txtValue: string) => {
|
||||
if (multiSelect) {
|
||||
const newValue = [...(crntSelected || [])].filter(v => v !== txtValue);
|
||||
setCrntSelected(newValue);
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setCrntSelected("");
|
||||
onChange("");
|
||||
}
|
||||
}, [crntSelected, multiSelect, onChange]);
|
||||
|
||||
const allChoices = useMemo(() => {
|
||||
if (dataEntries && dataFileKey) {
|
||||
return dataEntries.map((r: any) => ({
|
||||
id: r[dataFileKey],
|
||||
title: r[dataFileValue || dataFileKey] || r[dataFileKey]
|
||||
})).filter(r => r.id);
|
||||
}
|
||||
return [];
|
||||
}, [crntSelected, dataEntries, dataFileKey, dataFileValue]);
|
||||
|
||||
const availableChoices = useMemo(() => {
|
||||
if (allChoices) {
|
||||
return allChoices.filter(choice => {
|
||||
if (choice) {
|
||||
if (typeof crntSelected === 'string') {
|
||||
return crntSelected !== choice.id;
|
||||
} else if (crntSelected instanceof Array) {
|
||||
return crntSelected.indexOf(choice.id) === -1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}, [allChoices]);
|
||||
|
||||
const getChoiceValue = useCallback((id: string) => {
|
||||
const choice = allChoices.find(r => r.id === id);
|
||||
if (choice) {
|
||||
return choice.title;
|
||||
}
|
||||
return "";
|
||||
}, [allChoices]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
if (multiSelect) {
|
||||
setCrntSelected(typeof selected === 'string' ? [selected] : selected);
|
||||
return;
|
||||
} else {
|
||||
setCrntSelected(selected instanceof Array ? selected[0] : selected);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCrntSelected(multiSelect ? [] : "");
|
||||
}, [selected, multiSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataFileId) {
|
||||
Messenger.send(CommandToCode.getDataEntries, dataFileId);
|
||||
}
|
||||
}, [dataFileId]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<DatabaseIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
<Downshift
|
||||
ref={dsRef}
|
||||
onSelect={(selected) => onValueChange(selected || "")}
|
||||
itemToString={item => (item ? item : '')}>
|
||||
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
|
||||
<div {...getRootProps(undefined, {suppressRefError: true})} className={`metadata_field__choice`}>
|
||||
<button
|
||||
{...getToggleButtonProps({
|
||||
className: `metadata_field__choice__toggle`,
|
||||
disabled: availableChoices.length === 0
|
||||
})}>
|
||||
<span>{`Select ${label}`}</span>
|
||||
<ChevronDownIcon className="icon" />
|
||||
</button>
|
||||
|
||||
<ul className={`metadata_field__choice_list ${isOpen ? "open" : "closed" }`} {...getMenuProps()}>
|
||||
{
|
||||
isOpen ? availableChoices.map((choice, index) => (
|
||||
<li {...getItemProps({
|
||||
key: choice.id,
|
||||
index,
|
||||
item: choice.id,
|
||||
})}>
|
||||
{ choice.title || <span className={`metadata_field__choice_list__item`}>Clear value</span> }
|
||||
</li>
|
||||
)) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
|
||||
{
|
||||
crntSelected instanceof Array ? crntSelected.map((value: string) => (
|
||||
<ChoiceButton
|
||||
key={value}
|
||||
value={value}
|
||||
title={getChoiceValue(value)}
|
||||
onClick={removeSelected} />
|
||||
)) : (
|
||||
crntSelected && (
|
||||
<ChoiceButton
|
||||
key={crntSelected}
|
||||
value={crntSelected}
|
||||
title={getChoiceValue(crntSelected)}
|
||||
onClick={removeSelected} />
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
102
src/panelWebView/components/Fields/FileField.tsx
Normal file
102
src/panelWebView/components/Fields/FileField.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DocumentIcon, PaperClipIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { basename } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { BlockFieldData } from '../../../models';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
|
||||
export interface IFileFieldProps {
|
||||
label: string;
|
||||
fieldName: string;
|
||||
filePath: string;
|
||||
multiple?: boolean;
|
||||
value: string | string[] | null;
|
||||
fileExtensions?: string[];
|
||||
parents?: string[];
|
||||
blockData?: BlockFieldData;
|
||||
onChange: (value: string | string[] | null) => void;
|
||||
}
|
||||
|
||||
const File = ({ value, onRemove }: { value: string, onRemove: (value: string) => void }) => {
|
||||
return (
|
||||
<div className='metadata_field__file__list__item' title={value}>
|
||||
<div className='metadata_field__file__item__icon'>
|
||||
<PaperClipIcon style={{ width: "16px", height: "16px" }} />
|
||||
</div>
|
||||
<span className='metadata_field__file__item__text' style={{ lineHeight: "16px" }}>{basename(value)}</span>
|
||||
|
||||
<button className='metadata_field__file__item__remove' type='button' onClick={() => onRemove(value)}>
|
||||
<TrashIcon style={{ width: "16px", height: "16px" }} />
|
||||
<span className='sr-only'>Delete file</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange }: React.PropsWithChildren<IFileFieldProps>) => {
|
||||
|
||||
const selectFile = useCallback(() => {
|
||||
Messenger.send(CommandToCode.selectFile, {
|
||||
filePath,
|
||||
fieldName,
|
||||
value,
|
||||
multiple,
|
||||
parents,
|
||||
blockData,
|
||||
fileExtensions,
|
||||
type: "file"
|
||||
});
|
||||
}, [filePath, fieldName, value, multiple, parents]);
|
||||
|
||||
const onRemove = useCallback((crntValue) => {
|
||||
const newValue = value && Array.isArray(value) ? value.filter(v => v !== crntValue) : null;
|
||||
onChange(newValue);
|
||||
} , [value]);
|
||||
|
||||
const isEmpty = useMemo(() => {
|
||||
return !value || (Array.isArray(value) && value.length === 0);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<DocumentIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
<div className={`metadata_field__file`}>
|
||||
{
|
||||
(isEmpty || multiple) && (
|
||||
<button className={`metadata_field__file__button ${isEmpty ? "" : "not_empty"}`} title={`Add your ${label?.toLowerCase() || "file"}`} type="button" onClick={selectFile}>
|
||||
<DocumentIcon />
|
||||
<span>Add your {label?.toLowerCase() || "file"}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
value && !Array.isArray(value) && (
|
||||
<div className='metadata_field__file__list'>
|
||||
<File value={value} onRemove={onRemove} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
multiple && value && Array.isArray(value) && (
|
||||
<div className='metadata_field__file__list multiple'>
|
||||
{
|
||||
value.map((v, idx) => (
|
||||
<File key={idx} value={v} onRemove={onRemove} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MessageHelper } from '../../../helpers/MessageHelper';
|
||||
import { Command } from '../../Command';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { ImageFallback } from './ImageFallback';
|
||||
@@ -29,7 +29,7 @@ export const PreviewImage: React.FunctionComponent<IPreviewImageProps> = ({ valu
|
||||
if (value?.webviewUrl) {
|
||||
setImgUrl(value.webviewUrl);
|
||||
} else {
|
||||
MessageHelper.sendMessage(CommandToCode.getImageUrl, value)
|
||||
Messenger.send(CommandToCode.getImageUrl, value)
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import {PhotographIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { MessageHelper } from '../../../helpers/MessageHelper';
|
||||
import { BlockFieldData } from '../../../models';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
@@ -35,14 +35,15 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
|
||||
}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
|
||||
|
||||
const selectImage = useCallback(() => {
|
||||
MessageHelper.sendMessage(CommandToCode.selectImage, {
|
||||
Messenger.send(CommandToCode.selectImage, {
|
||||
filePath: filePath,
|
||||
fieldName,
|
||||
value,
|
||||
multiple,
|
||||
metadataInsert: true,
|
||||
parents,
|
||||
blockData
|
||||
blockData,
|
||||
type: "media"
|
||||
});
|
||||
}, [filePath, fieldName, value, multiple, parents]);
|
||||
|
||||
@@ -64,7 +65,7 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
|
||||
(!value || multiple) && (
|
||||
<button className={`metadata_field__preview_image__button`} title={`Add your ${label?.toLowerCase() || "image"}`} type="button" onClick={selectImage}>
|
||||
<PhotographIcon />
|
||||
<span className="mt-2 block text-sm font-medium text-gray-900">Add your {label?.toLowerCase() || "image"}</span>
|
||||
<span>Add your {label?.toLowerCase() || "image"}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ export interface ITextFieldProps {
|
||||
label: string;
|
||||
value: string | null;
|
||||
singleLine: boolean | undefined;
|
||||
wysiwyg: boolean | undefined;
|
||||
limit: number | undefined;
|
||||
rows?: number;
|
||||
onChange: (txtValue: string) => void;
|
||||
}
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
const WysiwygField = React.lazy(() => import('./WysiwygField'));
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, wysiwyg, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
const [ text, setText ] = React.useState<string | null>(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -37,25 +40,31 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
<PencilIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
|
||||
{
|
||||
singleLine ? (
|
||||
<input
|
||||
className={`metadata_field__input`}
|
||||
value={text || ""}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
|
||||
}} />
|
||||
wysiwyg ? (
|
||||
<React.Suspense fallback={(<div>Loading field</div>)}>
|
||||
<WysiwygField text={text || ""} onChange={onTextChange} />
|
||||
</React.Suspense>
|
||||
) : (
|
||||
<textarea
|
||||
className={`metadata_field__textarea`}
|
||||
rows={rows || 2}
|
||||
value={text || ""}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
|
||||
}} />
|
||||
singleLine ? (
|
||||
<input
|
||||
className={`metadata_field__input`}
|
||||
value={text || ""}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
|
||||
}} />
|
||||
) : (
|
||||
<textarea
|
||||
className={`metadata_field__textarea`}
|
||||
rows={rows || 2}
|
||||
value={text || ""}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
|
||||
}} />
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user