diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a2d8091..d7c6d201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [1.10.0] - 2020-12-03 + +- FrontMatter panel implemented. This panel allows you to control all extension actions, but not only that. It makes adding tags and categories in a easier way to your page. + ## [1.9.0] - 2020-11-25 - [#23](https://github.com/estruyf/vscode-front-matter/issues/23): Implemented the option to create and use templates for article creation (front matter will be updates as well). diff --git a/README.md b/README.md index 3cbbc58c..9bbe340d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,18 @@ The extension will automatically verify if your title and description are SEO co > If you see something missing in your article creation flow, please feel free to reach out. +## FrontMatter Panel (introduced in 1.10.0) + +In version `1.10.0` of this extension, the FrontMatter panel got introduced. This panel allows you to perform most of the extension actions by just a click on the button. + +![FrontMatter Panel](./assets/frontmatter-panel.png) + +Originally this panel was created to make it easier to add tags and categories to your articles. As the current vscode multi-select is not optimal to use. + +To leverage most of the capabilities of the extension. SEO information and common actions like slug optimization, updating the date and publish/drafting the article. + +> **Info**: By default the tags/categories picker allows you to insert none existing tags/categories. When you enter a none existing tag/category, the panel shows an add `+` icon in front of that button. This allows you to store this tag/category to your settings. If you want to disable this feature, you can do that by setting the `frontMatter.panel.freeform` setting to `false`. + ## Creating articles from templates By default, the extension looks for files stored in a `.templates` folder which should be located in the root of your website project. diff --git a/assets/frontmatter-panel.png b/assets/frontmatter-panel.png new file mode 100644 index 00000000..2d4a4ffb Binary files /dev/null and b/assets/frontmatter-panel.png differ diff --git a/assets/media/main.js b/assets/media/main.js deleted file mode 100644 index 69118208..00000000 --- a/assets/media/main.js +++ /dev/null @@ -1,23 +0,0 @@ - -(function () { - const vscode = acquireVsCodeApi(); - - window.addEventListener('message', event => { - const message = event.data; - - switch (message.type) { - case 'addColor': - addColor(); - break; - case 'clearColors': - colors = []; - updateColorList(colors); - break; - } - }); - - window.onload = function() { - vscode.postMessage({ command: 'get-data' }); - console.log('Ready to accept data.'); - }; -}()); \ No newline at end of file diff --git a/assets/media/styles.css b/assets/media/styles.css index 1afcb9d8..5820b27c 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -101,6 +101,10 @@ border: 1px solid rgba(0, 0, 0, .9); } +.article__tags input { + border: 1px solid var(--vscode-inputValidation-infoBorder); +} + .article__tags ul { color: var(--vscode-dropdown-foreground); background-color: var(--vscode-dropdown-background); @@ -117,6 +121,7 @@ } .article__tags li[data-focus="true"] { + color: var(--vscode-button-foreground); background-color: var(--vscode-button-hoverBackground); } @@ -128,18 +133,50 @@ margin-top: 1rem; } -.article__tags__items__btn { +.article__tags__items__item { display: inline-block; margin-bottom: .5rem; margin-right: .5rem; +} + +.article__tags__items__item_add, +.article__tags__items__item_delete { + display: inline-block; width: auto; } -.article__tags__items__btn span { +.article__tags__items__item svg { + display: inline; + vertical-align: bottom; +} + +.article__tags__items__item_delete span { margin-left: .5rem; } .article__tags__items__pill_notexists { color: var(--vscode-inputValidation-errorForeground); background-color: var(--vscode-inputValidation-errorBackground); + padding-left: .5rem; +} + +.article__tags__items__pill_notexists:hover { + color: var(--vscode-inputValidation-errorForeground); + background-color: var(--vscode-inputValidation-errorBackground); + + filter: contrast(60%); +} + +.article__tags__items__item_add { + color: var(--vscode-inputValidation-infoForeground); + background-color: var(--vscode-inputValidation-infoBackground); + border-right: 1px solid var(--vscode-inputValidation-infoBorder); +} + +.article__tags__items__item_add:hover { + color: var(--vscode-inputValidation-infoForeground); + background-color: var(--vscode-inputValidation-infoBackground); + border-right: 1px solid var(--vscode-inputValidation-infoBorder); + + filter: contrast(60%); } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 191db0f8..c8ae26f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,15 @@ "react-transition-group": "^4.4.0" } }, + "@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4" + } + }, "@material-ui/lab": { "version": "4.0.0-alpha.56", "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz", diff --git a/package.json b/package.json index 040c266d..97852f27 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,11 @@ "type": "string", "default": "yyyy-MM-dd", "description": "Specify the prefix you want to add for your new article filenames." + }, + "frontMatter.panel.freeform": { + "type": "boolean", + "default": true, + "markdownDescription": "Specifies if you want to allow yourself from entering unknown tags/categories in the tag picker (when enabled, you will have the option to store them afterwards). Default: true." } } }, @@ -217,6 +222,10 @@ "test-compile": "tsc -p ./" }, "devDependencies": { + "@iarna/toml": "2.2.3", + "@material-ui/core": "4.11.1", + "@material-ui/icons": "4.11.2", + "@material-ui/lab": "4.0.0-alpha.56", "@types/glob": "7.1.3", "@types/js-yaml": "3.12.1", "@types/mocha": "^5.2.6", @@ -229,16 +238,13 @@ "gray-matter": "4.0.2", "html-loader": "1.3.2", "html-webpack-plugin": "4.5.0", + "react": "17.0.1", + "react-dom": "17.0.1", "ts-loader": "8.0.3", "tslint": "6.1.3", "typescript": "4.0.2", "webpack": "4.44.2", - "webpack-cli": "3.3.12", - "@iarna/toml": "2.2.3", - "@material-ui/core": "4.11.1", - "@material-ui/lab": "4.0.0-alpha.56", - "react": "17.0.1", - "react-dom": "17.0.1" + "webpack-cli": "3.3.12" }, "dependencies": {} } diff --git a/src/constants/settings.ts b/src/constants/settings.ts index a8958c5a..c690f22f 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -20,4 +20,6 @@ export const SETTING_SEO_TITLE_LENGTH = "taxonomy.seoTitleLength"; export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength"; export const SETTING_TEMPLATES_FOLDER = "templates.folder"; -export const SETTING_TEMPLATES_PREFIX = "templates.prefix"; \ No newline at end of file +export const SETTING_TEMPLATES_PREFIX = "templates.prefix"; + +export const SETTING_PANEL_FREEFORM = "panel.freeform"; \ No newline at end of file diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 30b0154f..03d6061a 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -4,6 +4,7 @@ export interface PanelSettings { slug: Slug; tags: string[]; categories: string[]; + freeform: boolean; } export interface SEO { diff --git a/src/viewpanel/CommandToCode.ts b/src/viewpanel/CommandToCode.ts index 52c9e630..b3e02fcf 100644 --- a/src/viewpanel/CommandToCode.ts +++ b/src/viewpanel/CommandToCode.ts @@ -4,5 +4,7 @@ export enum CommandToCode { updateDate = 'update-date', publish = 'publish', updateTags = "update-tags", - updateCategories = "update-categories" + updateCategories = "update-categories", + addTagToSettings = "add-tag", + addCategoryToSettings = "add-category" } \ No newline at end of file diff --git a/src/viewpanel/ViewPanel.tsx b/src/viewpanel/ViewPanel.tsx index 3291e68d..e51bd77e 100644 --- a/src/viewpanel/ViewPanel.tsx +++ b/src/viewpanel/ViewPanel.tsx @@ -35,10 +35,10 @@ export const ViewPanel: React.FunctionComponent = (props: React settings && metadata && } { - (settings && settings.tags && settings.tags.length > 0) && + (settings && settings.tags && settings.tags.length > 0) && } { - (settings && settings.categories && settings.categories.length > 0) && + (settings && settings.categories && settings.categories.length > 0) && } ); diff --git a/src/viewpanel/components/Tag.tsx b/src/viewpanel/components/Tag.tsx index c94d7ecc..3646ec5e 100644 --- a/src/viewpanel/components/Tag.tsx +++ b/src/viewpanel/components/Tag.tsx @@ -1,17 +1,26 @@ import * as React from 'react'; +import AddIcon from '@material-ui/icons/Add'; +import DeleteIcon from '@material-ui/icons/Delete'; export interface ITagProps { className: string; value: string; title: string; + onCreate?: (tags: string) => void; onRemove: (tags: string) => void; } export const Tag: React.FunctionComponent = (props: React.PropsWithChildren) => { - const { value, className, title, onRemove } = props; + const { value, className, title, onRemove, onCreate } = props; return ( - +
+ { + onCreate && + + } + +
); }; \ No newline at end of file diff --git a/src/viewpanel/components/TagPicker.tsx b/src/viewpanel/components/TagPicker.tsx index e7ade408..6f78e577 100644 --- a/src/viewpanel/components/TagPicker.tsx +++ b/src/viewpanel/components/TagPicker.tsx @@ -10,10 +10,11 @@ export interface ITagPickerProps { type: string; crntSelected: string[]; options: string[]; + freeform: boolean; } export const TagPicker: React.FunctionComponent = (props: React.PropsWithChildren) => { - const { type, crntSelected, options } = props; + const { type, crntSelected, options, freeform } = props; const [ selected, setSelected ] = React.useState([]); const prevSelected = usePrevious(crntSelected); @@ -21,7 +22,7 @@ export const TagPicker: React.FunctionComponent = (props: React id: 'use-autocomplete', options: options, multiple: true, - autoComplete: true, + freeSolo: freeform, value: crntSelected, getOptionDisabled: (option) => selected.includes(option), onChange: (e, values: string[]) => { @@ -37,6 +38,11 @@ export const TagPicker: React.FunctionComponent = (props: React sendUpdate(newSelection); }; + const onCreate = (tag: string) => { + const cmdType = type === TagType.tags ? CommandToCode.addTagToSettings : CommandToCode.addCategoryToSettings; + MessageHelper.sendMessage(cmdType, tag); + }; + const sendUpdate = (values: string[]) => { const cmdType = type === TagType.tags ? CommandToCode.updateTags : CommandToCode.updateCategories; MessageHelper.sendMessage(cmdType, values); @@ -66,7 +72,7 @@ export const TagPicker: React.FunctionComponent = (props: React ) : null } - + ); }; \ No newline at end of file diff --git a/src/viewpanel/components/Tags.tsx b/src/viewpanel/components/Tags.tsx index 25d6b520..131161d7 100644 --- a/src/viewpanel/components/Tags.tsx +++ b/src/viewpanel/components/Tags.tsx @@ -5,17 +5,26 @@ export interface ITagsProps { values: string[]; options: string[]; + onCreate: (tags: string) => void; onRemove: (tags: string) => void; } export const Tags: React.FunctionComponent = (props: React.PropsWithChildren) => { - const { values, options, onRemove } = props; + const { values, options, onCreate, onRemove } = props; + + const knownTags = values.filter(v => options.includes(v)); + const unknownTags = values.filter(v => !options.includes(v)); return (
{ - values.map(t => ( - + knownTags.map(t => ( + + )) + } + { + unknownTags.map(t => ( + )) }
diff --git a/src/webview/ExplorerView.ts b/src/webview/ExplorerView.ts index 4d6169e0..18a4b6be 100644 --- a/src/webview/ExplorerView.ts +++ b/src/webview/ExplorerView.ts @@ -1,11 +1,12 @@ import { PanelSettings } from './../models/PanelSettings'; import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace } from "vscode"; -import { CONFIG_KEY, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from "../constants"; -import { ArticleHelper } from "../helpers"; +import { CONFIG_KEY, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from "../constants"; +import { ArticleHelper, SettingsHelper } from "../helpers"; import { Command } from "../viewpanel/Command"; import { CommandToCode } from '../viewpanel/CommandToCode'; import { Article } from '../commands'; import { TagType } from '../viewpanel/TagType'; +import { TaxonomyType } from '../models'; export class ExplorerView implements WebviewViewProvider, Disposable { @@ -88,6 +89,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable { case CommandToCode.updateCategories: this.updateTags(TagType.categories, msg.data || []); break; + case CommandToCode.addTagToSettings: + this.addTags(TagType.tags, msg.data); + break; + case CommandToCode.addCategoryToSettings: + this.addTags(TagType.categories, msg.data); + break; } }); @@ -135,7 +142,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable { suffix: config.get(SETTING_SLUG_SUFFIX) || "" }, tags: config.get(SETTING_TAXONOMY_TAGS) || [], - categories: config.get(SETTING_TAXONOMY_CATEGORIES) || [] + categories: config.get(SETTING_TAXONOMY_CATEGORIES) || [], + freeform: config.get(SETTING_PANEL_FREEFORM) } as PanelSettings }); } @@ -172,6 +180,26 @@ export class ExplorerView implements WebviewViewProvider, Disposable { } } + /** + * Add tag to the settings + * @param tagType + * @param value + */ + private async addTags(tagType: TagType, value: string) { + if (value) { + const config = workspace.getConfiguration(CONFIG_KEY); + let options = tagType === TagType.tags ? config.get(SETTING_TAXONOMY_TAGS) : config.get(SETTING_TAXONOMY_CATEGORIES); + + if (!options) { + options = []; + } + + options.push(value); + const taxType = tagType === TagType.tags ? TaxonomyType.Tag : TaxonomyType.Category; + await SettingsHelper.update(taxType, options); + } + } + /** * Post data to the panel * @param msg