From bd43ba8a6dcd42df2e65ce5a64c3a105eacb6912 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 3 Jun 2022 15:58:19 +0200 Subject: [PATCH] #349 - Slug field --- CHANGELOG.md | 1 + assets/media/styles.css | 19 ----- package.json | 10 ++- src/commands/Article.ts | 40 ++++++--- src/extension.ts | 2 +- src/listeners/panel/ArticleListener.ts | 17 +++- src/listeners/panel/DataListener.ts | 17 ++-- src/models/PanelSettings.ts | 3 +- src/panelWebView/Command.ts | 1 + src/panelWebView/CommandToCode.ts | 1 + .../components/Fields/ListField.tsx | 19 +++-- .../components/Fields/SlugField.tsx | 82 +++++++++++++++++++ .../components/Fields/WrapperField.tsx | 12 +++ src/panelWebView/styles.css | 74 +++++++++++++++++ 14 files changed, 252 insertions(+), 46 deletions(-) create mode 100644 src/panelWebView/components/Fields/SlugField.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f01b76..16df29cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [#307](https://github.com/estruyf/vscode-front-matter/issues/307): New `list` field which allows to create a list of items - [#345](https://github.com/estruyf/vscode-front-matter/issues/345): Media dashboard UI improvements to visualize the content and public folders +- [#349](https://github.com/estruyf/vscode-front-matter/issues/349): New `slug` field which allows you to manage the slug of your post from the Front Matter panel ### 🐞 Fixes diff --git a/assets/media/styles.css b/assets/media/styles.css index 4828cc53..f13676f6 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -163,14 +163,6 @@ border: 1px solid rgba(0, 0, 0, .9); } -.article__tags__input input { - border: 1px solid var(--vscode-inputValidation-infoBorder); -} - -.article__tags__input input:disabled { - border-color: transparent; -} - .article__tags__input.freeform { position: relative; outline: 1px solid var(--vscode-inputValidation-infoBorder); @@ -182,17 +174,6 @@ border: 0; } -.article__tags__input button { - position: absolute; - bottom: 0; - top: 0; - right: 0; - width: 30px; - display: inline-flex; - align-items: center; - justify-content: center; -} - .article__tags ul { color: var(--vscode-dropdown-foreground); background-color: var(--vscode-dropdown-background); diff --git a/package.json b/package.json index 94aa6ae8..f8e6095d 100644 --- a/package.json +++ b/package.json @@ -807,7 +807,8 @@ "json", "block", "list", - "dataFile" + "dataFile", + "slug" ], "description": "Define the type of field" }, @@ -944,6 +945,11 @@ "type": "string", "default": "", "description": "Specify the property name that will be used to show the value for the field" + }, + "editable": { + "type": "boolean", + "default": true, + "description": "Specify if the field is editable" } }, "additionalProperties": false, @@ -1820,7 +1826,7 @@ { "command": "frontMatter.dashboard", "group": "navigation@2", - "when": "view == frontMatter.explorer" + "when": "view == frontMatter.explorer || view == explorer" } ] }, diff --git a/src/commands/Article.ts b/src/commands/Article.ts index c1d825a4..00b42508 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -168,13 +168,34 @@ export class Article { } /** - * Generate the slug based on the article title + * Generate the new slug */ - public static async generateSlug() { - Telemetry.send(TelemetryEvent.generateSlug); - + public static generateSlug(title: string) { + if (!title) { + return; + } + const prefix = Settings.get(SETTING_SLUG_PREFIX) as string; const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string; + + const slug = SlugHelper.createSlug(title); + + if (slug) { + return { + slug, + slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}` + }; + } + + return undefined; + } + + /** + * Generate the slug based on the article title + */ + public static async updateSlug() { + Telemetry.send(TelemetryEvent.generateSlug); + const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string; const filePrefix = Settings.get(SETTING_TEMPLATES_PREFIX); const editor = vscode.window.activeTextEditor; @@ -191,17 +212,16 @@ export class Article { const contentType = ArticleHelper.getContentType(article.data); const titleField = "title"; const articleTitle: string = article.data[titleField]; + const slugInfo = Article.generateSlug(articleTitle); - const slug = SlugHelper.createSlug(articleTitle); - if (slug) { - let slugFieldValue = `${prefix}${slug}${suffix}`; - article.data["slug"] = slugFieldValue; + if (slugInfo && slugInfo.slug && slugInfo.slugWithPrefixAndSuffix) { + article.data["slug"] = slugInfo.slugWithPrefixAndSuffix; if (contentType) { // Update the fields containing the slug placeholder let fieldsToUpdate: Field[] = contentType.fields.filter(f => f.default === "{{slug}}"); for (const field of fieldsToUpdate) { - article.data[field.name] = slug; + article.data[field.name] = slugInfo.slug; } // Update the fields containing a custom placeholder that depends on slug @@ -227,7 +247,7 @@ export class Article { const ext = extname(editor.document.fileName); const fileName = basename(editor.document.fileName); - let slugName = slug.startsWith("/") ? slug.substring(1) : slug; + let slugName = slugInfo.slug.startsWith("/") ? slugInfo.slug.substring(1) : slugInfo.slug; slugName = slugName.endsWith("/") ? slugName.substring(0, slugName.length - 1) : slugName; let newFileName = `${slugName}${ext}`; diff --git a/src/extension.ts b/src/extension.ts index b8d17d55..1e935643 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -126,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) { const setLastModifiedDate = vscode.commands.registerCommand(COMMAND_NAME.setLastModifiedDate, Article.setLastModifiedDate); - const generateSlug = vscode.commands.registerCommand(COMMAND_NAME.generateSlug, Article.generateSlug); + const generateSlug = vscode.commands.registerCommand(COMMAND_NAME.generateSlug, Article.updateSlug); const createFromTemplate = vscode.commands.registerCommand(COMMAND_NAME.createFromTemplate, (folder: vscode.Uri) => { const folderPath = Folders.getFolderPath(folder); diff --git a/src/listeners/panel/ArticleListener.ts b/src/listeners/panel/ArticleListener.ts index b602732f..fd1d06f7 100644 --- a/src/listeners/panel/ArticleListener.ts +++ b/src/listeners/panel/ArticleListener.ts @@ -1,4 +1,5 @@ import { Article } from "../../commands"; +import { Command } from "../../panelWebView/Command"; import { CommandToCode } from "../../panelWebView/CommandToCode"; import { BaseListener } from "./BaseListener"; @@ -14,7 +15,10 @@ export class ArticleListener extends BaseListener { switch(msg.command) { case CommandToCode.updateSlug: - Article.generateSlug(); + Article.updateSlug(); + break; + case CommandToCode.generateSlug: + this.generateSlug(msg.data); break; case CommandToCode.updateLastMod: Article.setLastModifiedDate(); @@ -24,4 +28,15 @@ export class ArticleListener extends BaseListener { break; } } + + /** + * Generate a slug + * @param title + */ + private static generateSlug(title: string) { + const slug = Article.generateSlug(title); + if (slug) { + this.sendMsg(Command.updatedSlug, slug) + } + } } \ No newline at end of file diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index abb7d28e..aa056ed9 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -107,17 +107,20 @@ export class DataListener extends BaseListener { if (keys.length > 0 && contentTypes && wsFolder) { // Get the current content type const contentType = ArticleHelper.getContentType(updatedMetadata); + let slugField; if (contentType) { ImageHelper.processImageFields(updatedMetadata, contentType.fields); + + slugField = contentType.fields.find((f) => f.type === "slug"); } - } + + // Check slug + if (!slugField && !updatedMetadata[DefaultFields.Slug]) { + const slug = Article.getSlug(); - // Check slug - if (!updatedMetadata[DefaultFields.Slug]) { - const slug = Article.getSlug(); - - if (slug) { - updatedMetadata[DefaultFields.Slug] = slug; + if (slug) { + updatedMetadata[DefaultFields.Slug] = slug; + } } } diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 2fd27387..b844d282 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -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" | "dataFile" | "list"; +export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file" | "dataFile" | "list" | "slug"; export interface Field { title?: string; @@ -67,6 +67,7 @@ export interface Field { dataType?: string | string[]; taxonomyLimit?: number; fileExtensions?: string[]; + editable?: boolean; // Date fields isPublishDate?: boolean; diff --git a/src/panelWebView/Command.ts b/src/panelWebView/Command.ts index ac16ce33..489758f0 100644 --- a/src/panelWebView/Command.ts +++ b/src/panelWebView/Command.ts @@ -10,4 +10,5 @@ export enum Command { sendMediaUrl = "sendMediaUrl", updatePlaceholder = "updatePlaceholder", dataFileEntries = "dataFileEntries", + updatedSlug = "updatedSlug", } \ No newline at end of file diff --git a/src/panelWebView/CommandToCode.ts b/src/panelWebView/CommandToCode.ts index ec574cc0..935b3757 100644 --- a/src/panelWebView/CommandToCode.ts +++ b/src/panelWebView/CommandToCode.ts @@ -38,4 +38,5 @@ export enum CommandToCode { addMissingFields = "add-missing-fields", setContentType = "set-content-type", getDataEntries = "get-data-entries", + generateSlug = "generate-slug", } \ No newline at end of file diff --git a/src/panelWebView/components/Fields/ListField.tsx b/src/panelWebView/components/Fields/ListField.tsx index 4acef0ed..b286d33f 100644 --- a/src/panelWebView/components/Fields/ListField.tsx +++ b/src/panelWebView/components/Fields/ListField.tsx @@ -1,6 +1,6 @@ import { PencilIcon, TrashIcon, ViewListIcon } from '@heroicons/react/outline'; import * as React from 'react'; -import { useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { VsLabel } from '../VscodeComponents'; export interface IListFieldProps { @@ -11,13 +11,12 @@ export interface IListFieldProps { export const ListField: React.FunctionComponent = ({ label, value, onChange }: React.PropsWithChildren) => { const [ text, setText ] = React.useState(""); - const [ list, setList ] = React.useState(value); + const [ list, setList ] = React.useState(null); const [ itemToEdit, setItemToEdit ] = React.useState(null); const inputRef = useRef(null); const onTextChange = (txtValue: string) => { setText(txtValue); - // onChange(txtValue); }; const onSaveForm = useCallback(() => { @@ -59,6 +58,16 @@ export const ListField: React.FunctionComponent = ({ label, val } }, [list]); + useEffect(() => { + if (value) { + if (typeof value === "string") { + setList([value]); + } else { + setList(value); + } + } + }, [value]); + let isValid = true; return ( @@ -80,7 +89,7 @@ export const ListField: React.FunctionComponent = ({ label, val } }} style={{ - border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)" + border: "1px solid var(--vscode-inputValidation-infoBorder)" }} />
@@ -101,7 +110,7 @@ export const ListField: React.FunctionComponent = ({ label, val
    { - list && list.map((item, index) => ( + list && list.length > 0 && list.map((item, index) => (
  • {item} diff --git a/src/panelWebView/components/Fields/SlugField.tsx b/src/panelWebView/components/Fields/SlugField.tsx new file mode 100644 index 00000000..d934a6ba --- /dev/null +++ b/src/panelWebView/components/Fields/SlugField.tsx @@ -0,0 +1,82 @@ +import { Messenger } from '@estruyf/vscode/dist/client'; +import { EventData } from '@estruyf/vscode/dist/models'; +import {LinkIcon, RefreshIcon} from '@heroicons/react/outline'; +import * as React from 'react'; +import { useCallback, useEffect } from 'react'; +import { Command } from '../../Command'; +import { CommandToCode } from '../../CommandToCode'; +import { VsLabel } from '../VscodeComponents'; + +export interface ISlugFieldProps { + label: string; + value: string | null; + titleValue: string | null; + editable?: boolean; + onChange: (txtValue: string) => void; +} + +export const SlugField: React.FunctionComponent = ({ label, editable, value, titleValue, onChange }: React.PropsWithChildren) => { + const [ text, setText ] = React.useState(value); + const [ slug, setSlug ] = React.useState(value); + + useEffect(() => { + if (text !== value) { + setText(value); + } + }, [ value ]); + + const onTextChange = (txtValue: string) => { + setText(txtValue); + onChange(txtValue); + }; + + const updateSlug = () => { + Messenger.send(CommandToCode.updateSlug); + }; + + const messageListener = useCallback((message: MessageEvent>) => { + const {command, data} = message.data; + if (command === Command.updatedSlug) { + setSlug(data?.slugWithPrefixAndSuffix); + } + }, [text]); + + useEffect(() => { + if (titleValue) { + Messenger.send(CommandToCode.generateSlug, titleValue); + } + }, [titleValue]); + + useEffect(() => { + Messenger.listen(messageListener); + + return () => { + Messenger.unlisten(messageListener); + } + }, []); + + return ( +
    + +
    + {label} +
    +
    + +
    + onTextChange(e.currentTarget.value)} /> + + +
    +
    + ); +} \ No newline at end of file diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 16025490..6b2a24a9 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -24,6 +24,7 @@ import { FileField } from './FileField'; import { ListField } from './ListField'; import { NumberField } from './NumberField'; import { PreviewImageField, PreviewImageValue } from './PreviewImageField'; +import { SlugField } from './SlugField'; import { TextField } from './TextField'; import { Toggle } from './Toggle'; @@ -370,6 +371,17 @@ export const WrapperField: React.FunctionComponent = ({ onChange={(value => onSendUpdate(field.name, value, parentFields))} /> ); + } else if (field.type === 'slug') { + return ( + + onSendUpdate(field.name, value, parentFields))} /> + + ); } else { console.warn(`Unknown field type: ${field.type}`); return null; diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index 200d0ec5..e7bed06c 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -368,6 +368,29 @@ vscode-divider { } /* Tags */ +.article__tags__input button { + margin: 1px 0; + padding: 0 .5rem; + border-left: 1px solid var(--vscode-inputValidation-infoBorder); + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + + display: flex; + align-items: center; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: auto; + + &:disabled { + background: none; + filter: brightness(100%); + color: var(--vscode-disabledForeground); + } +} + .article__tags__items { display: flex; flex-flow: row wrap; @@ -433,6 +456,57 @@ vscode-divider { white-space: nowrap; } +/* Slug field */ +.metadata_field__slug { + position: relative; + width: 100%; +} + +.metadata_field__slug input { + padding-right: 2.5rem; + border: 1px solid var(--vscode-inputValidation-infoBorder); + outline: none; + + &:disabled { + color: var(--vscode-disabledForeground); + } +} + +.metadata_field__slug button { + background: var(--vscode-input-background); + border: none; + border-left: 1px solid var(--vscode-inputValidation-infoBorder); + color: inherit; + outline: none !important; + outline-offset: inherit !important; + margin: 1px; + padding: 0 .5rem; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: auto; + + display: flex; + align-items: center; + + span { + margin-right: .5rem; + font-size: .8rem; + } + + svg { + width: 16px; + height: 16px; + } + + &.metadata_field__slug__button_update { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + margin: 0; + } +} /* Quill changes */ .ql-toolbar.ql-snow,