#126 - Create new content from conent type

This commit is contained in:
Elio Struyf
2021-10-03 12:55:58 +02:00
parent 544f24bcba
commit 05ce2d3537
15 changed files with 327 additions and 40 deletions
+1
View File
@@ -14,6 +14,7 @@
- [#121](https://github.com/estruyf/vscode-front-matter/issues/121): Choice fields support ID/title objects as well as a regular string
- [#122](https://github.com/estruyf/vscode-front-matter/issues/122): Update the filenames of your media
- [#124](https://github.com/estruyf/vscode-front-matter/issues/124): Add new `isPreviewImage` property to the content type field to specify custom preview images
- [#126](https://github.com/estruyf/vscode-front-matter/issues/126): Create new content from the available content types
- [#127](https://github.com/estruyf/vscode-front-matter/issues/127): Title bar action added to open the dashboard
### 🐞 Fixes
+5 -5
View File
@@ -334,7 +334,7 @@
"type": "datetime"
},
{
"title": "Article preview",
"title": "Content preview",
"name": "preview",
"type": "image"
},
@@ -503,7 +503,7 @@
},
{
"command": "frontMatter.generateSlug",
"title": "Generate slug based on article title",
"title": "Generate slug based on content title",
"category": "Front matter"
},
{
@@ -518,7 +518,7 @@
},
{
"command": "frontMatter.insertImage",
"title": "Insert image into article",
"title": "Insert image into your content",
"category": "Front matter",
"icon": "$(device-camera)"
},
@@ -529,7 +529,7 @@
},
{
"command": "frontMatter.createContent",
"title": "New article from template",
"title": "Create new content from defined content type or template",
"category": "Front matter"
},
{
@@ -540,7 +540,7 @@
},
{
"command": "frontMatter.preview",
"title": "Preview article",
"title": "Preview content",
"category": "Front matter"
},
{
+31
View File
@@ -0,0 +1,31 @@
import { commands, QuickPickItem, window } from 'vscode';
import { COMMAND_NAME } from '../constants';
export class Content {
public static async create() {
const options: QuickPickItem[] = [{
label: "Create content by content type",
description: "Select if you want to create new content by the available content type(s)"
}, {
label: "Create content by template",
description: "Select if you want to create new content by the available template(s)"
} as QuickPickItem];
const selectedOption = await window.showQuickPick(options, {
placeHolder: `Select how you want to create your new content`,
canPickMany: false
});
if (selectedOption) {
if (selectedOption.label === options[0].label) {
commands.executeCommand(COMMAND_NAME.createByContentType);
} else {
commands.executeCommand(COMMAND_NAME.createByTemplate);
}
}
return;
}
}
+6
View File
@@ -140,6 +140,12 @@ export class Dashboard {
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.createByContentType:
await commands.executeCommand(COMMAND_NAME.createByContentType);
break;
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
case DashboardMessage.updateSetting:
Dashboard.updateSetting(msg.data);
break;
+3 -17
View File
@@ -1,3 +1,4 @@
import { Questions } from './../helpers/Questions';
import { SETTINGS_CONTENT_PAGE_FOLDERS } from './../constants/settings';
import { commands, Uri, workspace, window } from "vscode";
import { basename, join } from "path";
@@ -17,27 +18,12 @@ export class Folders {
* @returns
*/
public static async create() {
const folders = Folders.get();
if (!folders || folders.length === 0) {
Notifications.warning(`There are no known content locations defined in this project.`);
return;
}
let selectedFolder: string | undefined;
if (folders.length > 1) {
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
placeHolder: `Select where you want to create your content`
});
} else {
selectedFolder = folders[0].title;
}
const selectedFolder = await Questions.SelectContentFolder();
if (!selectedFolder) {
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
return;
}
const folders = Folders.get();
const location = folders.find(f => f.title === selectedFolder);
if (location) {
const folderPath = Folders.getFolderPath(Uri.file(location.path));
+6 -9
View File
@@ -1,3 +1,4 @@
import { Questions } from './../helpers/Questions';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
@@ -67,7 +68,7 @@ export class Template {
["yes", "no"],
{
canPickMany: false,
placeHolder: `Do you want to keep the article its contents for the template?`,
placeHolder: `Do you want to keep the contents for the template?`,
}
);
@@ -113,26 +114,22 @@ export class Template {
}
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
placeHolder: `Select the article template to use`
placeHolder: `Select the content template to use`
});
if (!selectedTemplate) {
Notifications.warning(`No template selected.`);
return;
}
const titleValue = await vscode.window.showInputBox({
prompt: `What would you like to use as a title for the new article?`,
placeHolder: `Article title`
});
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
Notifications.warning(`You did not specify an article title.`);
return;
}
// Start the template read
const template = templates.find(t => t.fsPath.endsWith(selectedTemplate));
if (!template) {
Notifications.warning(`Article template could not be found.`);
Notifications.warning(`Content template could not be found.`);
return;
}
@@ -180,7 +177,7 @@ export class Template {
vscode.window.showTextDocument(txtDoc);
}
Notifications.info(`Your new article has been created.`);
Notifications.info(`Your new content has been created.`);
}
/**
+2
View File
@@ -25,6 +25,8 @@ export const COMMAND_NAME = {
registerFolder: getCommandName("registerFolder"),
unregisterFolder: getCommandName("unregisterFolder"),
createContent: getCommandName("createContent"),
createByContentType: getCommandName("createByContentType"),
createByTemplate: getCommandName("createByTemplate"),
createTemplate: getCommandName("createTemplate"),
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
+2
View File
@@ -4,6 +4,8 @@ export enum DashboardMessage {
openFile = 'openFile',
getTheme = 'getTheme',
createContent = 'createContent',
createByContentType = 'createByContentType',
createByTemplate = 'createByTemplate',
updateSetting = 'updateSetting',
initializeProject = 'initializeProject',
reload = 'reload',
@@ -0,0 +1,52 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { MenuItem, MenuItems } from './Menu';
export interface IChoiceButtonProps {
title: string;
choices: {
title: string;
disabled?: boolean;
onClick: () => void;
}[];
disabled?: boolean;
onClick: () => void;
}
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
return (
<span className="relative z-50 inline-flex shadow-sm rounded-md">
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
onClick={onClick}
disabled={disabled}
>
{title}
</button>
<Menu as="span" className="-ml-px relative block">
<Menu.Button
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
disabled={disabled}>
<span className="sr-only">Open options</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<MenuItems widthClass={`w-56`}>
<div className="py-1">
{choices.map((choice) => (
<MenuItem
key={choice.title}
title={choice.title}
value={null}
onClick={choice.onClick}
disabled={choice.disabled} />
))}
</div>
</MenuItems>
</Menu>
</span>
);
};
@@ -6,7 +6,6 @@ import { Folders } from './Folders';
import { Settings } from '../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Startup } from '../Startup';
import { Button } from '../Button';
import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
@@ -17,6 +16,7 @@ import { ClearFilters } from './ClearFilters';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { PhotographIcon } from '@heroicons/react/outline';
import { Pagination } from '../Media/Pagination';
import { ChoiceButton } from '../ChoiceButton';
export interface IHeaderProps {
settings: Settings | null;
@@ -37,6 +37,14 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
Messenger.send(DashboardMessage.createContent);
};
const createByContentType = () => {
Messenger.send(DashboardMessage.createByContentType);
};
const createByTemplate = () => {
Messenger.send(DashboardMessage.createByTemplate);
};
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
@@ -60,7 +68,19 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
<div className={`flex items-center space-x-4`}>
<Startup settings={settings} />
<Button onClick={createContent} disabled={!settings?.initialized}>Create content</Button>
<ChoiceButton
title={`Create content`}
choices={[{
title: `Create by content type`,
onClick: createByContentType,
disabled: !settings?.initialized
}, {
title: `Create by template`,
onClick: createByTemplate,
disabled: !settings?.initialized
}]}
onClick={createContent}
disabled={!settings?.initialized} />
</div>
</div>
@@ -4,16 +4,18 @@ import * as React from 'react';
export interface IMenuItemProps {
title: string;
value: any;
isCurrent: boolean;
isCurrent?: boolean;
disabled?: boolean;
onClick: (value: any) => void;
}
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, disabled, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
return (
<Menu.Item>
<button
disabled={disabled}
onClick={() => onClick(value)}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100`}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
>
{title}
</button>
@@ -2,9 +2,11 @@ import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment } from 'react';
export interface IMenuItemsProps {}
export interface IMenuItemsProps {
widthClass?: string;
}
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({children}: React.PropsWithChildren<IMenuItemsProps>) => {
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, children}: React.PropsWithChildren<IMenuItemsProps>) => {
return (
<Transition
as={Fragment}
@@ -15,7 +17,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({children}:
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto">
<Menu.Items className={`${widthClass || "w-40"} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<div className="py-1">
{children}
</div>
+7 -1
View File
@@ -1,3 +1,4 @@
import { ContentType } from './helpers/ContentType';
import { Dashboard } from './commands/Dashboard';
import * as vscode from 'vscode';
import { Article, Settings, StatusListener } from './commands';
@@ -13,6 +14,7 @@ import { ExplorerView } from './explorerView/ExplorerView';
import { Extension } from './helpers/Extension';
import { DashboardData } from './models/DashboardData';
import { Settings as SettingsHelper } from './helpers';
import { Content } from './commands/Content';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
@@ -104,7 +106,9 @@ export async function activate(context: vscode.ExtensionContext) {
const unregisterFolder = vscode.commands.registerCommand(COMMAND_NAME.unregisterFolder, Folders.unregister);
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Folders.create);
const createByContentType = vscode.commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent);
const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create);
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create);
// Initialize command
Template.init();
@@ -184,6 +188,8 @@ export async function activate(context: vscode.ExtensionContext) {
registerFolder,
unregisterFolder,
createContent,
createByContentType,
createByTemplate,
projectInit,
collapseAll
);
+96
View File
@@ -0,0 +1,96 @@
import { ArticleHelper, Settings } from ".";
import { SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from "../constants";
import { ContentType as IContentType } from '../models';
import { Uri, workspace, window } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import sanitize from '../helpers/Sanitize';
import { format } from "date-fns";
import { join } from "path";
import { existsSync, writeFileSync } from "fs";
import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
export class ContentType {
public static async createContent() {
const selectedContentType = await Questions.SelectContentType();
if (!selectedContentType) {
return;
}
const selectedFolder = await Questions.SelectContentFolder();
if (!selectedFolder) {
return;
}
const contentTypes = ContentType.getAll();
const folders = Folders.get();
const location = folders.find(f => f.title === selectedFolder);
if (contentTypes && location) {
const folderPath = Folders.getFolderPath(Uri.file(location.path));
const contentType = contentTypes.find(ct => ct.name === selectedContentType);
if (folderPath && contentType) {
ContentType.create(contentType, folderPath);
}
}
}
/**
* Retrieve all content types
* @returns
*/
public static getAll() {
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
}
private static async create(contentType: IContentType, folderPath: string) {
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const titleValue = await Questions.ContentTitle();
if (!titleValue) {
return;
}
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
let newFileName = `${sanitizedName}.md`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
}
const newFilePath = join(folderPath, newFileName);
if (existsSync(newFilePath)) {
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
return;
}
const data: any = {};
for (const field of contentType.fields) {
if (field.name === "title") {
data[field.name] = titleValue;
} else {
data[field.name] = null;
}
}
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
data['type'] = contentType.name;
}
const content = ArticleHelper.stringifyFrontMatter(``, data);
writeFileSync(newFilePath, content, { encoding: "utf8" });
const txtDoc = await workspace.openTextDocument(Uri.parse(newFilePath));
if (txtDoc) {
window.showTextDocument(txtDoc);
}
Notifications.info(`Your new content has been created.`);
}
}
+84
View File
@@ -0,0 +1,84 @@
import { window } from 'vscode';
import { Folders } from '../commands/Folders';
import { ContentType } from './ContentType';
import { Notifications } from './Notifications';
export class Questions {
/**
* Specify the name of the content to create
* @param showWarning
* @returns
*/
public static async ContentTitle(showWarning: boolean = true): Promise<string | undefined> {
const title = await window.showInputBox({
prompt: `What would you like to use as a title for the content to create?`,
placeHolder: `Content title`
});
if (!title && showWarning) {
Notifications.warning(`You did not specify a title for your content.`);
return;
}
return title;
}
/**
* Select the folder for your content creation
* @param showWarning
* @returns
*/
public static async SelectContentFolder(showWarning: boolean = true): Promise<string | undefined> {
const folders = Folders.get();
let selectedFolder: string | undefined;
if (folders.length > 1) {
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
placeHolder: `Select where you want to create your content`
});
} else {
selectedFolder = folders[0].title;
}
if (!selectedFolder && showWarning) {
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
return;
}
return selectedFolder;
}
/**
* Select the content type to create new content
* @param showWarning
* @returns
*/
public static async SelectContentType(showWarning: boolean = true): Promise<string | undefined> {
const contentTypes = ContentType.getAll();
if (!contentTypes || contentTypes.length === 0) {
Notifications.warning("No content types found. Please create a content type first.");
return;
}
if (contentTypes.length === 1) {
return contentTypes[0].name;
}
const options = contentTypes.map(contentType => ({
label: contentType.name
}));
const selectedOption = await window.showQuickPick(options, {
placeHolder: `Select the content type to create your new content`,
canPickMany: false
});
if (!selectedOption && showWarning) {
Notifications.warning("No content type was selected.");
return;
}
return selectedOption?.label;
}
}