diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec3bbf9..8812d0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## [5.1.0] - 2021-10-13 + +### 🎨 Enhancements + +- [#141](https://github.com/estruyf/vscode-front-matter/issues/141): Allow content creation for page bundles or single files +- [#145](https://github.com/estruyf/vscode-front-matter/issues/145): Moved folder registration settings to `frontmatter.json` file +- [#147](https://github.com/estruyf/vscode-front-matter/issues/147): Error boundary added for metadata fields + +### 🐞 Fixes + +- Rendered more hooks than during the previous render in `FileList` +- [#142](https://github.com/estruyf/vscode-front-matter/issues/142): Fix for unknown tags where it throws an error +- [#143](https://github.com/estruyf/vscode-front-matter/issues/143): Fix for duplicate values in the file list +- [#144](https://github.com/estruyf/vscode-front-matter/issues/144): Fix for `toISOString` does not exist on object +- [#146](https://github.com/estruyf/vscode-front-matter/issues/146): Date parsing logic added with fallbacks + ## [5.0.0] - 2021-10-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.0.0) ### ✨ New features diff --git a/assets/icons/frontmatter-short-dark.svg b/assets/icons/frontmatter-short-dark.svg new file mode 100644 index 00000000..be58baa5 --- /dev/null +++ b/assets/icons/frontmatter-short-dark.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/assets/icons/frontmatter-short-light.svg b/assets/icons/frontmatter-short-light.svg new file mode 100644 index 00000000..874be8f6 --- /dev/null +++ b/assets/icons/frontmatter-short-light.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/assets/media/styles.css b/assets/media/styles.css index e792f0a9..40ec8a0f 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -437,6 +437,25 @@ input:checked + .field__toggle__slider:before { margin-right: .5rem; } +.metadata_field__error { + color: var(--vscode-errorForeground); + display: flex; + justify-content: space-between; + align-items: center; +} + +.metadata_field__error button { + color: var(--vscode-button-secondaryForeground); + background-color: var(--vscode-button-secondaryBackground); + padding-left: 1rem; + padding-right: 1rem; + width: auto; +} + +.metadata_field__error button:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + .metadata_field__input, .metadata_field__input:focus, .metadata_field__textarea, .metadata_field__textarea:focus { outline: none; diff --git a/package-lock.json b/package-lock.json index d923a1fc..5be83853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-front-matter-beta", - "version": "5.0.0", + "version": "5.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -236,6 +236,60 @@ "integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==", "dev": true }, + "@microsoft/fast-element": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.6.0.tgz", + "integrity": "sha512-ePTcBuCA99n7He0BYLIzFr5YOHYPSiBLJeDbDsyyQ5JUs4oXaZD0v54Pq0GAtSZuagnCGTDqcxEcBPUhTqLmQw==", + "dev": true + }, + "@microsoft/fast-foundation": { + "version": "1.24.8", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-1.24.8.tgz", + "integrity": "sha512-n4O9jPh8BBliF/Yl9FAVhrSoopsRCnva2L432s/fHwLelY9WUeswjO3DidVBFbzXD5u/gzC4LGWJScNe/ZGU4Q==", + "dev": true, + "requires": { + "@microsoft/fast-element": "^1.4.0", + "@microsoft/fast-web-utilities": "^4.8.0", + "@microsoft/tsdoc-config": "^0.13.4", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@microsoft/fast-web-utilities": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-4.8.1.tgz", + "integrity": "sha512-P3xeyUwQ9nPkFrgAdmkOzaXxIq8YqMU5K+LXcoHgJddJCBCKfGWW9OZQOTigLddItTyVyfO8qsJpDQb1TskKHA==", + "dev": true, + "requires": { + "exenv-es6": "^1.0.0" + } + }, + "@microsoft/tsdoc": { + "version": "0.12.24", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.24.tgz", + "integrity": "sha512-Mfmij13RUTmHEMi9vRUhMXD7rnGR2VvxeNYtaGtaJ4redwwjT4UXYJ+nzmVJF7hhd4pn/Fx5sncDKxMVFJSWPg==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.9.tgz", + "integrity": "sha512-VqqZn+rT9f6XujFPFR2aN9XKF/fuir/IzKVzoxI0vXIzxysp4ee6S2jCakmlGFHEasibifFTsJr7IYmRPxfzYw==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.12.24", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -269,92 +323,92 @@ "dev": true }, "@sentry/browser": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz", - "integrity": "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.3.tgz", + "integrity": "sha512-jwlpsk2/u1cofvfYsjmqcnx50JJtf/T6HTgdW+ih8+rqWC5ABEZf4IiB/H+KAyjJ3wVzCOugMq5irL83XDCfqQ==", "dev": true, "requires": { - "@sentry/core": "6.13.2", - "@sentry/types": "6.13.2", - "@sentry/utils": "6.13.2", + "@sentry/core": "6.13.3", + "@sentry/types": "6.13.3", + "@sentry/utils": "6.13.3", "tslib": "^1.9.3" } }, "@sentry/core": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz", - "integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.3.tgz", + "integrity": "sha512-obm3SjgCk8A7nB37b2AU1eq1q7gMoJRrGMv9VRIyfcG0Wlz/5lJ9O3ohUk+YZaaVfZMxXn6hFtsBiOWmlv7IIA==", "dev": true, "requires": { - "@sentry/hub": "6.13.2", - "@sentry/minimal": "6.13.2", - "@sentry/types": "6.13.2", - "@sentry/utils": "6.13.2", + "@sentry/hub": "6.13.3", + "@sentry/minimal": "6.13.3", + "@sentry/types": "6.13.3", + "@sentry/utils": "6.13.3", "tslib": "^1.9.3" } }, "@sentry/hub": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz", - "integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.3.tgz", + "integrity": "sha512-eYppBVqvhs5cvm33snW2sxfcw6G20/74RbBn+E4WDo15hozis89kU7ZCJDOPkXuag3v1h9igns/kM6PNBb41dw==", "dev": true, "requires": { - "@sentry/types": "6.13.2", - "@sentry/utils": "6.13.2", + "@sentry/types": "6.13.3", + "@sentry/utils": "6.13.3", "tslib": "^1.9.3" } }, "@sentry/minimal": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz", - "integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.3.tgz", + "integrity": "sha512-63MlYYRni3fs5Bh8XBAfVZ+ctDdWg0fapSTP1ydIC37fKvbE+5zhyUqwrEKBIiclEApg1VKX7bkKxVdu/vsFdw==", "dev": true, "requires": { - "@sentry/hub": "6.13.2", - "@sentry/types": "6.13.2", + "@sentry/hub": "6.13.3", + "@sentry/types": "6.13.3", "tslib": "^1.9.3" } }, "@sentry/react": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz", - "integrity": "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.3.tgz", + "integrity": "sha512-fdfmD9XNpGDwdkeLyd+iq+kqtNeghpH3wiez2rD81ZBvrn70uKaO2/yYDE71AXC6fUOwQuJmdfAuqBcNJkYIEw==", "dev": true, "requires": { - "@sentry/browser": "6.13.2", - "@sentry/minimal": "6.13.2", - "@sentry/types": "6.13.2", - "@sentry/utils": "6.13.2", + "@sentry/browser": "6.13.3", + "@sentry/minimal": "6.13.3", + "@sentry/types": "6.13.3", + "@sentry/utils": "6.13.3", "hoist-non-react-statics": "^3.3.2", "tslib": "^1.9.3" } }, "@sentry/tracing": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz", - "integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.3.tgz", + "integrity": "sha512-yyOFIhqlprPM0g4f35Icear3eZk2mwyYcGEzljJfY2iU6pJwj1lzia5PfSwiCW7jFGMmlBJNhOAIpfhlliZi8Q==", "dev": true, "requires": { - "@sentry/hub": "6.13.2", - "@sentry/minimal": "6.13.2", - "@sentry/types": "6.13.2", - "@sentry/utils": "6.13.2", + "@sentry/hub": "6.13.3", + "@sentry/minimal": "6.13.3", + "@sentry/types": "6.13.3", + "@sentry/utils": "6.13.3", "tslib": "^1.9.3" } }, "@sentry/types": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz", - "integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.3.tgz", + "integrity": "sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A==", "dev": true }, "@sentry/utils": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz", - "integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==", + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.3.tgz", + "integrity": "sha512-zYFuFH3MaYtBZTeJ4Yajg7pDf0pM3MWs3+9k5my9Fd+eqNcl7dYQYJbT9gyC0HXK1QI4CAMNNlHNl4YXhF91ag==", "dev": true, "requires": { - "@sentry/types": "6.13.2", + "@sentry/types": "6.13.3", "tslib": "^1.9.3" } }, @@ -586,6 +640,16 @@ "integrity": "sha512-LlO6K7nzrIWDCZN1Zi6J6ibxrpMibSAct+zNjAwpkNkwup6cJLx5diYvsOJODMPWOuQlBO21qkxtdkSRzW6+Jw==", "dev": true }, + "@vscode/webview-ui-toolkit": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-0.8.1.tgz", + "integrity": "sha512-SOgeaZ+/6yFDXLsgH+JvKcs4E0ThvuohNhkr9mvnggjl+OfFxc+Yqkyf5B4B5kuu3EppOCzEMiUwPC8APuLEGQ==", + "dev": true, + "requires": { + "@microsoft/fast-element": "^1.2.0", + "@microsoft/fast-foundation": "^1.24.7" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -2264,6 +2328,12 @@ "safe-buffer": "^5.1.1" } }, + "exenv-es6": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.0.0.tgz", + "integrity": "sha512-fcG/TX8Ruv9Ma6PBaiNsUrHRJzVzuFMP6LtPn/9iqR+nr9mcLeEOGzXQGLC5CVQSXGE98HtzW2mTZkrCA3XrDg==", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3512,6 +3582,12 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3645,6 +3721,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, "lodash.topath": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", @@ -5774,6 +5856,12 @@ } } }, + "tabbable": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.1.tgz", + "integrity": "sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==", + "dev": true + }, "tailwindcss": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.7.tgz", diff --git a/package.json b/package.json index 96167159..2d22a508 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Front Matter", "description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...", "icon": "assets/frontmatter-teal-128x128.png", - "version": "5.0.0", + "version": "5.1.0", "preview": false, "publisher": "eliostruyf", "galleryBanner": { @@ -314,6 +314,11 @@ "name" ] } + }, + "pageBundle": { + "type": "boolean", + "default": false, + "description": "Specify if you want to create a folder when creating new content." } }, "additionalProperties": false, @@ -325,6 +330,7 @@ "default": [ { "name": "default", + "pageBundle": false, "fields": [ { "title": "Title", @@ -658,8 +664,8 @@ "@headlessui/react": "^1.4.1", "@heroicons/react": "1.0.4", "@iarna/toml": "2.2.3", - "@sentry/react": "^6.13.2", - "@sentry/tracing": "^6.13.2", + "@sentry/react": "^6.13.3", + "@sentry/tracing": "^6.13.3", "@tailwindcss/forms": "^0.3.3", "@types/glob": "7.1.3", "@types/js-yaml": "3.12.1", @@ -671,6 +677,7 @@ "@types/react-dom": "17.0.0", "@types/vscode": "1.51.0", "@vscode/codicons": "0.0.20", + "@vscode/webview-ui-toolkit": "^0.8.1", "autoprefixer": "^10.3.2", "css-loader": "5.2.7", "date-fns": "2.23.0", @@ -681,6 +688,7 @@ "html-loader": "1.3.2", "html-webpack-plugin": "4.5.0", "image-size": "^1.0.0", + "lodash-es": "^4.17.21", "lodash.uniqby": "4.7.0", "mdast-util-from-markdown": "1.0.0", "node-json-db": "^1.3.0", diff --git a/src/commands/Article.ts b/src/commands/Article.ts index b6a3f44f..6a7f014f 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -239,13 +239,13 @@ export class Article { /** * Format the date to the defined format */ - public static formatDate(dateValue: Date) { + public static formatDate(dateValue: Date): string { const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; if (dateFormat && typeof dateFormat === "string") { return format(dateValue, dateFormat); } else { - return dateValue.toISOString(); + return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString(); } } diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 62f8588d..c0487931 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -1,6 +1,6 @@ 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 } from '../constants'; import { ArticleHelper } from './../helpers/ArticleHelper'; -import { basename, dirname, extname, join } from "path"; +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'; @@ -14,7 +14,6 @@ import { Template } from './Template'; import { Notifications } from '../helpers/Notifications'; import { Settings } from '../dashboardWebView/models/Settings'; import { Extension } from '../helpers/Extension'; -import { parseJSON } from 'date-fns'; import { ViewType } from '../dashboardWebView/state'; import { EditorHelper, WebviewHelper } from '@estruyf/vscode'; import { MediaInfo, MediaPaths } from './../models/MediaPaths'; @@ -24,6 +23,7 @@ import { ExplorerView } from '../explorerView/ExplorerView'; import { MediaLibrary } from '../helpers/MediaLibrary'; import imageSize from 'image-size'; import { parseWinPath } from '../helpers/parseWinPath'; +import { DateHelper } from '../helpers/DateHelper'; export class Dashboard { private static webview: WebviewPanel | null = null; @@ -97,8 +97,8 @@ export class Dashboard { Dashboard.isDisposed = false; Dashboard.webview.iconPath = { - dark: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter-dark.svg')), - light: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter.svg')) + dark: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-dark.svg')), + light: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-light.svg')) }; Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri); @@ -220,11 +220,28 @@ export class Dashboard { const panel = ExplorerView.getInstance(extensionUri); if (data?.position) { + const wsFolder = Folders.getWorkspaceFolder(); const editor = window.activeTextEditor; const line = data.position.line; const character = data.position.character; if (line) { - await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${data.image})`)); + let imgPath = data.image; + const filePath = data.file; + const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath); + + const imgDir = dirname(absImgPath); + const fileDir = dirname(filePath); + + if (imgDir === fileDir) { + imgPath = join('/', basename(imgPath)); + + // Snippets are already parsed, so update the URL of the image + if (data.snippet) { + data.snippet = data.snippet.replace(data.image, imgPath); + } + } + + await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`)); } panel.getMediaSelection(); } else { @@ -272,16 +289,25 @@ export class Dashboard { /** * Retrieve all media files */ - private static async getMedia(page: number = 0, selectedFolder: string = '') { + private static async getMedia(page: number = 0, requestedFolder: string = '') { const wsFolder = Folders.getWorkspaceFolder(); const staticFolder = SettingsHelper.get(SETTINGS_CONTENT_STATIC_FOLDER); const contentFolders = Folders.get(); + const viewData = Dashboard.viewData; + let selectedFolder = requestedFolder; // If the static folder is not set, retreive the last opened location if (!selectedFolder) { const stateValue = await Extension.getInstance().getState(ExtensionState.SelectedFolder); - if (stateValue && existsSync(stateValue)) { - selectedFolder = stateValue; + + if (stateValue !== HOME_PAGE_NAVIGATION_ID) { + // Support for page bundles + if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) { + const folderPath = parse(viewData.data.filePath).dir; + selectedFolder = folderPath; + } else if (stateValue && existsSync(stateValue)) { + selectedFolder = stateValue; + } } } @@ -379,7 +405,7 @@ export class Dashboard { } // Store the last opened folder - await Extension.getInstance().setState(ExtensionState.SelectedFolder, selectedFolder); + await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder); Dashboard.postWebviewMessage({ command: DashboardCommand.media, @@ -421,7 +447,7 @@ export class Dashboard { fmFilePath: file.filePath, fmFileName: file.fileName, fmDraft: article?.data.draft ? "Draft" : "Published", - fmYear: article?.data[dateField] ? parseJSON(article?.data[dateField]).getFullYear() : null, + fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null, // Make sure these are always set title: article?.data.title, slug: article?.data.slug, @@ -432,7 +458,7 @@ export class Dashboard { const contentType = ArticleHelper.getContentType(article.data); const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview"; - + if (article?.data[previewField] && wsFolder) { let fieldValue = article?.data[previewField]; if (fieldValue && Array.isArray(fieldValue)) { @@ -596,7 +622,9 @@ export class Dashboard { const nonce = WebviewHelper.getNonce(); - const version = Extension.getInstance().getVersion(); + const ext = Extension.getInstance(); + const version = ext.getVersion(); + const isBeta = ext.isBetaVersion(); return ` @@ -608,7 +636,7 @@ export class Dashboard { Front Matter Dashboard -
+
Daily usage diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 163cacc3..f528a2b4 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -274,7 +274,11 @@ export class Folders { */ private static async update(folders: ContentFolder[]) { const wsFolder = Folders.getWorkspaceFolder(); - await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folders.map(folder => ({ title: folder.title, path: Folders.relWsFolder(folder, wsFolder) }))); + let folderDetails = folders.map(folder => ({ + title: folder.title, + path: Folders.relWsFolder(folder, wsFolder) + })); + await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true); } /** diff --git a/src/commands/Template.ts b/src/commands/Template.ts index da9c7120..c7cdecd1 100644 --- a/src/commands/Template.ts +++ b/src/commands/Template.ts @@ -3,14 +3,14 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import { SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants'; -import { format } from 'date-fns'; -import sanitize from '../helpers/Sanitize'; import { ArticleHelper, Settings } from '../helpers'; import { Article } from '.'; import { Notifications } from '../helpers/Notifications'; import { CONTEXT } from '../constants'; import { Project } from './Project'; import { Folders } from './Folders'; +import { ContentType } from '../helpers/ContentType'; +import { ContentType as IContentType } from '../models'; export class Template { @@ -95,7 +95,7 @@ export class Template { */ public static async create(folderPath: string) { const folder = Settings.get(SETTING_TEMPLATES_FOLDER); - const prefix = Settings.get(SETTING_TEMPLATES_PREFIX); + const contentTypes = ContentType.getAll(); if (!folderPath) { Notifications.warning(`Incorrect project folder path retrieved.`); @@ -133,16 +133,14 @@ export class Template { return; } - const fileExt = path.parse(selectedTemplate).ext; - const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-")); - let newFileName = `${sanitizedName}${fileExt}`; - if (prefix && typeof prefix === "string") { - newFileName = `${format(new Date(), prefix)}-${newFileName}`; + const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath); + let contentType: IContentType | undefined; + if (templateData && templateData.data && templateData.data.type) { + contentType = contentTypes?.find(t => t.name === templateData.data.type); } - const newFilePath = path.join(folderPath, newFileName); - if (fs.existsSync(newFilePath)) { - Notifications.warning(`File already exists, please remove it before creating a new one with the same title.`); + let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue); + if (!newFilePath) { return; } @@ -162,7 +160,7 @@ export class Template { fmData.title = titleValue; } if (typeof fmData.slug !== "undefined") { - fmData.slug = sanitizedName; + fmData.slug = ArticleHelper.sanitize(titleValue); } frontMatter = Article.updateDate(frontMatter); diff --git a/src/constants/ContentType.ts b/src/constants/ContentType.ts index 6041395e..181993f2 100644 --- a/src/constants/ContentType.ts +++ b/src/constants/ContentType.ts @@ -4,6 +4,7 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default'; export const DEFAULT_CONTENT_TYPE: ContentType = { "name": "default", + "pageBundle": false, "fields": [ { "title": "Title", diff --git a/src/dashboardWebView/components/DateField.tsx b/src/dashboardWebView/components/DateField.tsx index f783209d..9a97478f 100644 --- a/src/dashboardWebView/components/DateField.tsx +++ b/src/dashboardWebView/components/DateField.tsx @@ -1,5 +1,6 @@ -import { format, parseJSON } from 'date-fns'; +import { format } from 'date-fns'; import * as React from 'react'; +import { DateHelper } from '../../helpers/DateHelper'; export interface IDateFieldProps { value: Date | string; @@ -10,9 +11,9 @@ export const DateField: React.FunctionComponent = ({value}: Rea React.useEffect(() => { try { - const parsedValue = typeof value === 'string' ? parseJSON(value) : value; - const dateString = format(parsedValue, 'yyyy-MM-dd'); - setDateValue(dateString); + const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value) : value; + const dateString = parsedValue ? format(parsedValue, 'yyyy-MM-dd') : parsedValue; + setDateValue(dateString || ""); } catch (e) { // Date is invalid } diff --git a/src/dashboardWebView/index.tsx b/src/dashboardWebView/index.tsx index 94144d19..8b23f39f 100644 --- a/src/dashboardWebView/index.tsx +++ b/src/dashboardWebView/index.tsx @@ -7,10 +7,17 @@ import { Integrations } from "@sentry/tracing"; import { SENTRY_LINK } from "../constants"; import './styles.css'; +const elm = document.querySelector("#app"); +const welcome = elm?.getAttribute("data-showWelcome"); +const version = elm?.getAttribute("data-version"); +const environment = elm?.getAttribute("data-environment"); + Sentry.init({ dsn: SENTRY_LINK, integrations: [new Integrations.BrowserTracing()], tracesSampleRate: 0, // No performance tracing required + release: version || "", + environment: environment || "" }); declare const acquireVsCodeApi: () => { @@ -18,7 +25,4 @@ declare const acquireVsCodeApi: () => { setState: (data: T) => void; postMessage: (msg: unknown) => void; }; - -const elm = document.querySelector("#app"); -const welcome = elm?.getAttribute("data-showWelcome"); render(, elm); \ No newline at end of file diff --git a/src/dashboardWebView/models/Page.ts b/src/dashboardWebView/models/Page.ts index 536ecd69..d1e037bb 100644 --- a/src/dashboardWebView/models/Page.ts +++ b/src/dashboardWebView/models/Page.ts @@ -6,7 +6,7 @@ export interface Page { fmFileName: string; fmModified: number; fmDraft: "Draft" | "Published", - fmYear: number | null; + fmYear: number | null | undefined; title: string; slug: string; diff --git a/src/explorerView/ExplorerView.ts b/src/explorerView/ExplorerView.ts index fcd52e51..deb91a98 100644 --- a/src/explorerView/ExplorerView.ts +++ b/src/explorerView/ExplorerView.ts @@ -633,7 +633,9 @@ export class ExplorerView implements WebviewViewProvider, Disposable { const nonce = WebviewHelper.getNonce(); - const version = Extension.getInstance().getVersion(); + const ext = Extension.getInstance(); + const version = ext.getVersion(); + const isBeta = ext.isBetaVersion(); return ` @@ -648,7 +650,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable { Front Matter -
+
Daily usage diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 63db4cad..d89fd355 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -1,17 +1,19 @@ import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType'; -import { ContentType } from './../models/PanelSettings'; import * as vscode from 'vscode'; import * as matter from "gray-matter"; import * as fs from "fs"; -import { DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES } from '../constants'; +import { DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants'; import { DumpOptions } from 'js-yaml'; import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine'; import { Settings } from '.'; -import { parse } from 'date-fns'; +import { format, parse } from 'date-fns'; import { Notifications } from './Notifications'; import { Article } from '../commands'; -import { basename } from 'path'; +import { basename, join } from 'path'; import { EditorHelper } from '@estruyf/vscode'; +import sanitize from '../helpers/Sanitize'; +import { existsSync, mkdirSync } from 'fs'; +import { ContentType } from '../models'; export class ArticleHelper { @@ -164,6 +166,57 @@ export class ArticleHelper { return metadata; } + /** + * Sanitize the value + * @param value + * @returns + */ + public static sanitize(value: string): string { + return sanitize(value.toLowerCase().replace(/ /g, "-")); + } + + /** + * Create the file or folder for the new content + * @param contentType + * @param folderPath + * @param titleValue + * @returns The new file path + */ + public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string): string | undefined { + const prefix = Settings.get(SETTING_TEMPLATES_PREFIX); + + // Name of the file or folder to create + const sanitizedName = ArticleHelper.sanitize(titleValue); + let newFilePath: string | undefined; + + // Create a folder with the `index.md` file + if (contentType?.pageBundle) { + const newFolder = join(folderPath, sanitizedName); + if (existsSync(newFolder)) { + Notifications.error(`A page bundle with the name ${sanitizedName} already exists in ${folderPath}`); + return; + } else { + mkdirSync(newFolder); + newFilePath = join(newFolder, `index.md`); + } + } else { + let newFileName = `${sanitizedName}.md`; + + if (prefix && typeof prefix === "string") { + newFileName = `${format(new Date(), prefix)}-${newFileName}`; + } + + newFilePath = join(folderPath, newFileName); + + if (existsSync(newFilePath)) { + Notifications.warning(`Content with the title already exists. Please specify a new title.`); + return; + } + } + + return newFilePath; + } + /** * Parse a markdown file and its front matter * @param fileContents diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 11924f68..b7500fe5 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -4,10 +4,9 @@ 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 { existsSync, mkdirSync, writeFileSync } from "fs"; import { Notifications } from "./Notifications"; import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType"; @@ -51,27 +50,18 @@ export class ContentType { } private static async create(contentType: IContentType, folderPath: string) { - const prefix = Settings.get(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.`); + let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue); + if (!newFilePath) { return; } - const data: any = {}; + let data: any = {}; for (const field of contentType.fields) { if (field.name === "title") { @@ -81,6 +71,8 @@ export class ContentType { } } + data = ArticleHelper.updateDates(Object.assign({}, data)); + if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) { data['type'] = contentType.name; } diff --git a/src/helpers/DateHelper.ts b/src/helpers/DateHelper.ts new file mode 100644 index 00000000..c6e069e4 --- /dev/null +++ b/src/helpers/DateHelper.ts @@ -0,0 +1,64 @@ +import { parse, parseISO, parseJSON } from "date-fns"; + + +export class DateHelper { + + public static tryParse(date: any, format?: string): Date | null { + if (!date) { + return null; + } + + if (date instanceof Date) { + return date; + } + + if (typeof date === 'string') { + const jsonParsed = DateHelper.tryParseJson(date); + if (DateHelper.isValid(jsonParsed)) { + return jsonParsed; + } + + const isoParsed = DateHelper.tryParseIso(date); + if (DateHelper.isValid(isoParsed)) { + return isoParsed; + } + + if (format) { + const formatParsed = DateHelper.tryFormatParse(date, format); + if (DateHelper.isValid(formatParsed)) { + return formatParsed; + } + } + } + + return null; + } + + public static isValid(date: any): boolean { + return !isNaN(date.getTime()); + } + + public static tryFormatParse(date: string, format: string): Date | null { + try { + return parse(date, format, new Date()); + } catch (err) { + return null; + } + } + + public static tryParseJson(date: string): Date | null { + try { + return parseJSON(date); + } catch (err) { + return null; + } + } + + public static tryParseIso(date: string): Date | null { + try { + return parseISO(date); + } catch (err) { + return null; + } + } +} \ No newline at end of file diff --git a/src/helpers/Extension.ts b/src/helpers/Extension.ts index c63cc30e..a445a379 100644 --- a/src/helpers/Extension.ts +++ b/src/helpers/Extension.ts @@ -97,6 +97,10 @@ export class Extension { return; } + if (!versionInfo.usedVersion) { + return; + } + // Split semantic version const version = versionInfo.usedVersion.split('.'); const major = parseInt(version[0]); diff --git a/src/helpers/MediaLibrary.ts b/src/helpers/MediaLibrary.ts index b86736bc..f7f09a75 100644 --- a/src/helpers/MediaLibrary.ts +++ b/src/helpers/MediaLibrary.ts @@ -14,11 +14,15 @@ interface MediaRecord { } export class MediaLibrary { - private db: JsonDB; + private db: JsonDB | undefined; private static instance: MediaLibrary; private constructor() { const wsFolder = Folders.getWorkspaceFolder(); + if (!wsFolder) { + return; + } + this.db = new JsonDB(join(parseWinPath(wsFolder?.fsPath || ""), LocalStore.rootFolder, LocalStore.contentFolder, LocalStore.mediaDatabaseFile), true, false, '/'); workspace.onDidRenameFiles(e => { @@ -46,7 +50,7 @@ export class MediaLibrary { public get(id: string): MediaRecord | undefined { try { const fileId = this.parsePath(id); - if (this.db.exists(fileId)) { + if (this.db?.exists(fileId)) { return this.db.getData(fileId); } return undefined; @@ -57,16 +61,16 @@ export class MediaLibrary { public set(id: string, metadata: any): void { const fileId = this.parsePath(id); - this.db.push(fileId, metadata, true); + this.db?.push(fileId, metadata, true); } public rename(oldId: string, newId: string): void { const fileId = this.parsePath(oldId); const newFileId = this.parsePath(newId); - const data = this.db.getData(fileId); + const data = this.db?.getData(fileId); if (data) { - this.db.delete(fileId); - this.db.push(newFileId, data, true); + this.db?.delete(fileId); + this.db?.push(newFileId, data, true); } } diff --git a/src/helpers/Questions.ts b/src/helpers/Questions.ts index 51eb8bbd..1051dfd3 100644 --- a/src/helpers/Questions.ts +++ b/src/helpers/Questions.ts @@ -5,6 +5,16 @@ import { Notifications } from './Notifications'; export class Questions { + /** + * Yes/No question + * @param placeholder + * @returns + */ + public static async yesOrNo(placeholder: string) { + const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: false }); + return answer === "yes"; + } + /** * Specify the name of the content to create * @param showWarning diff --git a/src/helpers/SettingsHelper.ts b/src/helpers/SettingsHelper.ts index 36ff27d0..ab4d2176 100644 --- a/src/helpers/SettingsHelper.ts +++ b/src/helpers/SettingsHelper.ts @@ -136,7 +136,7 @@ export class Settings { Settings.globalConfig = JSON.parse(localConfig); Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value; writeFileSync(fmConfig, JSON.stringify(Settings.globalConfig, null, 2), 'utf8'); - + const workspaceSettingValue = Settings.hasWorkspaceSettings(name); if (workspaceSettingValue) { await Settings.update(name, undefined); diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 5f5b1b4d..d1b5513d 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -22,6 +22,8 @@ export interface PanelSettings { export interface ContentType { name: string; fields: Field[]; + + pageBundle?: boolean; } export interface Field { diff --git a/src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx b/src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx new file mode 100644 index 00000000..be3fa44d --- /dev/null +++ b/src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import * as Sentry from "@sentry/react"; +import { VsLabel } from '../VscodeComponents'; + +export interface IFieldBoundaryProps { + fieldName: string; +} + +export interface IFieldBoundaryState { + hasError: boolean; +} + +export default class FieldBoundary extends React.Component { + + constructor(props: IFieldBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + public static getDerivedStateFromError(error: any) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + public componentDidCatch(error: any, errorInfo: any) { + Sentry.captureMessage(`Field boundary: ${error?.message || error}`); + } + + public render(): React.ReactElement { + if (this.state.hasError) { + return ( +
+ +
+ {this.props.fieldName} +
+
+
+ Error loading field + + +
+
+ ); + } + + return this.props.children as any; + } +} \ No newline at end of file diff --git a/src/panelWebView/components/Fields/DateTimeField.tsx b/src/panelWebView/components/Fields/DateTimeField.tsx index b18b0c65..8b125ceb 100644 --- a/src/panelWebView/components/Fields/DateTimeField.tsx +++ b/src/panelWebView/components/Fields/DateTimeField.tsx @@ -3,6 +3,7 @@ import { VsLabel } from '../VscodeComponents'; import { ClockIcon } from '@heroicons/react/outline'; import DatePicker from 'react-datepicker'; import { forwardRef } from 'react'; +import { DateHelper } from '../../../helpers/DateHelper'; export interface IDateTimeFieldProps { label: string; @@ -22,7 +23,7 @@ const CustomInput = forwardRef(({ value, onClick } }); export const DateTimeField: React.FunctionComponent = ({label, date, format, onChange}: React.PropsWithChildren) => { - const [ dateValue, setDateValue ] = React.useState(date); + const [ dateValue, setDateValue ] = React.useState(null); const onDateChange = (date: Date) => { setDateValue(date); @@ -30,7 +31,10 @@ export const DateTimeField: React.FunctionComponent = ({lab }; React.useEffect(() => { - if (dateValue?.toISOString() !== date?.toISOString()) { + const crntValue = DateHelper.tryParse(date, format); + const stateValue = DateHelper.tryParse(dateValue, format); + + if (crntValue?.toISOString() !== stateValue?.toISOString()) { setDateValue(date); } }, [ date ]); @@ -45,7 +49,7 @@ export const DateTimeField: React.FunctionComponent = ({lab
= ({ name, path }: React.PropsWithChildren) => { + + const openFile = () => { + MessageHelper.sendMessage(CommandToCode.openInEditor, path); + }; + + return ( +
  • + { + (name.endsWith('.md') || name.endsWith('.mdx')) ? ( + + ) : ( + + ) + } + + {name} +
  • + ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/FileList.tsx b/src/panelWebView/components/FileList.tsx index 716daeb7..857d6373 100644 --- a/src/panelWebView/components/FileList.tsx +++ b/src/panelWebView/components/FileList.tsx @@ -1,9 +1,6 @@ import * as React from 'react'; import { FileInfo } from '../../models'; -import { CommandToCode } from '../CommandToCode'; -import { MessageHelper } from '../../helpers/MessageHelper'; -import { FileIcon } from './Icons/FileIcon'; -import { MarkdownIcon } from './Icons/MarkdownIcon'; +import { FileItem } from './FileItem'; import { VsLabel } from './VscodeComponents'; export interface IFileListProps { @@ -13,10 +10,6 @@ export interface IFileListProps { } export const FileList: React.FunctionComponent = ({files, folderName, totalFiles}: React.PropsWithChildren) => { - - const openFile = (filePath: string) => { - MessageHelper.sendMessage(CommandToCode.openInEditor, filePath); - }; if (!files || files.length === 0) { return null; @@ -28,20 +21,8 @@ export const FileList: React.FunctionComponent = ({files, folder
      { - files.map(file => ( -
    • openFile(file.filePath)}> - { - (file.fileName.endsWith('.md') || file.fileName.endsWith('.mdx')) ? ( - - ) : ( - - ) - } - - {file.fileName} -
    • + (files && files.length > 0) && files.map(file => ( + )) }
    diff --git a/src/panelWebView/components/GlobalSettings.tsx b/src/panelWebView/components/GlobalSettings.tsx index 0acd6bb6..85fd8446 100644 --- a/src/panelWebView/components/GlobalSettings.tsx +++ b/src/panelWebView/components/GlobalSettings.tsx @@ -48,11 +48,11 @@ export const GlobalSettings: React.FunctionComponent = ({s
    Modified date - + Auto-update modified date
    Front Matter highlight - + Highlight Front Matter
    Local preview diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index 697ad3cb..389f32a1 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -8,7 +8,6 @@ import { Toggle } from './Fields/Toggle'; import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon'; import { TagIcon } from './Icons/TagIcon'; import { TagPicker } from './TagPicker'; -import { parseJSON } from 'date-fns'; import { DateTimeField } from './Fields/DateTimeField'; import { TextField } from './Fields/TextField'; import "react-datepicker/dist/react-datepicker.css"; @@ -17,6 +16,8 @@ import { ListUnorderedIcon } from './Icons/ListUnorderedIcon'; import { NumberField } from './Fields/NumberField'; import { ChoiceField } from './Fields/ChoiceField'; import useContentType from '../../hooks/useContentType'; +import { DateHelper } from '../../helpers/DateHelper'; +import FieldBoundary from './ErrorBoundary/FieldBoundary'; export interface IMetadataProps { settings: PanelSettings | undefined; @@ -39,11 +40,9 @@ export const Metadata: React.FunctionComponent = ({settings, met }); }; - const getDate = (date: string | Date) => { - if (typeof date === 'string') { - return parseJSON(date); - } - return date; + const getDate = (date: string | Date): Date | null => { + const parsedDate = DateHelper.tryParse(date, settings?.date?.format); + return parsedDate || date as Date | null; } if (!settings) { @@ -64,20 +63,23 @@ export const Metadata: React.FunctionComponent = ({settings, met const dateValue = metadata[field.name] ? getDate(metadata[field.name] as string) : null; return ( - sendUpdate(field.name, date))} /> + + sendUpdate(field.name, date))} /> + ); } else if (field.type === 'boolean') { return ( - sendUpdate(field.name, checked)} /> + + sendUpdate(field.name, checked)} /> + ); } else if (field.type === 'string') { const textValue = metadata[field.name]; @@ -90,14 +92,15 @@ export const Metadata: React.FunctionComponent = ({settings, met } return ( - sendUpdate(field.name, value)} - value={textValue as string || null} /> + + sendUpdate(field.name, value)} + value={textValue as string || null} /> + ); } else if (field.type === 'number') { const fieldValue = metadata[field.name]; @@ -107,61 +110,67 @@ export const Metadata: React.FunctionComponent = ({settings, met } return ( - sendUpdate(field.name, value)} - value={nrValue} /> + + sendUpdate(field.name, value)} + value={nrValue} /> + ); } else if (field.type === 'image') { return ( - sendUpdate(field.name, value))} /> + + sendUpdate(field.name, value))} /> + ); } else if (field.type === 'choice') { const choices = field.choices || []; const choiceValue = metadata[field.name]; return ( - sendUpdate(field.name, value))} /> + + sendUpdate(field.name, value))} /> + ); } else if (field.type === 'tags') { return ( - } - crntSelected={metadata[field.name] as string[] || []} - options={settings?.tags || []} - freeform={settings.freeform} - focussed={focusElm === TagType.tags} - unsetFocus={unsetFocus} /> + + } + crntSelected={metadata[field.name] as string[] || []} + options={settings?.tags || []} + freeform={settings.freeform} + focussed={focusElm === TagType.tags} + unsetFocus={unsetFocus} /> + ); } else if (field.type === 'categories') { return ( - } - crntSelected={metadata.categories as string[] || []} - options={settings.categories} - freeform={settings.freeform} - focussed={focusElm === TagType.categories} - unsetFocus={unsetFocus} /> + + } + crntSelected={metadata.categories as string[] || []} + options={settings.categories} + freeform={settings.freeform} + focussed={focusElm === TagType.categories} + unsetFocus={unsetFocus} /> + ); } else { return null; diff --git a/src/panelWebView/components/TagPicker.tsx b/src/panelWebView/components/TagPicker.tsx index 2afdba20..9be777ef 100644 --- a/src/panelWebView/components/TagPicker.tsx +++ b/src/panelWebView/components/TagPicker.tsx @@ -83,7 +83,7 @@ export const TagPicker: React.FunctionComponent = (props: React if (selectedItem) { let value = selectedItem || ""; - const item = options.find(o => o.toLowerCase() === selectedItem.toLowerCase()); + const item = options.find(o => o?.toLowerCase() === selectedItem?.toLowerCase()); if (item) { value = item; } @@ -187,7 +187,7 @@ export const TagPicker: React.FunctionComponent = (props: React a.toLowerCase() < b.toLowerCase() ? -1 : 1 )} + values={(selected || []).sort((a: string, b: string) => a?.toLowerCase() < b?.toLowerCase() ? -1 : 1 )} onRemove={onRemove} onCreate={onCreate} options={options} diff --git a/src/panelWebView/components/Tags.tsx b/src/panelWebView/components/Tags.tsx index f60dbf15..9a5091a3 100644 --- a/src/panelWebView/components/Tags.tsx +++ b/src/panelWebView/components/Tags.tsx @@ -16,17 +16,24 @@ export const Tags: React.FunctionComponent = (props: React.PropsWith const knownTags = values.filter(v => options.includes(v)); const unknownTags = values.filter(v => !options.includes(v)); + + const generateKey = (tag: string, idx: number) => { + if (tag) { + return `${tag.replace(/ /g, "_")}-${idx}`; + } + return `tag-${idx}`; + }; return (
    { - knownTags.map(t => ( - + knownTags.map((t, idx) => ( + )) } { - unknownTags.map(t => ( - + unknownTags.map((t, idx) => ( + )) }
    diff --git a/src/panelWebView/components/VscodeComponents.ts b/src/panelWebView/components/VscodeComponents.ts index 113587bb..2cddfe86 100644 --- a/src/panelWebView/components/VscodeComponents.ts +++ b/src/panelWebView/components/VscodeComponents.ts @@ -1,5 +1,6 @@ import {wrapWc} from 'wc-react'; +// @bendera/vscode-webview-elements export const VsTable = wrapWc(`vscode-table`); export const VsTableHeader = wrapWc(`vscode-table-header`); export const VsTableHeaderCell = wrapWc(`vscode-table-header-cell`); @@ -7,5 +8,7 @@ export const VsTableBody = wrapWc(`vscode-table-body`); export const VsTableRow = wrapWc(`vscode-table-row`); export const VsTableCell = wrapWc(`vscode-table-cell`); export const VsCollapsible = wrapWc(`vscode-collapsible`); -export const VsCheckbox = wrapWc(`vscode-checkbox`); -export const VsLabel = wrapWc(`vscode-label`); \ No newline at end of file +export const VsLabel = wrapWc(`vscode-label`); + +// @vscode/webview-ui-toolkit +export const VsCheckbox = wrapWc(`vscode-checkbox`); \ No newline at end of file diff --git a/src/panelWebView/index.tsx b/src/panelWebView/index.tsx index 6e8c6157..eb3242d5 100644 --- a/src/panelWebView/index.tsx +++ b/src/panelWebView/index.tsx @@ -13,13 +13,20 @@ import '@bendera/vscode-webview-elements/dist/vscode-table-body'; import '@bendera/vscode-webview-elements/dist/vscode-table-row'; import '@bendera/vscode-webview-elements/dist/vscode-table-cell'; import '@bendera/vscode-webview-elements/dist/vscode-collapsible'; -import '@bendera/vscode-webview-elements/dist/vscode-checkbox'; import '@bendera/vscode-webview-elements/dist/vscode-label'; +import '@vscode/webview-ui-toolkit/dist/esm/checkbox'; + +const elm = document.querySelector("#app"); +const version = elm?.getAttribute("data-version"); +const environment = elm?.getAttribute("data-environment"); + Sentry.init({ dsn: SENTRY_LINK, integrations: [new Integrations.BrowserTracing()], tracesSampleRate: 0, // No performance tracing required + release: version || "", + environment: environment || "" }); declare const acquireVsCodeApi: () => { @@ -28,5 +35,4 @@ declare const acquireVsCodeApi: () => { postMessage: (msg: unknown) => void; }; -const elm = document.querySelector("#app"); render(, elm);