mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-07 01:13:08 +02:00
@@ -1,7 +1,23 @@
|
||||
# Change Log
|
||||
|
||||
## [5.2.0] - 2021-10-19
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#151](https://github.com/estruyf/vscode-front-matter/issues/151): Detect which site-generator or framework is used
|
||||
- [#152](https://github.com/estruyf/vscode-front-matter/issues/152): Automatically set setting based on the used site-generator or framework
|
||||
- [#154](https://github.com/estruyf/vscode-front-matter/issues/154): Bulk script support added
|
||||
- [#155](https://github.com/estruyf/vscode-front-matter/issues/155): Fallback image added for the images shown in the editor panel
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#153](https://github.com/estruyf/vscode-front-matter/issues/153): Support old date formatting for date-fns
|
||||
- [#156](https://github.com/estruyf/vscode-front-matter/issues/156): Fix for uploading media files into a new folder
|
||||
|
||||
## [5.1.1] - 2021-10-14
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#149](https://github.com/estruyf/vscode-front-matter/issues/149): Fix panel rendering when incorrect type for keywords is provided
|
||||
|
||||
## [5.1.0] - 2021-10-13
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "5.1.1",
|
||||
"version": "5.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+22
-1
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter",
|
||||
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "5.1.1",
|
||||
"version": "5.2.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -151,6 +151,22 @@
|
||||
"nodeBin": {
|
||||
"type": "string",
|
||||
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use."
|
||||
},
|
||||
"bulk": {
|
||||
"type": "boolean",
|
||||
"description": "Run the script for all content files"
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"editor",
|
||||
"notification"
|
||||
],
|
||||
"description": "Define where you want to output your script output. Default is a notification, but you can specify to show it in an editor panel."
|
||||
},
|
||||
"outputType": {
|
||||
"type": "string",
|
||||
"description": "The type of output for the editor panel. Can be used to change it to 'markdown' for example"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -180,6 +196,11 @@
|
||||
"markdownDescription": "Specify if you want to open the dashboard when you start VS Code. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.openonstart)",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.framework.id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the ID of your static site generator or framework you are using for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.framework.id)"
|
||||
},
|
||||
"frontMatter.panel.freeform": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { extname, basename } from 'path';
|
||||
import { COMMAND_NAME, DefaultFields } from '../constants';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
|
||||
|
||||
export class Article {
|
||||
@@ -171,7 +172,7 @@ export class Article {
|
||||
|
||||
let newFileName = `${slugName}${ext}`;
|
||||
if (filePrefix && typeof filePrefix === "string") {
|
||||
newFileName = `${format(new Date(), filePrefix)}-${newFileName}`;
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix))}-${newFileName}`;
|
||||
}
|
||||
|
||||
const newPath = editor.document.uri.fsPath.replace(fileName, newFileName);
|
||||
@@ -243,7 +244,7 @@ export class Article {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
return format(dateValue, dateFormat);
|
||||
return format(dateValue, DateHelper.formatUpdate(dateFormat));
|
||||
} else {
|
||||
return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME } from '../constants';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID } from '../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { basename, dirname, extname, join, parse } from "path";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
|
||||
import { Settings as SettingsHelper } from '../helpers';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { Framework, TaxonomyType } from '../models';
|
||||
import { Folders } from './Folders';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
|
||||
@@ -24,6 +24,7 @@ import { MediaLibrary } from '../helpers/MediaLibrary';
|
||||
import imageSize from 'image-size';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { FrameworkDetector } from '../helpers/FrameworkDetector';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
@@ -187,6 +188,9 @@ export class Dashboard {
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
|
||||
break;
|
||||
case DashboardMessage.setFramework:
|
||||
Dashboard.setFramework(msg?.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -257,6 +261,7 @@ export class Dashboard {
|
||||
private static async getSettings() {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = await Template.isInitialized();
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.settings,
|
||||
@@ -265,7 +270,7 @@ export class Dashboard {
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
|
||||
folders: Folders.get(),
|
||||
initialized: await Template.isInitialized(),
|
||||
initialized: isInitialized,
|
||||
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
|
||||
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
|
||||
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
|
||||
@@ -274,10 +279,30 @@ export class Dashboard {
|
||||
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
contentFolders: Folders.get().map(f => f.path),
|
||||
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
} as Settings
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
*/
|
||||
private static setFramework(frameworkId: string | null) {
|
||||
SettingsHelper.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
|
||||
|
||||
if (frameworkId) {
|
||||
const allFrameworks = FrameworkDetector.getAll();
|
||||
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
|
||||
if (framework) {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
} else {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting from the dashboard
|
||||
*/
|
||||
@@ -316,7 +341,15 @@ export class Dashboard {
|
||||
selectedFolder = '';
|
||||
}
|
||||
|
||||
const relSelectedFolderPath = selectedFolder ? selectedFolder.substring((parseWinPath(wsFolder?.fsPath || "")).length + 1) : '';
|
||||
let relSelectedFolderPath = selectedFolder;
|
||||
const parsedPath = parseWinPath(wsFolder?.fsPath || "");
|
||||
if (selectedFolder && selectedFolder.startsWith(parsedPath)) {
|
||||
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
|
||||
}
|
||||
|
||||
if (relSelectedFolderPath.startsWith('/')) {
|
||||
relSelectedFolderPath = relSelectedFolderPath.substring(1);
|
||||
}
|
||||
|
||||
let allMedia: MediaInfo[] = [];
|
||||
|
||||
@@ -549,9 +582,9 @@ export class Dashboard {
|
||||
|
||||
if (imgData) {
|
||||
writeFileSync(staticPath, imgData.data);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${staticFolder}/${folder}`);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
|
||||
|
||||
const folderPath = `${staticFolder}/${folder}`;
|
||||
const folderPath = `${folder}`;
|
||||
if (Dashboard.timers[folderPath]) {
|
||||
clearTimeout(Dashboard.timers[folderPath]);
|
||||
delete Dashboard.timers[folderPath];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { commands, env, Uri, ViewColumn, window } from "vscode";
|
||||
import { Settings } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
|
||||
|
||||
export class Preview {
|
||||
@@ -34,7 +35,7 @@ export class Preview {
|
||||
if (settings.pathname) {
|
||||
const articleDate = ArticleHelper.getDate(article);
|
||||
try {
|
||||
slug = join(format(articleDate || new Date(), settings.pathname), slug);
|
||||
slug = join(format(articleDate || new Date(), DateHelper.formatUpdate(settings.pathname)), slug);
|
||||
} catch (error) {
|
||||
slug = join(settings.pathname, slug);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const FrameworkDetectors = [
|
||||
{
|
||||
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
|
||||
"requiredFiles": ["gatsby-config.js"],
|
||||
"requiredDependencies": ["gatsby"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
|
||||
"requiredFiles": ["config.toml", "config.yaml", "config.yml"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
|
||||
"requiredFiles": ["next.config.js"],
|
||||
"requiredDependencies": ["next"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
|
||||
"requiredFiles": ["nuxt.config.js"],
|
||||
"requiredDependencies": ["nuxt"]
|
||||
}
|
||||
];
|
||||
@@ -42,6 +42,8 @@ export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
export const SETTINGS_FRAMEWORK_ID = "framework.id";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -17,5 +17,6 @@ export enum DashboardMessage {
|
||||
deleteMedia = 'deleteMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder'
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
setFramework = 'setFramework',
|
||||
}
|
||||
@@ -4,22 +4,17 @@ import { Status } from '../../models/Status';
|
||||
|
||||
export interface IStepProps {
|
||||
name: string;
|
||||
description: string;
|
||||
description: JSX.Element;
|
||||
status: Status;
|
||||
showLine: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: () => void | undefined;
|
||||
}
|
||||
|
||||
export const Step: React.FunctionComponent<IStepProps> = ({name, description, status, showLine, onClick}: React.PropsWithChildren<IStepProps>) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLine ? (
|
||||
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
|
||||
) : null
|
||||
}
|
||||
|
||||
<button className={`relative flex items-start group text-left ${onClick ? "" : "cursor-default"}`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
|
||||
const renderChildren = () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
status === Status.NotStarted && (
|
||||
<span className="h-9 flex items-center" aria-hidden="true">
|
||||
@@ -52,9 +47,31 @@ export const Step: React.FunctionComponent<IStepProps> = ({name, description, st
|
||||
|
||||
<span className="ml-4 min-w-0 flex flex-col">
|
||||
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
|
||||
<span className="text-sm text-vulcan-400 dark:text-whisper-600" dangerouslySetInnerHTML={{__html: description}} />
|
||||
<div className="text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLine ? (
|
||||
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
onClick ? (
|
||||
<button className={`relative flex items-start group text-left`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
|
||||
{renderChildren()}
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative flex items-start group text-left">
|
||||
{renderChildren()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -4,36 +4,99 @@ import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Settings } from '../../models/Settings';
|
||||
import { Status } from '../../models/Status';
|
||||
import { Step } from './Step';
|
||||
import { useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { Framework } from '../../../models';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
const [framework, setFramework] = useState<string | null>(null);
|
||||
|
||||
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
|
||||
const setFrameworkAndSendMessage = (framework: string) => {
|
||||
setFramework(framework);
|
||||
Messenger.send(DashboardMessage.setFramework, framework);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
name: 'Initialize project',
|
||||
description: 'Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.',
|
||||
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
|
||||
status: settings.initialized ? Status.Completed : Status.NotStarted,
|
||||
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
|
||||
},
|
||||
{
|
||||
name: 'Framework presets',
|
||||
description: (
|
||||
<div>
|
||||
<div>Select your site-generator or framework to prefill some of the recommended settings.</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left mt-4">
|
||||
<div>
|
||||
<Menu.Button className="group flex justify-center text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600 p-2 rounded-md border border-vulcan-400 dark:border-white">
|
||||
{framework ? framework : 'Select your framework'}
|
||||
<ChevronDownIcon
|
||||
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Menu.Items className={`w-40 origin-top-left absolute left-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
<MenuItem
|
||||
title={`other`}
|
||||
value={`other`}
|
||||
isCurrent={!framework}
|
||||
onClick={(value) => setFrameworkAndSendMessage(value)} />
|
||||
|
||||
<hr />
|
||||
|
||||
{frameworks.map((f) => (
|
||||
<MenuItem
|
||||
key={f.name}
|
||||
title={f.name}
|
||||
value={f.name}
|
||||
isCurrent={f.name === framework}
|
||||
onClick={(value) => setFrameworkAndSendMessage(value)} />
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
),
|
||||
status: settings.crntFramework ? Status.Completed : Status.NotStarted,
|
||||
onClick: undefined
|
||||
},
|
||||
{
|
||||
name: 'Register content folders (manual action)',
|
||||
description: 'Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.',
|
||||
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
|
||||
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
name: 'Show the dashboard',
|
||||
description: 'Once both actions are completed, click on this action to load the dashboard.',
|
||||
description: <>Once both actions are completed, click on this action to load the dashboard.</>,
|
||||
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
|
||||
}
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings.crntFramework) {
|
||||
setFramework(settings.crntFramework);
|
||||
}
|
||||
}, [settings.crntFramework]);
|
||||
|
||||
return (
|
||||
<nav aria-label="Progress">
|
||||
<ol role="list" className="overflow-hidden">
|
||||
<ol role="list">
|
||||
{steps.map((step, stepIdx) => (
|
||||
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
|
||||
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />
|
||||
|
||||
@@ -33,11 +33,11 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({set
|
||||
</h1>
|
||||
|
||||
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
Thank you for taking the time to test out Front Matter!
|
||||
Thank you for using Front Matter!
|
||||
</p>
|
||||
|
||||
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
We try to aim to make Front Matter as easy to use as possible, but if you have any questions, please don't hesitate to reach out to us on GitHub.
|
||||
We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 w-full sm:mx-auto sm:max-w-lg lg:ml-0">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ViewType } from '../state';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType } from '../../models';
|
||||
import { ContentType, Framework } from '../../models';
|
||||
|
||||
export interface Settings {
|
||||
beta: boolean;
|
||||
@@ -17,4 +17,6 @@ export interface Settings {
|
||||
mediaSnippet: string[];
|
||||
contentTypes: ContentType[];
|
||||
contentFolders: string[];
|
||||
crntFramework: string;
|
||||
framework: Framework | null | undefined;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { Template } from '../commands/Template';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
|
||||
import * as os from 'os';
|
||||
import { PanelSettings, CustomScript } from '../models/PanelSettings';
|
||||
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
|
||||
import { ArticleHelper, Settings } from "../helpers";
|
||||
import { Command } from "../panelWebView/Command";
|
||||
@@ -11,10 +11,8 @@ import { Article } from '../commands';
|
||||
import { TagType } from '../panelWebView/TagType';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { Content } from 'mdast';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { COMMAND_NAME } from '../constants/Extension';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { Preview } from '../commands/Preview';
|
||||
@@ -23,6 +21,7 @@ import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
import { Dashboard } from '../commands/Dashboard';
|
||||
import { ImageHelper } from '../helpers/ImageHelper';
|
||||
import { CustomScript } from '../helpers/CustomScript';
|
||||
|
||||
const FILE_LIMIT = 10;
|
||||
|
||||
@@ -352,38 +351,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* @param msg
|
||||
*/
|
||||
private runCustomScript(msg: { command: string, data: any}) {
|
||||
const scripts: CustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
|
||||
const scripts: ICustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
|
||||
|
||||
if (msg?.data?.title && msg?.data?.script && scripts) {
|
||||
const customScript = scripts.find((s: CustomScript) => s.title === msg.data.title);
|
||||
const customScript = scripts.find((s: ICustomScript) => s.title === msg.data.title);
|
||||
if (customScript?.script && customScript?.title) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const wsPath = wsFolder.fsPath;
|
||||
|
||||
let articleData = `'${JSON.stringify(article?.data)}'`;
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
}
|
||||
|
||||
exec(`${customScript.nodeBin || "node"} ${path.join(wsPath, msg.data.script)} "${wsPath}" "${editor?.document.uri.fsPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${msg?.data?.title}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.showInformationMessage(`${msg?.data?.title}: ${stdout || "Executed your custom script."}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
CustomScript.run(customScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Extension } from './helpers/Extension';
|
||||
import { DashboardData } from './models/DashboardData';
|
||||
import { Settings as SettingsHelper } from './helpers';
|
||||
import { Content } from './commands/Content';
|
||||
import ContentProvider from './providers/ContentProvider';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
let statusDebouncer: { (fnc: any, time: number): void; };
|
||||
@@ -175,6 +176,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Inserting an image in Markdown
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
|
||||
|
||||
// Create the editor experience for bulk scripts
|
||||
subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(ContentProvider.scheme, new ContentProvider()));
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(
|
||||
insertTags,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { EditorHelper } from '@estruyf/vscode';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { ContentType } from '../models';
|
||||
import { DateHelper } from './DateHelper';
|
||||
|
||||
export class ArticleHelper {
|
||||
|
||||
@@ -31,9 +32,9 @@ export class ArticleHelper {
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static getFrontMatterByPath(filePath: string) {
|
||||
public static getFrontMatterByPath(filePath: string, surpressNotification: boolean = false) {
|
||||
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
|
||||
return ArticleHelper.parseFile(file, filePath);
|
||||
return ArticleHelper.parseFile(file, filePath, surpressNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +204,7 @@ export class ArticleHelper {
|
||||
let newFileName = `${sanitizedName}.md`;
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix))}-${newFileName}`;
|
||||
}
|
||||
|
||||
newFilePath = join(folderPath, newFileName);
|
||||
@@ -222,7 +223,7 @@ export class ArticleHelper {
|
||||
* @param fileContents
|
||||
* @returns
|
||||
*/
|
||||
private static parseFile(fileContents: string, fileName: string): matter.GrayMatterFile<string> | null {
|
||||
private static parseFile(fileContents: string, fileName: string, surpressNotification: boolean = false): matter.GrayMatterFile<string> | null {
|
||||
try {
|
||||
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
|
||||
|
||||
@@ -254,15 +255,17 @@ export class ArticleHelper {
|
||||
await EditorHelper.showFile(fileName)
|
||||
}
|
||||
}];
|
||||
|
||||
Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => {
|
||||
if (result?.title) {
|
||||
const item = items.find(i => i.title === result.title);
|
||||
if (item) {
|
||||
item.action();
|
||||
|
||||
if (!surpressNotification) {
|
||||
Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => {
|
||||
if (result?.title) {
|
||||
const item = items.find(i => i.title === result.title);
|
||||
if (item) {
|
||||
item.action();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
|
||||
import { ArticleHelper } from '.';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { exec } from 'child_process';
|
||||
import matter = require('gray-matter');
|
||||
import * as os from 'os';
|
||||
import { join } from 'path';
|
||||
import { Notifications } from './Notifications';
|
||||
import ContentProvider from '../providers/ContentProvider';
|
||||
|
||||
export class CustomScript {
|
||||
|
||||
public static async run(script: ICustomScript): Promise<void> {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
if (wsFolder) {
|
||||
const wsPath = wsFolder.fsPath;
|
||||
|
||||
if (script.bulk) {
|
||||
// Run script on all files
|
||||
CustomScript.bulkRun(wsPath, script);
|
||||
} else {
|
||||
// Run script on current file.
|
||||
CustomScript.singleRun(wsPath, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async singleRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
|
||||
if (article) {
|
||||
const output = await CustomScript.runScript(wsPath, article, editor.document.uri.fsPath, script);
|
||||
|
||||
CustomScript.showOutput(output, script);
|
||||
} else {
|
||||
Notifications.warning(`${script.title}: Current article couldn't be retrieved.`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async bulkRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const folders = await Folders.getInfo();
|
||||
|
||||
if (!folders || folders.length === 0) {
|
||||
Notifications.warning(`${script.title}: No files found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let output: string[] = [];
|
||||
|
||||
window.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: `Executing: ${script.title}`,
|
||||
cancellable: false
|
||||
}, async (progress, token) => {
|
||||
for await (const folder of folders) {
|
||||
if (folder.lastModified.length > 0) {
|
||||
for await (const file of folder.lastModified) {
|
||||
try {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath, true);
|
||||
if (article) {
|
||||
const crntOutput = await CustomScript.runScript(wsPath, article, file.filePath, script);
|
||||
if (crntOutput) {
|
||||
output.push(crntOutput);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skipping file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomScript.showOutput(output.join(`\n`), script);
|
||||
});
|
||||
}
|
||||
|
||||
private static async runScript(wsPath: string, article: matter.GrayMatterFile<string> | null, contentPath: string, script: ICustomScript): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let articleData = "";
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
} else {
|
||||
articleData = JSON.stringify(article?.data).replace(/'/g, "%27");
|
||||
articleData = `'${articleData}'`;
|
||||
}
|
||||
|
||||
console.log(articleData);
|
||||
|
||||
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${script.title}: ${error.message}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static showOutput(output: string | null, script: ICustomScript): void {
|
||||
if (output) {
|
||||
if (script.output === "editor") {
|
||||
ContentProvider.show(output, script.title, script.outputType || "text");
|
||||
} else {
|
||||
window.showInformationMessage(`${script.title}: ${output}}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Notifications.info(`${script.title}: Executed your custom script.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@ import { parse, parseISO, parseJSON } from "date-fns";
|
||||
|
||||
export class DateHelper {
|
||||
|
||||
public static formatUpdate(value: string) {
|
||||
value = value.replace(/YYYY/g, 'yyyy');
|
||||
value = value.replace(/DD/g, 'dd');
|
||||
return value;
|
||||
}
|
||||
|
||||
public static tryParse(date: any, format?: string): Date | null {
|
||||
if (!date) {
|
||||
return null;
|
||||
@@ -35,7 +41,7 @@ export class DateHelper {
|
||||
}
|
||||
|
||||
public static isValid(date: any): boolean {
|
||||
return !isNaN(date.getTime());
|
||||
return date instanceof Date && !isNaN(date?.getTime());
|
||||
}
|
||||
|
||||
public static tryFormatParse(date: string, format: string): Date | null {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { existsSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
|
||||
import { Extension } from "./Extension";
|
||||
|
||||
export class FrameworkDetector {
|
||||
|
||||
public static get(folder: string) {
|
||||
return this.check(folder);
|
||||
}
|
||||
|
||||
public static getAll() {
|
||||
return FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
}
|
||||
|
||||
private static check(folder: string) {
|
||||
const { dependencies, devDependencies } = Extension.getInstance().packageJson;
|
||||
|
||||
for (const detector of FrameworkDetectors) {
|
||||
if (detector && folder) {
|
||||
|
||||
// Verify by dependencies
|
||||
for (const dependency of detector.requiredDependencies ?? []) {
|
||||
const inDependencies = dependencies && dependencies[dependency]
|
||||
const inDevDependencies = devDependencies && devDependencies[dependency]
|
||||
if (inDependencies || inDevDependencies) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify by files
|
||||
for (const filename of detector.requiredFiles ?? []) {
|
||||
const fileExists = existsSync(resolve(folder, filename));
|
||||
if (fileExists) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,9 @@ export class SlugHelper {
|
||||
* @param value
|
||||
*/
|
||||
private static removePunctuation(value: string): string {
|
||||
const punctuationless = value.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
const punctuationless = value?.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
// Remove double spaces
|
||||
return punctuationless.replace(/\s{2,}/g," ");
|
||||
return punctuationless?.replace(/\s{2,}/g," ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export interface Framework {
|
||||
name: string;
|
||||
dist: string;
|
||||
static: string;
|
||||
build: string;
|
||||
}
|
||||
@@ -72,6 +72,9 @@ export interface CustomScript {
|
||||
title: string;
|
||||
script: string;
|
||||
nodeBin?: string;
|
||||
bulk?: boolean;
|
||||
output?: "notification" | "editor";
|
||||
outputType?: string;
|
||||
}
|
||||
|
||||
export interface PreviewSettings {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export * from './Choice';
|
||||
export * from './ContentFolder';
|
||||
export * from './DashboardData';
|
||||
export * from './Framework';
|
||||
export * from './MediaPaths';
|
||||
export * from './PanelSettings';
|
||||
export * from './TaxonomyType';
|
||||
export * from './VersionInfo';
|
||||
|
||||
@@ -7,10 +7,13 @@ export interface IActionButtonProps {
|
||||
onClick: (e: React.SyntheticEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const ActionButton: React.FunctionComponent<IActionButtonProps> = ({className, onClick, disabled,title}: React.PropsWithChildren<IActionButtonProps>) => {
|
||||
const ActionButton: React.FunctionComponent<IActionButtonProps> = ({className, onClick, disabled,title}: React.PropsWithChildren<IActionButtonProps>) => {
|
||||
return (
|
||||
<div className={`article__action`}>
|
||||
<button onClick={onClick} className={className || ""} disabled={disabled}>{title}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ActionButton.displayName = 'ActionButton';
|
||||
export { ActionButton };
|
||||
@@ -10,7 +10,7 @@ export interface IActionsProps {
|
||||
settings: PanelSettings;
|
||||
}
|
||||
|
||||
export const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithChildren<IActionsProps>) => {
|
||||
const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithChildren<IActionsProps>) => {
|
||||
const { metadata, settings } = props;
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0 || !settings) {
|
||||
@@ -35,4 +35,7 @@ export const Actions: React.FunctionComponent<IActionsProps> = (props: React.Pro
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Actions.displayName = 'Actions';
|
||||
export { Actions };
|
||||
@@ -9,7 +9,7 @@ export interface IArticleDetailsProps {
|
||||
}
|
||||
}
|
||||
|
||||
export const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({details}: React.PropsWithChildren<IArticleDetailsProps>) => {
|
||||
const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({details}: React.PropsWithChildren<IArticleDetailsProps>) => {
|
||||
|
||||
if (!details || (details.headings === undefined && details.paragraphs === undefined)) {
|
||||
return null;
|
||||
@@ -46,4 +46,7 @@ export const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({d
|
||||
</VsTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ArticleDetails.displayName = 'ArticleDetails';
|
||||
export { ArticleDetails };
|
||||
@@ -13,7 +13,7 @@ export interface IBaseViewProps {
|
||||
folderAndFiles: FolderInfo[] | undefined;
|
||||
}
|
||||
|
||||
export const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles}: React.PropsWithChildren<IBaseViewProps>) => {
|
||||
const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles}: React.PropsWithChildren<IBaseViewProps>) => {
|
||||
|
||||
const openDashboard = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openDashboard);
|
||||
@@ -48,4 +48,7 @@ export const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, fol
|
||||
<SponsorMsg />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
BaseView.displayName = 'BaseView';
|
||||
export { BaseView };
|
||||
@@ -10,10 +10,26 @@ export interface ICollapsibleProps {
|
||||
sendUpdate?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children, title, sendUpdate, className}: React.PropsWithChildren<ICollapsibleProps>) => {
|
||||
const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, children, title, sendUpdate, className}: React.PropsWithChildren<ICollapsibleProps>) => {
|
||||
const [ isOpen, setIsOpen ] = React.useState(false);
|
||||
const collapseKey = `collapse-${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const collapsed = window.localStorage.getItem(collapseKey);
|
||||
if (collapsed === null || collapsed === 'true') {
|
||||
setIsOpen(true);
|
||||
updateStorage(true);
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.command === Command.closeSections) {
|
||||
setIsOpen(false);
|
||||
updateStorage(false);
|
||||
}
|
||||
});
|
||||
}, ['']);
|
||||
|
||||
const updateStorage = (value: boolean) => {
|
||||
window.localStorage.setItem(collapseKey, value.toString());
|
||||
}
|
||||
@@ -32,22 +48,6 @@ export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, chi
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const collapsed = window.localStorage.getItem(collapseKey);
|
||||
if (collapsed === null || collapsed === 'true') {
|
||||
setIsOpen(true);
|
||||
updateStorage(true);
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.command === Command.closeSections) {
|
||||
setIsOpen(false);
|
||||
updateStorage(false);
|
||||
}
|
||||
});
|
||||
}, ['']);
|
||||
|
||||
return (
|
||||
<VsCollapsible title={title} onClick={triggerClick} open={isOpen}>
|
||||
<div className={`section collapsible__body ${className || ""}`} slot="body">
|
||||
@@ -55,4 +55,7 @@ export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({id, chi
|
||||
</div>
|
||||
</VsCollapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Collapsible.displayName = 'Collapsible';
|
||||
export { Collapsible };
|
||||
@@ -8,7 +8,7 @@ export interface ICustomScriptProps {
|
||||
script: string;
|
||||
}
|
||||
|
||||
export const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title, script}: React.PropsWithChildren<ICustomScriptProps>) => {
|
||||
const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title, script}: React.PropsWithChildren<ICustomScriptProps>) => {
|
||||
|
||||
const runCustomScript = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.runCustomScript, { title, script });
|
||||
@@ -17,4 +17,7 @@ export const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({title
|
||||
return (
|
||||
<ActionButton onClick={runCustomScript} title={title} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
CustomScript.displayName = 'CustomScript';
|
||||
export { CustomScript };
|
||||
@@ -24,11 +24,6 @@ const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ value, onClick }
|
||||
|
||||
export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({label, date, format, onChange}: React.PropsWithChildren<IDateTimeFieldProps>) => {
|
||||
const [ dateValue, setDateValue ] = React.useState<Date | null>(null);
|
||||
|
||||
const onDateChange = (date: Date) => {
|
||||
setDateValue(date);
|
||||
onChange(date);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const crntValue = DateHelper.tryParse(date, format);
|
||||
@@ -38,6 +33,11 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
|
||||
setDateValue(date);
|
||||
}
|
||||
}, [ date ]);
|
||||
|
||||
const onDateChange = (date: Date) => {
|
||||
setDateValue(date);
|
||||
onChange(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { XCircleIcon } from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IImageFallbackProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export const ImageFallback: React.FunctionComponent<IImageFallbackProps> = ({ src }: React.PropsWithChildren<IImageFallbackProps>) => {
|
||||
|
||||
if (!src) {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '120px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'var(--button-secondary-hover-background)'
|
||||
}}>
|
||||
<XCircleIcon style={{
|
||||
height: '8rem',
|
||||
width: '8rem',
|
||||
color: 'var(--vscode-errorForeground)',
|
||||
}} />
|
||||
|
||||
<p style={{
|
||||
marginBottom: '1rem',
|
||||
color: 'var(--button-secondary-foreground)',
|
||||
}}>The image couldn't be loaded</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img src={src} />
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ImageFallback } from './ImageFallback';
|
||||
import { PreviewImageValue } from './PreviewImageField';
|
||||
|
||||
export interface IPreviewImageProps {
|
||||
@@ -7,9 +8,10 @@ export interface IPreviewImageProps {
|
||||
}
|
||||
|
||||
export const PreviewImage: React.FunctionComponent<IPreviewImageProps> = ({ value, onRemove }: React.PropsWithChildren<IPreviewImageProps>) => {
|
||||
|
||||
return (
|
||||
<div className={`metadata_field__preview_image__preview`}>
|
||||
<img src={value.webviewUrl} />
|
||||
<ImageFallback src={value.webviewUrl} />
|
||||
|
||||
<button type="button" onClick={() => onRemove(value.original)} className={`metadata_field__preview_image__remove`}>Remove image</button>
|
||||
</div>
|
||||
|
||||
@@ -13,17 +13,17 @@ export interface ITextFieldProps {
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
const [ text, setText ] = React.useState<string | null>(value);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
onChange(txtValue);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
onChange(txtValue);
|
||||
};
|
||||
|
||||
let isValid = true;
|
||||
if (limit && limit !== -1) {
|
||||
@@ -59,8 +59,6 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
{
|
||||
limit && limit > 0 && (text || "").length > limit && (
|
||||
<div className={`metadata_field__limit`}>
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface IFileItemProps {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }: React.PropsWithChildren<IFileItemProps>) => {
|
||||
const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }: React.PropsWithChildren<IFileItemProps>) => {
|
||||
|
||||
const openFile = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openInEditor, path);
|
||||
@@ -29,4 +29,7 @@ export const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }
|
||||
<span>{name}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
FileItem.displayName = 'FileItem';
|
||||
export { FileItem };
|
||||
@@ -9,7 +9,7 @@ export interface IFileListProps {
|
||||
totalFiles: number;
|
||||
}
|
||||
|
||||
export const FileList: React.FunctionComponent<IFileListProps> = ({files, folderName, totalFiles}: React.PropsWithChildren<IFileListProps>) => {
|
||||
const FileList: React.FunctionComponent<IFileListProps> = ({files, folderName, totalFiles}: React.PropsWithChildren<IFileListProps>) => {
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return null;
|
||||
@@ -28,4 +28,7 @@ export const FileList: React.FunctionComponent<IFileListProps> = ({files, folder
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
FileList.displayName = 'FileList';
|
||||
export { FileList };
|
||||
@@ -9,7 +9,7 @@ export interface IFolderAndFilesProps {
|
||||
isBase?: boolean;
|
||||
}
|
||||
|
||||
export const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({data, isBase}: React.PropsWithChildren<IFolderAndFilesProps>) => {
|
||||
const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({data, isBase}: React.PropsWithChildren<IFolderAndFilesProps>) => {
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
@@ -42,4 +42,7 @@ export const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({d
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
FolderAndFiles.displayName = 'FolderAndFiles';
|
||||
export { FolderAndFiles };
|
||||
@@ -11,7 +11,7 @@ export interface IGlobalSettingsProps {
|
||||
isBase?: boolean;
|
||||
}
|
||||
|
||||
export const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings, isBase}: React.PropsWithChildren<IGlobalSettingsProps>) => {
|
||||
const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({settings, isBase}: React.PropsWithChildren<IGlobalSettingsProps>) => {
|
||||
const { modifiedDateUpdate, fmHighlighting } = settings || {};
|
||||
const [ previewUrl, setPreviewUrl ] = React.useState<string>("");
|
||||
const [ isDirty, setIsDirty ] = React.useState<boolean>(false);
|
||||
@@ -65,4 +65,7 @@ export const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({s
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
GlobalSettings.displayName = 'GlobalSettings';
|
||||
export { GlobalSettings };
|
||||
@@ -4,7 +4,10 @@ export interface IIconProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const Icon: React.FunctionComponent<IIconProps> = ({ name }: React.PropsWithChildren<IIconProps>) => {
|
||||
const Icon: React.FunctionComponent<IIconProps> = ({ name }: React.PropsWithChildren<IIconProps>) => {
|
||||
|
||||
return (<i className={`codicon codicon-${name}`}></i>);
|
||||
};
|
||||
};
|
||||
|
||||
Icon.displayName = 'Icon';
|
||||
export { Icon };
|
||||
@@ -26,7 +26,7 @@ export interface IMetadataProps {
|
||||
unsetFocus: () => void;
|
||||
}
|
||||
|
||||
export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren<IMetadataProps>) => {
|
||||
const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren<IMetadataProps>) => {
|
||||
const contentType = useContentType(settings, metadata);
|
||||
|
||||
const sendUpdate = (field: string | undefined, value: any) => {
|
||||
@@ -200,4 +200,7 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
|
||||
}
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Metadata.displayName = 'Metadata';
|
||||
export { Metadata };
|
||||
@@ -6,10 +6,13 @@ export interface IOtherActionButtonProps {
|
||||
onClick: (e: React.SyntheticEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const OtherActionButton: React.FunctionComponent<IOtherActionButtonProps> = ({ className, disabled, onClick, children}: React.PropsWithChildren<IOtherActionButtonProps>) => {
|
||||
const OtherActionButton: React.FunctionComponent<IOtherActionButtonProps> = ({ className, disabled, onClick, children}: React.PropsWithChildren<IOtherActionButtonProps>) => {
|
||||
return (
|
||||
<div className={`ext_link_block`}>
|
||||
<button onClick={onClick} className={className || ""} disabled={disabled}>{children}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
OtherActionButton.displayName = 'OtherActionButton';
|
||||
export { OtherActionButton };
|
||||
@@ -19,7 +19,7 @@ export interface IOtherActionsProps {
|
||||
isBase?: boolean;
|
||||
}
|
||||
|
||||
export const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFile, settings, isBase}: React.PropsWithChildren<IOtherActionsProps>) => {
|
||||
const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFile, settings, isBase}: React.PropsWithChildren<IOtherActionsProps>) => {
|
||||
|
||||
const openSettings = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openSettings);
|
||||
@@ -64,4 +64,7 @@ export const OtherActions: React.FunctionComponent<IOtherActionsProps> = ({isFil
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
OtherActions.displayName = 'OtherActions';
|
||||
export { OtherActions };
|
||||
@@ -7,7 +7,7 @@ export interface IPreviewProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.PropsWithChildren<IPreviewProps>) => {
|
||||
const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.PropsWithChildren<IPreviewProps>) => {
|
||||
|
||||
const open = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openPreview);
|
||||
@@ -20,4 +20,7 @@ export const Preview: React.FunctionComponent<IPreviewProps> = ({slug}: React.Pr
|
||||
return (
|
||||
<ActionButton onClick={open} title={`Open preview`} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Preview.displayName = 'Preview';
|
||||
export { Preview };
|
||||
@@ -9,7 +9,7 @@ export interface IPublishActionProps {
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export const PublishAction: React.FunctionComponent<IPublishActionProps> = (props: React.PropsWithChildren<IPublishActionProps>) => {
|
||||
const PublishAction: React.FunctionComponent<IPublishActionProps> = (props: React.PropsWithChildren<IPublishActionProps>) => {
|
||||
const { draft } = props;
|
||||
|
||||
const publish = () => {
|
||||
@@ -19,4 +19,7 @@ export const PublishAction: React.FunctionComponent<IPublishActionProps> = (prop
|
||||
return (
|
||||
<ActionButton onClick={publish} className={`${draft ? "" : "secondary"}`} title={draft ? "Publish" : "Revert to draft"} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
PublishAction.displayName = 'PublishAction';
|
||||
export { PublishAction };
|
||||
@@ -9,7 +9,7 @@ export interface ISeoDetailsProps {
|
||||
noValidation?: boolean;
|
||||
}
|
||||
|
||||
export const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: React.PropsWithChildren<ISeoDetailsProps>) => {
|
||||
const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: React.PropsWithChildren<ISeoDetailsProps>) => {
|
||||
const { allowedLength, title, value, valueTitle, noValidation } = props;
|
||||
|
||||
const validate = () => {
|
||||
@@ -38,4 +38,7 @@ export const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: Rea
|
||||
</VsTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoDetails.displayName = 'SeoDetails';
|
||||
export { SeoDetails };
|
||||
@@ -9,7 +9,7 @@ export interface ISeoFieldInfoProps {
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
export const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({ title, value, recommendation, isValid }: React.PropsWithChildren<ISeoFieldInfoProps>) => {
|
||||
const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({ title, value, recommendation, isValid }: React.PropsWithChildren<ISeoFieldInfoProps>) => {
|
||||
return (
|
||||
<VsTableRow>
|
||||
<VsTableCell className={`table__cell table__title`}>{title}</VsTableCell>
|
||||
@@ -19,4 +19,7 @@ export const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({ titl
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoFieldInfo.displayName = 'SeoFieldInfo';
|
||||
export { SeoFieldInfo };
|
||||
@@ -10,7 +10,11 @@ export interface ISeoKeywordInfoProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword, title, description, slug, content}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword, title, description, slug, content}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
|
||||
if (!keyword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VsTableRow>
|
||||
@@ -29,4 +33,7 @@ export const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({k
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoKeywordInfo.displayName = 'SeoKeywordInfo';
|
||||
export { SeoKeywordInfo };
|
||||
@@ -11,7 +11,7 @@ export interface ISeoKeywordsProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
|
||||
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
|
||||
|
||||
const validateKeywords = () => {
|
||||
if (!keywords) {
|
||||
@@ -55,8 +55,9 @@ export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keyword
|
||||
}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoKeywords.displayName = 'SeoKeywords';
|
||||
export { SeoKeywords };
|
||||
@@ -11,14 +11,34 @@ export interface ISeoStatusProps {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const { data, seo } = props;
|
||||
const { title } = data;
|
||||
const [ isOpen, setIsOpen ] = React.useState(true);
|
||||
const tableRef = React.useRef<HTMLElement>();
|
||||
const pushUpdate = React.useRef((value: boolean) => {
|
||||
setTimeout(() => {
|
||||
setIsOpen(value);
|
||||
}, 10);
|
||||
}).current;
|
||||
|
||||
const { descriptionField } = seo;
|
||||
|
||||
// Workaround for lit components not updating render
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
let height = 0;
|
||||
|
||||
tableRef.current?.childNodes.forEach((elm: any) => {
|
||||
height += elm.clientHeight;
|
||||
});
|
||||
|
||||
if (height > 0 && tableRef.current) {
|
||||
tableRef.current.style.height = `${height}px`;
|
||||
}
|
||||
}, 10);
|
||||
}, [title, data[descriptionField], data?.articleDetails?.wordCount]);
|
||||
|
||||
if (!title && !data[descriptionField]) {
|
||||
return null;
|
||||
}
|
||||
@@ -72,24 +92,14 @@ export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React
|
||||
);
|
||||
};
|
||||
|
||||
// Workaround for lit components not updating render
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
let height = 0;
|
||||
|
||||
tableRef.current?.childNodes.forEach((elm: any) => {
|
||||
height += elm.clientHeight;
|
||||
});
|
||||
|
||||
if (height > 0 && tableRef.current) {
|
||||
tableRef.current.style.height = `${height}px`;
|
||||
}
|
||||
}, 10);
|
||||
}, [title, data[descriptionField], data?.articleDetails?.wordCount]);
|
||||
|
||||
|
||||
return (
|
||||
<Collapsible id={`seo`} title="SEO Status" sendUpdate={(value) => setIsOpen(value)}>
|
||||
<Collapsible id={`seo`} title="SEO Status" sendUpdate={pushUpdate}>
|
||||
{ renderContent() }
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SeoStatus.displayName = 'SeoStatus';
|
||||
export { SeoStatus };
|
||||
@@ -11,7 +11,7 @@ export interface ISlugActionProps {
|
||||
slugOpts: Slug;
|
||||
}
|
||||
|
||||
export const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
const { value, crntValue, slugOpts } = props;
|
||||
|
||||
let slug = SlugHelper.createSlug(value);
|
||||
@@ -24,4 +24,7 @@ export const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: Rea
|
||||
return (
|
||||
<ActionButton onClick={optimize} disabled={crntValue === slug} title={`Optimize slug`} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SlugAction.displayName = 'SlugAction';
|
||||
export { SlugAction };
|
||||
@@ -2,8 +2,11 @@ import * as React from 'react';
|
||||
|
||||
export interface ISpinnerProps {}
|
||||
|
||||
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
return (
|
||||
<div className="spinner">Loading...</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Spinner.displayName = 'Spinner';
|
||||
export { Spinner };
|
||||
@@ -4,7 +4,7 @@ import { HeartIcon } from './Icons/HeartIcon';
|
||||
|
||||
export interface ISponsorMsgProps {}
|
||||
|
||||
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
return (
|
||||
<p className={`sponsor`}>
|
||||
<a href={SPONSOR_LINK} title="Sponsor Front Matter">
|
||||
@@ -12,4 +12,7 @@ export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = (props: Rea
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
SponsorMsg.displayName = 'SponsorMsg';
|
||||
export { SponsorMsg };
|
||||
@@ -13,7 +13,7 @@ export interface ITagProps {
|
||||
onRemove: (tags: string) => void;
|
||||
}
|
||||
|
||||
export const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithChildren<ITagProps>) => {
|
||||
const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithChildren<ITagProps>) => {
|
||||
const { value, className, title, onRemove, onCreate, disableConfigurable } = props;
|
||||
|
||||
return (
|
||||
@@ -25,4 +25,7 @@ export const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithCh
|
||||
<button title={title} className={`article__tags__items__item_delete ${className}`} onClick={() => onRemove(value)}>{value} <span><ArchiveIcon /></span></button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Tag.displayName = 'Tag';
|
||||
export { Tag };
|
||||
@@ -20,7 +20,7 @@ export interface ITagPickerProps {
|
||||
disableConfigurable?: boolean;
|
||||
}
|
||||
|
||||
export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const { label, icon, type, crntSelected, options, freeform, focussed, unsetFocus, disableConfigurable } = props;
|
||||
const [ selected, setSelected ] = React.useState<string[]>([]);
|
||||
const [ inputValue, setInputValue ] = React.useState<string>("");
|
||||
@@ -194,4 +194,7 @@ export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React
|
||||
disableConfigurable={!!disableConfigurable} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
TagPicker.displayName = 'TagPicker';
|
||||
export { TagPicker };
|
||||
@@ -11,7 +11,7 @@ export interface ITagsProps {
|
||||
onRemove: (tags: string) => void;
|
||||
}
|
||||
|
||||
export const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWithChildren<ITagsProps>) => {
|
||||
const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWithChildren<ITagsProps>) => {
|
||||
const { values, options, onCreate, onRemove, disableConfigurable } = props;
|
||||
|
||||
const knownTags = values.filter(v => options.includes(v));
|
||||
@@ -38,4 +38,7 @@ export const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWith
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Tags.displayName = 'Tags';
|
||||
export { Tags };
|
||||
@@ -6,7 +6,7 @@ export interface IValidInfoProps {
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
export const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: React.PropsWithChildren<IValidInfoProps>) => {
|
||||
const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: React.PropsWithChildren<IValidInfoProps>) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
@@ -18,4 +18,7 @@ export const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: R
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ValidInfo.displayName = 'ValidInfo';
|
||||
export { ValidInfo };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { window, Position, TextDocumentContentProvider, Uri, workspace, WorkspaceEdit, Range, languages, ViewColumn } from "vscode";
|
||||
|
||||
|
||||
export default class ContentProvider implements TextDocumentContentProvider {
|
||||
|
||||
public static get scheme() { return "frontmatter" };
|
||||
|
||||
provideTextDocumentContent(uri: Uri): string {
|
||||
return uri.query;
|
||||
}
|
||||
|
||||
public static async show(data: string, title: string, outputType?: string) {
|
||||
const apiData = JSON.stringify(data, null, 2);
|
||||
|
||||
const uri = Uri.parse(`${ContentProvider.scheme}:${title} output`);
|
||||
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
|
||||
await window.showTextDocument(doc, { preview: true, viewColumn: ViewColumn.Beside, preserveFocus: true });
|
||||
|
||||
const workEdits = new WorkspaceEdit();
|
||||
workEdits.replace(doc.uri, new Range(new Position(0, 0), new Position(doc.lineCount, 0)), data);
|
||||
await workspace.applyEdit(workEdits);
|
||||
|
||||
await languages.setTextDocumentLanguage(doc, outputType || "text");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user