#369 - required fields

This commit is contained in:
Elio Struyf
2022-09-15 11:13:26 +02:00
parent c6273fa9c1
commit e455fa764b
41 changed files with 974 additions and 459 deletions
+1
View File
@@ -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
-236
View File
@@ -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);
+13
View File
@@ -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,
+102 -3
View File
@@ -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;
}
}
+5
View File
@@ -0,0 +1,5 @@
export const DefaultFieldValues = {
faultyCustomPlaceholder: "<failed to process>"
}
+5
View File
@@ -0,0 +1,5 @@
export const NOTIFICATION_TYPE = {
requiredFieldValidation: "requiredFieldValidation",
}
+2
View File
@@ -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';
+1
View File
@@ -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";
+2 -2
View File
@@ -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;
}
}
+47 -2
View File
@@ -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
+7
View File
@@ -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}`);
+60 -1
View File
@@ -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) {
+13 -3
View File
@@ -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);
+4 -2
View File
@@ -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) {
+7
View File
@@ -0,0 +1,7 @@
export interface BaseFieldProps<T> {
label: string;
value: T | null;
required?: boolean;
}
+1
View File
@@ -71,6 +71,7 @@ export interface Field {
taxonomyLimit?: number;
fileExtensions?: string[];
editable?: boolean;
required?: boolean;
// Date fields
isPublishDate?: boolean;
+1
View File
@@ -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>
);
};
+22 -17
View File
@@ -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';
+20 -13
View File
@@ -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 )}
+2 -1
View File
@@ -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: []
});
+1
View File
@@ -0,0 +1 @@
export * from './RequiredFieldsAtom';
+1
View File
@@ -0,0 +1 @@
export * from './atom';
+307
View File
@@ -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;