diff --git a/package.json b/package.json index 45959f04..8ae47c11 100644 --- a/package.json +++ b/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index c6ac8102..e90f0b2e 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -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(ExtensionState.PagesView), mediaSnippet: SettingsHelper.get(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [], contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [], + draftField: SettingsHelper.get(SETTINGS_CONTENT_DRAFT_FIELD), contentFolders: Folders.get().map(f => f.path), crntFramework: SettingsHelper.get(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, diff --git a/src/commands/StatusListener.ts b/src/commands/StatusListener.ts index ac5b887c..7b11e678 100644 --- a/src/commands/StatusListener.ts +++ b/src/commands/StatusListener.ts @@ -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()) { diff --git a/src/constants/ContentType.ts b/src/constants/ContentType.ts index 181993f2..5f243b15 100644 --- a/src/constants/ContentType.ts +++ b/src/constants/ContentType.ts @@ -29,7 +29,7 @@ export const DEFAULT_CONTENT_TYPE: ContentType = { { "title": "Is in draft", "name": "draft", - "type": "boolean" + "type": "draft" }, { "title": "Tags", diff --git a/src/constants/settings.ts b/src/constants/settings.ts index 4643dbc0..55a4dd12 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -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"; diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index 31488d7c..08479f58 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -41,7 +41,7 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti
- +
@@ -64,7 +64,7 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti
- +
diff --git a/src/dashboardWebView/components/Navigation.tsx b/src/dashboardWebView/components/Navigation.tsx index 1fcd927d..3f04e806 100644 --- a/src/dashboardWebView/components/Navigation.tsx +++ b/src/dashboardWebView/components/Navigation.tsx @@ -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 = ({totalPages}: React.PropsWithChildren) => { const [ crntTab, setCrntTab ] = useRecoilState(TabAtom); + const settings = useRecoilValue(SettingsAtom); return ( ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Status.tsx b/src/dashboardWebView/components/Status.tsx index fea36c35..cc7b6677 100644 --- a/src/dashboardWebView/components/Status.tsx +++ b/src/dashboardWebView/components/Status.tsx @@ -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 = ({draft}: React.PropsWithChildren) => { + const settings = useRecoilValue(SettingsAtom); + + if (settings?.draftField && settings.draftField.type === "choice") { + if (draft) { + return {draft}; + } else { + return null; + } + } + return ( {draft ? "Draft" : "Published"} ); diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index 1c13dbe8..59af0a75 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -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 = { keys: [ @@ -16,6 +16,7 @@ const fuseOptions: Fuse.IFuseOptions = { export default function usePages(pages: Page[]) { const [ pageItems, setPageItems ] = useState([]); + 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 diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index f6eae415..36b8d5c2 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -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; } \ No newline at end of file diff --git a/src/dashboardWebView/state/atom/TabAtom.ts b/src/dashboardWebView/state/atom/TabAtom.ts index abb17261..49d95e88 100644 --- a/src/dashboardWebView/state/atom/TabAtom.ts +++ b/src/dashboardWebView/state/atom/TabAtom.ts @@ -1,7 +1,7 @@ import { atom } from 'recoil'; import { Tab } from '../../constants/Tab'; -export const TabAtom = atom({ +export const TabAtom = atom({ key: 'TabAtom', default: Tab.All }); \ No newline at end of file diff --git a/src/explorerView/ExplorerView.ts b/src/explorerView/ExplorerView.ts index 2a3f8a2c..07033d73 100644 --- a/src/explorerView/ExplorerView.ts +++ b/src/explorerView/ExplorerView.ts @@ -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(SETTINGS_CONTENT_DRAFT_FIELD) } as PanelSettings }); } diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index b7500fe5..9118042a 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -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(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 diff --git a/src/models/DraftField.ts b/src/models/DraftField.ts new file mode 100644 index 00000000..d2edd162 --- /dev/null +++ b/src/models/DraftField.ts @@ -0,0 +1,5 @@ +export interface DraftField { + name: string; + type: "boolean" | "choice"; + choices?: string[]; +} \ No newline at end of file diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index f5e73b59..88254121 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -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; diff --git a/src/models/index.ts b/src/models/index.ts index ef7e31d9..7f2c2f0a 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -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'; diff --git a/src/panelWebView/components/Fields/DraftField.tsx b/src/panelWebView/components/Fields/DraftField.tsx new file mode 100644 index 00000000..ddc5456f --- /dev/null +++ b/src/panelWebView/components/Fields/DraftField.tsx @@ -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 = ({ label, type, value, choices, onChanged }: React.PropsWithChildren) => { + + if (type === "boolean") { + return ( + onChanged(checked)} /> + ); + } + + if (type === "choice") { + return ( + onChanged(value as string)} /> + ); + } + + return null; +}; \ No newline at end of file diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index eff704c1..102232a7 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -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 = ({settings, metadata, unsetFocus={unsetFocus} /> ); + } else if (field.type === 'draft') { + const draftField = settings?.draftField; + const value = metadata[field.name]; + + return ( + + sendUpdate(field.name, value)} /> + + ); } else { return null; }