#349 - Slug field

This commit is contained in:
Elio Struyf
2022-06-03 15:58:19 +02:00
parent bd2860e225
commit bd43ba8a6d
14 changed files with 252 additions and 46 deletions

View File

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

View File

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

View File

@@ -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"
}
]
},

View File

@@ -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<string>(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}`;

View File

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

View File

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

View File

@@ -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;
}
}
}

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" | "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;

View File

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

View File

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

View File

@@ -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<IListFieldProps> = ({ label, value, onChange }: React.PropsWithChildren<IListFieldProps>) => {
const [ text, setText ] = React.useState<string | null>("");
const [ list, setList ] = React.useState<string[] | null>(value);
const [ list, setList ] = React.useState<string[] | null>(null);
const [ itemToEdit, setItemToEdit ] = React.useState<number | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const onTextChange = (txtValue: string) => {
setText(txtValue);
// onChange(txtValue);
};
const onSaveForm = useCallback(() => {
@@ -59,6 +58,16 @@ export const ListField: React.FunctionComponent<IListFieldProps> = ({ 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<IListFieldProps> = ({ label, val
}
}}
style={{
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
border: "1px solid var(--vscode-inputValidation-infoBorder)"
}} />
<div className={`list_field__form__buttons`}>
@@ -101,7 +110,7 @@ export const ListField: React.FunctionComponent<IListFieldProps> = ({ label, val
<ul className='list_field__list'>
{
list && list.map((item, index) => (
list && list.length > 0 && list.map((item, index) => (
<li className='list_field__list__item' key={index}>
<div>
{item}

View File

@@ -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<ISlugFieldProps> = ({ label, editable, value, titleValue, onChange }: React.PropsWithChildren<ISlugFieldProps>) => {
const [ text, setText ] = React.useState<string | null>(value);
const [ slug, setSlug ] = React.useState<string | null>(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<EventData<any>>) => {
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 (
<div className={`metadata_field`}>
<VsLabel>
<div className={`metadata_field__label`}>
<LinkIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>
<div className='metadata_field__slug'>
<input
className={`metadata_field__slug__input`}
value={text || ""}
disabled={editable !== undefined ? !editable : false}
onChange={(e) => onTextChange(e.currentTarget.value)} />
<button
title={slug !== text ? "Update available" : "Generate slug"}
className={`metadata_field__slug__button ${slug !== text ? "metadata_field__slug__button_update" : ""}`}
onClick={updateSlug}>
<RefreshIcon aria-hidden={true} />
</button>
</div>
</div>
);
}

View File

@@ -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<IWrapperFieldProps> = ({
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
</FieldBoundary>
);
} else if (field.type === 'slug') {
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<SlugField
label={field.title || field.name}
titleValue={metadata.title as string}
value={fieldValue}
editable={field.editable}
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
</FieldBoundary>
);
} else {
console.warn(`Unknown field type: ${field.type}`);
return null;

View File

@@ -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,