mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-05 04:52:28 +02:00
#349 - Slug field
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
10
package.json
10
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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,4 +10,5 @@ export enum Command {
|
||||
sendMediaUrl = "sendMediaUrl",
|
||||
updatePlaceholder = "updatePlaceholder",
|
||||
dataFileEntries = "dataFileEntries",
|
||||
updatedSlug = "updatedSlug",
|
||||
}
|
||||
@@ -38,4 +38,5 @@ export enum CommandToCode {
|
||||
addMissingFields = "add-missing-fields",
|
||||
setContentType = "set-content-type",
|
||||
getDataEntries = "get-data-entries",
|
||||
generateSlug = "generate-slug",
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
82
src/panelWebView/components/Fields/SlugField.tsx
Normal file
82
src/panelWebView/components/Fields/SlugField.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user