Compare commits

...

20 Commits

Author SHA1 Message Date
Elio Struyf
731212264c New content type relationship field implementation 2023-11-01 14:41:04 +01:00
Elio Struyf
3a8dcbe22f New combobox implementation 2023-11-01 12:55:28 +01:00
Elio Struyf
6cbe76096b Fix css name spaces 2023-11-01 12:19:46 +01:00
Elio Struyf
039170eae5 Clear cache on new version 2023-10-31 15:05:18 +01:00
Elio Struyf
14ddd8b53c Added the path to the parsed article 2023-10-31 11:44:02 +01:00
Elio Struyf
b60aadc4a3 Status optimizations 2023-10-31 10:24:34 +01:00
Elio Struyf
79ee6607ca Filter out empty folder settings + added the content type name 2023-10-30 11:06:38 +01:00
Elio Struyf
fe9fbe899d #699 - Change border color 2023-10-27 13:08:57 +02:00
Elio Struyf
a196c4192a Merge localization fix 2023-10-27 09:25:09 +02:00
Elio Struyf
0f1085756c #694 - Start terminal session from the folder where the frontmatter.json file is located 2023-10-26 09:28:27 +02:00
Elio Struyf
83f103b991 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2023-10-26 09:15:04 +02:00
Elio Struyf
13f5446163 #696 - Close the local server terminal on restart 2023-10-26 09:14:59 +02:00
Elio Struyf
96c496caba #273 - Allow single value arrays to be set as a string 2023-10-24 15:14:39 +02:00
Elio Struyf
7c2a59615f #691 - Silent authentication retrieval for GitHub sponsors 2023-10-23 11:23:15 +02:00
Elio Struyf
0ea972e4f3 Grid optimizations 2023-10-23 11:13:03 +02:00
Elio Struyf
d1eb252380 #688 - Add scheduled articles 2023-10-19 16:08:37 +02:00
Elio Struyf
3e038c58a3 Fix answers 2023-10-11 11:18:11 +02:00
Elio Struyf
2cb72a607b Enhancement: Extend the scripting feature to allow users to ask questions #686 2023-10-09 20:19:25 +02:00
Elio Struyf
2f7d8e5816 9.4.0 2023-10-09 09:12:41 +02:00
Elio Struyf
1337b21789 #685 - Fix when using non-string values in the tag picker 2023-10-09 09:12:37 +02:00
59 changed files with 900 additions and 491 deletions

View File

@@ -0,0 +1 @@
{}

View File

@@ -1,5 +1,34 @@
# Change Log
## [9.4.0] - 2023-xx-xx
### ✨ New features
### 🎨 Enhancements
- [#273](https://github.com/estruyf/vscode-front-matter/issues/273): Allow single value arrays to be set as a string with the `singleValueAsString` field property
- [#686](https://github.com/estruyf/vscode-front-matter/issues/686): Allow script authors to ask questions during script execution
- [#688](https://github.com/estruyf/vscode-front-matter/issues/688): Allow to show the scheduled articles in the content dashboard (filter and group)
### ⚡️ Optimizations
- Dashboard layout grid optimizations
- Added the content-type name to the metadata section in the panel
### 🐞 Fixes
- [#685](https://github.com/estruyf/vscode-front-matter/issues/685): Fix when using non-string values in the tag picker
- [#691](https://github.com/estruyf/vscode-front-matter/issues/691): Silent authentication retrieval for GitHub sponsors
- [#694](https://github.com/estruyf/vscode-front-matter/issues/694): Start terminal session from the folder where the `frontmatter.json` file is located
- [#696](https://github.com/estruyf/vscode-front-matter/issues/696): Close the local server terminal on restart
- [#699](https://github.com/estruyf/vscode-front-matter/issues/699): Changing border theme variable for the dashboard header
## [9.3.1] - 2023-10-27
### 🐞 Fixes
- [#697](https://github.com/estruyf/vscode-front-matter/issues/697): Fix missing localization key
## [9.3.0] - 2023-10-06 - [Release notes](https://beta.frontmatter.codes/updates/v9.3.0)
### ✨ New features

View File

@@ -27,6 +27,7 @@
"common.refreshSettings": "Refresh settings",
"common.pin": "Pin",
"common.unpin": "Unpin",
"common.noResults": "No results",
"settings.view.common": "Common",
"settings.view.contentFolders": "Content folders",
@@ -84,6 +85,7 @@
"dashboard.contents.status.draft": "Draft",
"dashboard.contents.status.published": "Published",
"dashboard.contents.status.scheduled": "Scheduled",
"dashboard.dataView.dataForm.modify": "Modify the data",
"dashboard.dataView.dataForm.add": "Add new data",
@@ -122,6 +124,7 @@
"dashboard.header.navigation.allArticles": "All articles",
"dashboard.header.navigation.published": "Published",
"dashboard.header.navigation.scheduled": "Scheduled",
"dashboard.header.navigation.draft": "In draft",
"dashboard.header.header.createContent": "Create content",

34
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "vscode-front-matter-beta",
"version": "9.3.0",
"version": "9.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-front-matter-beta",
"version": "9.3.0",
"version": "9.4.0",
"license": "MIT",
"devDependencies": {
"@actions/core": "^1.10.0",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "^1.1.0",
"@headlessui/react": "1.5.0",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@octokit/rest": "^18.12.0",
@@ -348,9 +348,13 @@
}
},
"node_modules/@headlessui/react": {
"version": "1.5.0",
"version": "1.7.17",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz",
"integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==",
"dev": true,
"license": "MIT",
"dependencies": {
"client-only": "^0.0.1"
},
"engines": {
"node": ">=10"
},
@@ -2960,6 +2964,12 @@
"node": ">= 4.0"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"dev": true
},
"node_modules/clipboardy": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
@@ -13314,9 +13324,13 @@
}
},
"@headlessui/react": {
"version": "1.5.0",
"version": "1.7.17",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz",
"integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==",
"dev": true,
"requires": {}
"requires": {
"client-only": "^0.0.1"
}
},
"@heroicons/react": {
"version": "1.0.4",
@@ -15272,6 +15286,12 @@
"source-map": "~0.6.0"
}
},
"client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"dev": true
},
"clipboardy": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",

View File

@@ -3,14 +3,15 @@
"displayName": "Front Matter CMS",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "9.3.0",
"version": "9.4.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
"color": "#0e131f",
"theme": "dark"
},
"badges": [{
"badges": [
{
"description": "version",
"url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square",
"href": "https://github.com/estruyf/vscode-front-matter"
@@ -70,7 +71,8 @@
"**/.frontmatter/config/*.json": "jsonc"
}
},
"keybindings": [{
"keybindings": [
{
"command": "frontMatter.dashboard",
"key": "alt+d"
},
@@ -88,19 +90,23 @@
}
],
"viewsContainers": {
"activitybar": [{
"id": "frontmatter-explorer",
"title": "FM",
"icon": "$(fm-logo)"
}]
"activitybar": [
{
"id": "frontmatter-explorer",
"title": "FM",
"icon": "$(fm-logo)"
}
]
},
"views": {
"frontmatter-explorer": [{
"id": "frontMatter.explorer",
"name": "Front Matter",
"icon": "$(fm-logo)",
"type": "webview"
}]
"frontmatter-explorer": [
{
"id": "frontMatter.explorer",
"name": "Front Matter",
"icon": "$(fm-logo)",
"type": "webview"
}
]
},
"configuration": {
"title": "%settings.configuration.title%",
@@ -168,7 +174,8 @@
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"oneOf": [{
"oneOf": [
{
"enum": [
"md",
"mdx"
@@ -184,7 +191,8 @@
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
"oneOf": [{
"oneOf": [
{
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
@@ -536,7 +544,8 @@
"command": {
"$id": "#scriptCommand",
"type": "string",
"anyOf": [{
"anyOf": [
{
"enum": [
"node",
"bash",
@@ -743,7 +752,8 @@
"title",
"file"
],
"anyOf": [{
"anyOf": [
{
"required": [
"schema"
]
@@ -797,7 +807,8 @@
"id",
"path"
],
"anyOf": [{
"anyOf": [
{
"required": [
"schema"
]
@@ -993,7 +1004,7 @@
"frontMatter.panel.actions.disabled": {
"type": "array",
"default": [],
"markdownDescription": "%setting.frontMatter.panel.actions.disabeld.markdownDescription%",
"markdownDescription": "%setting.frontMatter.panel.actions.disabled.markdownDescription%",
"enum": [
"openDashboard",
"createContent",
@@ -1185,7 +1196,8 @@
"default": "",
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
"not": {
"anyOf": [{
"anyOf": [
{
"const": ""
},
{
@@ -1260,6 +1272,11 @@
"default": 0,
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyLimit.description%"
},
"singleValueAsString": {
"type": "boolean",
"default": false,
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.singleValueAsString.description%"
},
"isPublishDate": {
"type": "boolean",
"default": false,
@@ -1374,7 +1391,8 @@
"type",
"name"
],
"allOf": [{
"allOf": [
{
"if": {
"properties": {
"type": {
@@ -1574,48 +1592,51 @@
"fields"
]
},
"default": [{
"name": "default",
"pageBundle": false,
"fields": [{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}],
"default": [
{
"name": "default",
"pageBundle": false,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}
],
"scope": "Taxonomy"
},
"frontMatter.taxonomy.customTaxonomy": {
@@ -1628,7 +1649,8 @@
"type": "string",
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
"not": {
"anyOf": [{
"anyOf": [
{
"const": ""
},
{
@@ -1814,7 +1836,8 @@
}
}
},
"commands": [{
"commands": [
{
"command": "frontMatter.project.switch",
"title": "%command.frontMatter.project.switch%",
"category": "Front Matter",
@@ -2131,12 +2154,15 @@
"category": "Front Matter"
}
],
"submenus": [{
"id": "frontmatter.submenu",
"label": "Front Matter"
}],
"submenus": [
{
"id": "frontmatter.submenu",
"label": "Front Matter"
}
],
"menus": {
"editor/title": [{
"editor/title": [
{
"command": "frontMatter.markup.heading",
"group": "navigation@-133",
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
@@ -2217,11 +2243,14 @@
"when": "resourceFilename == 'frontmatter.json'"
}
],
"explorer/context": [{
"submenu": "frontmatter.submenu",
"group": "frontmatter@1"
}],
"frontmatter.submenu": [{
"explorer/context": [
{
"submenu": "frontmatter.submenu",
"group": "frontmatter@1"
}
],
"frontmatter.submenu": [
{
"command": "frontMatter.createFromTemplate",
"when": "explorerResourceIsFolder",
"group": "frontmatter@1"
@@ -2237,7 +2266,8 @@
"group": "frontmatter@3"
}
],
"commandPalette": [{
"commandPalette": [
{
"command": "frontMatter.init",
"when": "frontMatterCanInit"
},
@@ -2382,7 +2412,8 @@
"when": "frontMatter:file:isValid == true"
}
],
"view/title": [{
"view/title": [
{
"command": "frontMatter.chatbot",
"group": "navigation@0",
"when": "view == frontMatter.explorer"
@@ -2414,52 +2445,57 @@
}
]
},
"grammars": [{
"path": "./syntaxes/hugo.tmLanguage.json",
"scopeName": "frontmatter.markdown.hugo",
"injectTo": [
"text.html.markdown"
]
}],
"walkthroughs": [{
"id": "frontmatter.welcome",
"title": "Get started with Front Matter",
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
"steps": [{
"id": "frontmatter.welcome.init",
"title": "Get started",
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
"media": {
"markdown": "assets/walkthrough/get-started.md"
"grammars": [
{
"path": "./syntaxes/hugo.tmLanguage.json",
"scopeName": "frontmatter.markdown.hugo",
"injectTo": [
"text.html.markdown"
]
}
],
"walkthroughs": [
{
"id": "frontmatter.welcome",
"title": "Get started with Front Matter",
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
"steps": [
{
"id": "frontmatter.welcome.init",
"title": "Get started",
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
"media": {
"markdown": "assets/walkthrough/get-started.md"
},
"completionEvents": [
"onContext:frontMatterInitialized"
]
},
"completionEvents": [
"onContext:frontMatterInitialized"
]
},
{
"id": "frontmatter.welcome.documentation",
"title": "Documentation",
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
"media": {
"markdown": "assets/walkthrough/documentation.md"
{
"id": "frontmatter.welcome.documentation",
"title": "Documentation",
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
"media": {
"markdown": "assets/walkthrough/documentation.md"
},
"completionEvents": [
"onLink:https://frontmatter.codes/docs"
]
},
"completionEvents": [
"onLink:https://frontmatter.codes/docs"
]
},
{
"id": "frontmatter.welcome.supporter",
"title": "Support the project",
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
"media": {
"markdown": "assets/walkthrough/support-the-project.md"
},
"completionEvents": [
"onLink:https://github.com/sponsors/estruyf"
]
}
]
}]
{
"id": "frontmatter.welcome.supporter",
"title": "Support the project",
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
"media": {
"markdown": "assets/walkthrough/support-the-project.md"
},
"completionEvents": [
"onLink:https://github.com/sponsors/estruyf"
]
}
]
}
]
},
"scripts": {
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
@@ -2490,7 +2526,7 @@
"@actions/core": "^1.10.0",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "^1.1.0",
"@headlessui/react": "1.5.0",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@octokit/rest": "^18.12.0",
@@ -2596,4 +2632,4 @@
"vsce": {
"dependencies": false
}
}
}

View File

@@ -154,6 +154,7 @@
"setting.frontMatter.media.defaultSorting.markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.defaultsorting)",
"setting.frontMatter.media.supportedMimeTypes.markdownDescription": "Specify the mime types to support for the media files. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.supportedmimetypes)",
"setting.frontMatter.panel.freeform.markdownDescription": "Specifies if you want to allow yourself from entering unknown tags/categories in the tag picker (when enabled, you will have the option to store them afterwards). Default: true. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.freeform)",
"setting.frontMatter.panel.actions.disabled.markdownDescription": "Specify the actions you want to disable in the panel. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.actions.disabled)",
"setting.frontMatter.preview.host.markdownDescription": "Specify the host URL (example: http://localhost:1313) to be used when opening the preview. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.host)",
"setting.frontMatter.preview.pathName.markdownDescription": "Specify the path you want to add after the host and before your slug. This can be used for instance to include the year/month like: `yyyy/MM`. The date will be generated based on the article its date field value. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.pathname)",
"setting.frontMatter.site.baseURL.markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.site.baseurl)",
@@ -190,6 +191,7 @@
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.properties.max.description": "The maximum value",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.properties.step.description": "The step value",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyLimit.description": "Limit the number of taxonomies to select. Set to 0 to allow unlimited.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.singleValueAsString.description": "Specify if you want to store a single array value as a string instead of an array.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isPublishDate.description": "Specify if the field is the publish date field",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isModifiedDate.description": "Specify if the field is the modified date field",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileId.description": "Specify the ID of the data file to use for this field",

View File

@@ -127,7 +127,7 @@ export class Article {
* @param article
*/
public static updateDate(article: ParsedFrontMatter) {
article.data = ArticleHelper.updateDates(article.data);
article.data = ArticleHelper.updateDates(article);
return article;
}
@@ -227,7 +227,7 @@ export class Article {
}
let filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
filePrefix = ArticleHelper.getFilePrefix(filePrefix, editor.document.uri.fsPath, contentType);
const titleField = 'title';
@@ -393,7 +393,7 @@ export class Article {
const article = ArticleHelper.getFrontMatter(editor);
const contentType =
article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
article && article.data ? ArticleHelper.getContentType(article) : DEFAULT_CONTENT_TYPE;
const position = editor.selection.active;
const selectionText = editor.document.getText(editor.selection);

View File

@@ -1,6 +1,6 @@
import { commands } from 'vscode';
import { COMMAND_NAME, ExtensionState } from '../constants';
import { Extension, Notifications } from '../helpers';
import { Extension, Logger, Notifications } from '../helpers';
export class Cache {
public static async registerCommands() {
@@ -29,6 +29,8 @@ export class Cache {
if (showNotification) {
Notifications.info('Cache cleared');
} else {
Logger.info('Cache cleared');
}
}
}

View File

@@ -361,7 +361,10 @@ export class Folders {
*/
public static get(): ContentFolder[] {
const wsFolder = Folders.getWorkspaceFolder();
const folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
let folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
// Filter out folders without a path
folders = folders.filter((f) => f.path);
const contentFolders = folders.map((folder) => {
if (!folder.title) {

View File

@@ -213,7 +213,7 @@ export class Preview {
* @returns
*/
public static async getContentSlug(
article: ParsedFrontMatter | null,
article: ParsedFrontMatter | null | undefined,
filePath?: string
): Promise<string | undefined> {
if (!filePath) {
@@ -230,7 +230,7 @@ export class Preview {
let contentType: ContentType | undefined = undefined;
if (article?.data) {
contentType = ArticleHelper.getContentType(article.data);
contentType = ArticleHelper.getContentType(article);
}
// Check if there is a pathname defined on content folder level

View File

@@ -165,11 +165,15 @@ export class Template {
newFilePath
);
frontMatter = Article.updateDate(frontMatter);
const article = Article.updateDate(frontMatter);
if (!article) {
return;
}
await writeFileAsync(
newFilePath,
ArticleHelper.stringifyFrontMatter(frontMatter.content, frontMatter.data),
ArticleHelper.stringifyFrontMatter(article.content, article.data),
{ encoding: 'utf8' }
);

View File

@@ -37,7 +37,7 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
return (
<PageLayout folders={pageFolders} totalPages={pageItems.length}>
<div className="w-full flex-grow max-w-7xl mx-auto pb-6 px-4">
<div className="w-full flex-grow max-w-full mx-auto pb-6 px-4">
{loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} />}
</div>

View File

@@ -91,7 +91,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
<img
src={`${pageData[PREVIEW_IMAGE_FIELD]}`}
alt={escapedTitle || ""}
className="absolute inset-0 h-full w-full object-cover group-hover:brightness-75"
className="absolute inset-0 h-full w-full object-cover object-left-top group-hover:brightness-75"
loading="lazy"
/>
) : (
@@ -110,7 +110,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
statusHtml ? (
<div dangerouslySetInnerHTML={{ __html: statusHtml }} />
) : (
cardFields?.state && draftField && draftField.name && <Status draft={pageData[draftField.name]} />
cardFields?.state && draftField && draftField.name && <Status draft={pageData[draftField.name]} published={pageData.fmPublished} />
)
}
@@ -214,7 +214,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
<DateField value={pageData.date} />
</div>
<div className="col-span-2">
{draftField && draftField.name && <Status draft={pageData[draftField.name]} />}
{draftField && draftField.name && <Status draft={pageData[draftField.name]} published={pageData.fmPublished} />}
</div>
</div>
</li>

View File

@@ -16,7 +16,7 @@ export const List: React.FunctionComponent<IListProps> = ({
let className = '';
if (view === DashboardViewType.Grid) {
className = `grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`;
className = `grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4`;
} else if (view === DashboardViewType.List) {
className = `-mx-4`;
}

View File

@@ -66,6 +66,44 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
[grouping]
);
const { groupKeys, groupedPages } = useMemo(() => {
if (grouping === GroupOption.none) {
return { groupKeys: [], groupedPages: {} };
}
let groupedPages = groupBy(pages, grouping === GroupOption.Year ? 'fmYear' : 'fmDraft');
let groupKeys = Object.keys(groupedPages);
if (grouping === GroupOption.Year) {
groupKeys = groupKeys.sort((a, b) => {
return parseInt(b) - parseInt(a);
});
} else if (grouping === GroupOption.Draft && settings?.draftField?.type !== 'choice') {
const isInverted = settings?.draftField?.invert;
const allPublished: Page[] = groupedPages['Published'] || [];
const allDrafts: Page[] = groupedPages['Draft'] || [];
if (allPublished.length > 0) {
const drafts = !isInverted ? allDrafts : allPublished;
const published = (!isInverted ? allPublished : allDrafts).filter((page) => !page.fmPublished || page.fmPublished <= Date.now());
const scheduled = (!isInverted ? allPublished : allDrafts).filter((page) => page.fmPublished && page.fmPublished > Date.now());
delete groupedPages["Published"];
delete groupedPages["Draft"];
groupKeys = ['Scheduled', ...groupKeys];
groupedPages = {
"Scheduled": scheduled,
"Published": published,
"Draft": drafts,
...groupedPages,
}
}
}
return { groupKeys, groupedPages };
}, [pages, grouping, settings?.draftField]);
React.useEffect(() => {
messageHandler.request<string[]>(DashboardMessage.getPinnedItems).then((items) => {
setIsReady(true);
@@ -99,40 +137,33 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
}
if (grouping !== GroupOption.none) {
const groupedPages = groupBy(pages, grouping === GroupOption.Year ? 'fmYear' : 'fmDraft');
let groupKeys = Object.keys(groupedPages);
if (grouping === GroupOption.Year) {
groupKeys = groupKeys.sort((a, b) => {
return parseInt(b) - parseInt(a);
});
}
return (
<>
{groupKeys.map((groupId, idx) => (
<Disclosure key={groupId} as={`div`} className={`w-full`} defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className={`mb-4 ${idx !== 0 ? 'mt-8' : ''}`}>
<h2 className={`text-2xl font-bold flex items-center`}>
<ChevronRightIcon
className={`w-8 h-8 mr-1 ${open ? 'transform rotate-90' : ''}`}
/>
{groupName(groupId, groupedPages)}
</h2>
</Disclosure.Button>
groupedPages[groupId].length > 0 && (
<Disclosure key={groupId} as={`div`} className={`w-full`} defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className={`mb-4 ${idx !== 0 ? 'mt-8' : ''}`}>
<h2 className={`text-2xl font-bold flex items-center`}>
<ChevronRightIcon
className={`w-8 h-8 mr-1 ${open ? 'transform rotate-90' : ''}`}
/>
{groupName(groupId, groupedPages)}
</h2>
</Disclosure.Button>
<Disclosure.Panel>
<List>
{groupedPages[groupId].map((page: Page) => (
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>
</Disclosure.Panel>
</>
)}
</Disclosure>
<Disclosure.Panel>
<List>
{groupedPages[groupId].map((page: Page) => (
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>
</Disclosure.Panel>
</>
)}
</Disclosure>
)
))}
</>
);

View File

@@ -7,15 +7,18 @@ import { LocalizationKey } from '../../../localization';
export interface IStatusProps {
draft: boolean | string;
published: number | null | undefined;
}
export const Status: React.FunctionComponent<IStatusProps> = ({
draft
draft,
published
}: React.PropsWithChildren<IStatusProps>) => {
const settings = useRecoilValue(SettingsAtom);
const tabInfo = useRecoilValue(TabInfoAtom);
const draftField = useMemo(() => settings?.draftField, [settings]);
const isFuture = useMemo(() => published ? published > Date.now() : false, [published]);
const draftValue = useMemo(() => {
if (draftField && draftField.type === 'choice') {
@@ -31,7 +34,7 @@ export const Status: React.FunctionComponent<IStatusProps> = ({
if (draftValue) {
return (
<span
className={`inline-block px-2 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-xs text-[var(--vscode-badge-foreground)] bg-[var(--vscode-badge-background)]`}
className={`inline-block px-1 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-[0.7rem] text-[var(--vscode-badge-foreground)] bg-[var(--vscode-badge-background)]`}
>
{draftValue}
</span>
@@ -48,16 +51,21 @@ export const Status: React.FunctionComponent<IStatusProps> = ({
return (
<span
className={`draft__status
inline-block px-2 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-xs
inline-block px-1 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-[0.7rem]
${draftValue ?
'bg-[var(--vscode-statusBarItem-errorBackground)] text-[var(--vscode-statusBarItem-errorForeground)]' :
'bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)]'
isFuture ?
'bg-[var(--vscode-statusBarItem-warningBackground)] text-[var(--vscode-statusBarItem-warningForeground)]' :
'bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)]'
}`}
>
{
draftValue ?
l10n.t(LocalizationKey.dashboardContentsStatusDraft) :
l10n.t(LocalizationKey.dashboardContentsStatusPublished)
l10n.t(LocalizationKey.dashboardContentsStatusDraft) : (
isFuture ?
l10n.t(LocalizationKey.dashboardContentsStatusScheduled) :
l10n.t(LocalizationKey.dashboardContentsStatusPublished)
)
}
</span>
);

View File

@@ -5,7 +5,6 @@ import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';
import { AutoFields, AutoForm, ErrorsField } from '../../../components/uniforms-frontmatter';
import { ErrorBoundary } from '@sentry/react';
import { DataFormControls } from './DataFormControls';
import useThemeColors from '../../hooks/useThemeColors';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
@@ -24,7 +23,6 @@ export const DataForm: React.FunctionComponent<IDataFormProps> = ({
}: React.PropsWithChildren<IDataFormProps>) => {
const [bridge, setBridge] = useState<JSONSchemaBridge | null>(null);
const [error, setError] = useState<string | undefined>(undefined);
const { getColors } = useThemeColors();
const ajv = new Ajv({
allErrors: true,
@@ -81,11 +79,11 @@ export const DataForm: React.FunctionComponent<IDataFormProps> = ({
<ErrorBoundary>
<div className="autoform">
{model ? (
<h2 className={getColors(`text - gray - 500 dark: text - whisper - 900`, `text - [var(--frontmatter - secondary - text)]`)}>
<h2 className={`text-[var(--frontmatter-secondary-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewDataFormModify)}
</h2>
) : (
<h2 className={getColors(`text - gray - 500 dark: text - whisper - 900`, `text - [var(--frontmatter - secondary - text)]`)}>
<h2 className={`text-[var(--frontmatter-secondary-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewDataFormAdd)}
</h2>
)}

View File

@@ -39,7 +39,7 @@ export const FilterInput: React.FunctionComponent<IFilterInputProps> = ({
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded ${getColors(
'bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
placeholder={placeholder || l10n.t(LocalizationKey.commonSearch)}

View File

@@ -25,7 +25,6 @@ import { Pagination } from './Pagination';
import { GroupOption } from '../../constants/GroupOption';
import usePagination from '../../hooks/usePagination';
import { PaginationStatus } from './PaginationStatus';
import useThemeColors from '../../hooks/useThemeColors';
import { Navigation } from './Navigation';
import { ProjectSwitcher } from './ProjectSwitcher';
import * as l10n from '@vscode/l10n';
@@ -55,7 +54,6 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
const location = useLocation();
const navigate = useNavigate();
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
const { getColors } = useThemeColors();
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
@@ -144,16 +142,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
}, [location.search]);
return (
<div className={`w-full sticky top-0 z-20 ${getColors(
`bg-gray-100 dark:bg-vulcan-500`,
`bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]`
)
}`}>
<div className={`mb-0 border-b flex justify-between ${getColors(
`bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300`,
`bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--vscode-editorWidget-border)]`
)
}`}>
<div className={`w-full sticky top-0 z-20 bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]`}>
<div className={`mb-0 border-b flex justify-between bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--frontmatter-border)]`}>
<Tabs onNavigate={updateView} />
<div className='flex'>
@@ -180,8 +170,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
</div>
</div>
<div className={`px-4 flex flex-row items-center border-b justify-between ${getColors(`border-gray-200 dark:border-vulcan-100`, `border-[var(--vscode-editorWidget-border)]`)
}`}>
<div className={`px-4 flex flex-row items-center border-b justify-between border-[var(--frontmatter-border)]`}>
<div>
<Navigation totalPages={totalPages || 0} />
</div>
@@ -192,8 +181,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
</div>
<div
className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end border-b space-x-4 lg:space-x-6 xl:space-x-8 ${getColors(`bg-gray-200 border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100`, `bg-[var(--vscode-panel-background)] border-[var(--vscode-editorWidget-border)]`)
}`}
className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end border-b space-x-4 lg:space-x-6 xl:space-x-8 bg-[var(--vscode-panel-background)] border-[var(--frontmatter-border)]`}
>
<ClearFilters />
@@ -222,8 +210,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
(totalPages || 0) > pageSetNr &&
(!grouping || grouping === GroupOption.none) && (
<div
className={`px-4 flex justify-between py-2 border-b ${getColors(`border-gray-300 dark:border-vulcan-100`, `border-[var(--vscode-editorWidget-border)]`)
}`}
className={`px-4 flex justify-between py-2 border-b border-[var(--frontmatter-border)]`}
>
<PaginationStatus totalPages={totalPages || 0} />

View File

@@ -51,6 +51,11 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({
if (settings?.draftField?.type === 'boolean' && tabInfo && Object.keys(tabInfo).length > 1) {
crntTabs.push({ name: l10n.t(LocalizationKey.dashboardHeaderNavigationPublished), id: Tab.Published });
if (tabInfo.scheduled) {
crntTabs.push({ name: l10n.t(LocalizationKey.dashboardHeaderNavigationScheduled), id: Tab.Scheduled });
}
crntTabs.push({ name: l10n.t(LocalizationKey.dashboardHeaderNavigationDraft), id: Tab.Draft });
}

View File

@@ -56,7 +56,7 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded ${getColors(
'bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border, --vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
placeholder={placeholder || l10n.t(LocalizationKey.commonSearch)}

View File

@@ -26,7 +26,7 @@ export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({
<div
className={
contentClass ||
'w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4'
'w-full flex justify-between flex-col flex-grow mx-auto pt-6 px-4 max-w-full xl:max-w-[90%]'
}
>
{children}

View File

@@ -14,12 +14,11 @@ export const DetailsInput: React.FunctionComponent<IDetailsInputProps> = ({ valu
return (
<textarea
rows={3}
className={`py-1 px-2 sm:text-sm border w-full ${
getColors(
'bg-white dark:bg-vulcan-300 border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
className={`py-1 px-2 sm:text-sm border w-full ${getColors(
'bg-white dark:bg-vulcan-300 border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
value={value}
onChange={onChange}
/>
@@ -28,12 +27,11 @@ export const DetailsInput: React.FunctionComponent<IDetailsInputProps> = ({ valu
return (
<input
className={`py-1 px-2 sm:text-sm border w-full ${
getColors(
'bg-white dark:bg-vulcan-300 border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
className={`py-1 px-2 sm:text-sm border w-full ${getColors(
'bg-white dark:bg-vulcan-300 border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
value={value}
onChange={onChange}
/>

View File

@@ -8,12 +8,12 @@ export const List: React.FunctionComponent<IListProps> = ({
gap,
children
}: React.PropsWithChildren<IListProps>) => {
const gapClass = gap !== undefined ? `gap-y-${gap}` : `gap-y-8`;
const gapClass = gap !== undefined ? `gap-y-${gap}` : ``;
return (
<ul
role="list"
className={`grid grid-cols-2 gap-x-4 ${gapClass} sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}
className={`grid gap-4 ${gapClass} grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5`}
>
{children}
</ul>

View File

@@ -105,7 +105,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
onChange={(e) => onMediaSnippetUpdate(e.currentTarget.checked)}
className={`h-4 w-4 rounded ${getColors(
`focus:ring-teal-500 text-teal-600 border-gray-300 dark:border-vulcan-50`,
`focus:ring-[var(--frontmatter-button-background)] text-[var(--frontmatter-button-background)] border-[var(--vscode-editorWidget-border)]`
`focus:ring-[var(--frontmatter-button-background)] text-[var(--frontmatter-button-background)] border-[var(--frontmatter-border)]`
)
}`}
/>

View File

@@ -37,7 +37,7 @@ export const FilterInput: React.FunctionComponent<IFilterInputProps> = ({
<input
type="text"
name="filter"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent`}
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent`}
placeholder={placeholder || ""}
value={value}
onChange={(e) => onChange && onChange(e.target.value)}

View File

@@ -1,5 +1,6 @@
export enum Tab {
All = 'all',
Published = 'published',
Draft = 'draft'
Draft = 'draft',
Scheduled = 'scheduled'
}

View File

@@ -152,23 +152,32 @@ export default function usePages(pages: Page[]) {
const usesDraft = crntPages.some(x => typeof x[draftFieldName] !== 'undefined');
if (usesDraft) {
const drafts = crntPages.filter(
const allDrafts = crntPages.filter(
(page) => page[draftFieldName] == true || page[draftFieldName] === 'true'
);
const published = crntPages.filter(
const allPublished = crntPages.filter(
(page) =>
page[draftFieldName] == false ||
page[draftFieldName] === 'false' ||
typeof page[draftFieldName] === 'undefined'
);
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
// If the invert is set, the drafts become published and vice versa
const isInverted = draftField?.invert;
const drafts = !isInverted ? allDrafts : allPublished;
const published = (!isInverted ? allPublished : allDrafts).filter((page) => !page.fmPublished || page.fmPublished <= Date.now());
const scheduled = (!isInverted ? allPublished : allDrafts).filter((page) => page.fmPublished && page.fmPublished > Date.now());
draftTypes[Tab.Draft] = drafts.length;
draftTypes[Tab.Published] = published.length;
draftTypes[Tab.Scheduled] = scheduled.length;
if (tab === Tab.Published) {
crntPages = draftField?.invert ? drafts : published;
crntPages = published;
} else if (tab === Tab.Draft) {
crntPages = draftField?.invert ? published : drafts;
crntPages = drafts;
} else if (tab === Tab.Scheduled) {
crntPages = scheduled;
} else {
crntPages = crntPages;
}

View File

@@ -136,7 +136,7 @@ if (elm) {
});
Sentry.setTag("type", "dashboard");
if (document.body.getAttribute(`data - vscode - theme - id`)) {
if (document.body.getAttribute(`data-vscode-theme-id`)) {
Sentry.setTag("theme", document.body.getAttribute(`data-vscode-theme-id`));
}
}

View File

@@ -351,7 +351,7 @@
select {
color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background);
border: 1px solid var(--vscode-editorWidget-border);
border: 1px solid var(--vscode-panel-border);
font-size: var(--vscode-font-size);
@apply rounded;

View File

@@ -36,6 +36,7 @@ import {
Chatbot
} from './commands';
import { join } from 'path';
import { Terminal } from './services';
let pageUpdateDebouncer: { (fnc: any, time: number): void };
let editDebounce: { (fnc: any, time: number): void };
@@ -58,6 +59,9 @@ export async function activate(context: vscode.ExtensionContext) {
});
}
// Make sure the terminal windows are closed
Terminal.closeLocalServerTerminal();
if (!extension.checkIfExtensionCanRun()) {
return undefined;
}

View File

@@ -60,9 +60,19 @@ export class ArticleHelper {
*
* @param document The document to parse.
*/
public static getFrontMatterFromDocument(document: vscode.TextDocument) {
public static getFrontMatterFromDocument(
document: vscode.TextDocument
): ParsedFrontMatter | undefined {
const fileContents = document.getText();
return ArticleHelper.parseFile(fileContents, document.fileName);
const article = ArticleHelper.parseFile(fileContents, document.fileName);
if (!article) {
return undefined;
}
return {
...article,
path: document.uri.fsPath
};
}
/**
@@ -79,7 +89,10 @@ export class ArticleHelper {
return;
}
return article;
return {
...article,
path: editor.document.uri.fsPath
};
}
/**
@@ -88,7 +101,15 @@ export class ArticleHelper {
*/
public static async getFrontMatterByPath(filePath: string) {
const file = await readFileAsync(filePath, { encoding: 'utf-8' });
return ArticleHelper.parseFile(file, filePath);
const article = ArticleHelper.parseFile(file, filePath);
if (!article) {
return undefined;
}
return {
...article,
path: filePath
};
}
/**
@@ -240,7 +261,7 @@ export class ArticleHelper {
/**
* Get date from front matter
*/
public static getDate(article: ParsedFrontMatter | null) {
public static getDate(article: ParsedFrontMatter | null | undefined) {
if (!article || !article.data) {
return;
}
@@ -270,7 +291,7 @@ export class ArticleHelper {
return;
}
const articleCt = ArticleHelper.getContentType(article.data);
const articleCt = ArticleHelper.getContentType(article);
const pubDateField = articleCt.fields.find((f) => f.isPublishDate);
return (
@@ -290,7 +311,7 @@ export class ArticleHelper {
return;
}
const articleCt = ArticleHelper.getContentType(article.data);
const articleCt = ArticleHelper.getContentType(article);
const modDateField = articleCt.fields.find((f) => f.isModifiedDate);
return (
@@ -312,16 +333,38 @@ export class ArticleHelper {
* Retrieve the content type of the current file
* @param updatedMetadata
*/
public static getContentType(metadata: { [field: string]: string }): IContentType {
public static getContentType(article: ParsedFrontMatter): IContentType {
const contentTypes = ArticleHelper.getContentTypes();
if (!contentTypes || !metadata) {
if (!contentTypes || !article.data) {
return DEFAULT_CONTENT_TYPE;
}
let contentType = contentTypes.find(
(ct) => ct.name === (metadata.type || DEFAULT_CONTENT_TYPE_NAME)
);
let contentType: IContentType | undefined = undefined;
// Get content type by type name in the front matter
if (article.data.type) {
contentType = contentTypes.find((ct) => ct.name === article.data.type);
} else if (!contentType && article.path) {
// Get the content type by the folder name
let folders = Folders.get();
let parsedPath = parseWinPath(article.path);
let pageFolderMatches = folders.filter(
(folder) => parsedPath && folder.path && parsedPath.includes(folder.path)
);
// Sort by longest path
pageFolderMatches = pageFolderMatches.sort((a, b) => b.path.length - a.path.length);
if (
pageFolderMatches.length > 0 &&
pageFolderMatches[0].contentTypes &&
pageFolderMatches[0].contentTypes.length === 1
) {
const contentTypeName = pageFolderMatches[0].contentTypes[0];
contentType = contentTypes.find((ct) => ct.name === contentTypeName);
}
}
if (!contentType) {
contentType = contentTypes.find((ct) => ct.name === DEFAULT_CONTENT_TYPE_NAME);
}
@@ -343,17 +386,17 @@ export class ArticleHelper {
* Update all dates in the metadata
* @param metadata
*/
public static updateDates(metadata: { [field: string]: string }) {
const contentType = ArticleHelper.getContentType(metadata);
public static updateDates(article: ParsedFrontMatter) {
const contentType = ArticleHelper.getContentType(article);
const dateFields = contentType.fields.filter((field) => field.type === 'datetime');
for (const dateField of dateFields) {
if (typeof metadata[dateField.name] !== 'undefined') {
metadata[dateField.name] = Article.formatDate(new Date(), dateField.dateFormat);
if (typeof article?.data[dateField.name] !== 'undefined') {
article.data[dateField.name] = Article.formatDate(new Date(), dateField.dateFormat);
}
}
return metadata;
return article.data;
}
/**

View File

@@ -49,18 +49,18 @@ export class ContentType {
* @param data
* @returns
*/
public static getDraftStatus(data: { [field: string]: any }) {
const contentType = ArticleHelper.getContentType(data);
public static getDraftStatus(article: ParsedFrontMatter) {
const contentType = ArticleHelper.getContentType(article);
const draftSetting = ContentType.getDraftField();
const draftField = contentType.fields.find((f) => f.type === 'draft');
let fieldValue = null;
if (draftField) {
fieldValue = data[draftField.name];
} else if (draftSetting && data && data[draftSetting.name]) {
fieldValue = data[draftSetting.name];
if (draftField && article?.data) {
fieldValue = article?.data[draftField.name];
} else if (draftSetting && article?.data && article?.data[draftSetting.name]) {
fieldValue = article?.data[draftSetting.name];
}
if (draftSetting && fieldValue !== null) {
@@ -282,7 +282,7 @@ export class ContentType {
return;
}
const contentType = ArticleHelper.getContentType(content?.data);
const contentType = ArticleHelper.getContentType(content);
const updatedFields = ContentType.generateFields(content.data, contentType.fields);
const contentTypes = ContentType.getAll() || [];
@@ -492,7 +492,7 @@ export class ContentType {
* Find the required fields
*/
public static findEmptyRequiredFields(article: ParsedFrontMatter): Field[][] | undefined {
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
if (!contentType) {
return;
}
@@ -793,7 +793,7 @@ export class ContentType {
}
let templatePath = contentType.template;
let templateData: ParsedFrontMatter | null = null;
let templateData: ParsedFrontMatter | null | undefined = null;
if (templatePath) {
templatePath = Folders.getAbsFilePath(templatePath);
templateData = await ArticleHelper.getFrontMatterByPath(templatePath);

View File

@@ -71,7 +71,7 @@ export class CustomScript {
path: string | null = null
): Promise<void> {
let articlePath: string | null = path;
let article: ParsedFrontMatter | null = null;
let article: ParsedFrontMatter | null | undefined = null;
if (!path) {
const editor = window.activeTextEditor;
@@ -214,7 +214,7 @@ export class CustomScript {
*/
private static async runScript(
wsPath: string,
article: ParsedFrontMatter | null,
article: ParsedFrontMatter | null | undefined,
contentPath: string,
script: ICustomScript
): Promise<string | null> {
@@ -223,7 +223,7 @@ export class CustomScript {
if (os.type() === 'Windows_NT') {
const jsonData = JSON.stringify(article?.data);
if (script.command && script.command.toLowerCase() === "powershell") {
if (script.command && script.command.toLowerCase() === 'powershell') {
articleData = `'${jsonData.replace(/"/g, `\\"`)}'`;
} else {
articleData = `"${jsonData.replace(/"/g, `\\"`)}"`;
@@ -323,55 +323,97 @@ export class CustomScript {
wsPath: string,
args: string
): Promise<string> {
return new Promise(async (resolve, reject) => {
const osType = os.type();
const osType = os.type();
// Check the command to use
let command = script.nodeBin || 'node';
if (script.command && script.command !== CommandType.Node) {
command = script.command;
// Check the command to use
let command = script.nodeBin || 'node';
if (script.command && script.command !== CommandType.Node) {
command = script.command;
}
let scriptPath = join(wsPath, script.script);
if (script.script.includes(WORKSPACE_PLACEHOLDER)) {
scriptPath = Folders.getAbsFilePath(script.script);
}
// Check if there is an environments overwrite required
if (script.environments) {
let crntType: EnvironmentType | null = null;
if (osType === 'Windows_NT') {
crntType = 'windows';
} else if (osType === 'Darwin') {
crntType = 'macos';
} else {
crntType = 'linux';
}
let scriptPath = join(wsPath, script.script);
if (script.script.includes(WORKSPACE_PLACEHOLDER)) {
scriptPath = Folders.getAbsFilePath(script.script);
}
// Check if there is an environments overwrite required
if (script.environments) {
let crntType: EnvironmentType | null = null;
if (osType === 'Windows_NT') {
crntType = 'windows';
} else if (osType === 'Darwin') {
crntType = 'macos';
} else {
crntType = 'linux';
}
const environment = script.environments.find((e) => e.type === crntType);
if (environment && environment.script && environment.command) {
if (await CustomScript.validateCommand(environment.command)) {
command = environment.command;
scriptPath = join(wsPath, environment.script);
if (environment.script.includes(WORKSPACE_PLACEHOLDER)) {
scriptPath = Folders.getAbsFilePath(environment.script);
}
const environment = script.environments.find((e) => e.type === crntType);
if (environment && environment.script && environment.command) {
if (await CustomScript.validateCommand(environment.command)) {
command = environment.command;
scriptPath = join(wsPath, environment.script);
if (environment.script.includes(WORKSPACE_PLACEHOLDER)) {
scriptPath = Folders.getAbsFilePath(environment.script);
}
}
}
}
if (!(await existsAsync(scriptPath))) {
reject(new Error(`Script not found: ${scriptPath}`));
return;
if (!(await existsAsync(scriptPath))) {
throw new Error(`Script not found: ${scriptPath}`);
}
if (osType === 'Windows_NT' && command.toLowerCase() === 'powershell') {
command = `${command} -File`;
}
const fullScript = `${command} "${scriptPath}" ${args}`;
Logger.info(`Executing: ${fullScript}`);
const output: string = await CustomScript.executeScriptAsync(fullScript, wsPath);
try {
const data = JSON.parse(output);
if (data.questions) {
const answers: string[] = [];
for (const question of data.questions) {
if (question.name && question.message) {
const answer = await window.showInputBox({
prompt: question.message,
value: question.default || '',
ignoreFocusOut: true,
title: question.message
});
if (answer) {
answers.push(`${question.name}="${answer}"`);
} else {
return '';
}
}
}
if (answers.length > 0) {
const newScript = `${fullScript} ${answers.join(' ')}`;
return await CustomScript.executeScriptAsync(newScript, wsPath);
}
}
} catch (error) {
// Not a JSON
}
if (osType === 'Windows_NT' && command.toLowerCase() === "powershell") {
command = `${command} -File`;
}
const fullScript = `${command} "${scriptPath}" ${args}`;
Logger.info(`Executing: ${fullScript}`);
return output;
}
/**
* Execute script async
* @param fullScript
* @param wsPath
* @returns
*/
private static async executeScriptAsync(fullScript: string, wsPath: string): Promise<string> {
return new Promise(async (resolve, reject) => {
exec(fullScript, { cwd: wsPath }, (error, stdout) => {
if (error) {
reject(error.message);

View File

@@ -32,6 +32,7 @@ import { ContentFolder, Snippet, TaxonomyType } from '../models';
import { Notifications } from './Notifications';
import { Settings } from './SettingsHelper';
import { TaxonomyHelper } from './TaxonomyHelper';
import { Cache } from '../commands/Cache';
export class Extension {
private static instance: Extension;
@@ -104,6 +105,9 @@ export class Extension {
});
this.setVersion(installedVersion);
// Reset the cache
Cache.clear(false);
}
return {

View File

@@ -380,7 +380,7 @@ export class MediaHelpers {
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
const articleCt =
article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
article && article.data ? ArticleHelper.getContentType(article) : DEFAULT_CONTENT_TYPE;
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ''), relPath);
const fileDir = parseWinPath(dirname(filePath));

View File

@@ -305,7 +305,7 @@ export class TaxonomyHelper {
if (mdFile) {
try {
const article = FrontMatterParser.fromFile(mdFile);
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
let fieldNames: string[] = this.getFieldsHierarchy(taxonomyType, contentType);
@@ -415,7 +415,7 @@ export class TaxonomyHelper {
if (mdFile) {
try {
const article = FrontMatterParser.fromFile(mdFile);
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
let oldFieldNames: string[] = this.getFieldsHierarchy(oldType, contentType);
let newFieldNames: string[] = this.getFieldsHierarchy(newType, contentType, true);

View File

@@ -19,7 +19,7 @@ export default function useContentType(
// Get the content type by the folder name
const pageFolders = settings.contentFolders;
let pageFolderMatches = pageFolders.filter((folder) => metadata.filePath.includes(folder.path));
let pageFolderMatches = pageFolders.filter((folder) => metadata.filePath && folder.path && metadata.filePath.includes(folder.path));
// Sort by longest path
pageFolderMatches = pageFolderMatches.sort((a, b) => b.path.length - a.path.length);

View File

@@ -119,7 +119,7 @@ export class PagesListener extends BaseListener {
if (!article) {
return;
}
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
Logger.info(`Deleting file: ${path}`);
@@ -240,7 +240,10 @@ export class PagesListener extends BaseListener {
* @param pages
*/
private static async createSearchIndex(pages: Page[]) {
const pagesIndex = Fuse.createIndex(['title', 'slug', 'description', 'fmBody', 'type'], pages);
const pagesIndex = Fuse.createIndex(
['title', 'slug', 'description', 'fmBody', 'type', 'fmContentType'],
pages
);
await Extension.getInstance().setState(
ExtensionState.Dashboard.Pages.Index,
pagesIndex.toJSON(),

View File

@@ -5,7 +5,7 @@ import { Folders } from '../../commands/Folders';
import { Command } from '../../panelWebView/Command';
import { CommandToCode } from '../../panelWebView/CommandToCode';
import { BaseListener } from './BaseListener';
import { authentication, commands, ThemeIcon, window } from 'vscode';
import { authentication, commands, window } from 'vscode';
import { ArticleHelper, ContentType, Extension, Logger, Settings } from '../../helpers';
import {
COMMAND_NAME,
@@ -23,12 +23,12 @@ import { encodeEmoji } from '../../utils';
import { PanelProvider } from '../../panelWebView/PanelProvider';
import { MessageHandlerData } from '@estruyf/vscode';
import { SponsorAi } from '../../services/SponsorAI';
import { Terminal } from '../../services';
const FILE_LIMIT = 10;
export class DataListener extends BaseListener {
private static lastMetadataUpdate: any = {};
private static readonly terminalName: string = 'Local server';
/**
* Process the messages for the dashboard views
@@ -254,7 +254,7 @@ export class DataListener extends BaseListener {
return;
}
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
if (!value && field !== titleField && contentType.clearEmpty) {
value = undefined;
@@ -374,7 +374,7 @@ export class DataListener extends BaseListener {
) {
let parentObj = data;
let allParents = Object.assign([], parents);
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
let selectedIndexes: number[] = [];
if (blockData?.selectedIndex) {
if (typeof blockData.selectedIndex === 'string') {
@@ -509,27 +509,7 @@ export class DataListener extends BaseListener {
*/
private static openTerminalWithCommand(command: string) {
if (command) {
let localServerTerminal = DataListener.findServerTerminal();
if (localServerTerminal) {
localServerTerminal.dispose();
}
if (
!localServerTerminal ||
(localServerTerminal && localServerTerminal.state.isInteractedWith === true)
) {
localServerTerminal = window.createTerminal({
name: this.terminalName,
iconPath: new ThemeIcon('server-environment'),
message: `Starting local server`
});
}
if (localServerTerminal) {
localServerTerminal.sendText(command);
localServerTerminal.show(false);
}
Terminal.openLocalServerTerminal(command);
this.sendMsg(Command.serverStarted, true);
}
}
@@ -538,11 +518,7 @@ export class DataListener extends BaseListener {
* Stop the local server
*/
private static stopServer() {
const localServerTerminal = DataListener.findServerTerminal();
if (localServerTerminal) {
localServerTerminal.dispose();
}
Terminal.closeLocalServerTerminal();
this.sendMsg(Command.serverStarted, false);
}
@@ -554,22 +530,10 @@ export class DataListener extends BaseListener {
return;
}
const localServerTerminal = DataListener.findServerTerminal();
const localServerTerminal = Terminal.findLocalServerTerminal();
this.sendRequest(command, requestId, !!localServerTerminal);
}
/**
* Find the server terminal
* @returns
*/
private static findServerTerminal() {
let terminals = window.terminals;
if (terminals) {
const localServerTerminal = terminals.find((t) => t.name === DataListener.terminalName);
return localServerTerminal;
}
}
/**
* Update the placeholder
* @param field

View File

@@ -36,7 +36,7 @@ export class FieldsListener extends BaseListener {
PagesListener.getPagesData(false, async (pages) => {
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [{ name: 'type', weight: 1 }]
keys: [{ name: 'fmContentType', weight: 1 }]
};
const pagesIndex = await Extension.getInstance().getState<Fuse.FuseIndex<Page>>(
@@ -48,7 +48,7 @@ export class FieldsListener extends BaseListener {
const results = fuse.search({
$and: [
{
type
fmContentType: type
}
]
});

View File

@@ -28,6 +28,7 @@ export class TaxonomyListener extends BaseListener {
msg.payload?.fieldName,
msg.payload?.values || [],
msg.payload?.parents || [],
typeof msg.payload?.renderAsString !== 'undefined' ? msg.payload?.renderAsString : false,
msg.payload?.blockData
);
break;
@@ -36,6 +37,7 @@ export class TaxonomyListener extends BaseListener {
msg.payload?.fieldName,
msg.payload?.values || [],
msg.payload?.parents || [],
typeof msg.payload?.renderAsString !== 'undefined' ? msg.payload?.renderAsString : false,
msg.payload?.blockData
);
break;
@@ -134,6 +136,7 @@ export class TaxonomyListener extends BaseListener {
fieldName: string,
values: string[],
parents: string[],
renderAsString: boolean,
blockData?: BlockFieldData
) {
const editor = window.activeTextEditor;
@@ -145,7 +148,17 @@ export class TaxonomyListener extends BaseListener {
if (article && article.data) {
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
parentObj[fieldName] = values || [];
if (renderAsString) {
if (values.length === 0) {
parentObj[fieldName] = '';
} else if (values.length === 1) {
parentObj[fieldName] = values[0];
} else {
parentObj[fieldName] = values || [];
}
} else {
parentObj[fieldName] = values || [];
}
ArticleHelper.update(editor, article);
DataListener.pushMetadata(article!.data);
}
@@ -174,7 +187,18 @@ export class TaxonomyListener extends BaseListener {
data.blockData
);
parentObj[data.name] = data.options || [];
if (data.renderAsString) {
if (!data.options || data.options.length === 0) {
parentObj[data.name] = '';
} else if (data.options.length === 1) {
parentObj[data.name] = data.options[0];
} else {
parentObj[data.name] = data.options || [] || [];
}
} else {
parentObj[data.name] = data.options || [];
}
ArticleHelper.update(editor, article);
DataListener.pushMetadata(article!.data);
}

View File

@@ -111,6 +111,10 @@ export enum LocalizationKey {
* Unpin
*/
commonUnpin = 'common.unpin',
/**
* No results
*/
commonNoResults = 'common.noResults',
/**
* Common
*/
@@ -283,6 +287,10 @@ export enum LocalizationKey {
* Published
*/
dashboardContentsStatusPublished = 'dashboard.contents.status.published',
/**
* Scheduled
*/
dashboardContentsStatusScheduled = 'dashboard.contents.status.scheduled',
/**
* Modify the data
*/
@@ -391,6 +399,10 @@ export enum LocalizationKey {
* Published
*/
dashboardHeaderNavigationPublished = 'dashboard.header.navigation.published',
/**
* Scheduled
*/
dashboardHeaderNavigationScheduled = 'dashboard.header.navigation.scheduled',
/**
* In draft
*/

View File

@@ -6,5 +6,6 @@ export interface CustomTaxonomyData {
options?: string[] | undefined;
option?: string | undefined;
parents?: string[];
renderAsString?: boolean;
blockData?: BlockFieldData;
}

View File

@@ -108,6 +108,7 @@ export interface Field {
fieldGroup?: string | string[];
dataType?: string | string[];
taxonomyLimit?: number;
singleValueAsString?: boolean;
fileExtensions?: string[];
editable?: boolean;
required?: boolean;

View File

@@ -6,21 +6,23 @@ import { LocalizationKey } from '../../../localization';
export interface IChoiceButtonProps {
title: string;
value: string;
className?: string;
onClick: (value: string) => void;
}
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({
title,
value,
className,
onClick
}: React.PropsWithChildren<IChoiceButtonProps>) => {
return (
<button
title={l10n.t(LocalizationKey.commonRemoveValue, title)}
className="metadata_field__choice__button"
className={`metadata_field__choice__button text-left ${className || ""}`}
onClick={() => onClick(value)}
>
{title}
<span>{title}</span>
<XIcon className={`metadata_field__choice__button_icon`} />
</button>
);

View File

@@ -1,16 +1,16 @@
import { ChevronDownIcon, DocumentAddIcon } from '@heroicons/react/outline';
import Downshift from 'downshift';
import { Combobox, Transition } from '@headlessui/react';
import * as React from 'react';
import { useEffect, useMemo } from 'react';
import { BaseFieldProps } from '../../../models';
import { ChoiceButton } from './ChoiceButton';
import { FieldTitle } from './FieldTitle';
import { FieldMessage } from './FieldMessage';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { CommandToCode } from '../../CommandToCode';
import { Fragment, useCallback, useEffect, useMemo } from 'react';
import { Page } from '../../../dashboardWebView/models';
import { messageHandler } from '@estruyf/vscode/dist/client/webview';
import { CommandToCode } from '../../CommandToCode';
import { ChevronDownIcon, DocumentAddIcon } from '@heroicons/react/outline';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { FieldTitle } from './FieldTitle';
import { FieldMessage } from './FieldMessage';
import { ChoiceButton } from './ChoiceButton';
import useDropdownStyle from '../../hooks/useDropdownStyle';
export interface IContentTypeRelationshipFieldProps extends BaseFieldProps<string | string[]> {
@@ -34,34 +34,25 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
const [choices, setChoices] = React.useState<string[]>([]);
const [pages, setPages] = React.useState<Page[]>([]);
const [crntSelected, setCrntSelected] = React.useState<string | string[] | null>(value);
const dsRef = React.useRef<Downshift<string> | null>(null);
const [filter, setFilter] = React.useState<string | undefined>(undefined);
const inputRef = React.useRef<HTMLInputElement | null>(null);
const { getDropdownStyle } = useDropdownStyle(inputRef as any);
const { getDropdownStyle } = useDropdownStyle(inputRef as any, '6px');
const onValueChange = (txtValue: string) => {
if (multiSelect) {
const crntValue = typeof crntSelected === 'string' ? [crntSelected] : crntSelected;
const newValue = [...((crntValue || []) as string[]), txtValue];
const uniqueValues = [...new Set(newValue)];
setCrntSelected(uniqueValues);
onChange(uniqueValues);
} else {
setCrntSelected(txtValue);
onChange(txtValue);
}
};
const removeSelected = (txtValue: string) => {
if (multiSelect) {
const newValue = [...(crntSelected || [])].filter((v) => v !== txtValue);
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected('');
onChange('');
}
};
/**
* Check the required state
*/
const showRequiredState = useMemo(() => {
return (
required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected)
);
}, [required, crntSelected]);
/**
* Retrieve the value based on the field setting
* @param value
* @param type
* @returns
*/
const getValue = (value: Page, type: string = "path") => {
if (type === 'path') {
return value.fmRelFilePath || value.fmFilePath;
@@ -70,7 +61,10 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
return `${value[type]}`;
};
const getChoiceValue = React.useCallback((value: string) => {
/**
* Retrieve choice value to display
*/
const getChoiceValue = useCallback((value: string) => {
const choice = pages.find(
(p: Page) => getValue(p, contentTypeValue) === value
);
@@ -79,35 +73,80 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
return choice.title;
}
return '';
}, [choices, contentTypeValue]);
}, [pages, choices, contentTypeValue]);
/**
* On selecting an option
* @param txtValue
*/
const onSelect = (option: string) => {
setFilter(undefined);
if (multiSelect) {
if (option) {
const crntValue = typeof crntSelected === 'string' ? [crntSelected] : crntSelected;
const newValue = [...((crntValue || []) as string[]), option];
const uniqueValues = [...new Set(newValue)];
setCrntSelected(uniqueValues);
onChange(uniqueValues);
}
} else if (option) {
setCrntSelected(option);
onChange(option);
}
};
/**
* Remove a selected value
* @param txtValue
*/
const removeSelected = useCallback((txtValue: string) => {
if (multiSelect) {
const newValue = [...(crntSelected || [])].filter((v) => v !== txtValue);
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected('');
onChange('');
}
}, [multiSelect, crntSelected, onChange]);
/**
* Retrieve the available choices
*/
const availableChoices = useMemo(() => {
return pages.filter((page: Page) => {
const value = contentTypeValue === "slug" ? page.slug : page.fmFilePath;
let toShow = true;
if (typeof crntSelected === 'string') {
return crntSelected !== `${value}` && !value.includes(crntSelected);
toShow = crntSelected !== `${value}` && !value.includes(crntSelected);
} else if (crntSelected instanceof Array) {
const selected = crntSelected.filter((v) => v === `${value}` || value.includes(v));
return selected.length === 0;
toShow = selected.length === 0;
}
return true;
if (toShow && filter) {
return page.title.toLowerCase().includes(filter);
}
return toShow;
});
}, [choices, crntSelected, multiSelect, contentTypeValue]);
const showRequiredState = useMemo(() => {
return (
required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected)
);
}, [required, crntSelected]);
}, [choices, crntSelected, multiSelect, contentTypeValue, filter]);
/**
* Retrieve the selected value
*/
useEffect(() => {
if (crntSelected !== value) {
setCrntSelected(value);
}
}, [value]);
/**
* Retrieve the pages based on the content type
*/
useEffect(() => {
if (contentTypeName) {
setLoading(true);
@@ -137,83 +176,94 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
</div>
</div>
) : (
<>
<Downshift
ref={dsRef}
onSelect={(selected) => onValueChange(selected || '')}
itemToString={(item) => (item ? item : '')}
>
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
<div
{...getRootProps(undefined, { suppressRefError: true })}
ref={inputRef}
className={`metadata_field__choice`}
>
<button
{...getToggleButtonProps({
className: `metadata_field__choice__toggle`,
disabled: availableChoices.length === 0
})}
>
<span>{`Select ${label}`}</span>
<ChevronDownIcon className="icon" />
</button>
<Combobox
value={crntSelected}
onChange={onSelect}
>
{({ open }) => (
<div className="relative">
<ul
className={`field_dropdown metadata_field__choice_list ${isOpen ? 'open' : 'closed'}`}
style={{
bottom: getDropdownStyle(isOpen)
}}
{...getMenuProps()}
>
{
availableChoices.map((choice: Page, index) => (
<li
{...getItemProps({
key: getValue(choice, contentTypeValue),
index,
item: getValue(choice, contentTypeValue),
})}
>
{choice.title || (
<span className={`metadata_field__choice_list__item`}>
{l10n.t(LocalizationKey.commonClearValue)}
</span>
)}
</li>
))
}
</ul>
<div className="relative w-full cursor-default overflow-hidden text-left focus:outline-none border border-solid border-[var(--vscode-inputValidation-infoBorder)] rounded">
<Combobox.Input
className="w-full border-none py-2 pl-3 pr-10 leading-5 focus:ring-0 text-[var(--vscode-input-foreground)]"
onChange={(e) => setFilter(e.target.value)}
value={filter || ""}
placeholder={l10n.t(LocalizationKey.panelFieldsChoiceFieldSelect, label)}
ref={inputRef} />
<Combobox.Button
className="absolute inset-y-0 right-0 flex items-center w-8 bg-inherit rounded-none text-[var(--vscode-input-foreground)] hover:text-[var(--vscode-button-foreground)]">
<ChevronDownIcon
className="h-5 w-5"
aria-hidden="true"
/>
</Combobox.Button>
</div>
)}
</Downshift>
<FieldMessage
name={label.toLowerCase()}
description={description}
showRequired={showRequiredState}
/>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setFilter('')}
>
<Combobox.Options
className="field_dropdown absolute max-h-60 w-full shadow-lg overflow-auto py-1 px-0 space-y-1 text-base focus:outline-none z-50 bg-[var(--vscode-dropdown-background)] text-[var(--vscode-dropdown-foreground)] border border-solid border-[var(--vscode-dropdown-border)]"
style={{
bottom: getDropdownStyle(open)
}}
>
{availableChoices.map((choice) => (
<Combobox.Option
key={choice.fmFilePath}
value={getValue(choice, contentTypeValue)}
className={({ active }) => `py-[var(--input-padding-vertical)] px-[var(--input-padding-horizontal)] list-none cursor-pointer hover:text-[var(--vscode-button-foreground)] hover:bg-[var(--vscode-button-hoverBackground)] ${active ? "text-[var(--vscode-button-foreground)] bg-[var(--vscode-button-hoverBackground)] " : ""}`}>
{choice.title}
</Combobox.Option>
))}
{crntSelected instanceof Array
? crntSelected.map((value: string) => (
<ChoiceButton
key={value}
value={value}
title={getChoiceValue(value)}
onClick={removeSelected}
/>
))
: crntSelected && (
<ChoiceButton
key={crntSelected}
value={crntSelected}
title={getChoiceValue(crntSelected)}
onClick={removeSelected}
/>
)}
</>
{availableChoices.length === 0 ? (
<div className="relative cursor-default select-none text-center">
{l10n.t(LocalizationKey.commonNoResults)}
</div>
) : null}
</Combobox.Options>
</Transition>
</div>
)}
</Combobox>
)
}
<FieldMessage
name={label.toLowerCase()}
description={description}
showRequired={showRequiredState}
/>
{
pages.length > 0 && (
crntSelected instanceof Array
? crntSelected.map((value: string) => (
<ChoiceButton
key={value}
value={value}
className='w-full mr-0 flex justify-between'
title={getChoiceValue(value)}
onClick={removeSelected}
/>
))
: crntSelected && (
<ChoiceButton
key={crntSelected}
value={crntSelected}
className='w-full mr-0 flex justify-between'
title={getChoiceValue(crntSelected)}
onClick={removeSelected}
/>
)
)
}
</div>
);
};
)
};

View File

@@ -311,6 +311,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
parents={parentFields}
blockData={blockData}
limit={field.taxonomyLimit}
renderAsString={field.singleValueAsString}
required={!!field.required}
/>
</FieldBoundary>
@@ -335,6 +336,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
parents={parentFields}
blockData={blockData}
limit={field.taxonomyLimit}
renderAsString={field.singleValueAsString}
required={!!field.required}
/>
</FieldBoundary>
@@ -356,6 +358,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
parents={parentFields}
blockData={blockData}
limit={field.taxonomyLimit}
renderAsString={field.singleValueAsString}
required={!!field.required}
/>
</FieldBoundary>

View File

@@ -89,7 +89,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({
};
return (
<Collapsible id={`tags`} title={l10n.t(LocalizationKey.panelMetadataTitle)} className={`inherit z-20`}>
<Collapsible id={`tags`} title={`${l10n.t(LocalizationKey.panelMetadataTitle)}${contentType?.name ? ` (${contentType?.name})` : ""}`} className={`inherit z-20`}>
<FeatureFlag features={features || []} flag={FEATURE_FLAG.panel.contentType}>
<ContentTypeValidator fields={contentType?.fields || []} metadata={metadata} />
</FeatureFlag>

View File

@@ -35,6 +35,7 @@ export interface ITagPickerProps {
taxonomyId?: string;
limit?: number;
required?: boolean;
renderAsString?: boolean;
}
const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
@@ -53,7 +54,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
parents,
blockData,
limit,
required
required,
renderAsString
}: React.PropsWithChildren<ITagPickerProps>) => {
const [selected, setSelected] = React.useState<string[]>([]);
const [inputValue, setInputValue] = React.useState<string>('');
@@ -96,11 +98,12 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
* Send an update to VSCode
* @param values
*/
const sendUpdate = (values: string[]) => {
const sendUpdate = useCallback((values: string[]) => {
if (type === TagType.tags) {
Messenger.send(CommandToCode.updateTags, {
fieldName,
values,
renderAsString,
parents,
blockData
});
@@ -108,6 +111,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
Messenger.send(CommandToCode.updateCategories, {
fieldName,
values,
renderAsString,
parents,
blockData
});
@@ -121,11 +125,12 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
id: taxonomyId,
name: fieldName,
options: values,
renderAsString,
parents,
blockData
} as CustomTaxonomyData);
}
};
}, [renderAsString]);
/**
* Triggers the focus to the input when command is executed
@@ -145,7 +150,10 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
if (selectedItem) {
let value = selectedItem || '';
const item = options.find((o) => o?.toLowerCase() === selectedItem?.toLowerCase());
const item = options.find((o) => {
o = typeof o === 'string' ? o : `${o}`;
return o?.toLowerCase() === value?.toLowerCase();
});
if (item) {
value = item;
}
@@ -174,8 +182,12 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
* @param inputValue
*/
const filterList = (option: string, inputValue: string | null) => {
if (typeof option !== 'string') {
return false;
}
return (
!selected.includes(option) && option.toLowerCase().includes((inputValue || '').toLowerCase())
option && !selected.includes(option) && option.toLowerCase().includes((inputValue || '').toLowerCase())
);
};
@@ -201,7 +213,10 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
for (let crntValue of values) {
crntValue = crntValue.trim();
if (crntValue) {
const item = options.find((o) => o?.toLowerCase() === crntValue?.toLowerCase());
const item = options.find((o) => {
o = typeof o === 'string' ? o : `${o}`;
return o?.toLowerCase() === crntValue?.toLowerCase();
});
if (item) {
newValues.push(item);
} else if (freeform) {
@@ -279,6 +294,15 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
);
}, [settings?.aiEnabled, label, type]);
const sortedSelectedTags = useMemo(() => {
return (selected || []).sort((a: string, b: string) => {
const aString = typeof a === 'string' ? a : `${a}`;
const bString = typeof b === 'string' ? b : `${b}`;
return aString?.toLowerCase() < bString?.toLowerCase() ? -1 : 1;
});
}, [selected]);
useEffect(() => {
setTimeout(() => {
triggerFocus();
@@ -400,9 +424,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
/>
<Tags
values={(selected || []).sort((a: string, b: string) =>
a?.toLowerCase() < b?.toLowerCase() ? -1 : 1
)}
values={sortedSelectedTags}
onRemove={onRemove}
onCreate={onCreate}
options={options}

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
export default function useDropdownStyle(inputRef: React.MutableRefObject<HTMLInputElement | null>) {
const bottomStyle = "calc(100% - 38px)";
export default function useDropdownStyle(inputRef: React.MutableRefObject<HTMLInputElement | null>, inputHeight?: string) {
const bottomStyle = `calc(100% - ${inputHeight || '38px'})`;
const listItemHeight = 28;
const getDropdownStyle = useCallback((isOpen) => {

View File

@@ -5,6 +5,7 @@ import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { SENTRY_LINK, SentryIgnore } from '../constants';
import { RecoilRoot } from 'recoil';
import './styles.css';
// require('@vscode/codicons/dist/codicon.css');

View File

@@ -1,3 +1,6 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
/* Animations */
@keyframes spin {
from {
@@ -969,7 +972,7 @@ vscode-divider {
/* Divider */
.divider {
background: var(--divider-background);
background: var(--vscode-panel-border);
}
/* Git actions */

View File

@@ -11,6 +11,7 @@ export interface Format {
export interface ParsedFrontMatter {
data: { [key: string]: any };
content: string;
path?: string;
}
export class FrontMatterParser {

View File

@@ -21,7 +21,7 @@ export class Credentials {
* prompting the user to sign in.
* */
const session = await authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, {
createIfNone: false
silent: true
});
if (session) {
@@ -62,8 +62,13 @@ export class Credentials {
* Note that this can throw if the user clicks cancel.
*/
const session = await authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, {
createIfNone: true
silent: true
});
if (!session) {
throw new Error('No GitHub authentication session available.');
}
this.octokit = new Octokit.Octokit({
auth: session.accessToken
});

View File

@@ -171,7 +171,7 @@ export class PagesParser {
const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
const contentType = ArticleHelper.getContentType(article.data);
const contentType = ArticleHelper.getContentType(article);
let dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
const ctDateField = ContentType.findFieldByName(contentType.fields, dateField);
if (ctDateField && ctDateField.dateFormat) {
@@ -211,7 +211,7 @@ export class PagesParser {
fmRelFileWsPath: FilesHelper.absToRelPath(filePath),
fmRelFilePath: parseWinPath(filePath).replace(wsFolder?.fsPath || '', ''),
fmFileName: fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
fmDraft: ContentType.getDraftStatus(article),
fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime,
fmPublished: dateFieldValue ? dateFieldValue.getTime() : null,
fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null,

View File

@@ -1,11 +1,14 @@
import { workspace } from 'vscode';
import { workspace, window, ThemeIcon, TerminalOptions } from 'vscode';
import * as os from 'os';
import { Folders } from '../commands';
interface ShellSetting {
path: string;
}
export class Terminal {
public static readonly terminalName: string = 'Local server';
/**
* Return the shell path for the current platform
*/
@@ -22,6 +25,68 @@ export class Terminal {
return shellPath;
}
/**
* Open a new local server terminal
*/
public static openLocalServerTerminal(command: string) {
let localServerTerminal = Terminal.findLocalServerTerminal();
if (localServerTerminal) {
localServerTerminal.dispose();
}
if (!command) {
return;
}
if (
!localServerTerminal ||
(localServerTerminal && localServerTerminal.state.isInteractedWith === true)
) {
const terminalOptions: TerminalOptions = {
name: Terminal.terminalName,
iconPath: new ThemeIcon('server-environment'),
message: `Starting local server`
};
// Check if workspace
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 1) {
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
terminalOptions.cwd = wsFolder;
}
}
localServerTerminal = window.createTerminal(terminalOptions);
}
if (localServerTerminal) {
localServerTerminal.sendText(command);
localServerTerminal.show(false);
}
}
/**
* Close local server terminal
*/
public static closeLocalServerTerminal() {
const localServerTerminal = Terminal.findLocalServerTerminal();
if (localServerTerminal) {
localServerTerminal.dispose();
}
}
/**
* Find the server terminal
* @returns
*/
public static findLocalServerTerminal() {
let terminals = window.terminals;
if (terminals) {
const localServerTerminal = terminals.find((t) => t.name === Terminal.terminalName);
return localServerTerminal;
}
}
/**
* Retrieve the automation profile for the current platform
* @returns

8
tailwind.panel.js Normal file
View File

@@ -0,0 +1,8 @@
const defaultConfig = require("./tailwind.config")
module.exports = {
...defaultConfig,
corePlugins: {
preflight: false,
}
}

View File

@@ -31,7 +31,21 @@ const config = [{
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
use: ['style-loader', 'css-loader', {
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {
config: path.resolve(__dirname, '../tailwind.panel.js')
},
autoprefixer: {},
},
},
},
}]
},
{
test: /\.m?js/,