Add content type create, update, setting

This commit is contained in:
Elio Struyf
2022-04-25 14:57:59 +02:00
parent dee28397cb
commit 17a98fba68
12 changed files with 263 additions and 12 deletions
+1
View File
@@ -17,6 +17,7 @@
### 🐞 Fixes
- Updated JSON schema link to supported version by VS Code (draft-07)
- Hide the view mode action from the Front Matter panel if no custom modes are defined
- Fix in decode base64 uploaded video files
- Fix for a lightbox on other types of documents (pdf, etc.)
+20 -2
View File
@@ -1207,10 +1207,20 @@
"category": "Front matter"
},
{
"command": "frontMatter.generate.contenttype",
"command": "frontMatter.contenttype.generate",
"title": "Generate content type from current file",
"category": "Front matter"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"title": "Add missing fields from front matter to content type",
"category": "Front matter"
},
{
"command": "frontMatter.contenttype.setContentType",
"title": "Set the content type to use for the current file",
"category": "Front matter"
},
{
"command": "frontMatter.markup.blockquote",
"title": "Blockquote",
@@ -1688,7 +1698,15 @@
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.generate.contenttype",
"command": "frontMatter.contenttype.generate",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.setContentType",
"when": "frontMatter:file:isValid == true"
}
],
+3 -1
View File
@@ -55,5 +55,7 @@ export const COMMAND_NAME = {
options: getCommandName("markup.options"),
// Content types
generateContentType: getCommandName("generate.contenttype"),
generateContentType: getCommandName("contenttype.generate"),
addMissingFields: getCommandName("contenttype.addMissingFields"),
setContentType: getCommandName("contenttype.setContentType"),
};
+5
View File
@@ -26,6 +26,11 @@ export const TelemetryEvent = {
updateMediaMetadata: 'updateMediaMetadata',
openExplorerView: 'openExplorerView',
// Content types
generateContentType: 'generateContentType',
addMissingFields: 'addMissingFields',
setContentType: 'setContentType',
// Custom scripts
runCustomScript: 'runCustomScript',
runMediaScript: 'runMediaScript',
+15 -1
View File
@@ -158,6 +158,14 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate)
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.addMissingFields, ContentType.addMissingFields)
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.setContentType, ContentType.setContentType)
);
// Initialize command
Template.init();
const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => {
@@ -197,7 +205,13 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => triggerShowDraftStatus(`onDidChangeActiveTextEditor`)));
subscriptions.push(vscode.window.onDidChangeTextEditorSelection((e) => {
if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) {
triggerShowDraftStatus(`onDidChangeTextEditorSelection`);
statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200);
}
}));
subscriptions.push(vscode.workspace.onDidChangeTextDocument((TextDocumentChangeEvent) => {
const filePath = TextDocumentChangeEvent.document.uri.fsPath;
if (filePath && !filePath.toLowerCase().startsWith(`extension-output`)) {
statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200);
}
}));
+103 -4
View File
@@ -10,6 +10,7 @@ import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { Telemetry } from './Telemetry';
import { processKnownPlaceholders } from './PlaceholderHelper';
import { basename } from 'path';
export class ContentType {
@@ -96,8 +97,13 @@ export class ContentType {
* Generate a content type
*/
public static async generate() {
Telemetry.send(TelemetryEvent.generateContentType);
const content = ArticleHelper.getCurrent();
const editor = window.activeTextEditor;
const filePath = editor?.document.uri.fsPath;
if (!content || !content.data) {
Notifications.warning(`No front matter data found to generate a content type.`);
return;
@@ -108,10 +114,12 @@ export class ContentType {
ignoreFocusOut: true,
title: "Override default content type"
});
const overrideBool = override === "Yes";
let contentTypeName: string | undefined = `default`;
if (override === "No") {
// Ask for the new content type name
if (!overrideBool) {
contentTypeName = await window.showInputBox({
ignoreFocusOut: true,
placeHolder: "Enter the name of the content type to generate",
@@ -137,16 +145,43 @@ export class ContentType {
}
}
// Ask if the content type needs to be used as a page bundle
let pageBundle = false;
const fileName = filePath ? basename(filePath) : undefined;
if (fileName?.startsWith(`index.`)) {
const pageBundleAnswer = await window.showQuickPick(["Yes", "No"], {
placeHolder: "Do you want to use this content type as a page bundle?",
ignoreFocusOut: true,
title: "Use as page bundle"
});
pageBundle = pageBundleAnswer === "Yes";
}
const fields = ContentType.generateFields(content.data);
if (!overrideBool && !fields.some(f => f.name === "type")) {
fields.push({
name: "type",
type: "string",
default: contentTypeName,
hidden: true
} as Field);
}
// Update the type field in the page
if (!overrideBool && editor) {
content.data["type"] = contentTypeName;
ArticleHelper.update(editor, content);
}
const newContentType: IContentType = {
name: contentTypeName,
pageBundle,
fields
};
const contentTypes = ContentType.getAll() || [];
if (override === "Yes") {
if (overrideBool) {
const index = contentTypes.findIndex(ct => ct.name === contentTypeName);
contentTypes[index].fields = fields;
} else {
@@ -156,11 +191,71 @@ export class ContentType {
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = Settings.projectConfigPath;
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${override === "Yes" ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
}
/**
* Add missing fields to the content type
*/
public static async addMissingFields() {
Telemetry.send(TelemetryEvent.addMissingFields);
const content = ArticleHelper.getCurrent();
if (!content || !content.data) {
Notifications.warning(`No front matter data found to add missing fields.`);
return;
}
const contentType = ArticleHelper.getContentType(content?.data);
const updatedFields = ContentType.generateFields(content.data, contentType.fields);
const contentTypes = ContentType.getAll() || [];
const index = contentTypes.findIndex(ct => ct.name === contentType.name);
contentTypes[index].fields = updatedFields;
Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = Settings.projectConfigPath;
const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && existsSync(configPath) ? `Open settings` : undefined);
if (notificationAction === "Open settings" && configPath && existsSync(configPath)) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
/**
* Set the content type to be used for the current file
*/
public static async setContentType() {
Telemetry.send(TelemetryEvent.setContentType);
const content = ArticleHelper.getCurrent();
const contentTypes = ContentType.getAll() || [];
if (!content || !content.data) {
Notifications.warning(`No front matter data found to set the content type.`);
return;
}
const ctAnswer = await window.showQuickPick(contentTypes.map(ct => ct.name), {
title: "Select the content type",
ignoreFocusOut: true,
placeHolder: "Which content type would you like to use?"
});
if (!ctAnswer) {
return;
}
content.data.type = ctAnswer;
const editor = window.activeTextEditor;
ArticleHelper.update(editor!, content);
}
/**
@@ -173,6 +268,10 @@ export class ContentType {
for (const field in data) {
const fieldData = data[field];
if (fields.some(f => f.name === field)) {
continue;
}
if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "string") {
if (field.toLowerCase() === "tag" || field.toLowerCase() === "tags") {
fields.push({
+1 -1
View File
@@ -21,7 +21,7 @@ export default function useContentType(settings: PanelSettings | Settings | unde
setContentType(ct || DEFAULT_CONTENT_TYPE)
}
}, [settings?.contentTypes, metadata?.data]);
}, [settings?.contentTypes, metadata?.type]);
return contentType;
}
+6
View File
@@ -43,6 +43,12 @@ export class DataListener extends BaseListener {
case CommandToCode.updatePlaceholder:
this.updatePlaceholder(msg?.data?.field, msg?.data?.value, msg?.data?.title);
break;
case CommandToCode.generateContentType:
commands.executeCommand(COMMAND_NAME.generateContentType);
case CommandToCode.addMissingFields:
commands.executeCommand(COMMAND_NAME.addMissingFields);
case CommandToCode.setContentType:
commands.executeCommand(COMMAND_NAME.setContentType);
}
}
+3
View File
@@ -34,4 +34,7 @@ export enum CommandToCode {
getImageUrl = "get-image-url",
updatePlaceholder = "update-placeholder",
getMode = "get-mode",
generateContentType = "generate-content-type",
addMissingFields = "add-missing-fields",
setContentType = "set-content-type",
}
@@ -0,0 +1,74 @@
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';
import { VsLabel } from '../VscodeComponents';
export interface IContentTypeValidatorProps {
fields: Field[];
metadata: IMetadata
}
const fieldsToIgnore = [`filePath`, `articleDetails`, `slug`];
export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidatorProps> = ({ fields, metadata}: React.PropsWithChildren<IContentTypeValidatorProps>) => {
const isValid = useMemo(() => {
const metadataFields = Object.keys(metadata).filter(key => !fieldsToIgnore.includes(key));
for (const mField of metadataFields) {
if (!fields.find(field => field.name === mField)) {
return false;
}
}
return true;
}, [fields, metadata]);
const generateContentType = () => {
MessageHelper.sendMessage(CommandToCode.generateContentType);
};
const addMissingFields = () => {
MessageHelper.sendMessage(CommandToCode.addMissingFields);
};
const setContentType = () => {
MessageHelper.sendMessage(CommandToCode.setContentType);
};
if (isValid) {
return null;
}
return (
<div className='hint'>
<VsLabel>
<div className={`metadata_field__label metadata_field__alert`}>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fillRule="evenodd" clipRule="evenodd" d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"/></svg>
<span>Content type</span>
</div>
</VsLabel>
<p className='inline_hint'>We noticed field differences between the content type and the front matter data.</p>
<p className='inline_hint'>Would you like to generate or update the content type for this page?</p>
<div className='hint__buttons'>
<VSCodeButton appearance={`secondary`} onClick={generateContentType}>Generate content type</VSCodeButton>
<VSCodeButton appearance={`secondary`} onClick={addMissingFields}>Add missing fields</VSCodeButton>
<VSCodeButton appearance={`secondary`} onClick={setContentType}>Set content type</VSCodeButton>
</div>
<VSCodeDivider />
</div>
);
};
+5 -3
View File
@@ -4,12 +4,10 @@ import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../../helpers/MessageHelper';
import { TagType } from '../TagType';
import { Collapsible } from './Collapsible';
import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
import { TagPicker } from './TagPicker';
import "react-datepicker/dist/react-datepicker.css";
import useContentType from '../../hooks/useContentType';
import FieldBoundary from './ErrorBoundary/FieldBoundary';
import { WrapperField } from './Fields/WrapperField';
import { ContentTypeValidator } from './ContentType/ContentTypeValidator';
export interface IMetadata {
[prop: string]: string[] | string | null | IMetadata;
@@ -80,6 +78,10 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
return (
<Collapsible id={`tags`} title="Metadata" className={`inherit z-20`}>
<ContentTypeValidator
fields={contentType?.fields || []}
metadata={metadata} />
{
renderFields(contentType?.fields || [], metadata)
}
+27
View File
@@ -253,6 +253,33 @@
display: none;
}
/* Metadata section - Content type */
.metadata_field__alert svg {
color: var(--vscode-editorWarning-foreground)
}
.hint {
margin-bottom: 1rem;
}
.hint__buttons vscode-button {
display: block;
margin-bottom: .5rem;
text-align: center;
}
.hint__buttons vscode-button:last-child {
margin-bottom: 0;
}
vscode-divider {
margin-top: 1rem;
}
.inline_hint {
color: var(--vscode-editorInlayHint-foreground);
}
/* File field */
.metadata_field__file__button.not_empty {
display: flex;