mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-06 17:02:54 +02:00
#369 - required fields
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#369](https://github.com/estruyf/vscode-front-matter/issues/369): New `required` property to specify if a content-type field is required
|
||||
- [#376](https://github.com/estruyf/vscode-front-matter/issues/376): Ability to run scripts after content was created
|
||||
- [#377](https://github.com/estruyf/vscode-front-matter/issues/377): Git sync actions added on panel and content dashboard (pull and push your changes to remote)
|
||||
- [#379](https://github.com/estruyf/vscode-front-matter/issues/377): New `frontMatter.config.reload` command to reload the configuration file + reinitialize its listeners
|
||||
|
||||
@@ -164,17 +164,6 @@
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.article__tags__input.freeform {
|
||||
position: relative;
|
||||
outline: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.article__tags__input.freeform input {
|
||||
padding-right: 35px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.article__tags ul {
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
@@ -382,162 +371,6 @@ input:checked + .field__toggle__slider:before {
|
||||
}
|
||||
|
||||
/* Metadata */
|
||||
.metadata_field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__box {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: .5rem;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__label.metadata_field__label_parent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metadata_field__label svg {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__error {
|
||||
color: var(--vscode-errorForeground);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata_field__error button {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.metadata_field__error button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__input, .metadata_field__input:focus,
|
||||
.metadata_field__textarea, .metadata_field__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.metadata_field__limit {
|
||||
color: var(--vscode-inputValidation-warningBorder);
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__number {
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
width: 100%;
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
background-color: var(--vscode-input-background);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle:hover,
|
||||
.metadata_field__choice__toggle:focus,
|
||||
.metadata_field__choice__toggle:active,
|
||||
.metadata_field__choice__toggle:disabled {
|
||||
background-color: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle span {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle svg.icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-left: .25rem;
|
||||
|
||||
position: absolute;
|
||||
right: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list {
|
||||
width: 90%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list.open {
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li {
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li:active {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-selected="true"] {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-disabled="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list__item {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button {
|
||||
margin-top: .5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button_icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__datetime {
|
||||
display: flex;
|
||||
@@ -560,75 +393,6 @@ input:checked + .field__toggle__slider:before {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__multiple_images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.metadata_field__file__button,
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--vscode-button-background);
|
||||
padding: 1.5rem;
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button:hover,
|
||||
.metadata_field__preview_image__button:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button svg,
|
||||
.metadata_field__preview_image__button svg {
|
||||
color: var(--vscode-foreground);
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.metadata_field__file__button span,
|
||||
.metadata_field__preview_image__button span {
|
||||
color: var(--vscode-foreground);
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove:hover {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
/* File list */
|
||||
.file_list vscode-label {
|
||||
border-bottom: 1px solid var(--vscode-foreground);
|
||||
|
||||
@@ -743,6 +743,14 @@
|
||||
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.global.notifications.disabled": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "This is an array with the notifications types that can be disabled for Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notification.types)",
|
||||
"enum": [
|
||||
"requiredFieldValidation"
|
||||
]
|
||||
},
|
||||
"frontMatter.media.defaultSorting": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
@@ -1020,6 +1028,11 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Specify if the field is editable"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the field is required"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { CONTEXT, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
|
||||
import { ParsedFrontMatter } from './../parsers/FrontMatterParser';
|
||||
import { CONTEXT, NOTIFICATION_TYPE, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
|
||||
import { ArticleHelper, Notifications, SeoHelper, Settings } from '../helpers';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { DataListener } from '../listeners/panel';
|
||||
import { commands } from 'vscode';
|
||||
import { Field } from '../models';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -42,7 +44,7 @@ export class StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
// Check SEO for title and description length
|
||||
// Check SEO and required fields
|
||||
if (article && article.data) {
|
||||
collection.clear();
|
||||
|
||||
@@ -58,6 +60,9 @@ export class StatusListener {
|
||||
if (article.data[fieldName] && descLength > -1) {
|
||||
SeoHelper.checkLength(editor, collection, article, fieldName, descLength);
|
||||
}
|
||||
|
||||
// Check the required fields
|
||||
StatusListener.verifyRequiredFields(editor, article, collection);
|
||||
}
|
||||
|
||||
const panel = ExplorerView.getInstance();
|
||||
@@ -80,4 +85,98 @@ export class StatusListener {
|
||||
|
||||
frontMatterSB.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the required fields
|
||||
* @param article
|
||||
* @param collection
|
||||
*/
|
||||
private static verifyRequiredFields(editor: vscode.TextEditor, article: ParsedFrontMatter, collection: vscode.DiagnosticCollection) {
|
||||
// Check for missing fields
|
||||
const emptyFields = ContentType.findEmptyRequiredFields(article);
|
||||
const fieldsToReport = [];
|
||||
|
||||
if (emptyFields && emptyFields.length > 0) {
|
||||
const text = editor.document.getText();
|
||||
const markdown = ArticleHelper.stringifyFrontMatter("", article.data);
|
||||
const editorSpaces = vscode.window.activeTextEditor?.options?.tabSize;
|
||||
|
||||
const requiredDiagnostics: vscode.Diagnostic[] = [];
|
||||
|
||||
for (const fields of emptyFields) {
|
||||
let txtIdx = -1;
|
||||
let fieldName = "";
|
||||
let level = 0;
|
||||
|
||||
for (const field of fields) {
|
||||
const totalSpaces = level * (typeof editorSpaces === "string" ? parseInt(editorSpaces) : editorSpaces || 2);
|
||||
const crntIdx = StatusListener.findFieldLine(text, txtIdx, totalSpaces, field);
|
||||
|
||||
if (crntIdx && crntIdx > txtIdx) {
|
||||
txtIdx = crntIdx;
|
||||
fieldName = field.name;
|
||||
}
|
||||
|
||||
++level;
|
||||
}
|
||||
|
||||
if (txtIdx !== -1 && txtIdx < markdown.length) {
|
||||
fieldsToReport.push(fields.map(f => f.title).join("/"));
|
||||
|
||||
const posStart = editor.document.positionAt(txtIdx);
|
||||
const posEnd = editor.document.positionAt(txtIdx + 1 + fieldName.length);
|
||||
|
||||
const diagnostic: vscode.Diagnostic = {
|
||||
code: '',
|
||||
message: `This ${fields.map(f => f.name).join("/")} field is required to contain a value.`,
|
||||
range: new vscode.Range(posStart, posEnd),
|
||||
severity: vscode.DiagnosticSeverity.Error,
|
||||
source: 'Front Matter'
|
||||
};
|
||||
|
||||
requiredDiagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.has(editor.document.uri)) {
|
||||
const otherDiag = collection.get(editor.document.uri) || [];
|
||||
collection.set(editor.document.uri, [...otherDiag, ...requiredDiagnostics]);
|
||||
} else {
|
||||
collection.set(editor.document.uri, [...requiredDiagnostics]);
|
||||
}
|
||||
|
||||
if (fieldsToReport.length > 0) {
|
||||
Notifications.showIfNotDisabled(NOTIFICATION_TYPE.requiredFieldValidation, "ERROR_ONCE", `The following fields are required to contain a value: ${fieldsToReport.join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the line of the field
|
||||
* @param text
|
||||
* @param startIdx
|
||||
* @param totalSpaces
|
||||
* @param field
|
||||
* @returns
|
||||
*/
|
||||
private static findFieldLine(text: string, startIdx: number, totalSpaces: number, field: Field): number | undefined {
|
||||
const crntIdx = text.indexOf(field.name, startIdx === -1 ? 0 : startIdx);
|
||||
|
||||
if (crntIdx > -1) {
|
||||
// Find the linebreak before the current index
|
||||
const txtFromStart = text.substring(0, crntIdx);
|
||||
const splitLineBreaks = txtFromStart.split(/\r?\n/);
|
||||
const lastLine = splitLineBreaks[splitLineBreaks.length - 1];
|
||||
|
||||
if (lastLine.length === totalSpaces) {
|
||||
if (crntIdx > startIdx) {
|
||||
return crntIdx;
|
||||
}
|
||||
} else {
|
||||
return StatusListener.findFieldLine(text, crntIdx + field.name.length, totalSpaces, field);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const DefaultFieldValues = {
|
||||
faultyCustomPlaceholder: "<failed to process>"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const NOTIFICATION_TYPE = {
|
||||
requiredFieldValidation: "requiredFieldValidation",
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './ContentType';
|
||||
export * from './DefaultFieldValues';
|
||||
export * from './DefaultFields';
|
||||
export * from './DefaultFileTypes';
|
||||
export * from './Extension';
|
||||
@@ -9,6 +10,7 @@ export * from './GeneralCommands';
|
||||
export * from './Links';
|
||||
export * from './LocalStore';
|
||||
export * from './Navigation';
|
||||
export * from './NotificationType';
|
||||
export * from './PreviewCommands';
|
||||
export * from './TelemetryEvent';
|
||||
export * from './charCode';
|
||||
|
||||
@@ -3,6 +3,7 @@ export const EXTENSION_NAME = "Front Matter";
|
||||
export const CONFIG_KEY = "frontMatter";
|
||||
|
||||
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
|
||||
export const SETTING_GLOBAL_NOTIFICATIONS_DISABLED = "global.notifications.disabled";
|
||||
export const SETTING_GLOBAL_MODES = "global.modes";
|
||||
export const SETTING_GLOBAL_ACTIVE_MODE = "global.activeMode";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider'
|
||||
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from "fs";
|
||||
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD } from '../constants';
|
||||
import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD, DefaultFieldValues } from '../constants';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
|
||||
import { Extension, Logger, Settings, SlugHelper } from '.';
|
||||
@@ -443,7 +443,7 @@ export class ArticleHelper {
|
||||
Notifications.error(`Error while processing the ${placeholder.id} placeholder`);
|
||||
Logger.error((e as Error).message);
|
||||
|
||||
value = "<Failed to process>";
|
||||
value = DefaultFieldValues.faultyCustomPlaceholder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ModeListener } from './../listeners/general/ModeListener';
|
||||
import { PagesListener } from './../listeners/dashboard';
|
||||
import { ArticleHelper, CustomScript, Settings } from ".";
|
||||
import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants";
|
||||
import { DefaultFieldValues, FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants";
|
||||
import { ContentType as IContentType, DraftField, Field, FieldGroup, FieldType, ScriptType } from '../models';
|
||||
import { Uri, commands, window, ProgressLocation, workspace } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
@@ -278,7 +278,7 @@ export class ContentType {
|
||||
* @param parents
|
||||
* @returns
|
||||
*/
|
||||
public static getFieldValue(data: any, parents: string[]): string[] {
|
||||
public static getFieldValue(data: any, parents: string[]): string | string[] {
|
||||
let fieldValue = [];
|
||||
let crntPageData = data;
|
||||
|
||||
@@ -375,6 +375,51 @@ export class ContentType {
|
||||
return parents;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the required fields
|
||||
*/
|
||||
public static findEmptyRequiredFields(article: ParsedFrontMatter): Field[][] | undefined {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (!contentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allRequiredFields = ContentType.findRequiredFieldsDeep(contentType.fields);
|
||||
|
||||
let emptyFields: Field[][] = [];
|
||||
|
||||
for (const fields of allRequiredFields) {
|
||||
const fieldValue = this.getFieldValue(article.data, fields.map(f => f.name));
|
||||
if ((fieldValue === null || fieldValue === undefined || fieldValue === "") || fieldValue.length === 0 || fieldValue === DefaultFieldValues.faultyCustomPlaceholder) {
|
||||
emptyFields.push(fields);
|
||||
}
|
||||
}
|
||||
|
||||
return emptyFields || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the required fields in the content type
|
||||
* @param fields
|
||||
* @param parents
|
||||
* @returns
|
||||
*/
|
||||
private static findRequiredFieldsDeep(fields: Field[], parents: Field[][] = [], parentFields: Field[] = []): Field[][] {
|
||||
for (const field of fields) {
|
||||
if (field.required) {
|
||||
parents.push([...parentFields, field]);
|
||||
}
|
||||
|
||||
if (field.type === "fields" && field.fields) {
|
||||
this.findRequiredFieldsDeep(field.fields, parents, [...parentFields, field]);
|
||||
}
|
||||
}
|
||||
|
||||
return parents;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Look for the preview image in the block field
|
||||
* @param field
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { TelemetryEvent } from '../constants/TelemetryEvent';
|
||||
import { SETTING_CUSTOM_SCRIPTS } from '../constants';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
export class CustomScript {
|
||||
|
||||
@@ -272,6 +273,12 @@ export class CustomScript {
|
||||
}
|
||||
|
||||
const scriptPath = join(wsPath, script.script);
|
||||
|
||||
if (!existsSync(scriptPath)) {
|
||||
reject(new Error(`Script not found: ${scriptPath}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const fullScript = `${command} ${scriptPath} ${args}`;
|
||||
Logger.info(`Executing: ${fullScript}`);
|
||||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { SETTING_GLOBAL_NOTIFICATIONS_DISABLED } from './../constants/settings';
|
||||
import { window } from "vscode";
|
||||
import { EXTENSION_NAME, SETTING_GLOBAL_NOTIFICATIONS } from "../constants";
|
||||
import { Logger } from "./Logger";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
|
||||
type NotificationType = "INFO" | "WARNING" | "ERROR" | "ERROR_ONCE";
|
||||
|
||||
export class Notifications {
|
||||
private static notifications: string[] = [];
|
||||
|
||||
/**
|
||||
* Show a notification to the user
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static info(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO");
|
||||
|
||||
@@ -17,6 +25,12 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning notification to the user
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static warning(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "WARNING");
|
||||
|
||||
@@ -27,6 +41,12 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error notification to the user
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static error(message: string, ...items: any): Thenable<string | undefined> {
|
||||
Logger.info(`${EXTENSION_NAME}: ${message}`, "ERROR");
|
||||
|
||||
@@ -37,6 +57,12 @@ export class Notifications {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error notification to the user only once
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static async errorShowOnce(message: string, ...items: any): Promise<string | undefined> {
|
||||
if (this.notifications.includes(message)) {
|
||||
return;
|
||||
@@ -47,7 +73,40 @@ export class Notifications {
|
||||
return this.error(message, ...items);
|
||||
}
|
||||
|
||||
private static shouldShow(level: "INFO" | "WARNING" | "ERROR"): boolean {
|
||||
/**
|
||||
* Show the notification if not disabled
|
||||
* @param type
|
||||
* @param notificationType
|
||||
* @param message
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
public static async showIfNotDisabled(type: string, notificationType: NotificationType, message: string, ...items: any): Promise<string | undefined> {
|
||||
const disabledTypes = Settings.get<string[]>(SETTING_GLOBAL_NOTIFICATIONS_DISABLED);
|
||||
|
||||
if (disabledTypes && disabledTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case "WARNING":
|
||||
return await Notifications.warning(message, ...items);
|
||||
case "ERROR":
|
||||
return await Notifications.error(message, ...items);
|
||||
case "ERROR_ONCE":
|
||||
return await Notifications.errorShowOnce(message, ...items);
|
||||
case "INFO":
|
||||
default:
|
||||
return await Notifications.info(message, ...items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the notification should be shown
|
||||
* @param level
|
||||
* @returns
|
||||
*/
|
||||
private static shouldShow(level: NotificationType): boolean {
|
||||
let levels = Settings.get<string[]>(SETTING_GLOBAL_NOTIFICATIONS);
|
||||
|
||||
if (!levels) {
|
||||
|
||||
@@ -197,7 +197,10 @@ export class TaxonomyHelper {
|
||||
|
||||
if (fieldNames.length > 0 && article && article.data) {
|
||||
const { data } = article;
|
||||
let taxonomies: string[] = ContentType.getFieldValue(data, fieldNames);
|
||||
let taxonomies: string| string[] = ContentType.getFieldValue(data, fieldNames);
|
||||
if (typeof taxonomies === "string") {
|
||||
taxonomies = taxonomies.split(`,`);
|
||||
}
|
||||
|
||||
if (taxonomies && taxonomies.length > 0) {
|
||||
const idx = taxonomies.findIndex(o => o === oldValue);
|
||||
@@ -300,8 +303,15 @@ export class TaxonomyHelper {
|
||||
|
||||
if (oldFieldNames.length > 0 && newFieldNames.length > 0 && article && article.data) {
|
||||
const { data } = article;
|
||||
let oldTaxonomies: string[] = ContentType.getFieldValue(data, oldFieldNames) || [];
|
||||
let newTaxonomies: string[] = ContentType.getFieldValue(data, newFieldNames) || [];
|
||||
let oldTaxonomies: string | string[] = ContentType.getFieldValue(data, oldFieldNames) || [];
|
||||
let newTaxonomies: string | string[] = ContentType.getFieldValue(data, newFieldNames) || [];
|
||||
|
||||
if (typeof oldTaxonomies === "string") {
|
||||
oldTaxonomies = oldTaxonomies.split(",");
|
||||
}
|
||||
if (typeof newTaxonomies === "string") {
|
||||
newTaxonomies = newTaxonomies.split(",");
|
||||
}
|
||||
|
||||
if (oldTaxonomies && oldTaxonomies.length > 0) {
|
||||
const idx = oldTaxonomies.findIndex(o => o === value);
|
||||
|
||||
@@ -306,10 +306,12 @@ export class PagesListener extends BaseListener {
|
||||
}
|
||||
|
||||
let tagParents = ContentType.findFieldByType(contentType.fields, "tags");
|
||||
page.fmTags = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]);
|
||||
const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]);
|
||||
page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : [];
|
||||
|
||||
let categoryParents = ContentType.findFieldByType(contentType.fields, "categories");
|
||||
page.fmCategories = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]);
|
||||
const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]);
|
||||
page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : [];
|
||||
|
||||
// Check if parent fields were retrieved, if not there was no image present
|
||||
if (previewFieldParents.length > 0) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
export interface BaseFieldProps<T> {
|
||||
label: string;
|
||||
value: T | null;
|
||||
required?: boolean;
|
||||
}
|
||||
@@ -71,6 +71,7 @@ export interface Field {
|
||||
taxonomyLimit?: number;
|
||||
fileExtensions?: string[];
|
||||
editable?: boolean;
|
||||
required?: boolean;
|
||||
|
||||
// Date fields
|
||||
isPublishDate?: boolean;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './BaseFieldProps';
|
||||
export * from './BlockFieldData';
|
||||
export * from './Choice';
|
||||
export * from './ContentFolder';
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as React from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { BlockFieldData, Field, FieldGroup, PanelSettings } from '../../../models';
|
||||
import { PencilIcon } from '@heroicons/react/outline';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { DataBlockRecords, DataBlockSelector } from '.';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { IMetadata } from '../Metadata';
|
||||
import { FieldTitle } from '../Fields/FieldTitle';
|
||||
|
||||
export interface IDataBlockFieldProps {
|
||||
label: string;
|
||||
@@ -25,9 +25,10 @@ export interface IDataBlockFieldProps {
|
||||
) => (JSX.Element | null)[] | undefined;
|
||||
onSubmit: (data: any) => void;
|
||||
parentBlock: string | null | undefined;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({ label, filePath, settings, field, parentFields = [], value, fieldsRenderer, onSubmit, parentBlock }: React.PropsWithChildren<IDataBlockFieldProps>) => {
|
||||
export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({ label, filePath, settings, field, parentFields = [], value, fieldsRenderer, onSubmit, parentBlock, required }: React.PropsWithChildren<IDataBlockFieldProps>) => {
|
||||
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
|
||||
const [ selectedGroup, setSelectedGroup ] = useState<FieldGroup | undefined | null>(null);
|
||||
const [ selectedBlockData, setSelectedBlockData ] = useState<any | null>(null);
|
||||
@@ -229,12 +230,10 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
|
||||
|
||||
return (
|
||||
<div className='block_field'>
|
||||
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<PencilIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<PencilIcon />}
|
||||
required={required} />
|
||||
|
||||
{
|
||||
(!hideSubBlock) ? (
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {CheckIcon, ChevronDownIcon} from '@heroicons/react/outline';
|
||||
import Downshift from 'downshift';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { Choice } from '../../../models/Choice';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { ChoiceButton } from './ChoiceButton';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface IChoiceFieldProps {
|
||||
label: string;
|
||||
selected: string | string[];
|
||||
export interface IChoiceFieldProps extends BaseFieldProps<string | string[]> {
|
||||
choices: string[] | Choice[];
|
||||
multiSelect?: boolean;
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label, selected, choices, multiSelect, onChange}: React.PropsWithChildren<IChoiceFieldProps>) => {
|
||||
const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>(selected);
|
||||
export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({ label, value, choices, multiSelect, onChange, required }: React.PropsWithChildren<IChoiceFieldProps>) => {
|
||||
const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>(value);
|
||||
const dsRef = React.useRef<Downshift<string> | null>(null);
|
||||
|
||||
const onValueChange = (txtValue: string) => {
|
||||
@@ -54,12 +54,6 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label,
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (crntSelected !== selected) {
|
||||
setCrntSelected(selected);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const availableChoices = !multiSelect ? choices : (choices as Array<string | Choice>).filter((choice: string | Choice) => {
|
||||
const value = typeof choice === 'string' || typeof choice === 'number' ? choice : choice.id;
|
||||
@@ -73,13 +67,22 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label,
|
||||
return true;
|
||||
});
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected);
|
||||
}, [required, crntSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (crntSelected !== value) {
|
||||
setCrntSelected(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<CheckIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<div className={`metadata_field ${showRequiredState ? "required" : ""}`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<CheckIcon />}
|
||||
required={required} />
|
||||
|
||||
<Downshift
|
||||
ref={dsRef}
|
||||
@@ -112,6 +115,8 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label,
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
|
||||
{
|
||||
crntSelected instanceof Array ? crntSelected.map((value: string) => (
|
||||
|
||||
@@ -5,9 +5,10 @@ import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Command } from '../../Command';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import Downshift from 'downshift';
|
||||
import { ChoiceButton } from './ChoiceButton';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface IDataFileFieldProps {
|
||||
label: string;
|
||||
@@ -16,10 +17,11 @@ export interface IDataFileFieldProps {
|
||||
dataFileValue?: string;
|
||||
selected: string | string[];
|
||||
multiSelect?: boolean;
|
||||
required?: boolean;
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ label, dataFileId, dataFileKey, dataFileValue, selected, multiSelect, onChange }: React.PropsWithChildren<IDataFileFieldProps>) => {
|
||||
export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ label, dataFileId, dataFileKey, dataFileValue, selected, multiSelect, onChange, required }: React.PropsWithChildren<IDataFileFieldProps>) => {
|
||||
const [ dataEntries, setDataEntries ] = useState<string[] | null>(null);
|
||||
const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>();
|
||||
const dsRef = React.useRef<Downshift<string> | null>(null);
|
||||
@@ -91,6 +93,10 @@ export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ la
|
||||
return "";
|
||||
}, [allChoices]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected);
|
||||
}, [required, crntSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
if (multiSelect) {
|
||||
@@ -120,12 +126,11 @@ export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ la
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<DatabaseIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<div className={`metadata_field ${showRequiredState ? "required" : ""}`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<DatabaseIcon />}
|
||||
required={required} />
|
||||
|
||||
<Downshift
|
||||
ref={dsRef}
|
||||
@@ -158,6 +163,8 @@ export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({ la
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
|
||||
{
|
||||
crntSelected instanceof Array ? crntSelected.map((value: string) => (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import {ClockIcon} from '@heroicons/react/outline';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { forwardRef } from 'react';
|
||||
import { forwardRef, useEffect, useMemo } from 'react';
|
||||
import { DateHelper } from '../../../helpers/DateHelper';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
|
||||
export interface IDateTimeFieldProps {
|
||||
label: string;
|
||||
date: Date | null;
|
||||
export interface IDateTimeFieldProps extends BaseFieldProps<Date | null> {
|
||||
format?: string;
|
||||
onChange: (date: Date) => void;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ value, onClick }
|
||||
)
|
||||
});
|
||||
|
||||
export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({label, date, format, onChange}: React.PropsWithChildren<IDateTimeFieldProps>) => {
|
||||
export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({label, value, required, format, onChange}: React.PropsWithChildren<IDateTimeFieldProps>) => {
|
||||
const [ dateValue, setDateValue ] = React.useState<Date | null>(null);
|
||||
|
||||
const onDateChange = (date: Date) => {
|
||||
@@ -30,23 +30,26 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
|
||||
onChange(date);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const crntValue = DateHelper.tryParse(date, format);
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && !dateValue;
|
||||
}, [required, dateValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const crntValue = DateHelper.tryParse(value, format);
|
||||
const stateValue = DateHelper.tryParse(dateValue, format);
|
||||
|
||||
if (crntValue?.toISOString() !== stateValue?.toISOString()) {
|
||||
setDateValue(date);
|
||||
setDateValue(value);
|
||||
}
|
||||
}, [ date, dateValue ]);
|
||||
}, [ value, dateValue ]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<ClockIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<ClockIcon />}
|
||||
required={required} />
|
||||
|
||||
<div className={`metadata_field__datetime`}>
|
||||
<DatePicker
|
||||
selected={DateHelper.tryParse(dateValue) as Date || new Date()}
|
||||
@@ -61,6 +64,8 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
|
||||
now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { RocketIcon } from '../Icons/RocketIcon';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { ChoiceField } from './ChoiceField';
|
||||
import { Toggle } from './Toggle';
|
||||
|
||||
export interface IDraftFieldProps {
|
||||
label: string;
|
||||
export interface IDraftFieldProps extends BaseFieldProps<boolean | string | null | undefined> {
|
||||
type: "boolean" | "choice";
|
||||
value: boolean | string | null | undefined;
|
||||
|
||||
choices?: string[];
|
||||
|
||||
onChanged: (value: string | boolean) => void;
|
||||
}
|
||||
|
||||
export const DraftField: React.FunctionComponent<IDraftFieldProps> = ({ label, type, value, choices, onChanged }: React.PropsWithChildren<IDraftFieldProps>) => {
|
||||
export const DraftField: React.FunctionComponent<IDraftFieldProps> = ({ label, type, value, choices, onChanged, required }: React.PropsWithChildren<IDraftFieldProps>) => {
|
||||
|
||||
if (type === "boolean") {
|
||||
return (
|
||||
<Toggle
|
||||
label={label}
|
||||
checked={!!value}
|
||||
value={!!value}
|
||||
required={required}
|
||||
onChanged={(checked) => onChanged(checked)} />
|
||||
);
|
||||
}
|
||||
@@ -29,9 +27,10 @@ export const DraftField: React.FunctionComponent<IDraftFieldProps> = ({ label, t
|
||||
return (
|
||||
<ChoiceField
|
||||
label={label}
|
||||
selected={value as string}
|
||||
value={value as string}
|
||||
choices={choices as string[]}
|
||||
multiSelect={false}
|
||||
required={required}
|
||||
onChange={value => onChanged(value as string)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { RequiredAsterix } from './RequiredAsterix';
|
||||
|
||||
export interface IFieldTitleProps {
|
||||
label: string | JSX.Element;
|
||||
icon?: JSX.Element;
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({ label, icon, className, required }: React.PropsWithChildren<IFieldTitleProps>) => {
|
||||
|
||||
const Icon = useMemo(() => {
|
||||
return icon ? React.cloneElement(icon, { style: { width: "16px", height: "16px" } }) : null;
|
||||
}, [icon])
|
||||
|
||||
return (
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label ${className || ""}`}>
|
||||
{Icon}
|
||||
<span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
<RequiredAsterix required={required} />
|
||||
</div>
|
||||
</VsLabel>
|
||||
);
|
||||
};
|
||||
@@ -3,16 +3,15 @@ import { DocumentIcon, PaperClipIcon, TrashIcon } from '@heroicons/react/outline
|
||||
import { basename } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { BlockFieldData } from '../../../models';
|
||||
import { BaseFieldProps, BlockFieldData } from '../../../models';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface IFileFieldProps {
|
||||
label: string;
|
||||
export interface IFileFieldProps extends BaseFieldProps<string | string[] | null> {
|
||||
fieldName: string;
|
||||
filePath: string;
|
||||
multiple?: boolean;
|
||||
value: string | string[] | null;
|
||||
fileExtensions?: string[];
|
||||
parents?: string[];
|
||||
blockData?: BlockFieldData;
|
||||
@@ -35,7 +34,7 @@ const File = ({ value, onRemove }: { value: string, onRemove: (value: string) =>
|
||||
)
|
||||
}
|
||||
|
||||
export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange }: React.PropsWithChildren<IFileFieldProps>) => {
|
||||
export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange, required }: React.PropsWithChildren<IFileFieldProps>) => {
|
||||
|
||||
const selectFile = useCallback(() => {
|
||||
Messenger.send(CommandToCode.selectFile, {
|
||||
@@ -59,15 +58,18 @@ export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, mul
|
||||
return !value || (Array.isArray(value) && value.length === 0);
|
||||
}, [value]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && isEmpty;
|
||||
}, [required, isEmpty]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<DocumentIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<DocumentIcon />}
|
||||
required={required} />
|
||||
|
||||
<div className={`metadata_field__file`}>
|
||||
<div className={`metadata_field__file ${showRequiredState ? "required" : ""}`}>
|
||||
{
|
||||
(isEmpty || multiple) && (
|
||||
<button className={`metadata_field__file__button ${isEmpty ? "" : "not_empty"}`} title={`Add your ${label?.toLowerCase() || "file"}`} type="button" onClick={selectFile}>
|
||||
@@ -76,6 +78,8 @@ export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, mul
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
|
||||
{
|
||||
value && !Array.isArray(value) && (
|
||||
|
||||
@@ -27,7 +27,9 @@ export const ImageFallback: React.FunctionComponent<IImageFallbackProps> = ({ sr
|
||||
<p style={{
|
||||
marginBottom: '1rem',
|
||||
color: 'var(--button-secondary-foreground)',
|
||||
}}>The image couldn't be loaded</p>
|
||||
}}>
|
||||
The image couldn't be loaded
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { PencilIcon, TrashIcon, ViewListIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface IListFieldProps {
|
||||
label: string;
|
||||
value: string[] | null;
|
||||
export interface IListFieldProps extends BaseFieldProps<string[] | null> {
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
export const ListField: React.FunctionComponent<IListFieldProps> = ({ label, value, onChange }: React.PropsWithChildren<IListFieldProps>) => {
|
||||
export const ListField: React.FunctionComponent<IListFieldProps> = ({ label, value, required, onChange }: React.PropsWithChildren<IListFieldProps>) => {
|
||||
const [ text, setText ] = React.useState<string | null>("");
|
||||
const [ list, setList ] = React.useState<string[] | null>(null);
|
||||
const [ itemToEdit, setItemToEdit ] = React.useState<number | null>(null);
|
||||
@@ -58,6 +58,10 @@ export const ListField: React.FunctionComponent<IListFieldProps> = ({ label, val
|
||||
}
|
||||
}, [list]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && (!list || list?.length === 0);
|
||||
}, [required, list]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
if (typeof value === "string") {
|
||||
@@ -68,15 +72,12 @@ export const ListField: React.FunctionComponent<IListFieldProps> = ({ label, val
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
let isValid = true;
|
||||
|
||||
return (
|
||||
<div className={`list_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<ViewListIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<div className={`list_field ${showRequiredState ? "required" : ""}`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<ViewListIcon />}
|
||||
required={required} />
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -87,11 +88,10 @@ export const ListField: React.FunctionComponent<IListFieldProps> = ({ label, val
|
||||
if (e.key === "Enter") {
|
||||
onSaveForm();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
border: "1px solid var(--vscode-inputValidation-infoBorder)"
|
||||
}} />
|
||||
|
||||
<RequiredMessage name={label} show={showRequiredState} />
|
||||
|
||||
<div className={`list_field__form__buttons`}>
|
||||
<button
|
||||
className={`list_field__form__button__save`}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {CalculatorIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface INumberFieldProps {
|
||||
label: string;
|
||||
value: number | null;
|
||||
export interface INumberFieldProps extends BaseFieldProps<number | null> {
|
||||
onChange: (nrValue: number | null) => void;
|
||||
}
|
||||
|
||||
export const NumberField: React.FunctionComponent<INumberFieldProps> = ({label, value, onChange}: React.PropsWithChildren<INumberFieldProps>) => {
|
||||
export const NumberField: React.FunctionComponent<INumberFieldProps> = ({label, value, required, onChange}: React.PropsWithChildren<INumberFieldProps>) => {
|
||||
const [ nrValue, setNrValue ] = React.useState<number | null>(value);
|
||||
|
||||
const onValueChange = (txtValue: string) => {
|
||||
@@ -22,6 +22,10 @@ export const NumberField: React.FunctionComponent<INumberFieldProps> = ({label,
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && (nrValue === null || nrValue === undefined);
|
||||
}, [required, nrValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nrValue !== value) {
|
||||
setNrValue(value);
|
||||
@@ -29,14 +33,15 @@ export const NumberField: React.FunctionComponent<INumberFieldProps> = ({label,
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<CalculatorIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<div className={`metadata_field ${showRequiredState ? "required" : ""}`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<CalculatorIcon />}
|
||||
required={required} />
|
||||
|
||||
<input type={`number`} className={`metadata_field__number`} value={`${nrValue}`} onChange={(e) => onValueChange(e.target.value)} />
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import {PhotographIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { BlockFieldData } from '../../../models';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { DefaultFieldValues } from '../../../constants';
|
||||
import { BaseFieldProps, BlockFieldData } from '../../../models';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { PreviewImage } from './PreviewImage';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface PreviewImageValue {
|
||||
original: string;
|
||||
webviewUrl: string;
|
||||
}
|
||||
|
||||
export interface IPreviewImageFieldProps {
|
||||
label: string;
|
||||
export interface IPreviewImageFieldProps extends BaseFieldProps<PreviewImageValue | PreviewImageValue[] | null> {
|
||||
fieldName: string;
|
||||
value: PreviewImageValue | PreviewImageValue[] | null;
|
||||
filePath: string | null;
|
||||
parents?: string[];
|
||||
multiple?: boolean;
|
||||
@@ -31,7 +31,8 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
|
||||
value,
|
||||
filePath,
|
||||
multiple,
|
||||
parents
|
||||
parents,
|
||||
required
|
||||
}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
|
||||
|
||||
const selectImage = useCallback(() => {
|
||||
@@ -52,17 +53,24 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
const isFaultyImage = useMemo(() => {
|
||||
return typeof value === "string" && value === DefaultFieldValues.faultyCustomPlaceholder;
|
||||
}, [value])
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && isFaultyImage || !value;
|
||||
}, [required, value, isFaultyImage]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<PhotographIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
|
||||
<div className={`metadata_field__preview_image ${multiple && value && (value as PreviewImageValue[]).length > 0 ? `metadata_field__multiple_images` : ''}`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<PhotographIcon />}
|
||||
required={required} />
|
||||
|
||||
<div className={`metadata_field__preview_image ${multiple && value && (value as PreviewImageValue[]).length > 0 ? `metadata_field__multiple_images` : ''} ${showRequiredState ? "required" : ""}`}>
|
||||
{
|
||||
(!value || multiple) && (
|
||||
(!value || isFaultyImage || multiple) && (
|
||||
<button className={`metadata_field__preview_image__button`} title={`Add your ${label?.toLowerCase() || "image"}`} type="button" onClick={selectImage}>
|
||||
<PhotographIcon />
|
||||
<span>Add your {label?.toLowerCase() || "image"}</span>
|
||||
@@ -71,19 +79,21 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
|
||||
}
|
||||
|
||||
{
|
||||
value && !Array.isArray(value) && (
|
||||
(value && !Array.isArray(value) && !isFaultyImage) && (
|
||||
<PreviewImage value={value} onRemove={() => onChange(null)} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
multiple && value && Array.isArray(value) && (
|
||||
(multiple && value && Array.isArray(value) && !isFaultyImage) && (
|
||||
value.map(image => (
|
||||
<PreviewImage key={image.original} value={image} onRemove={onImageRemove} />
|
||||
))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IRequiredAsterixProps {
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const RequiredAsterix: React.FunctionComponent<IRequiredAsterixProps> = ({ required }: React.PropsWithChildren<IRequiredAsterixProps>) => {
|
||||
if (!required) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`metadata_field__required__asterix`}>*</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IRequiredMessageProps {
|
||||
name: string;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
export const RequiredMessage: React.FunctionComponent<IRequiredMessageProps> = ({ name, show }: React.PropsWithChildren<IRequiredMessageProps>) => {
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`metadata_field__required__message`}>
|
||||
The {name} field is required.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,28 +2,22 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import {LinkIcon, RefreshIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { Command } from '../../Command';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface ISlugFieldProps {
|
||||
label: string;
|
||||
value: string | null;
|
||||
export interface ISlugFieldProps extends BaseFieldProps<string> {
|
||||
titleValue: string | null;
|
||||
editable?: boolean;
|
||||
onChange: (txtValue: string) => void;
|
||||
}
|
||||
|
||||
export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({ label, editable, value, titleValue, onChange }: React.PropsWithChildren<ISlugFieldProps>) => {
|
||||
export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({ label, editable, value, titleValue, onChange, required }: React.PropsWithChildren<ISlugFieldProps>) => {
|
||||
const [ text, setText ] = React.useState<string | null>(value);
|
||||
const [ slug, setSlug ] = React.useState<string | null>(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
@@ -41,6 +35,16 @@ export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({ label, edi
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && !text;
|
||||
}, [required, text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (titleValue) {
|
||||
Messenger.send(CommandToCode.generateSlug, titleValue);
|
||||
@@ -57,18 +61,20 @@ export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({ label, edi
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<LinkIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<LinkIcon />}
|
||||
required={required} />
|
||||
|
||||
<div className='metadata_field__slug'>
|
||||
<input
|
||||
className={`metadata_field__slug__input`}
|
||||
value={text || ""}
|
||||
disabled={editable !== undefined ? !editable : false}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)} />
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
borderColor: showRequiredState ? "var(--vscode-inputValidation-errorBorder)" : undefined,
|
||||
}} />
|
||||
|
||||
<button
|
||||
title={slug !== text ? "Update available" : "Generate slug"}
|
||||
@@ -77,6 +83,8 @@ export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({ label, edi
|
||||
<RefreshIcon aria-hidden={true} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import {PencilIcon} from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { RequiredFieldsAtom } from '../../state';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface ITextFieldProps {
|
||||
label: string;
|
||||
value: string | null;
|
||||
export interface ITextFieldProps extends BaseFieldProps<string> {
|
||||
singleLine: boolean | undefined;
|
||||
wysiwyg: boolean | undefined;
|
||||
limit: number | undefined;
|
||||
@@ -14,14 +17,9 @@ export interface ITextFieldProps {
|
||||
|
||||
const WysiwygField = React.lazy(() => import('./WysiwygField'));
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, wysiwyg, limit, label, value, rows, onChange}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine, wysiwyg, limit, label, value, rows, onChange, required}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
const [ requiredFields, setRequiredFields ] = useRecoilState(RequiredFieldsAtom);
|
||||
const [ text, setText ] = React.useState<string | null>(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
@@ -33,13 +31,49 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
isValid = ((text || "").length <= limit);
|
||||
}
|
||||
|
||||
const updateRequired = useCallback((isValid: boolean) => {
|
||||
setRequiredFields((prev) => {
|
||||
let clone = Object.assign([], prev);
|
||||
|
||||
if (isValid) {
|
||||
clone = clone.filter((item) => item !== label);
|
||||
} else {
|
||||
clone.push(label);
|
||||
}
|
||||
|
||||
return clone;
|
||||
});
|
||||
}, [setRequiredFields])
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && !text;
|
||||
}, [required, text]);
|
||||
|
||||
const border = useMemo(() => {
|
||||
if (showRequiredState) {
|
||||
updateRequired(false);
|
||||
return "1px solid var(--vscode-inputValidation-errorBorder)";
|
||||
} else if (!isValid) {
|
||||
updateRequired(true);
|
||||
return "1px solid var(--vscode-inputValidation-warningBorder)";
|
||||
} else {
|
||||
updateRequired(true);
|
||||
return "1px solid var(--vscode-inputValidation-infoBorder)";
|
||||
}
|
||||
}, [showRequiredState, isValid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<PencilIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<PencilIcon />}
|
||||
required={required} />
|
||||
|
||||
{
|
||||
wysiwyg ? (
|
||||
@@ -53,7 +87,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
value={text || ""}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
|
||||
border
|
||||
}} />
|
||||
) : (
|
||||
<textarea
|
||||
@@ -62,7 +96,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
value={text || ""}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
style={{
|
||||
border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)"
|
||||
border
|
||||
}} />
|
||||
)
|
||||
)
|
||||
@@ -75,6 +109,8 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({singleLine,
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { RocketIcon } from '../Icons/RocketIcon';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { ToggleIcon } from '../Icons/ToggleIcon';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { RequiredMessage } from './RequiredMessage';
|
||||
|
||||
export interface IToggleProps {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
export interface IToggleProps extends BaseFieldProps<boolean> {
|
||||
onChanged: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const Toggle: React.FunctionComponent<IToggleProps> = ({label, checked, onChanged}: React.PropsWithChildren<IToggleProps>) => {
|
||||
const [ isChecked, setIsChecked ] = React.useState(checked);
|
||||
export const Toggle: React.FunctionComponent<IToggleProps> = ({label, value, required, onChanged}: React.PropsWithChildren<IToggleProps>) => {
|
||||
const [ isChecked, setIsChecked ] = React.useState(value);
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsChecked(!isChecked);
|
||||
onChanged(!isChecked);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isChecked !== checked) {
|
||||
setIsChecked(checked);
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && value === null;
|
||||
}, [required, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isChecked !== value) {
|
||||
setIsChecked(value);
|
||||
}
|
||||
}, [ checked ]);
|
||||
}, [ value ]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<ToggleIcon /> <span style={{ lineHeight: "16px"}}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<ToggleIcon />}
|
||||
required={required} />
|
||||
|
||||
|
||||
<label className="field__toggle">
|
||||
<input type="checkbox" checked={isChecked} onChange={onChange} />
|
||||
<input type="checkbox" checked={value === null ? false : value} onChange={onChange} />
|
||||
<span className="field__toggle__slider"></span>
|
||||
</label>
|
||||
|
||||
<RequiredMessage name={label.toLowerCase()} show={showRequiredState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,18 +15,7 @@ import { TagIcon } from '../Icons/TagIcon';
|
||||
import { JsonField } from '../JsonField';
|
||||
import { IMetadata } from '../Metadata';
|
||||
import { TagPicker } from '../TagPicker';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { ChoiceField } from './ChoiceField';
|
||||
import { DataFileField } from './DataFileField';
|
||||
import { DateTimeField } from './DateTimeField';
|
||||
import { DraftField } from './DraftField';
|
||||
import { FileField } from './FileField';
|
||||
import { ListField } from './ListField';
|
||||
import { NumberField } from './NumberField';
|
||||
import { PreviewImageField, PreviewImageValue } from './PreviewImageField';
|
||||
import { SlugField } from './SlugField';
|
||||
import { TextField } from './TextField';
|
||||
import { Toggle } from './Toggle';
|
||||
import { ChoiceField, DataFileField, DateTimeField, DraftField, FieldTitle, FileField, ListField, Toggle, TextField, SlugField, PreviewImageField, PreviewImageValue, NumberField } from '.';
|
||||
|
||||
export interface IWrapperFieldProps {
|
||||
field: Field;
|
||||
@@ -127,7 +116,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<DateTimeField
|
||||
label={field.title || field.name}
|
||||
date={fieldValue}
|
||||
value={fieldValue}
|
||||
required={!!field.required}
|
||||
format={settings?.date?.format}
|
||||
onChange={(date => onSendUpdate(field.name, date, parentFields))} />
|
||||
</FieldBoundary>
|
||||
@@ -138,7 +128,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
<Toggle
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
checked={!!fieldValue}
|
||||
value={fieldValue}
|
||||
required={!!field.required}
|
||||
onChanged={(checked) => onSendUpdate(field.name, checked, parentFields)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
@@ -160,7 +151,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
wysiwyg={field.wysiwyg}
|
||||
rows={3}
|
||||
onChange={(value) => onSendUpdate(field.name, value, parentFields)}
|
||||
value={fieldValue as string || null} />
|
||||
value={fieldValue as string || null}
|
||||
required={!!field.required} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'number') {
|
||||
@@ -175,7 +167,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
key={field.name}
|
||||
label={field.title || field.name}
|
||||
onChange={(value) => onSendUpdate(field.name, value, parentFields)}
|
||||
value={nrValue} />
|
||||
value={nrValue}
|
||||
required={!!field.required} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'image') {
|
||||
@@ -187,6 +180,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
filePath={metadata.filePath as string}
|
||||
parents={parentFields}
|
||||
value={fieldValue as PreviewImageValue | PreviewImageValue[] | null}
|
||||
required={!!field.required}
|
||||
multiple={field.multiple}
|
||||
blockData={blockData}
|
||||
onChange={(value) => onSendUpdate(field.name, value, parentFields)} />
|
||||
@@ -202,6 +196,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
fileExtensions={field.fileExtensions}
|
||||
filePath={metadata.filePath as string}
|
||||
value={fieldValue as string | string[] | null}
|
||||
required={!!field.required}
|
||||
parents={parentFields}
|
||||
blockData={blockData}
|
||||
onChange={(value) => onSendUpdate(field.name, value, parentFields)} />
|
||||
@@ -214,7 +209,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<ChoiceField
|
||||
label={field.title || field.name}
|
||||
selected={fieldValue as string}
|
||||
value={fieldValue as string}
|
||||
required={!!field.required}
|
||||
choices={choices}
|
||||
multiSelect={field.multiple}
|
||||
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
|
||||
@@ -235,7 +231,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
unsetFocus={unsetFocus}
|
||||
parents={parentFields}
|
||||
blockData={blockData}
|
||||
limit={field.taxonomyLimit} />
|
||||
limit={field.taxonomyLimit}
|
||||
required={!!field.required} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'taxonomy') {
|
||||
@@ -256,7 +253,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
taxonomyId={field.taxonomyId}
|
||||
parents={parentFields}
|
||||
blockData={blockData}
|
||||
limit={field.taxonomyLimit} />
|
||||
limit={field.taxonomyLimit}
|
||||
required={!!field.required} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'categories') {
|
||||
@@ -274,7 +272,8 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
unsetFocus={unsetFocus}
|
||||
parents={parentFields}
|
||||
blockData={blockData}
|
||||
limit={field.taxonomyLimit} />
|
||||
limit={field.taxonomyLimit}
|
||||
required={!!field.required} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'draft') {
|
||||
@@ -293,6 +292,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
type={draftField?.type || "boolean"}
|
||||
choices={draftField?.choices || []}
|
||||
value={draftValue as boolean | string | null | undefined}
|
||||
required={!!field.required}
|
||||
onChanged={(value: boolean | string) => onSendUpdate(field.name, value, parentFields)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
@@ -306,11 +306,11 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<div className={`metadata_field__box`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label metadata_field__label_parent`}>
|
||||
<span style={{ lineHeight: "16px"}}>{field.title || field.name}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<FieldTitle
|
||||
className={`metadata_field__label_parent`}
|
||||
label={field.title || field.name}
|
||||
icon={undefined}
|
||||
required={field.required} />
|
||||
|
||||
{ renderFields(field.fields, subMetadata, [...parentFields, field.name], blockData, onSendUpdate) }
|
||||
</div>
|
||||
@@ -346,6 +346,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
parentFields={parentFields}
|
||||
filePath={metadata.filePath as string}
|
||||
parentBlock={parentBlock}
|
||||
required={!!field.required}
|
||||
onSubmit={(value) => onSendUpdate(field.name, value, parentFields)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
@@ -358,6 +359,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
dataFileKey={field.dataFileKey}
|
||||
dataFileValue={field.dataFileValue}
|
||||
selected={fieldValue as string}
|
||||
required={!!field.required}
|
||||
multiSelect={field.multiple}
|
||||
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
|
||||
</FieldBoundary>
|
||||
@@ -368,6 +370,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
<ListField
|
||||
label={field.title || field.name}
|
||||
value={fieldValue}
|
||||
required={!!field.required}
|
||||
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
@@ -378,6 +381,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
label={field.title || field.name}
|
||||
titleValue={metadata.title as string}
|
||||
value={fieldValue}
|
||||
required={!!field.required}
|
||||
editable={field.editable}
|
||||
onChange={(value => onSendUpdate(field.name, value, parentFields))} />
|
||||
</FieldBoundary>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export * from './ChoiceButton';
|
||||
export * from './ChoiceField';
|
||||
export * from './DataFileField';
|
||||
export * from './DateTimeField';
|
||||
export * from './DraftField';
|
||||
export * from './FieldTitle';
|
||||
export * from './FileField';
|
||||
export * from './ImageFallback';
|
||||
export * from './ListField';
|
||||
export * from './NumberField';
|
||||
export * from './PreviewImage';
|
||||
export * from './PreviewImageField';
|
||||
export * from './RequiredAsterix';
|
||||
export * from './RequiredMessage';
|
||||
export * from './SlugField';
|
||||
export * from './TextField';
|
||||
export * from './Toggle';
|
||||
export * from './WrapperField';
|
||||
export * from './WysiwygField';
|
||||
@@ -5,10 +5,11 @@ import { CommandToCode } from '../CommandToCode';
|
||||
import { TagType } from '../TagType';
|
||||
import Downshift from 'downshift';
|
||||
import { AddIcon } from './Icons/AddIcon';
|
||||
import { VsLabel } from './VscodeComponents';
|
||||
import { BlockFieldData, CustomTaxonomyData } from '../../models';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { RequiredMessage } from './Fields/RequiredMessage';
|
||||
import { FieldTitle } from './Fields/FieldTitle';
|
||||
|
||||
export interface ITagPickerProps {
|
||||
type: TagType;
|
||||
@@ -26,10 +27,11 @@ export interface ITagPickerProps {
|
||||
fieldName?: string;
|
||||
taxonomyId?: string;
|
||||
limit?: number;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const { label, icon, type, crntSelected, options, freeform, focussed, unsetFocus, disableConfigurable, fieldName, taxonomyId, parents, blockData, limit } = props;
|
||||
const { label, icon, type, crntSelected, options, freeform, focussed, unsetFocus, disableConfigurable, fieldName, taxonomyId, parents, blockData, limit, required } = props;
|
||||
const [ selected, setSelected ] = React.useState<string[]>([]);
|
||||
const [ inputValue, setInputValue ] = React.useState<string>("");
|
||||
const prevSelected = usePrevious(crntSelected);
|
||||
@@ -200,27 +202,30 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
|
||||
}
|
||||
|
||||
return `Pick your ${label || type.toLowerCase()}`;
|
||||
}, [label, type, checkIsDisabled]);
|
||||
}, [label, type, checkIsDisabled]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const showRequiredState = useMemo(() => {
|
||||
return required && (selected || []).length === 0;
|
||||
}, [required, selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
triggerFocus();
|
||||
}, 100);
|
||||
}, [focussed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (prevSelected !== crntSelected) {
|
||||
setSelected(typeof crntSelected === "string" ? [crntSelected] : crntSelected);
|
||||
}
|
||||
}, [crntSelected]);
|
||||
|
||||
return (
|
||||
<div className={`article__tags`}>
|
||||
<VsLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
{icon} <span style={{ lineHeight: "16px"}}>{label || type}{(limit !== undefined && limit > 0) ? <>{` `}<span style={{fontWeight: "lighter"}}>(Max.: {limit})</span></> : ``}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
<div className={`article__tags`}>
|
||||
<FieldTitle
|
||||
label={(<>{label || type}{(limit !== undefined && limit > 0) ? <>{` `}<span style={{fontWeight: "lighter"}}>(Max.: {limit})</span></> : ``}</>)}
|
||||
icon={icon}
|
||||
required={required} />
|
||||
|
||||
<Downshift ref={dsRef}
|
||||
onChange={(selected) => onSelect(selected || "")}
|
||||
@@ -230,7 +235,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
|
||||
{
|
||||
({ getInputProps, getItemProps, getMenuProps, isOpen, inputValue, getRootProps, openMenu, closeMenu, clearSelection, highlightedIndex }) => (
|
||||
<>
|
||||
<div {...getRootProps(undefined, {suppressRefError: true})} className={`article__tags__input ${freeform ? 'freeform' : ''}`}>
|
||||
<div {...getRootProps(undefined, {suppressRefError: true})} className={`article__tags__input ${freeform ? 'freeform' : ''} ${showRequiredState ? 'required' : ''}`}>
|
||||
<input {
|
||||
...getInputProps({
|
||||
ref: inputRef,
|
||||
@@ -274,6 +279,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsW
|
||||
)
|
||||
}
|
||||
</Downshift>
|
||||
|
||||
<RequiredMessage name={(label || type).toLowerCase()} show={showRequiredState} />
|
||||
|
||||
<Tags
|
||||
values={(selected || []).sort((a: string, b: string) => a?.toLowerCase() < b?.toLowerCase() ? -1 : 1 )}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ViewPanel } from "./ViewPanel";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { SENTRY_LINK } from "../constants";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import './styles.css';
|
||||
|
||||
// require('@vscode/codicons/dist/codicon.css');
|
||||
@@ -43,7 +44,7 @@ if (elm) {
|
||||
});
|
||||
}
|
||||
|
||||
render(<ViewPanel />, elm);
|
||||
render(<RecoilRoot><ViewPanel /></RecoilRoot>, elm);
|
||||
}
|
||||
|
||||
// Webpack HMR
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const RequiredFieldsAtom = atom<string[]>({
|
||||
key: 'RequiredFields',
|
||||
default: []
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RequiredFieldsAtom';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './atom';
|
||||
@@ -277,6 +277,66 @@
|
||||
}
|
||||
|
||||
/* Metadata section - Content type */
|
||||
.metadata_field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__label.metadata_field__label_parent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metadata_field__label svg {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__error {
|
||||
color: var(--vscode-errorForeground);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata_field__error button {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.metadata_field__error button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__input, .metadata_field__input:focus,
|
||||
.metadata_field__textarea, .metadata_field__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Required field */
|
||||
.metadata_field__required__asterix {
|
||||
color: var(--vscode-inputValidation-errorBorder);
|
||||
margin-left: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__required__message {
|
||||
color: var(--vscode-inputValidation-errorBorder);
|
||||
padding-top: .5rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
/* Text field */
|
||||
.metadata_field__limit {
|
||||
color: var(--vscode-inputValidation-warningBorder);
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__alert svg {
|
||||
color: var(--vscode-editorWarning-foreground)
|
||||
}
|
||||
@@ -303,7 +363,235 @@ vscode-divider {
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
}
|
||||
|
||||
/* Fields field */
|
||||
.vscode-dark .metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__box {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metadata_field__box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: .5rem;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
/* List field */
|
||||
.list_field {
|
||||
input {
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
}
|
||||
|
||||
&.required {
|
||||
input {
|
||||
border-color: var(--vscode-inputValidation-errorBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Image field */
|
||||
.metadata_field__preview_image {
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
&.required {
|
||||
.metadata_field__preview_image__button {
|
||||
border-color: var(--vscode-inputValidation-errorBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata_field__multiple_images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__file__button,
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--vscode-button-background);
|
||||
padding: 1.5rem;
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button:hover,
|
||||
.metadata_field__preview_image__button:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
.metadata_field__file__button svg,
|
||||
.metadata_field__preview_image__button svg {
|
||||
color: var(--vscode-foreground);
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.metadata_field__file__button span,
|
||||
.metadata_field__preview_image__button span {
|
||||
color: var(--vscode-foreground);
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.vscode-light .metadata_field__preview_image__preview {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vscode-dark .metadata_field__preview_image__preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove:hover {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
/* Number field */
|
||||
.metadata_field {
|
||||
&.required {
|
||||
.metadata_field__number {
|
||||
border-color: var(--vscode-inputValidation-errorBorder) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata_field__number {
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Choice field */
|
||||
.metadata_field {
|
||||
&.required {
|
||||
.metadata_field__choice__toggle {
|
||||
border-color: var(--vscode-inputValidation-errorBorder) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
width: 100%;
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
background-color: var(--vscode-input-background);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle:hover,
|
||||
.metadata_field__choice__toggle:focus,
|
||||
.metadata_field__choice__toggle:active,
|
||||
.metadata_field__choice__toggle:disabled {
|
||||
background-color: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle span {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle svg.icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-left: .25rem;
|
||||
|
||||
position: absolute;
|
||||
right: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list {
|
||||
width: 90%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list.open {
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li {
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li:active {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-selected="true"] {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-disabled="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list__item {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button {
|
||||
margin-top: .5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button_icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
/* File field */
|
||||
.metadata_field__file {
|
||||
&.required {
|
||||
.metadata_field__file__button {
|
||||
border-color: var(--vscode-inputValidation-errorBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata_field__file__button.not_empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -378,6 +666,25 @@ vscode-divider {
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.article__tags__input {
|
||||
position: relative;
|
||||
outline: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
outline-offset: -1px;
|
||||
|
||||
&.required {
|
||||
outline: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
|
||||
button {
|
||||
border-left-color: var(--vscode-inputValidation-errorBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article__tags__input.freeform input {
|
||||
padding-right: 35px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.article__tags__input button {
|
||||
margin: 1px 0;
|
||||
padding: 0 .5rem;
|
||||
|
||||
Reference in New Issue
Block a user