diff --git a/assets/media/styles.css b/assets/media/styles.css index 2df5fbea..049ae142 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -506,4 +506,22 @@ input:checked + .field__toggle__slider:before { .sponsor a > span { margin-right: .25rem; +} + +/* Timepicker */ +.react-datepicker button:hover { + background-color: none !important; +} + +.react-datepicker__triangle { + transform: translate3d(15px, 0px, 0px) !important; +} + +.react-datepicker-time__input { + background: transparent !important; + color: #000 !important; +} + +.react-datepicker-time__input input { + border: 1px solid #aeaeae !important; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2ccfde93..a4c96379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -262,6 +262,12 @@ "fastq": "^1.6.0" } }, + "@popperjs/core": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz", + "integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==", + "dev": true + }, "@tailwindcss/forms": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.3.tgz", @@ -375,6 +381,18 @@ "csstype": "^3.0.2" } }, + "@types/react-datepicker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.1.7.tgz", + "integrity": "sha512-8FZt62redGDsO/Dptb8/kdu/JZsAD17lsU3E8OwJqxhtNk4EsdVT0v2ArP8efDGkmtonIKVF2usoPzF6ZeL8zw==", + "dev": true, + "requires": { + "@popperjs/core": "^2.9.2", + "@types/react": "*", + "date-fns": "^2.0.1", + "react-popper": "^2.2.5" + } + }, "@types/react-dom": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", @@ -1360,6 +1378,12 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", + "dev": true + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -4808,6 +4832,20 @@ "object-assign": "^4.1.1" } }, + "react-datepicker": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.2.1.tgz", + "integrity": "sha512-0gcvHMnX8rS1fV90PjjsB7MQdsWNU77JeVHf6bbwK9HnFxgwjVflTx40ebKmHV+leqe+f+FgUP9Nvqbe5RGyfA==", + "dev": true, + "requires": { + "@popperjs/core": "^2.9.2", + "classnames": "^2.2.6", + "date-fns": "^2.0.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.10.0", + "react-popper": "^2.2.5" + } + }, "react-dom": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", @@ -4830,12 +4868,34 @@ "prop-types": "^15.7.2" } }, + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", + "dev": true + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "react-onclickoutside": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.0.tgz", + "integrity": "sha512-oPlOTYcISLHfpMog2lUZMFSbqOs4LFcA4+vo7fpfevB5v9Z0D5VBDBkfeO5lv+hpEcGoaGk67braLT+QT+eICA==", + "dev": true + }, + "react-popper": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", + "integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==", + "dev": true, + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -6161,6 +6221,15 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", diff --git a/package.json b/package.json index 14f75e41..70d49876 100644 --- a/package.json +++ b/package.json @@ -407,6 +407,7 @@ "@types/mocha": "^5.2.6", "@types/node": "10.17.48", "@types/react": "17.0.0", + "@types/react-datepicker": "^4.1.7", "@types/react-dom": "17.0.0", "@types/vscode": "1.51.0", "@vscode/codicons": "0.0.20", @@ -424,6 +425,7 @@ "postcss": "^8.3.6", "postcss-loader": "4.3.0", "react": "17.0.1", + "react-datepicker": "4.2.1", "react-dom": "17.0.1", "react-dropzone": "^11.3.4", "recoil": "^0.4.1", diff --git a/src/commands/Article.ts b/src/commands/Article.ts index 90c0b9d1..5bcfdfe7 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -7,6 +7,7 @@ import { ArticleHelper, SettingsHelper, SlugHelper } from '../helpers'; import matter = require('gray-matter'); import { Notifications } from '../helpers/Notifications'; import { extname, basename } from 'path'; +import { DefaultFields } from '../constants'; export class Article { @@ -102,12 +103,11 @@ export class Article { */ public static updateDate(article: matter.GrayMatterFile, forceCreate: boolean = false) { const config = SettingsHelper.getConfig(); - const dateFormat = config.get(SETTING_DATE_FORMAT) as string; - const dateField = config.get(SETTING_DATE_FIELD) as string || "date"; - const modField = config.get(SETTING_MODIFIED_FIELD) as string || "date"; + const dateField = config.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate; + const modField = config.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.PublishingDate; - article = this.articleDate(article, dateFormat, dateField, forceCreate); - article = this.articleDate(article, dateFormat, modField, false); + article = this.articleDate(article, dateField, forceCreate); + article = this.articleDate(article, modField, false); return article; } @@ -128,19 +128,13 @@ export class Article { } const cloneArticle = Object.assign({}, article); - const dateFormat = config.get(SETTING_DATE_FORMAT) as string; - const dateField = config.get(SETTING_MODIFIED_FIELD) as string || "lastmod"; + const dateField = config.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified; try { - if (dateFormat && typeof dateFormat === "string") { - cloneArticle.data[dateField] = format(new Date(), dateFormat); - } else { - cloneArticle.data[dateField] = new Date().toISOString(); - } + cloneArticle.data[dateField] = Article.formatDate(new Date()); ArticleHelper.update(editor, cloneArticle); - } catch (e) { + } catch (e: any) { Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`); - console.log(e.message); } } @@ -250,6 +244,20 @@ export class Article { } } + /** + * Format the date to the defined format + */ + public static formatDate(dateValue: Date) { + const config = SettingsHelper.getConfig(); + const dateFormat = config.get(SETTING_DATE_FORMAT) as string; + + if (dateFormat && typeof dateFormat === "string") { + return format(dateValue, dateFormat); + } else { + return dateValue.toISOString(); + } + } + /** * Get the current article */ @@ -274,13 +282,9 @@ export class Article { * @param field * @param forceCreate */ - private static articleDate(article: matter.GrayMatterFile, dateFormat: string, field: string, forceCreate: boolean) { + private static articleDate(article: matter.GrayMatterFile, field: string, forceCreate: boolean) { if (typeof article.data[field] !== "undefined" || forceCreate) { - if (dateFormat && typeof dateFormat === "string") { - article.data[field] = format(new Date(), dateFormat); - } else { - article.data[field] = new Date().toISOString(); - } + article.data[field] = Article.formatDate(new Date()); } return article; } diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 09a425a1..6da79d52 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -20,6 +20,7 @@ import { ViewType } from '../pagesView/state'; import { WebviewHelper } from '@estruyf/vscode'; import { MediaInfo, MediaPaths } from './../models/MediaPaths'; import { decodeBase64Image } from '../helpers/decodeBase64Image'; +import { DefaultFields } from '../constants'; export class Dashboard { @@ -280,8 +281,8 @@ export class Dashboard { const config = SettingsHelper.getConfig(); const wsFolder = Folders.getWorkspaceFolder(); - const descriptionField = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || "description"; - const dateField = config.get(SETTING_DATE_FIELD) as string || "date"; + const descriptionField = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; + const dateField = config.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate; const staticFolder = config.get(SETTINGS_CONTENT_STATIC_FOLDERS); const folderInfo = await Folders.getInfo(); diff --git a/src/commands/StatusListener.ts b/src/commands/StatusListener.ts index 200fc49b..deae23e5 100644 --- a/src/commands/StatusListener.ts +++ b/src/commands/StatusListener.ts @@ -2,6 +2,7 @@ import { SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_ import * as vscode from 'vscode'; import { ArticleHelper, SeoHelper, SettingsHelper } from '../helpers'; import { ExplorerView } from '../webview/ExplorerView'; +import { DefaultFields } from '../constants'; export class StatusListener { @@ -39,7 +40,7 @@ export class StatusListener { const config = SettingsHelper.getConfig(); const titleLength = config.get(SETTING_SEO_TITLE_LENGTH) as number || -1; const descLength = config.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1; - const fieldName = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || "description"; + const fieldName = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; if (article.data.title && titleLength > -1) { SeoHelper.checkLength(editor, collection, article, "title", titleLength); diff --git a/src/constants/DefaultFields.ts b/src/constants/DefaultFields.ts new file mode 100644 index 00000000..72dc3027 --- /dev/null +++ b/src/constants/DefaultFields.ts @@ -0,0 +1,6 @@ + +export const DefaultFields = { + PublishingDate: `date`, + LastModified: `lastmod`, + Description: `description` +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index fa8ac58b..cf830ff8 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1 +1,7 @@ +export * from './DefaultFields'; +export * from './Extension'; +export * from './Links'; +export * from './charMap'; +export * from './context'; export * from './settings'; +export * from './stopwords-en'; diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 9e4408be..be8c799e 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as matter from "gray-matter"; import * as fs from "fs"; -import { CONFIG_KEY, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES } from '../constants'; +import { CONFIG_KEY, DefaultFields, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES } from '../constants'; import { DumpOptions } from 'js-yaml'; import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine'; import { SettingsHelper } from '.'; @@ -117,7 +117,7 @@ export class ArticleHelper { const config = SettingsHelper.getConfig(); const dateFormat = config.get(SETTING_DATE_FORMAT) as string; - const dateField = config.get(SETTING_DATE_FIELD) as string || "date"; + const dateField = config.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate; if (typeof article.data[dateField] !== "undefined") { if (dateFormat && typeof dateFormat === "string") { diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index b40da5e4..7d53b5bc 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -4,6 +4,7 @@ export interface PanelSettings { seo: SEO; slug: Slug; tags: string[]; + date: DateInfo; categories: string[]; freeform: boolean; scripts: CustomScript[]; @@ -14,6 +15,12 @@ export interface PanelSettings { preview: PreviewSettings; } +export interface DateInfo { + format: string; + pubDate: string; + modDate: string; +} + export interface SEO { title: number; description: number; diff --git a/src/viewpanel/components/Fields/DateTime.tsx b/src/viewpanel/components/Fields/DateTime.tsx new file mode 100644 index 00000000..ad5d418b --- /dev/null +++ b/src/viewpanel/components/Fields/DateTime.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { VsLabel } from '../VscodeComponents'; +import { ClockIcon } from '@heroicons/react/outline'; +import DatePicker from 'react-datepicker'; +import { forwardRef } from 'react'; + +export interface IDateTimeProps { + label: string; + date: Date | null; + format?: string; + onChange: (date: Date) => void; +} + +type InputProps = JSX.IntrinsicElements["input"]; + +const CustomInput = forwardRef(({ value, onClick }, ref) => { + return ( + + ) +}); + +export const DateTime: React.FunctionComponent = ({label, date, format, onChange}: React.PropsWithChildren) => { + const [ dateValue, setDateValue ] = React.useState(date); + + const onDateChange = (date: Date) => { + setDateValue(date); + onChange(date); + }; + + React.useEffect(() => { + if (dateValue?.toISOString() !== date?.toISOString()) { + setDateValue(date); + } + }, [ date ]); + + return ( +
+ +
+ {label} +
+
+ + )} + showTimeInput + /> +
+ ); +}; \ No newline at end of file diff --git a/src/viewpanel/components/Metadata.tsx b/src/viewpanel/components/Metadata.tsx index 75e04952..c1657cde 100644 --- a/src/viewpanel/components/Metadata.tsx +++ b/src/viewpanel/components/Metadata.tsx @@ -10,40 +10,84 @@ import { RocketIcon } from './Icons/RocketIcon'; import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon'; import { TagIcon } from './Icons/TagIcon'; import { TagPicker } from './TagPicker'; -import { VsCheckbox, VsLabel } from './VscodeComponents'; +import { VsLabel } from './VscodeComponents'; +import { useState } from 'react'; + +import "react-datepicker/dist/react-datepicker.css"; +import { parseJSON } from 'date-fns'; +import { DateTime } from './Fields/DateTime'; export interface IMetadataProps { settings: PanelSettings | undefined; - metadata: { [prop: string]: string[] | null }; + metadata: { [prop: string]: string[] | string | null }; focusElm: TagType | null; unsetFocus: () => void; } export const Metadata: React.FunctionComponent = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren) => { - const sendUpdate = (field: string, value: any) => { + const sendUpdate = (field: string | undefined, value: any) => { + if (!field) { + return; + } + MessageHelper.sendMessage(CommandToCode.updateMetadata, { field, value }); }; + const getDate = (date: string | Date) => { + if (typeof date === 'string') { + return parseJSON(date); + } + return date; + } + + let publishing: Date | null = null; + let modifying: Date | null = null; + + if (settings?.date) { + const { modDate, pubDate } = settings.date; + publishing = metadata[pubDate] ? getDate(metadata[pubDate] as string) : null; + modifying = metadata[modDate] ? getDate(metadata[modDate] as string) : null; + } + return ( + sendUpdate(settings?.date?.pubDate, date))} /> + + { + modifying && ( + sendUpdate(settings?.date?.modDate, date))} /> + ) + } +
Published
- sendUpdate("draft", !checked)} /> + + sendUpdate("draft", !checked)} />
{ } - crntSelected={metadata.keywords || []} + crntSelected={metadata.keywords as string[] || []} options={[]} freeform={true} focussed={focusElm === TagType.keywords} @@ -54,7 +98,7 @@ export const Metadata: React.FunctionComponent = ({settings, met (settings && settings.tags && settings.tags.length > 0) && ( } - crntSelected={metadata.tags || []} + crntSelected={metadata.tags as string[] || []} options={settings.tags} freeform={settings.freeform} focussed={focusElm === TagType.tags} @@ -65,7 +109,7 @@ export const Metadata: React.FunctionComponent = ({settings, met (settings && settings.categories && settings.categories.length > 0) && ( } - crntSelected={metadata.categories || []} + crntSelected={metadata.categories as string[] || []} options={settings.categories} freeform={settings.freeform} focussed={focusElm === TagType.categories} diff --git a/src/webview/ExplorerView.ts b/src/webview/ExplorerView.ts index 29695cc8..1ecd5a51 100644 --- a/src/webview/ExplorerView.ts +++ b/src/webview/ExplorerView.ts @@ -1,9 +1,9 @@ import { Template } from './../commands/Template'; -import { 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 } from './../constants/settings'; +import { 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_DATE_FIELD, SETTING_MODIFIED_FIELD } from './../constants/settings'; import * as os from 'os'; import { PanelSettings, CustomScript } from './../models/PanelSettings'; import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode"; -import { 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, 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 { ArticleHelper, SettingsHelper } from "../helpers"; import { Command } from "../viewpanel/Command"; import { CommandToCode } from '../viewpanel/CommandToCode'; @@ -242,6 +242,10 @@ export class ExplorerView implements WebviewViewProvider, Disposable { * Update the metadata of the article */ private updateMetadata({field, value}: { field: string, value: string }) { + const config = SettingsHelper.getConfig(); + const pubDate = config.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate; + const modDate = config.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified; + if (!field) { return; } @@ -256,8 +260,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable { return; } - article.data[field] = value; - ArticleHelper.update(editor, article); + if ((field === pubDate || field === modDate) && value) { + article.data[field] = Article.formatDate(new Date(value)); + } else { + article.data[field] = value; + } + ArticleHelper.update(editor, article); } /** @@ -315,13 +323,18 @@ export class ExplorerView implements WebviewViewProvider, Disposable { title: config.get(SETTING_SEO_TITLE_LENGTH) as number || -1, description: config.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1, content: config.get(SETTING_SEO_CONTENT_MIN_LENGTH) as number || -1, - descriptionField: config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || "description" + descriptionField: config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description }, slug: { prefix: config.get(SETTING_SLUG_PREFIX) || "", suffix: config.get(SETTING_SLUG_SUFFIX) || "", updateFileName: !!config.get(SETTING_SLUG_UPDATE_FILE_NAME), }, + date: { + format: config.get(SETTING_DATE_FORMAT), + pubDate: config.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate, + modDate: config.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified + }, tags: config.get(SETTING_TAXONOMY_TAGS) || [], categories: config.get(SETTING_TAXONOMY_CATEGORIES) || [], freeform: config.get(SETTING_PANEL_FREEFORM), diff --git a/webpack.config.js b/webpack.config.js index ee1b632f..2edc0564 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -56,6 +56,10 @@ const config = [ use: [{ loader: 'ts-loader' }] + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'] } ] },