#158 - New draft field setting + choice implementation

This commit is contained in:
Elio Struyf
2021-10-25 12:53:04 +02:00
parent 7291e6aac6
commit 04c401207f
18 changed files with 235 additions and 39 deletions
+40 -2
View File
@@ -97,6 +97,43 @@
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
"scope": "Content"
},
"frontMatter.content.draftField": {
"type": "object",
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.draftField)",
"default": {
"name": "draft",
"type": "boolean"
},
"properties": {
"type": {
"type": "string",
"enum": [
"boolean",
"choice"
],
"description": "Define the type of field"
},
"name": {
"type": "string",
"description": "Name of the field to use"
},
"choices": {
"type": "array",
"description": "List of choices for the field",
"items": {
"type": [
"string"
]
}
}
},
"additionalProperties": false,
"required": [
"type",
"name"
],
"scope": "Content"
},
"frontMatter.content.fmHighlight": {
"type": "boolean",
"default": true,
@@ -273,7 +310,8 @@
"image",
"choice",
"tags",
"categories"
"categories",
"draft"
],
"description": "Define the type of field"
},
@@ -733,4 +771,4 @@
"dependencies": {
"@docsearch/js": "^3.0.0-alpha.40"
}
}
}
+5 -3
View File
@@ -1,10 +1,10 @@
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID } from '../constants';
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID, SETTINGS_CONTENT_DRAFT_FIELD } from '../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { basename, dirname, extname, join, parse } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
import { Settings as SettingsHelper } from '../helpers';
import { Framework, TaxonomyType } from '../models';
import { DraftField, Framework, TaxonomyType } from '../models';
import { Folders } from './Folders';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
@@ -25,6 +25,7 @@ import imageSize from 'image-size';
import { parseWinPath } from '../helpers/parseWinPath';
import { DateHelper } from '../helpers/DateHelper';
import { FrameworkDetector } from '../helpers/FrameworkDetector';
import { ContentType } from '../helpers/ContentType';
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -278,6 +279,7 @@ export class Dashboard {
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView),
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
draftField: SettingsHelper.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
contentFolders: Folders.get().map(f => f.path),
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
@@ -479,7 +481,7 @@ export class Dashboard {
fmModified: file.mtime,
fmFilePath: file.filePath,
fmFileName: file.fileName,
fmDraft: article?.data.draft ? "Draft" : "Published",
fmDraft: ContentType.getDraftStatus(article?.data),
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
// Make sure these are always set
title: article?.data.title,
+7
View File
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DefaultFields } from '../constants';
import { ContentType } from '../helpers/ContentType';
export class StatusListener {
@@ -15,6 +16,12 @@ export class StatusListener {
public static async verify(frontMatterSB: vscode.StatusBarItem, collection: vscode.DiagnosticCollection) {
const draftMsg = "in draft";
const publishMsg = "to publish";
const draft = ContentType.getDraftField();
if (!draft || draft.type !== "boolean") {
frontMatterSB.hide();
return;
}
let editor = vscode.window.activeTextEditor;
if (editor && ArticleHelper.isMarkdownFile()) {
+1 -1
View File
@@ -29,7 +29,7 @@ export const DEFAULT_CONTENT_TYPE: ContentType = {
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
"type": "draft"
},
{
"title": "Tags",
+1
View File
@@ -38,6 +38,7 @@ export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
@@ -41,7 +41,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<div className="p-4 w-full">
<div className={`flex justify-between items-center`}>
<Status draft={!!draft} />
<Status draft={draft} />
<DateField value={date} />
</div>
@@ -64,7 +64,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<DateField value={date} />
</div>
<div className="col-span-2">
<Status draft={!!draft} />
<Status draft={draft} />
</div>
</button>
</li>
+40 -12
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Tab } from '../constants/Tab';
import { TabAtom } from '../state';
import { SettingsAtom, TabAtom } from '../state';
export interface INavigationProps {
totalPages: number;
@@ -15,19 +15,47 @@ export const tabs = [
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
const settings = useRecoilValue(SettingsAtom);
return (
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))}
{
settings?.draftField?.type === "boolean" ? (
tabs.map((tab) => (
<button
key={tab.name}
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))
) : (
<>
<button
className={`${tabs[0].id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tabs[0].id)}
>
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
{
settings?.draftField?.choices?.map((value, idx) => (
<button
key={`${value}-${idx}`}
className={`${value === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm first-letter:uppercase`}
aria-current={value === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(value)}
>
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))
}
</>
)
}
</nav>
);
};
+13 -1
View File
@@ -1,10 +1,22 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsAtom } from '../state';
export interface IStatusProps {
draft: boolean;
draft: boolean | string;
}
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
const settings = useRecoilValue(SettingsAtom);
if (settings?.draftField && settings.draftField.type === "choice") {
if (draft) {
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
} else {
return null;
}
}
return (
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
);
+20 -7
View File
@@ -4,7 +4,7 @@ import { Tab } from '../constants/Tab';
import { Page } from '../models/Page';
import Fuse from 'fuse.js';
import { useRecoilValue } from 'recoil';
import { CategorySelector, FolderSelector, SearchSelector, SortingSelector, TabSelector, TagSelector } from '../state';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingSelector, TabSelector, TagSelector } from '../state';
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [
@@ -16,6 +16,7 @@ const fuseOptions: Fuse.IFuseOptions<Page> = {
export default function usePages(pages: Page[]) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
const settings = useRecoilValue(SettingsSelector);
const tab = useRecoilValue(TabSelector);
const sorting = useRecoilValue(SortingSelector);
const folder = useRecoilValue(FolderSelector);
@@ -24,6 +25,8 @@ export default function usePages(pages: Page[]) {
const category = useRecoilValue(CategorySelector);
useEffect(() => {
const draftField = settings?.draftField;
// Check if search needs to be performed
let searchedPages = pages;
if (search) {
@@ -34,12 +37,22 @@ export default function usePages(pages: Page[]) {
// Filter the pages
let pagesToShow: Page[] = Object.assign([], searchedPages);
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page.draft);
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page.draft);
if (draftField && draftField.type === 'choice') {
if (tab !== Tab.All) {
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
} else {
pagesToShow = searchedPages;
}
} else {
pagesToShow = searchedPages;
const draftFieldName = draftField?.name || "draft";
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
} else {
pagesToShow = searchedPages;
}
}
// Sort the pages
@@ -69,7 +82,7 @@ export default function usePages(pages: Page[]) {
}
setPageItems(pagesSorted);
}, [ pages, tab, sorting, folder, search, tag, category ]);
}, [ settings?.draftField, pages, tab, sorting, folder, search, tag, category ]);
return {
pageItems
+2 -1
View File
@@ -1,7 +1,7 @@
import { VersionInfo } from '../../models/VersionInfo';
import { ViewType } from '../state';
import { ContentFolder } from '../../models/ContentFolder';
import { ContentType, Framework } from '../../models';
import { ContentType, DraftField, Framework } from '../../models';
export interface Settings {
beta: boolean;
@@ -19,4 +19,5 @@ export interface Settings {
contentFolders: string[];
crntFramework: string;
framework: Framework | null | undefined;
draftField: DraftField | null | undefined;
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { atom } from 'recoil';
import { Tab } from '../../constants/Tab';
export const TabAtom = atom<Tab>({
export const TabAtom = atom<Tab | string>({
key: 'TabAtom',
default: Tab.All
});
+4 -3
View File
@@ -1,6 +1,6 @@
import { DashboardData } from '../models/DashboardData';
import { Template } from '../commands/Template';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD } from '../constants';
import * as os from 'os';
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
@@ -9,7 +9,7 @@ import { Command } from "../panelWebView/Command";
import { CommandToCode } from '../panelWebView/CommandToCode';
import { Article } from '../commands';
import { TagType } from '../panelWebView/TagType';
import { TaxonomyType } from '../models';
import { DraftField, TaxonomyType } from '../models';
import { exec } from 'child_process';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { Content } from 'mdast';
@@ -403,7 +403,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
preview: Preview.getSettings(),
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
dashboardViewData: Dashboard.viewData
dashboardViewData: Dashboard.viewData,
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD)
} as PanelSettings
});
}
+35 -5
View File
@@ -1,18 +1,48 @@
import { ArticleHelper, Settings } from ".";
import { SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from "../constants";
import { ContentType as IContentType } from '../models';
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
import { ContentType as IContentType, DraftField } from '../models';
import { Uri, workspace, window } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { format } from "date-fns";
import { join } from "path";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { writeFileSync } from "fs";
import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { GrayMatterFile } from "gray-matter";
export class ContentType {
/**
* Retrieve the draft field
* @returns
*/
public static getDraftField() {
const draftField = Settings.get<DraftField | null | undefined>(SETTINGS_CONTENT_DRAFT_FIELD);
if (draftField) {
return draftField;
}
return null;
}
/**
* Retrieve the field its status
* @param data
* @returns
*/
public static getDraftStatus(data: { [field: string]: any }) {
const draftField = ContentType.getDraftField();
if (draftField && data && data[draftField.name]) {
const fieldValue = data[draftField.name];
if (draftField.type === "boolean") {
return fieldValue ? "Draft" : "Published";
} else {
return fieldValue;
}
}
return null;
}
/**
* Create content based on content types
* @returns
+5
View File
@@ -0,0 +1,5 @@
export interface DraftField {
name: string;
type: "boolean" | "choice";
choices?: string[];
}
+3 -1
View File
@@ -1,4 +1,5 @@
import { FileType } from "vscode";
import { DraftField } from ".";
import { Choice } from "./Choice";
import { DashboardData } from "./DashboardData";
@@ -17,6 +18,7 @@ export interface PanelSettings {
preview: PreviewSettings;
contentTypes: ContentType[];
dashboardViewData: DashboardData | undefined;
draftField: DraftField;
}
export interface ContentType {
@@ -29,7 +31,7 @@ export interface ContentType {
export interface Field {
title?: string;
name: string;
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories";
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft";
choices?: string[] | Choice[];
single?: boolean;
multiple?: boolean;
+1
View File
@@ -1,6 +1,7 @@
export * from './Choice';
export * from './ContentFolder';
export * from './DashboardData';
export * from './DraftField';
export * from './Framework';
export * from './MediaPaths';
export * from './PanelSettings';
@@ -0,0 +1,40 @@
import * as React from 'react';
import { RocketIcon } from '../Icons/RocketIcon';
import { VsLabel } from '../VscodeComponents';
import { ChoiceField } from './ChoiceField';
import { Toggle } from './Toggle';
export interface IDraftFieldProps {
label: string;
type: "boolean" | "choice";
value: boolean | string | null | undefined;
choices?: string[];
onChanged: (value: string | boolean) => void;
}
export const DraftField: React.FunctionComponent<IDraftFieldProps> = ({ label, type, value, choices, onChanged }: React.PropsWithChildren<IDraftFieldProps>) => {
if (type === "boolean") {
return (
<Toggle
label={label}
checked={!!value}
onChanged={(checked) => onChanged(checked)} />
);
}
if (type === "choice") {
return (
<ChoiceField
label={label}
selected={value as string}
choices={choices as string[]}
multiSelect={false}
onChange={value => onChanged(value as string)} />
);
}
return null;
};
+15
View File
@@ -18,6 +18,7 @@ import { ChoiceField } from './Fields/ChoiceField';
import useContentType from '../../hooks/useContentType';
import { DateHelper } from '../../helpers/DateHelper';
import FieldBoundary from './ErrorBoundary/FieldBoundary';
import { DraftField } from './Fields/DraftField';
export interface IMetadataProps {
settings: PanelSettings | undefined;
@@ -172,6 +173,20 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
unsetFocus={unsetFocus} />
</FieldBoundary>
);
} else if (field.type === 'draft') {
const draftField = settings?.draftField;
const value = metadata[field.name];
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<DraftField
label={field.title || field.name}
type={draftField.type}
choices={draftField.choices || []}
value={value as boolean | string | null | undefined}
onChanged={(value: boolean | string) => sendUpdate(field.name, value)} />
</FieldBoundary>
);
} else {
return null;
}