Compare commits

...

46 Commits

Author SHA1 Message Date
Elio Struyf
e695bad1c6 Merge pull request #347 from estruyf/dev 2022-06-01 12:01:18 +02:00
Elio Struyf
fe31081907 7.3.2 2022-06-01 12:00:45 +02:00
Elio Struyf
248ccb3718 Update changelog 2022-06-01 12:00:36 +02:00
Elio Struyf
2260174ec2 Merge branch 'main' into dev 2022-06-01 11:59:30 +02:00
Elio Struyf
cd3a867422 #346 - Fix media refresh 2022-06-01 11:59:02 +02:00
Elio Struyf
05a63dd110 Create PULL_REQUEST_TEMPLATE.md 2022-05-30 13:32:48 +02:00
Elio Struyf
cfc0c3d5a1 Create CONTRIBUTING.md 2022-05-30 13:29:48 +02:00
Elio Struyf
d6dbca25ce Create CODE_OF_CONDUCT.md 2022-05-30 13:16:10 +02:00
Elio Struyf
d21ad14e89 Merge pull request #344 from estruyf/dev 2022-05-26 18:58:30 +02:00
Elio Struyf
8d00726322 Update changelog 2022-05-26 18:57:22 +02:00
Elio Struyf
af11c304d3 7.3.1 2022-05-26 18:53:35 +02:00
Elio Struyf
223276f6af #343 - Fix in schema for frontMatter.taxonomy.fieldGroups setting 2022-05-26 18:53:27 +02:00
Elio Struyf
a6fdfe0dfa Merge pull request #342 from estruyf/dev 2022-05-25 13:13:50 +02:00
Elio Struyf
6154164b1c Updated changelog 2022-05-25 13:09:16 +02:00
Elio Struyf
1cdb6c56a5 #336 - Fix for updating status field 2022-05-25 13:08:39 +02:00
Elio Struyf
c63310a2db Initialize template folder 2022-05-25 12:51:39 +02:00
Elio Struyf
9f681a7459 Remove template creation 2022-05-25 11:46:18 +02:00
Elio Struyf
2610032a38 Update for the dataFile field 2022-05-25 10:12:05 +02:00
Elio Struyf
d11e8112e0 Fix for data view 2022-05-25 10:09:21 +02:00
Elio Struyf
df5e346cf1 Changelog update 2022-05-24 12:03:30 +02:00
Elio Struyf
9892d14a62 default labels for issues 2022-05-20 16:55:51 +02:00
Elio Struyf
8c61f79885 #340 - Show notification for not existing content folders 2022-05-20 16:46:39 +02:00
Elio Struyf
ffa6638d3d #339 - Fix for content folders without a title 2022-05-20 16:25:08 +02:00
Elio Struyf
f9ef12bd3a #338 - Disable templates setting 2022-05-20 16:22:46 +02:00
Elio Struyf
b79216f2b5 Added project label flow 2022-05-18 16:52:43 +02:00
Elio Struyf
2597a63718 Updated workflow 2022-05-18 14:13:07 +02:00
Elio Struyf
126a21a6b5 Added github action info 2022-05-18 13:58:50 +02:00
Elio Struyf
3abfbd5302 Message handler updates 2022-05-18 13:20:06 +02:00
Elio Struyf
efdbce2d08 #332 - Adding the new fileData field 2022-05-18 13:19:55 +02:00
Elio Struyf
09eea16d60 Remove logging 2022-05-18 10:29:23 +02:00
Elio Struyf
71697a09b6 Update dependency version 2022-05-17 16:23:23 +02:00
Elio Struyf
3583a2b962 #337 - Add support for other fm types 2022-05-17 16:19:40 +02:00
Elio Struyf
2825d5ddd8 #336 - support for draft field invert 2022-05-13 09:01:30 +02:00
Elio Struyf
2e66174c4a Setting focus for questions 2022-05-13 08:26:30 +02:00
Elio Struyf
e78069ad17 #333 - collection and published field support 2022-05-13 08:18:42 +02:00
Elio Struyf
4c97993c5f #333 - better support for Jekyll 2022-05-12 20:53:47 +02:00
Elio Struyf
a452173d9a Added icons to snippets 2022-05-12 17:59:17 +02:00
Elio Struyf
60a38be923 #335 - Merge media snippets to content snippets 2022-05-12 15:51:50 +02:00
Elio Struyf
6c3d286282 #331 - Added functionality to run other type of scripts 2022-05-10 16:26:59 +02:00
Elio Struyf
32c7bbd3f9 #334 - Fix for locked content folders retrieval 2022-05-09 09:00:36 +02:00
Elio Struyf
426dbc2e46 Update beta script 2022-05-08 20:02:35 +02:00
Elio Struyf
9882dea960 Update beta release script 2022-05-08 19:52:31 +02:00
Elio Struyf
eb9a05e90c update beta release script 2022-05-08 18:18:14 +02:00
Elio Struyf
4e59e736ed Parse windows path 2022-05-08 18:09:19 +02:00
Elio Struyf
9f91ebf289 7.3.0 2022-05-05 17:41:32 +02:00
Elio Struyf
b80de402bd #330 - Update front matter from script 2022-05-05 17:41:25 +02:00
83 changed files with 1663 additions and 8909 deletions

View File

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

View File

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

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

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

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

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

View File

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

View File

@@ -1,5 +1,39 @@
# 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

128
CODE_OF_CONDUCT.md Normal file
View File

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

59
CONTRIBUTING.md Normal file
View File

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

View File

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

8610
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"displayName": "Front Matter",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "7.2.0",
"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": [
@@ -753,7 +805,8 @@
"draft",
"fields",
"json",
"block"
"block",
"dataFile"
],
"description": "Define the type of field"
},
@@ -875,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,
@@ -883,6 +951,21 @@
"name"
],
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "dataFile"
}
}
},
"then": {
"required": [
"dataFileId",
"dataFileKey"
]
}
},
{
"if": {
"properties": {
@@ -1091,7 +1174,7 @@
},
"additionalProperties": false,
"required": [
"name",
"id",
"fields"
]
}
@@ -1198,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"
}
}
},
@@ -1276,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"
},
{
@@ -1799,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",
@@ -1835,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",

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ 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';
@@ -98,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, '\\');
@@ -111,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,
@@ -287,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[];
}
/**
@@ -358,11 +383,15 @@ export class Folders {
// 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(wsFolder?.fsPath || "", "**", `*${fileType.startsWith('.') ? '' : '.'}${fileType}`)}`);
const patterns = supportedFiles.map(fileType => `${join(parseWinPath(wsFolder?.fsPath || ""), "**", `*${fileType.startsWith('.') ? '' : '.'}${fileType}`)}`);
let folders: string[] = [];
for (const pattern of patterns) {
folders = [...folders, ...(await this.findFolders(pattern))];
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

View File

@@ -24,35 +24,25 @@ 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);
// Check if you can find the framework
const wsFolder = Folders.getWorkspaceFolder();
const framework = FrameworkDetector.get(wsFolder?.fsPath || "");
@@ -68,6 +58,33 @@ categories: []
}
}
/**
* Creates the templates folder + sample if needed
* @param sampleTemplate
* @returns
*/
public static async createSampleTemplate(sampleTemplate?: boolean) {
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
const folder = Template.getSettings();
const templatePath = Project.templatePath();
if (!folder || !templatePath) {
return;
}
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
if (!fs.existsSync(templatePath.fsPath)) {
await workspace.fs.createDirectory(templatePath);
}
if (sampleTemplate) {
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
Notifications.info("Sample template created.");
}
}
/**
* Get the template path for the current project
*/

View File

@@ -17,7 +17,8 @@ export class Settings {
public static async create(type: TaxonomyType) {
const newOption = await vscode.window.showInputBox({
prompt: `Insert the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} that you want to add to your configuration.`,
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
ignoreFocusOut: true
});
if (newOption) {
@@ -36,7 +37,11 @@ export class Settings {
await SettingsHelper.updateTaxonomy(type, options);
// Ask if the new term needs to be added to the page
const addToPage = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?` });
const addToPage = await vscode.window.showQuickPick(["yes", "no"], {
canPickMany: false,
placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?`,
ignoreFocusOut: true
});
if (addToPage && addToPage === "yes") {
const editor = vscode.window.activeTextEditor;
@@ -149,7 +154,8 @@ export class Settings {
"Category"
], {
placeHolder: `What do you want to remap?`,
canPickMany: false
canPickMany: false,
ignoreFocusOut: true
});
if (!taxType) {
return;
@@ -165,7 +171,8 @@ export class Settings {
const selectedOption = await vscode.window.showQuickPick(options, {
placeHolder: `Select your ${type === TaxonomyType.Tag ? "tags" : "categories"} to insert`,
canPickMany: false
canPickMany: false,
ignoreFocusOut: true
});
if (!selectedOption) {
@@ -174,11 +181,16 @@ export class Settings {
const newOptionValue = await vscode.window.showInputBox({
prompt: `Specify the value of the ${type === TaxonomyType.Tag ? "tag" : "category"} with which you want to remap "${selectedOption}". Leave the input <blank> if you want to remove the ${type === TaxonomyType.Tag ? "tag" : "category"} from all articles.`,
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`
placeHolder: `Name of the ${type === TaxonomyType.Tag ? "tag" : "category"}`,
ignoreFocusOut: true
});
if (!newOptionValue) {
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?` });
const deleteAnswer = await vscode.window.showQuickPick(["yes", "no"], {
canPickMany: false,
placeHolder: `Delete ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"}?`,
ignoreFocusOut: true
});
if (deleteAnswer === "no") {
return;
}
@@ -226,7 +238,7 @@ export class Settings {
data[matterProp] = [...new Set(taxonomies)].sort();
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
// Update the file
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, {
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any), { encoding: "utf8" });
}

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ export const COMMAND_NAME = {
createByContentType: getCommandName("createByContentType"),
createByTemplate: getCommandName("createByTemplate"),
createTemplate: getCommandName("createTemplate"),
initTemplate: getCommandName("initTemplate"),
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
@@ -37,6 +38,8 @@ export const COMMAND_NAME = {
diagnostics: getCommandName("diagnostics"),
modeSwitch: getCommandName("mode.switch"),
showOutputChannel: getCommandName("showOutputChannel"),
// Insert dashboards
insertMedia: getCommandName("insertMedia"),
insertSnippet: getCommandName("insertSnippet"),

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { EyeIcon, TrashIcon } from '@heroicons/react/outline';
import { EyeIcon, TerminalIcon, TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { CustomScript, ScriptType } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
@@ -43,7 +43,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
<MenuItem
key={script.title}
title={script.title}
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
onClick={(value, e) => runCustomScript(e, script)} />
))
}, [scripts]);
@@ -70,15 +70,15 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
<ActionMenuButton title={`Menu`} />
<MenuItems widthClass='w-40' marginTopClass='mt-6'>
<MenuItems widthClass='w-44' marginTopClass='mt-6'>
<MenuItem
title={`View`}
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
onClick={(value, e) => onView(e)} />
{ customScriptActions }
<MenuItem
title={`Delete`}
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
onClick={(value, e) => onDelete(e)} />
</MenuItems>
</Menu>

View File

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

View File

@@ -149,9 +149,9 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
{
(dataFiles && dataFiles.length > 0) && (
dataFiles.map((dataFile) => (
dataFiles.map((dataFile, idx) => (
<button
key={dataFile.id}
key={`${dataFile.id}-${idx}`}
type='button'
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
onClick={() => setSchema(dataFile)}>

View File

@@ -1,13 +1,14 @@
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 { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
@@ -15,6 +16,7 @@ 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 {
@@ -25,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);
@@ -33,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();
@@ -104,25 +116,39 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
};
const insertSnippet = useCallback(() => {
const relPath = getRelPath();
let snippet = settings?.mediaSnippet.join("\n");
if (mediaSnippets.length === 1) {
processSnippet(mediaSnippets[0]);
} else {
// Show dialog to select
setShowSnippetSelection(true);
}
}, [mediaSnippets]);
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
snippet = snippet?.replace("{alt}", alt || "");
snippet = snippet?.replace("{caption}", caption || "");
snippet = snippet?.replace("{title}", media.title || "");
snippet = snippet?.replace("{filename}", basename(relPath || ""));
snippet = snippet?.replace("{mediaWidth}", media?.dimensions?.width?.toString() || "");
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
const processSnippet = useCallback((snippet: Snippet) => {
setShowSnippetSelection(false);
const relPath = getRelPath();
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() || "",
};
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]);
}, [alt, caption, media, settings, viewData, mediaSnippets]);
const deleteMedia = () => {
setShowAlert(true);
@@ -198,7 +224,7 @@ 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)} />
))
}
@@ -323,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}>
@@ -354,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}
/>
@@ -362,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() }
@@ -387,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>
@@ -436,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

View File

@@ -18,7 +18,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-10 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-20 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<div className="py-1">
{children}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import { Sorting } from '../../helpers/Sorting';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../DashboardMessage';
import { EventData } from '@estruyf/vscode/dist/models';
import { parseWinPath } from '../../helpers/parseWinPath';
export default function usePages(pages: Page[]) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
@@ -24,10 +25,27 @@ export default function usePages(pages: Page[]) {
const processPages = useCallback((searchedPages: Page[]) => {
const draftField = settings?.draftField;
const framework = settings?.crntFramework;
// Filter the pages
let pagesToShow: Page[] = Object.assign([], searchedPages);
// Framework specific actions
if (framework?.toLowerCase() === "jekyll") {
pagesToShow = pagesToShow.map(page => {
// https://jekyllrb.com/docs/posts/#drafts
const filePath = parseWinPath(page.fmFilePath);
page.draft = filePath.indexOf(`/_drafts/`) > -1;
// Published field: https://jekyllrb.com/docs/front-matter/#predefined-global-variables
if (typeof page.published !== "undefined") {
page.draft = !page.published;
}
return page;
});
}
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesToShow.length;
@@ -46,15 +64,19 @@ export default function usePages(pages: Page[]) {
pagesToShow = searchedPages;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesToShow.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesToShow.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = pagesToShow.filter(page => !!page[draftFieldName]).length;
draftTypes[Tab.Published] = pagesToShow.filter(page => !page[draftFieldName]).length;
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
pagesToShow = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
pagesToShow = draftField?.invert ? published : drafts;
} else {
pagesToShow = searchedPages;
}

View File

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

View File

@@ -16,7 +16,6 @@ export interface Settings {
openOnStart: boolean | null;
versionInfo: VersionInfo;
pageViewType: DashboardViewType | undefined;
mediaSnippet: string[];
contentTypes: ContentType[];
contentFolders: ContentFolder[];
crntFramework: string;

View File

@@ -136,6 +136,10 @@ export async function activate(context: vscode.ExtensionContext) {
});
let createTemplate = vscode.commands.registerCommand(COMMAND_NAME.createTemplate, Template.generate);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.initTemplate, () => Project.createSampleTemplate(true))
);
const toggleDraftCommand = COMMAND_NAME.toggleDraft;
const toggleDraft = vscode.commands.registerCommand(toggleDraftCommand, async () => {
@@ -211,6 +215,7 @@ export async function activate(context: vscode.ExtensionContext) {
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);
}
}));

View File

@@ -1,3 +1,4 @@
import { Uri, workspace } from 'vscode';
import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
import * as vscode from 'vscode';
@@ -82,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
@@ -97,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) {
@@ -132,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);
@@ -147,7 +166,7 @@ export class ArticleHelper {
}
}
return FrontMatterParser.toFile(content, data, ({
return FrontMatterParser.toFile(content, data, originalContent, ({
noArrayIndent: !indentArray,
skipInvalid: true,
noCompatMode: true,

View File

@@ -1,7 +1,7 @@
import { ModeListener } from './../listeners/general/ModeListener';
import { PagesListener } from './../listeners/dashboard';
import { ArticleHelper, Settings } from ".";
import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_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, window } from 'vscode';
import { Folders } from "../commands/Folders";
@@ -367,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));

View File

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

View File

@@ -1,10 +1,11 @@
import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Project } from "../commands/Project";
import { Template } from "../commands/Template";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
import { DataFolder } from "../models/DataFolder";
import { DataType } from "../models/DataType";
@@ -18,9 +19,7 @@ export class DashboardSettings {
public static async get() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = await Template.isInitialized();
const contentFolders = await Folders.getContentFolders();
const isInitialized = Project.isInitialized();
return {
beta: ext.isBetaVersion(),
@@ -32,7 +31,6 @@ export class DashboardSettings {
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),
@@ -56,7 +54,7 @@ export class DashboardSettings {
mimeTypes: Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES)
},
welcome: {
contentFolders
contentFolders: await Folders.getContentFolders()
}
},
dataFiles: await this.getDataFiles(),

View File

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

View File

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

View File

@@ -1,6 +1,12 @@
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 { Framework } from "../models";
import { Logger } from "./Logger";
export class FrameworkDetector {
@@ -73,4 +79,50 @@ export class FrameworkDetector {
return undefined;
}
public static checkDefaultSettings(framework: Framework) {
if (framework.name.toLowerCase() === "jekyll") {
FrameworkDetector.jekyll();
}
}
private static jekyll() {
try {
const wsFolder = Folders.getWorkspaceFolder();
const jekyllConfig = join(wsFolder?.fsPath || "", '_config.yml');
let collectionDir = "";
if (existsSync(jekyllConfig)) {
const content = readFileSync(jekyllConfig, "utf8");
// Convert YAML to JSON
const config = jsyaml.safeLoad(content);
if (config.collections_dir) {
collectionDir = config.collections_dir;
}
}
const draftsPath = join(wsFolder?.fsPath || "", collectionDir, "_drafts");
const postsPath = join(wsFolder?.fsPath || "", collectionDir, "_posts");
if (existsSync(draftsPath)) {
const folderUri = Uri.file(draftsPath);
commands.executeCommand(COMMAND_NAME.registerFolder, {
title: "drafts",
path: folderUri
});
}
if (existsSync(postsPath)) {
const folderUri = Uri.file(postsPath);
commands.executeCommand(COMMAND_NAME.registerFolder, {
title: "posts",
path: folderUri
});
}
} catch (e) {
Logger.error(`Something failed while processing your Jekyll configuration. ${(e as Error).message}`);
}
}
}

View File

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

View File

@@ -65,7 +65,7 @@ export class MediaHelpers {
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
}
if (relSelectedFolderPath.startsWith('/')) {
if (relSelectedFolderPath && relSelectedFolderPath.startsWith('/')) {
relSelectedFolderPath = relSelectedFolderPath.substring(1);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,6 +67,8 @@ 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);
}

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ export class ModeListener extends BaseListener {
const activeMode = ModeSwitch.getMode();
if (activeMode) {
const mode = modes.find(m => m.id === activeMode);
this.sendMsg(GeneralCommands.setMode as any, mode);
this.sendMsg(GeneralCommands.toWebview.setMode as any, mode);
// Check the commands that need to be enabled/disabled
const snippetsView = mode?.features.find(f => f === FEATURE_FLAG.dashboard.snippets.view);
@@ -44,7 +44,7 @@ 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();

View File

@@ -1,3 +1,4 @@
import { DataFileHelper } from './../../helpers/DataFileHelper';
import { BlockFieldData } from './../../models/BlockFieldData';
import { ImageHelper } from './../../helpers/ImageHelper';
import { Folders } from "../../commands/Folders";
@@ -45,10 +46,16 @@ export class DataListener extends BaseListener {
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;
}
}
@@ -101,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);
}
}
@@ -278,6 +285,17 @@ export class DataListener extends BaseListener {
}
}
/**
* Retrieve the data entries from local data files
* @param data
*/
private static async getDataFileEntries(data: any) {
const entries = await DataFileHelper.getById(data);
if (entries) {
this.sendMsg(Command.dataFileEntries, entries);
}
}
/**
* Open a terminal and run the passed command
* @param command

View File

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

View File

@@ -48,7 +48,7 @@ export interface ContentType {
pageBundle?: boolean;
}
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file";
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file" | "dataFile";
export interface Field {
title?: string;
@@ -71,6 +71,11 @@ export interface Field {
// Date fields
isPublishDate?: boolean;
isModifiedDate?: boolean;
// Data file
dataFileId?: string;
dataFileKey?: string;
dataFileValue?: string;
}
export interface DateInfo {
@@ -111,6 +116,7 @@ export interface CustomScript {
output?: "notification" | "editor";
outputType?: string;
type?: ScriptType;
command?: CommandType;
}
export interface PreviewSettings {
@@ -127,4 +133,12 @@ export enum ScriptType {
Content = "content",
MediaFolder = "mediaFolder",
MediaFile = "mediaFile"
}
export enum CommandType {
Node = "node",
Shell = "shell",
PowerShell = "powershell",
Python = "python",
Python3 = "python3"
}

View File

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

View File

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

View File

@@ -37,4 +37,5 @@ export enum CommandToCode {
generateContentType = "generate-content-type",
addMissingFields = "add-missing-fields",
setContentType = "set-content-type",
getDataEntries = "get-data-entries",
}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
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 { MessageHelper } from '../../../helpers/MessageHelper';
import { Field } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { IMetadata } from '../Metadata';
@@ -30,15 +30,15 @@ export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidator
const generateContentType = () => {
MessageHelper.sendMessage(CommandToCode.generateContentType);
Messenger.send(CommandToCode.generateContentType);
};
const addMissingFields = () => {
MessageHelper.sendMessage(CommandToCode.addMissingFields);
Messenger.send(CommandToCode.addMissingFields);
};
const setContentType = () => {
MessageHelper.sendMessage(CommandToCode.setContentType);
Messenger.send(CommandToCode.setContentType);
};

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
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 { MessageHelper } from '../../../helpers/MessageHelper';
import { BlockFieldData } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
@@ -38,7 +38,7 @@ const File = ({ value, onRemove }: { value: string, onRemove: (value: string) =>
export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange }: React.PropsWithChildren<IFileFieldProps>) => {
const selectFile = useCallback(() => {
MessageHelper.sendMessage(CommandToCode.selectFile, {
Messenger.send(CommandToCode.selectFile, {
filePath,
fieldName,
value,

View File

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

View File

@@ -1,7 +1,7 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import {PhotographIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback } from 'react';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { BlockFieldData } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
@@ -35,7 +35,7 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
const selectImage = useCallback(() => {
MessageHelper.sendMessage(CommandToCode.selectImage, {
Messenger.send(CommandToCode.selectImage, {
filePath: filePath,
fieldName,
value,

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import * as React from 'react';
import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { useDebounce } from '../../hooks/useDebounce';
import { Collapsible } from './Collapsible';
import { VsLabel } from './VscodeComponents';
import useStartCommand from '../hooks/useStartCommand';
import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IGlobalSettingsProps {
settings: PanelSettings | undefined;
@@ -24,11 +24,11 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings
const debouncePreviewUrl = useDebounce(previewUrl, 1000);
const onDateCheck = () => {
MessageHelper.sendMessage(CommandToCode.updateModifiedUpdating, !modifiedDateUpdate);
Messenger.send(CommandToCode.updateModifiedUpdating, !modifiedDateUpdate);
};
const onHighlightCheck = () => {
MessageHelper.sendMessage(CommandToCode.updateFmHighlight, !fmHighlighting);
Messenger.send(CommandToCode.updateFmHighlight, !fmHighlighting);
};
const previewChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -54,14 +54,14 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings
React.useEffect(() => {
if (isDirty) {
setIsDirty(false);
MessageHelper.sendMessage(CommandToCode.updatePreviewUrl, debouncePreviewUrl);
Messenger.send(CommandToCode.updatePreviewUrl, debouncePreviewUrl);
}
}, [debouncePreviewUrl]);
React.useEffect(() => {
if (isDirty) {
setIsDirty(false);
MessageHelper.sendMessage(CommandToCode.updateStartCommand, debounceStartCommand);
Messenger.send(CommandToCode.updateStartCommand, debounceStartCommand);
}
}, [debounceStartCommand]);

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { BlockFieldData, Field, PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { TagType } from '../TagType';
import { Collapsible } from './Collapsible';
import "react-datepicker/dist/react-datepicker.css";
@@ -10,6 +9,7 @@ import { WrapperField } from './Fields/WrapperField';
import { ContentTypeValidator } from './ContentType/ContentTypeValidator';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IMetadata {
[prop: string]: string[] | string | null | IMetadata;
@@ -30,7 +30,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, features,
return;
}
MessageHelper.sendMessage(CommandToCode.updateMetadata, {
Messenger.send(CommandToCode.updateMetadata, {
field,
parents,
value

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Collapsible } from './Collapsible';
import { BugIcon } from './Icons/BugIcon';
import { CenterIcon } from './Icons/CenterIcon';
@@ -12,6 +11,7 @@ import { TemplateIcon } from './Icons/TemplateIcon';
import { WritingIcon } from './Icons/WritingIcon';
import { OtherActionButton } from './OtherActionButton';
import { ISSUE_LINK } from '../../constants/Links';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IOtherActionsProps {
isFile: boolean;
@@ -22,23 +22,23 @@ export interface IOtherActionsProps {
const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFile, settings, isBase}: React.PropsWithChildren<IOtherActionsProps>) => {
const openSettings = () => {
MessageHelper.sendMessage(CommandToCode.openSettings);
Messenger.send(CommandToCode.openSettings);
};
const openFile = () => {
MessageHelper.sendMessage(CommandToCode.openFile);
Messenger.send(CommandToCode.openFile);
};
const openProject = () => {
MessageHelper.sendMessage(CommandToCode.openProject);
Messenger.send(CommandToCode.openProject);
};
const createAsTemplate = () => {
MessageHelper.sendMessage(CommandToCode.createTemplate);
Messenger.send(CommandToCode.createTemplate);
};
const toggleWritingSettings = () => {
MessageHelper.sendMessage(CommandToCode.toggleWritingSettings);
Messenger.send(CommandToCode.toggleWritingSettings);
};
return (
@@ -46,7 +46,7 @@ const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFile, sett
<Collapsible id={`${isBase ? "base_" : ""}other_actions`} title="Other actions" className={`other_actions`}>
<OtherActionButton className={settings?.writingSettingsEnabled ? "active" : ""} onClick={toggleWritingSettings} disabled={typeof settings?.writingSettingsEnabled === "undefined"}><WritingIcon /> <span>{settings?.writingSettingsEnabled ? "Writing settings enabled" : "Enable writing settings"}</span></OtherActionButton>
<OtherActionButton onClick={() => MessageHelper.sendMessage(CommandToCode.toggleCenterMode)}>
<OtherActionButton onClick={() => Messenger.send(CommandToCode.toggleCenterMode)}>
<CenterIcon /> <span>Toggle center mode</span>
</OtherActionButton>

View File

@@ -1,5 +1,5 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { ActionButton } from './ActionButton';
@@ -10,7 +10,7 @@ export interface IPreviewProps {
const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.PropsWithChildren<IPreviewProps>) => {
const open = () => {
MessageHelper.sendMessage(CommandToCode.openPreview);
Messenger.send(CommandToCode.openPreview);
};
if (!slug) {

View File

@@ -1,7 +1,7 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { ActionButton } from './ActionButton';
@@ -13,7 +13,7 @@ const PublishAction: React.FunctionComponent<IPublishActionProps> = (props: Reac
const { draft } = props;
const publish = () => {
MessageHelper.sendMessage(CommandToCode.publish);
Messenger.send(CommandToCode.publish);
};
return (

View File

@@ -1,5 +1,5 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { ActionButton } from './ActionButton';
@@ -8,7 +8,7 @@ export interface ISlugActionProps {}
const SlugAction: React.FunctionComponent<ISlugActionProps> = ({}: React.PropsWithChildren<ISlugActionProps>) => {
const optimize = () => {
MessageHelper.sendMessage(CommandToCode.updateSlug);
Messenger.send(CommandToCode.updateSlug);
};
return (

View File

@@ -1,6 +1,5 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { FrameworkDetectors } from '../../constants/FrameworkDetectors';
import { MessageHelper } from '../../helpers/MessageHelper';
import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import useStartCommand from '../hooks/useStartCommand';
@@ -13,7 +12,7 @@ export const StartServerButton: React.FunctionComponent<IStartServerButtonProps>
const { startCommand } = useStartCommand(settings);
const startLocalServer = (command: string) => {
MessageHelper.sendMessage(CommandToCode.frameworkCommand, { command });
Messenger.send(CommandToCode.frameworkCommand, { command });
};
return (

View File

@@ -6,9 +6,9 @@ import { TagType } from '../TagType';
import Downshift from 'downshift';
import { AddIcon } from './Icons/AddIcon';
import { VsLabel } from './VscodeComponents';
import { MessageHelper } from '../../helpers/MessageHelper';
import { BlockFieldData, CustomTaxonomyData } from '../../models';
import { useCallback, useMemo } from 'react';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface ITagPickerProps {
type: TagType;
@@ -52,11 +52,11 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
*/
const onCreate = (tag: string) => {
if (type === TagType.tags) {
MessageHelper.sendMessage(CommandToCode.addTagToSettings, tag);
Messenger.send(CommandToCode.addTagToSettings, tag);
} else if (type === TagType.categories) {
MessageHelper.sendMessage(CommandToCode.addCategoryToSettings, tag);
Messenger.send(CommandToCode.addCategoryToSettings, tag);
} else if (type === TagType.custom) {
MessageHelper.sendMessage(CommandToCode.addToCustomTaxonomy, {
Messenger.send(CommandToCode.addToCustomTaxonomy, {
id: taxonomyId,
name: fieldName,
option: tag
@@ -70,26 +70,26 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
*/
const sendUpdate = (values: string[]) => {
if (type === TagType.tags) {
MessageHelper.sendMessage(CommandToCode.updateTags, {
Messenger.send(CommandToCode.updateTags, {
fieldName,
values,
parents,
blockData
});
} else if (type === TagType.categories) {
MessageHelper.sendMessage(CommandToCode.updateCategories, {
Messenger.send(CommandToCode.updateCategories, {
fieldName,
values,
parents,
blockData
});
} else if (type === TagType.keywords) {
MessageHelper.sendMessage(CommandToCode.updateKeywords, {
Messenger.send(CommandToCode.updateKeywords, {
values,
parents
});
} else if (type === TagType.custom) {
MessageHelper.sendMessage(CommandToCode.updateCustomTaxonomy, {
Messenger.send(CommandToCode.updateCustomTaxonomy, {
id: taxonomyId,
name: fieldName,
options: values,

View File

@@ -1,14 +1,12 @@
import { useState, useEffect } from 'react';
import { GeneralCommands } from '../../constants';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Mode } from '../../models/Mode';
import { DashboardData } from '../../models/DashboardData';
import { FolderInfo, PanelSettings } from '../../models/PanelSettings';
import { Command } from '../Command';
import { CommandToCode } from '../CommandToCode';
import { TagType } from '../TagType';
const vscode = MessageHelper.getVsCodeAPI();
import { Messenger } from '@estruyf/vscode/dist/client';
export default function useMessages() {
const [metadata, setMetadata] = useState<any>({});
@@ -46,7 +44,7 @@ export default function useMessages() {
case Command.mediaSelectionData:
setMediaSelecting(message.data);
break;
case GeneralCommands.setMode:
case GeneralCommands.toWebview.setMode:
setMode(message.data);
break;
}
@@ -68,8 +66,8 @@ export default function useMessages() {
setLoading(false);
}, 5000);
vscode.postMessage({ command: CommandToCode.getData });
vscode.postMessage({ command: CommandToCode.getMode });
Messenger.send(CommandToCode.getData);
Messenger.send(CommandToCode.getMode);
}, []);
return {

View File

@@ -16,8 +16,13 @@ export interface ParsedFrontMatter {
export class FrontMatterParser {
public static currentContent: string | null = null;
/**
* Convert the current content to a Front Matter object
* @param content
* @returns
*/
public static fromFile(content: string): ParsedFrontMatter {
const format = getFormatOpts(this.getLanguage());
const format = getFormatOpts(this.getLanguageFromContent(content));
FrontMatterParser.currentContent = content;
const result = matter(content, { ...Engines, ...format });
// in the absent of a body when serializing an entry we use an empty one
@@ -29,13 +34,21 @@ export class FrontMatterParser {
};
}
/**
* Convert the Front Matter object to text
* @param content
* @param metadata
* @param options
* @returns
*/
public static toFile(
content: string,
metadata: Object,
originalContent?: string,
options?: any
) {
// Stringify to YAML if the format was not set
const format = getFormatOpts(this.getLanguage());
const format = getFormatOpts(this.getLanguageFromContent(originalContent));
const trimLastLineBreak = content.slice(-1) !== '\n';
const file = matter.stringify(content, metadata, {
@@ -46,6 +59,30 @@ export class FrontMatterParser {
return trimLastLineBreak && file.slice(-1) === '\n' ? file.substring(0, file.length - 1) : file;
}
/**
* Validate the type of front matter language that is used
* @param contents
*/
public static getLanguageFromContent(contents: string | undefined) {
if (!contents) {
return this.getLanguage();
}
if (contents.startsWith(`+++`)) {
return "toml";
} else if (contents.startsWith(`---`)) {
return "yaml";
} else if (contents.startsWith(`{`)) {
return "json";
} else {
return "yaml";
}
}
/**
* Get the front matter language type
* @returns
*/
private static getLanguage() {
const language = Settings.get(SETTING_FRONTMATTER_TYPE) as string || "YAML";
return language.toLowerCase();

View File

@@ -4,6 +4,7 @@ import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, Fold
import { SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FRONTMATTER_TYPE } from '../constants';
import { Settings } from '../helpers';
import { FrontMatterDecorationProvider } from './FrontMatterDecorationProvider';
import { FrontMatterParser } from '../parsers';
export class MarkdownFoldingProvider implements FoldingRangeProvider {
private static start: number | null = null;
@@ -43,7 +44,7 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider {
if (isSupported) {
const fmHighlight = Settings.get<boolean>(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT);
const range = this.getFrontMatterRange();
const range = MarkdownFoldingProvider.getFrontMatterRange();
if (range) {
if (MarkdownFoldingProvider.decType !== null) {
@@ -64,17 +65,22 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider {
* @returns
*/
public static getFrontMatterRange(document?: TextDocument) {
const language = Settings.get(SETTING_FRONTMATTER_TYPE) as string || "YAML";
const content = document?.getText();
const language = FrontMatterParser.getLanguageFromContent(content);
let lineStart = "---";
let lineEnd = "---";
if (language === "TOML") {
let lineEnd = lineStart;
if (language.toLowerCase() === "toml") {
lineStart = "+++";
lineEnd = lineStart;
} else if (language.toLowerCase() === "json") {
lineStart = "{";
lineEnd = "}";
}
if (document) {
const lines = document.getText().split('\n');
if (content) {
const lines = content.split('\n');
let start = null;
let end = null;