Enhancement: Ability to create new data files for a folder #834

This commit is contained in:
Elio Struyf
2024-08-14 14:06:52 +02:00
parent f13058c59b
commit d0b7af5c86
15 changed files with 482 additions and 262 deletions

View File

@@ -6,6 +6,8 @@
### 🎨 Enhancements
- [#834](https://github.com/estruyf/vscode-front-matter/issues/834): Added the ability to create new data files for a data folder
### 🐞 Fixes
## [10.3.0] - 2024-08-13 - [Release notes](https://beta.frontmatter.codes/updates/v10.3.0)

View File

@@ -140,6 +140,9 @@
"dashboard.dataView.dataView.noDataFiles": "No data files found",
"dashboard.dataView.dataView.getStarted.link": "Read more to get started using data files",
"dashboard.dataView.dataView.update.message": "Updated your data entries",
"dashboard.dataView.dataView.createNew": "Create new data file",
"dashboard.dataView.dataView.selectDataFolder": "Select data folder",
"dashboard.dataView.dataView.closeSelectedDataFile": "Close data file",
"dashboard.dataView.emptyView.heading": "Select your date type first",
@@ -783,6 +786,9 @@
"listeners.panel.dataListener.aiSuggestTaxonomy.noData.error": "No article data",
"listeners.panel.dataListener.getDataFileEntries.noDataFiles.error": "Couldn't find data file entries",
"listeners.panel.dataListener.pushMetadata.frontMatter.error": "Something went wrong while parsing your front matter. Please check the contents of your file.",
"listeners.panel.dataListener.createDataFile.inputTitle": "What is the name of the data file?",
"listeners.panel.dataListener.createDataFile.error": "No data file id or path defined.",
"listeners.panel.dataListener.createDataFile.noFileName": "No filename provided.",
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "No active editor",

View File

@@ -10,8 +10,7 @@
"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"
@@ -71,8 +70,7 @@
"**/.frontmatter/config/*.json": "jsonc"
}
},
"keybindings": [
{
"keybindings": [{
"command": "frontMatter.dashboard",
"key": "alt+d"
},
@@ -96,23 +94,19 @@
}
],
"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%",
@@ -180,8 +174,7 @@
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"oneOf": [
{
"oneOf": [{
"enum": [
"md",
"mdx"
@@ -197,8 +190,7 @@
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
"oneOf": [
{
"oneOf": [{
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
@@ -550,8 +542,7 @@
"categories"
],
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
"items": [
{
"items": [{
"type": "string",
"enum": [
"contentFolders",
@@ -625,8 +616,7 @@
"command": {
"$id": "#scriptCommand",
"type": "string",
"anyOf": [
{
"anyOf": [{
"enum": [
"node",
"bash",
@@ -822,8 +812,7 @@
"title",
"file"
],
"anyOf": [
{
"anyOf": [{
"required": [
"schema"
]
@@ -870,6 +859,20 @@
"type": "boolean",
"description": "%setting.frontMatter.data.folders.items.properties.singleEntry.description%",
"default": false
},
"enableFileCreation": {
"type": "boolean",
"description": "%setting.frontMatter.data.folders.items.properties.enableFileCreation.description%",
"default": false
},
"fileType": {
"type": "string",
"default": "json",
"enum": [
"json",
"yaml"
],
"description": "%setting.frontMatter.data.folders.items.properties.fileType.description%"
}
},
"additionalProperties": false,
@@ -877,8 +880,7 @@
"id",
"path"
],
"anyOf": [
{
"anyOf": [{
"required": [
"schema"
]
@@ -1119,29 +1121,26 @@
}
}
},
"default": [
{
"name": "default",
"fileTypes": null,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Caption",
"name": "caption",
"type": "string"
},
{
"title": "Alt text",
"name": "alt",
"type": "string"
}
]
}
],
"default": [{
"name": "default",
"fileTypes": null,
"fields": [{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Caption",
"name": "caption",
"type": "string"
},
{
"title": "Alt text",
"name": "alt",
"type": "string"
}
]
}],
"scope": "Media"
},
"frontMatter.media.supportedMimeTypes": {
@@ -1244,8 +1243,7 @@
"fileType": {
"type": "string",
"default": "",
"oneOf": [
{
"oneOf": [{
"enum": [
"md",
"mdx"
@@ -1384,8 +1382,7 @@
"default": "",
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
"not": {
"anyOf": [
{
"anyOf": [{
"const": ""
},
{
@@ -1586,8 +1583,7 @@
"type",
"name"
],
"allOf": [
{
"allOf": [{
"if": {
"properties": {
"type": {
@@ -1799,51 +1795,48 @@
"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": {
@@ -1856,8 +1849,7 @@
"type": "string",
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
"not": {
"anyOf": [
{
"anyOf": [{
"const": ""
},
{
@@ -2052,8 +2044,7 @@
}
}
},
"commands": [
{
"commands": [{
"command": "frontMatter.project.switch",
"title": "%command.frontMatter.project.switch%",
"category": "Front Matter",
@@ -2385,21 +2376,16 @@
}
}
],
"submenus": [
{
"id": "frontmatter.submenu",
"label": "Front Matter"
}
],
"submenus": [{
"id": "frontmatter.submenu",
"label": "Front Matter"
}],
"menus": {
"webview/context": [
{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
}
],
"editor/title": [
{
"webview/context": [{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
}],
"editor/title": [{
"command": "frontMatter.markup.heading",
"group": "navigation@-133",
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
@@ -2485,14 +2471,11 @@
"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"
@@ -2508,8 +2491,7 @@
"group": "frontmatter@3"
}
],
"commandPalette": [
{
"commandPalette": [{
"command": "frontMatter.init",
"when": "frontMatterCanInit"
},
@@ -2686,8 +2668,7 @@
"when": "frontMatter:file:isValid == true"
}
],
"view/title": [
{
"view/title": [{
"command": "frontMatter.docs",
"group": "navigation@-1",
"when": "view == frontMatter.explorer"
@@ -2724,16 +2705,13 @@
}
]
},
"languages": [
{
"id": "frontmatter.project.output",
"mimetypes": [
"text/x-code-output"
]
}
],
"grammars": [
{
"languages": [{
"id": "frontmatter.project.output",
"mimetypes": [
"text/x-code-output"
]
}],
"grammars": [{
"path": "./syntaxes/hugo.tmLanguage.json",
"scopeName": "frontmatter.markdown.hugo",
"injectTo": [
@@ -2746,48 +2724,45 @@
"path": "./syntaxes/frontmatter-output.tmLanguage.json"
}
],
"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"
]
"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"
},
{
"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": [
"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.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"
]
}
]
}
]
"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"
]
}
]
}]
},
"scripts": {
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
@@ -2915,4 +2890,4 @@
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6"
}
}
}

View File

@@ -145,6 +145,8 @@
"setting.frontMatter.data.folders.items.properties.path.description": "Path to the folder to load files.",
"setting.frontMatter.data.folders.items.properties.type.description": "If you are using data types, you can specify your type ID.",
"setting.frontMatter.data.folders.items.properties.singleEntry.description": "If you want to use a single entry for your data files in the folder.",
"setting.frontMatter.data.folders.items.properties.enableFileCreation.description": "Enable the creation of new data files in the folder.",
"setting.frontMatter.data.folders.items.properties.fileType.description": "Defines the file type for when the file creation is enabled. JSON is the default.",
"setting.frontMatter.data.types.markdownDescription": "Specify the data types. These types can be used in for your data files. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.data.types) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.data.types%22%5D)",
"setting.frontMatter.data.types.items.properties.id.description": "Your unique ID you want to use for your data type.",
"setting.frontMatter.file.preserveCasing.markdownDescription": "Specify if you want to preserve the casing of your file names from the title. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.file.preservecasing) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.file.preservecasing%22%5D)",

View File

@@ -557,6 +557,22 @@ export class Folders {
return parseWinPath(absPath);
}
/**
* Converts a given file path to a workspace-relative path.
*
* @param path - The file path to convert.
* @returns The workspace-relative path.
*/
public static wsPath(path: string) {
const wsFolder = Folders.getWorkspaceFolder();
let absPath = parseWinPath(path).replace(
parseWinPath(wsFolder?.fsPath || ''),
WORKSPACE_PLACEHOLDER
);
absPath = isWindows() ? absPath.split('\\').join('/') : absPath;
return absPath;
}
/**
* Generate relative folder path
* @param folder

View File

@@ -50,6 +50,7 @@ export enum DashboardMessage {
// Data dashboard
getDataEntries = 'getDataEntries',
putDataEntries = 'putDataEntries',
createDataFile = 'createDataFile',
// Snippets dashboard
insertSnippet = 'insertSnippet',

View File

@@ -5,7 +5,7 @@ import { SettingsSelector } from '../../state';
import { DataForm } from './DataForm';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { DataFile } from '../../../models/DataFile';
import { Messenger } from '@estruyf/vscode/dist/client';
import { messageHandler, Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { SponsorMsg } from '../Layout/SponsorMsg';
import { EventData } from '@estruyf/vscode';
@@ -15,15 +15,18 @@ import { arrayMoveImmutable } from 'array-move';
import { EmptyView } from './EmptyView';
import { Container } from './SortableContainer';
import { SortableItem } from './SortableItem';
import { ChevronRightIcon, CircleStackIcon } from '@heroicons/react/24/outline';
import { ChevronRightIcon, CircleStackIcon, EyeIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { DataType } from '../../../models/DataType';
import { GeneralCommands, WEBSITE_LINKS } from '../../../constants';
import { NavigationItem } from '../Layout';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { LocalizationKey, localize } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
import { MenuButton, MenuItem } from '../Menu';
import { Transition } from '@headlessui/react';
import { DataFolder } from '../../../models';
import { ActionsBarItem } from '../Header/ActionsBarItem';
import { Spinner } from '../Common/Spinner';
import { openFile } from '../../utils/MessageHandlers';
export interface IDataViewProps { }
@@ -33,6 +36,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
const [selectedData, setSelectedData] = useState<DataFile | null>(null);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [dataEntries, setDataEntries] = useState<any | any[] | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const settings = useRecoilValue(SettingsSelector);
const setSchema = (dataFile: DataFile) => {
@@ -71,6 +75,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
return;
}
debugger
const dataClone: any[] = Object.assign([], dataEntries);
if (selectedIndex !== null && selectedIndex !== undefined) {
dataClone[selectedIndex] = data;
@@ -110,11 +115,23 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
entries: data
});
Messenger.send(DashboardMessage.showNotification, l10n.t(LocalizationKey.dashboardDataViewDataViewUpdateMessage));
Messenger.send(DashboardMessage.showNotification, localize(LocalizationKey.dashboardDataViewDataViewUpdateMessage));
},
[selectedData]
);
const createDataFile = (folder: DataFolder) => {
setLoading(true);
messageHandler.request<DataFile>(DashboardMessage.createDataFile, folder).then(dataFile => {
if (dataFile) {
setSchema(dataFile);
}
setLoading(false);
}).catch((_: any) => {
setLoading(false);
});
}
const dataEntry = useMemo(() => {
if (selectedData?.singleEntry) {
return dataEntries || {};
@@ -125,10 +142,40 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
: null;
}, [selectedData, , dataEntries, selectedIndex]);
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
const dataFiles = useMemo(() => {
return (settings?.dataFiles || [])
.map((dataFile: DataFile) => {
if (!dataFile.schema && !dataFile.id) {
return null;
}
const clonedFile = Object.assign({}, dataFile);
if (clonedFile.type) {
const dataType = settings?.dataTypes?.find(
(dataType: DataType) => dataType.id === clonedFile.type
);
if (!dataType) {
return null;
}
clonedFile.schema = Object.assign({}, dataType.schema);
}
return clonedFile;
})
.filter((d) => d !== null) as DataFile[];
}, [settings?.dataFiles]);
const fileCreationFolders = useMemo(() => {
return (settings?.dataFolders || [])
.filter((folder) => folder.enableFileCreation);
}, [settings?.dataFolders]);
useEffect(() => {
Messenger.listen(messageListener);
Messenger.send(DashboardMessage.setTitle, l10n.t(LocalizationKey.dashboardHeaderTabsData));
Messenger.send(DashboardMessage.setTitle, localize(LocalizationKey.dashboardHeaderTabsData));
Messenger.send(GeneralCommands.toVSCode.logging.info, {
message: 'Data view loaded',
@@ -140,29 +187,6 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
};
}, []);
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
const dataFiles = (settings?.dataFiles || [])
.map((dataFile: DataFile) => {
if (!dataFile.schema && !dataFile.id) {
return null;
}
const clonedFile = Object.assign({}, dataFile);
if (clonedFile.type) {
const dataType = settings?.dataTypes?.find(
(dataType: DataType) => dataType.id === clonedFile.type
);
if (!dataType) {
return null;
}
clonedFile.schema = Object.assign({}, dataType.schema);
}
return clonedFile;
})
.filter((d) => d !== null) as DataFile[];
return (
<div className="flex flex-col h-full overflow-auto inset-y-0">
<Header settings={settings} />
@@ -176,7 +200,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
className={`flex flex-col flex-grow overflow-y-auto border-r py-6 px-4 overflow-auto border-[var(--frontmatter-border)]`}
>
<h2 className={`text-lg text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewDataViewSelect)}
{localize(LocalizationKey.dashboardDataViewDataViewSelect)}
</h2>
<nav className={`flex-1 py-4 -mx-4`}>
@@ -209,24 +233,70 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
enterTo="opacity-100">
<div className={`w-full px-4 py-2 border-b border-[var(--frontmatter-border)]`}>
{selectedData && (
<DropdownMenu>
<MenuButton
label={l10n.t(LocalizationKey.dashboardDataViewDataViewSelect)}
title={selectedData.title}
/>
<DropdownMenuContent>
{dataFiles.map((dataFile) => (
<MenuItem
key={dataFile.id}
title={dataFile.title}
value={dataFile}
isCurrent={selectedData.id === dataFile.id}
onClick={() => setSchema(dataFile)}
<div className={`flex justify-between`}>
<div className={`flex gap-4`}>
<DropdownMenu>
<MenuButton
label={localize(LocalizationKey.dashboardDataViewDataViewSelect)}
title={selectedData.title}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuContent>
{dataFiles.map((dataFile) => (
<MenuItem
key={dataFile.id}
title={dataFile.title}
value={dataFile}
isCurrent={selectedData.id === dataFile.id}
onClick={() => setSchema(dataFile)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
{
fileCreationFolders && fileCreationFolders.length > 0 && (
<DropdownMenu>
<MenuButton
label={localize(LocalizationKey.dashboardDataViewDataViewCreateNew)}
title={localize(LocalizationKey.dashboardDataViewDataViewSelectDataFolder)}
/>
<DropdownMenuContent>
{fileCreationFolders.map((folder) => (
<MenuItem
key={folder.id}
title={folder.id}
value={folder}
onClick={() => createDataFile(folder)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
</div>
<div className={`flex gap-2`}>
<ActionsBarItem
className='flex items-center'
onClick={() => openFile(selectedData.file)}
title={localize(LocalizationKey.commonView)}
>
<EyeIcon className="w-4 h-4 mr-1" aria-hidden="true" />
<span>{localize(LocalizationKey.commonView)}</span>
</ActionsBarItem>
<ActionsBarItem
className='flex items-center hover:text-[var(--vscode-statusBarItem-warningBackground)]'
onClick={() => setSelectedData(null)}
title={localize(LocalizationKey.dashboardDataViewDataViewCloseSelectedDataFile)}
>
<XMarkIcon className="w-4 h-4 mr-1" aria-hidden="true" />
<span>{localize(LocalizationKey.dashboardDataViewDataViewCloseSelectedDataFile)}</span>
</ActionsBarItem>
</div>
</div>
)}
</div>
</Transition>
@@ -239,7 +309,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
className={`w-1/3 py-6 px-4 flex-1 border-r overflow-auto border-[var(--frontmatter-border)]`}
>
<h2 className={`text-lg text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewDataViewTitle, selectedData?.title?.toLowerCase() || '')}
{localize(LocalizationKey.dashboardDataViewDataViewTitle, selectedData?.title?.toLowerCase() || '')}
</h2>
<div className="py-4">
@@ -259,13 +329,13 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
))}
</Container>
<Button className="mt-4" onClick={() => setSelectedIndex(null)}>
{l10n.t(LocalizationKey.dashboardDataViewDataViewAdd)}
{localize(LocalizationKey.dashboardDataViewDataViewAdd)}
</Button>
</>
) : (
<div className={`flex flex-col items-center justify-center`}>
<p className={`text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewDataViewEmpty, selectedData.title.toLowerCase())}
{localize(LocalizationKey.dashboardDataViewDataViewEmpty, selectedData.title.toLowerCase())}
</p>
</div>
)}
@@ -277,7 +347,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
} py-6 px-4 overflow-auto`}
>
<h2 className={`text-lg text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewDataViewCreateOrModify, selectedData.title.toLowerCase())}
{localize(LocalizationKey.dashboardDataViewDataViewCreateOrModify, selectedData.title.toLowerCase())}
</h2>
{selectedData ? (
<DataForm
@@ -287,12 +357,14 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
onClear={() => setSelectedIndex(null)}
/>
) : (
<p>{l10n.t(LocalizationKey.dashboardDataViewDataViewGetStarted)}</p>
<p>{localize(LocalizationKey.dashboardDataViewDataViewGetStarted)}</p>
)}
</div>
</>
) : (
<EmptyView />
<EmptyView
folders={fileCreationFolders}
onCreate={createDataFile} />
)}
</section>
</div>
@@ -300,14 +372,14 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
<div className="w-full h-full flex items-center justify-center">
<div className={`flex flex-col items-center text-[var(--frontmatter-text)]`}>
<CircleStackIcon className="w-32 h-32" />
<p className="text-3xl mt-2">{l10n.t(LocalizationKey.dashboardDataViewDataViewNoDataFiles)}</p>
<p className="text-3xl mt-2">{localize(LocalizationKey.dashboardDataViewDataViewNoDataFiles)}</p>
<p className="text-xl mt-4">
<a
className={`text-[var(--frontmatter-link)] hover:text-[var(--frontmatter-link-hover)]`}
href={WEBSITE_LINKS.docs.dataDashboard}
title={l10n.t(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
title={localize(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
>
{l10n.t(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
{localize(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
</a>
</p>
</div>
@@ -315,6 +387,8 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
)
}
{loading && <Spinner />}
<SponsorMsg
beta={settings?.beta}
version={settings?.versionInfo}

View File

@@ -1,12 +1,18 @@
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { LocalizationKey, localize } from '../../../localization';
import { DataFolder } from '../../../models';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
import { MenuButton, MenuItem } from '../Menu';
export interface IEmptyViewProps { }
export interface IEmptyViewProps {
folders: DataFolder[];
onCreate: (folder: DataFolder) => void;
}
export const EmptyView: React.FunctionComponent<IEmptyViewProps> = (
props: React.PropsWithChildren<IEmptyViewProps>
{ folders, onCreate }: React.PropsWithChildren<IEmptyViewProps>
) => {
return (
@@ -15,6 +21,32 @@ export const EmptyView: React.FunctionComponent<IEmptyViewProps> = (
<h2 className={`text-xl text-[var(--frontmatter-secondary-text)]`}>
{l10n.t(LocalizationKey.dashboardDataViewEmptyViewHeading)}
</h2>
{
onCreate && folders && folders.length > 0 && (
<div className='mt-4'>
<DropdownMenu>
<MenuButton
label={localize(LocalizationKey.dashboardDataViewDataViewCreateNew)}
title={localize(LocalizationKey.dashboardDataViewDataViewSelectDataFolder)}
className={`text-lg`}
labelClass={`font-normal text-[var(--frontmatter-secondary-text)]`}
/>
<DropdownMenuContent>
{folders.map((folder) => (
<MenuItem
key={folder.id}
title={folder.id}
value={folder}
onClick={() => onCreate(folder)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
</div>
);
};

View File

@@ -1,26 +1,33 @@
import { ChevronDownIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { cn } from '../../../utils/cn';
export interface IMenuButtonProps {
label: string | JSX.Element;
title: string;
disabled?: boolean;
className?: string;
labelClass?: string;
buttonClass?: string;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
label,
title,
disabled
disabled,
className,
labelClass,
buttonClass,
}: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`group flex items-center shrink-0 ${disabled ? 'opacity-50' : ''}`}>
<div className={`mr-2 font-medium flex items-center text-[var(--vscode-tab-inactiveForeground)]`}>
<div className={cn(`group flex items-center shrink-0 ${disabled ? 'opacity-50' : ''} ${className || ""}`)}>
<div className={cn(`mr-2 font-medium flex items-center text-[var(--vscode-tab-inactiveForeground)] ${labelClass || ""}`)}>
{label}:
</div>
<DropdownMenuTrigger
className='text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)] flex items-center focus:outline-none'
className={cn(`text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)] flex items-center focus:outline-none ${buttonClass || ""}`)}
disabled={disabled}>
<span>{title}</span>
<ChevronDownIcon className={`-mr-1 ml-1 h-4 w-4`} aria-hidden="true" />

View File

@@ -5,6 +5,7 @@ import {
ContentType,
CustomScript,
CustomTaxonomy,
DataFolder,
DraftField,
FilterType,
Framework,
@@ -43,6 +44,7 @@ export interface Settings {
dashboardState: DashboardState;
scripts: CustomScript[];
dataFiles: DataFile[] | undefined;
dataFolders: DataFolder[];
dataTypes: DataType[] | undefined;
isBacker: boolean | undefined;
snippets: Snippets | undefined;

View File

@@ -58,6 +58,7 @@ import { parseWinPath } from './parseWinPath';
import { TaxonomyHelper } from './TaxonomyHelper';
import { ContentType } from './ContentType';
import { Logger } from './Logger';
import { DataListener } from '../listeners/dashboard';
export class DashboardSettings {
private static cachedSettings: ISettings | undefined = undefined;
@@ -158,6 +159,7 @@ export class DashboardSettings {
}
},
dataFiles: await this.getDataFiles(),
dataFolders: Settings.get<DataFolder[]>(SETTING_DATA_FOLDERS) || [],
dataTypes: Settings.get<DataType[]>(SETTING_DATA_TYPES),
snippets: Settings.get<Snippets>(SETTING_CONTENT_SNIPPETS),
snippetsWrapper: Settings.get<boolean>(SETTING_SNIPPETS_WRAPPER),
@@ -217,16 +219,7 @@ export class DashboardSettings {
const dataFiles = [...dataJsonFiles, ...dataYmlFiles, ...dataYamlFiles];
for (let dataFile of dataFiles) {
clonedFiles.push({
id: basename(dataFile.fsPath),
title: basename(dataFile.fsPath),
file: dataFile.fsPath,
fileType: dataFile.fsPath.endsWith('.json') ? 'json' : 'yaml',
labelField: folder.labelField,
schema: folder.schema,
type: folder.type,
singleEntry: typeof folder.singleEntry === 'boolean' ? folder.singleEntry : false
} as DataFile);
clonedFiles.push(DataListener.createDataFileObject(dataFile.fsPath, folder));
}
}
}

View File

@@ -3,9 +3,14 @@ import { Logger } from './Logger';
import { Notifications } from './Notifications';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands';
export const openFileInEditor = async (filePath: string) => {
if (filePath) {
if (filePath.startsWith(WORKSPACE_PLACEHOLDER)) {
filePath = Folders.getAbsFilePath(filePath);
}
try {
const doc = await workspace.openTextDocument(Uri.file(filePath));
await window.showTextDocument(doc, 1, false);

View File

@@ -1,15 +1,17 @@
import { workspace } from 'vscode';
import { workspace, window } from 'vscode';
import { DataFile } from './../../models/DataFile';
import { DashboardMessage } from '../../dashboardWebView/DashboardMessage';
import { BaseListener } from './BaseListener';
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
import { Folders } from '../../commands/Folders';
import { dirname } from 'path';
import { basename, dirname, join } from 'path';
import * as yaml from 'js-yaml';
import { DataFileHelper, Logger } from '../../helpers';
import { existsAsync, readFileAsync, writeFileAsync } from '../../utils';
import { mkdirAsync } from '../../utils/mkdirAsync';
import { PostMessageData } from '../../models';
import { DataFolder, PostMessageData } from '../../models';
import { LocalizationKey, localize } from '../../localization';
import { SettingsListener } from './SettingsListener';
export class DataListener extends BaseListener {
public static process(msg: PostMessageData) {
@@ -26,11 +28,35 @@ export class DataListener extends BaseListener {
case DashboardMessage.putDataEntries:
this.processDataUpdate(msg?.payload);
break;
case DashboardMessage.createDataFile:
this.createDataFile(msg.command, msg.requestId || '', msg.payload);
break;
default:
return;
}
}
/**
* Creates a DataFile object based on the provided path and folder.
*
* @param path - The path of the file.
* @param folder - The DataFolder object.
* @returns The created DataFile object.
*/
public static createDataFileObject(path: string, folder: DataFolder): DataFile {
const filePath = Folders.wsPath(path);
return {
id: basename(path),
title: basename(path),
file: filePath,
fileType: path.endsWith('.json') ? 'json' : 'yaml',
labelField: folder.labelField,
schema: folder.schema,
type: folder.type,
singleEntry: typeof folder.singleEntry === 'boolean' ? folder.singleEntry : false
};
}
/**
* Process the data update
* @param msgData
@@ -81,4 +107,57 @@ export class DataListener extends BaseListener {
const entries = await DataFileHelper.process(msgData);
this.sendMsg(DashboardCommand.dataFileEntries, entries);
}
/**
* Create a new data file
* @param command
* @param requestId
* @param data
*/
private static async createDataFile(command: string, requestId: string, dataFolder: DataFolder) {
if (!command || !requestId || !dataFolder) {
return;
}
if (!dataFolder.id || !dataFolder.path) {
this.sendError(
command as DashboardCommand,
requestId,
localize(LocalizationKey.listenersPanelDataListenerCreateDataFileError)
);
return;
}
const fileName = await window.showInputBox({
title: localize(LocalizationKey.listenersPanelDataListenerCreateDataFileInputTitle),
prompt: localize(LocalizationKey.listenersPanelDataListenerCreateDataFileInputTitle),
ignoreFocusOut: true
});
if (!fileName || fileName.trim() === '') {
this.sendError(
command as DashboardCommand,
requestId,
localize(LocalizationKey.listenersPanelDataListenerCreateDataFileNoFileName)
);
return;
}
const absPath = Folders.getAbsFilePath(dataFolder.path);
if (!(await existsAsync(absPath))) {
const dirPath = dirname(absPath);
if (!(await existsAsync(dirPath))) {
await mkdirAsync(dirPath, { recursive: true });
}
}
// Check the file type and create the file
const filePath = join(absPath, `${fileName}.${dataFolder.fileType || 'json'}`);
await writeFileAsync(filePath, dataFolder.fileType === 'json' ? '{}' : '', 'utf8');
// Update the settings
await SettingsListener.getSettings(true);
const dataFile = DataListener.createDataFileObject(filePath, dataFolder);
this.sendRequest(command as DashboardCommand, requestId, dataFile);
}
}

View File

@@ -479,6 +479,18 @@ export enum LocalizationKey {
* Updated your data entries
*/
dashboardDataViewDataViewUpdateMessage = 'dashboard.dataView.dataView.update.message',
/**
* Create new data file
*/
dashboardDataViewDataViewCreateNew = 'dashboard.dataView.dataView.createNew',
/**
* Select data folder
*/
dashboardDataViewDataViewSelectDataFolder = 'dashboard.dataView.dataView.selectDataFolder',
/**
* Close data file
*/
dashboardDataViewDataViewCloseSelectedDataFile = 'dashboard.dataView.dataView.closeSelectedDataFile',
/**
* Select your date type first
*/
@@ -2592,6 +2604,18 @@ export enum LocalizationKey {
* Something went wrong while parsing your front matter. Please check the contents of your file.
*/
listenersPanelDataListenerPushMetadataFrontMatterError = 'listeners.panel.dataListener.pushMetadata.frontMatter.error',
/**
* What is the name of the data file?
*/
listenersPanelDataListenerCreateDataFileInputTitle = 'listeners.panel.dataListener.createDataFile.inputTitle',
/**
* No data file id or path defined.
*/
listenersPanelDataListenerCreateDataFileError = 'listeners.panel.dataListener.createDataFile.error',
/**
* No filename provided.
*/
listenersPanelDataListenerCreateDataFileNoFileName = 'listeners.panel.dataListener.createDataFile.noFileName',
/**
* No active editor
*/

View File

@@ -5,4 +5,6 @@ export interface DataFolder {
schema?: any;
type?: string;
singleEntry?: boolean;
enableFileCreation?: boolean;
fileType?: 'json' | 'yaml';
}