#308 - Implementation of the file field

This commit is contained in:
Elio Struyf
2022-04-20 11:02:32 +02:00
parent 60fde1711e
commit 55a14b3fbe
20 changed files with 306 additions and 37 deletions
+4
View File
@@ -639,6 +639,7 @@ input:checked + .field__toggle__slider:before {
max-height: 16rem;
}
.metadata_field__file__button,
.metadata_field__preview_image__button {
background-color: transparent;
border: 1px dashed var(--vscode-button-background);
@@ -646,11 +647,13 @@ input:checked + .field__toggle__slider:before {
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;
@@ -659,6 +662,7 @@ input:checked + .field__toggle__slider:before {
margin: 0 auto;
}
.metadata_field__file__button span,
.metadata_field__preview_image__button span {
color: var(--vscode-foreground);
display: inline-block;
+26 -4
View File
@@ -744,6 +744,7 @@
"datetime",
"boolean",
"image",
"file",
"choice",
"taxonomy",
"tags",
@@ -827,6 +828,13 @@
"default": "",
"description": "The ID of your taxonomy field"
},
"fileExtensions": {
"type": "array",
"description": "Specify the file extensions to allow for the file picker",
"items": {
"type": "string"
}
},
"fields": {
"$ref": "#contenttypefield"
},
@@ -874,6 +882,20 @@
"name"
],
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "file"
}
}
},
"then": {
"required": [
"fileExtensions"
]
}
},
{
"if": {
"properties": {
@@ -1305,8 +1327,8 @@
"category": "Front matter"
},
{
"command": "frontMatter.insertImage",
"title": "Insert image into your content",
"command": "frontMatter.insertMedia",
"title": "Insert media into your content",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/media-dark.svg",
@@ -1468,7 +1490,7 @@
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
"command": "frontMatter.insertMedia",
"group": "navigation@-128",
"when": "frontMatter:file:isValid == true"
},
@@ -1625,7 +1647,7 @@
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
},
{
"command": "frontMatter.insertImage",
"command": "frontMatter.insertMedia",
"when": "frontMatter:file:isValid == true"
},
{
+1 -1
View File
@@ -324,7 +324,7 @@ export class Article {
/**
* Insert an image from the media dashboard into the article
*/
public static async insertImage() {
public static async insertMedia() {
let editor = vscode.window.activeTextEditor;
if (!editor) {
return;
+1 -1
View File
@@ -38,7 +38,7 @@ export const COMMAND_NAME = {
modeSwitch: getCommandName("mode.switch"),
// Insert dashboards
insertImage: getCommandName("insertImage"),
insertMedia: getCommandName("insertMedia"),
insertSnippet: getCommandName("insertSnippet"),
// WYSIWYG
+1
View File
@@ -22,6 +22,7 @@ export const TelemetryEvent = {
refreshMedia: 'refreshMedia',
deleteMedia: 'deleteMedia',
insertMediaToContent: 'insertMediaToContent',
insertFileToContent: 'insertFileToContent',
updateMediaMetadata: 'updateMediaMetadata',
openExplorerView: 'openExplorerView',
+1
View File
@@ -25,6 +25,7 @@ export enum DashboardMessage {
insertPreviewImage = 'insertPreviewImage',
updateMediaMetadata = 'updateMediaMetadata',
createMediaFolder = 'createMediaFolder',
insertFile = 'insertFile',
// Data dashboard
getDataEntries = 'getDataEntries',
+27 -14
View File
@@ -9,7 +9,6 @@ import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { ScriptType } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { FileIcon } from '../../../panelWebView/components/Icons/FileIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
@@ -74,18 +73,32 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const insertToArticle = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
alt: alt || "",
caption: caption || ""
});
if (viewData?.data?.type === "file") {
Messenger.send(DashboardMessage.insertFile, {
relPath: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined
});
} else {
Messenger.send(DashboardMessage.insertPreviewImage, {
relPath: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
parents: viewData?.data?.parents,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
blockData: typeof viewData?.data?.blockData !== "undefined" ? viewData?.data?.blockData : undefined,
alt: alt || "",
caption: caption || ""
});
}
};
const insertSnippet = () => {
@@ -100,7 +113,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
relPath: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
position: viewData?.data?.position || null,
@@ -17,7 +17,7 @@ import useMedia from '../../hooks/useMedia';
import { TelemetryEvent } from '../../../constants';
import { PageLayout } from '../Layout/PageLayout';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { join } from 'path';
import { extname, join } from 'path';
export interface IMediaProps {}
@@ -40,12 +40,23 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
}, [folders, viewData, settings?.staticFolder]);
const allMedia = React.useMemo(() => {
let mediaFiles = media;
// Check if content allows page bundle
if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
return media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
mediaFiles = media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
}
return media;
if (viewData && viewData.data && viewData.data.type === "file" && viewData.data.fileExtensions && viewData.data.fileExtensions.length > 0) {
const supportedExtensions = viewData.data.fileExtensions;
mediaFiles = mediaFiles.filter(m => {
const ext = extname(m.fsPath);
// Remove the dot from the extension
const extWithoutDot = ext.substring(1);
return supportedExtensions.includes(extWithoutDot);
});
}
return mediaFiles;
}, [media, viewData, settings?.staticFolder]);
const onDrop = useCallback((acceptedFiles: File[]) => {
+1 -1
View File
@@ -211,7 +211,7 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
// Inserting an image in Markdown
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia));
// Inserting a snippet in Markdown
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet));
+14 -7
View File
@@ -270,7 +270,7 @@ export class MediaHelpers {
* @param data
*/
public static async insertMediaToMarkdown(data: any) {
if (data?.file && data?.image) {
if (data?.file && data?.relPath) {
if (!data?.position) {
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
}
@@ -281,12 +281,12 @@ export class MediaHelpers {
const editor = window.activeTextEditor;
const wsFolder = Folders.getWorkspaceFolder();
const filePath = data.file;
let imgPath = data.image;
let relPath = data.relPath;
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
const articleCt = article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), relPath);
const fileDir = parseWinPath(dirname(filePath));
const imgDir = parseWinPath(dirname(absImgPath));
const contentFolders = Folders.get();
@@ -303,11 +303,11 @@ export class MediaHelpers {
if (existsInContent) {
const relImgPath = relative(fileDir, imgDir);
imgPath = join(relImgPath, basename(imgPath));
relPath = join(relImgPath, basename(relPath));
// Snippets are already parsed, so update the URL of the image
if (data.snippet) {
data.snippet = data.snippet.replace(data.image, imgPath);
data.snippet = data.snippet.replace(data.relPath, relPath);
}
}
}
@@ -319,7 +319,14 @@ export class MediaHelpers {
if (line) {
const selection = editor?.selection;
await editor?.edit(builder => {
const snippet = data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`;
const mimeType = lookup(relPath)
let isFile = true;
if (mimeType) {
isFile = !mimeType.startsWith('image');
}
const snippet = data.snippet || `${isFile ? "" : "!"}[${data.alt || data.caption || ""}](${relPath})`;
if (selection !== undefined) {
builder.replace(selection, snippet);
} else {
@@ -333,7 +340,7 @@ export class MediaHelpers {
DataListener.updateMetadata({
field: data.fieldName,
value: imgPath,
value: relPath,
parents: data.parents,
blockData: data.blockData
});
+4
View File
@@ -40,6 +40,10 @@ export class MediaListener extends BaseListener {
Telemetry.send(TelemetryEvent.insertMediaToContent);
MediaHelpers.insertMediaToMarkdown(msg?.data);
break;
case DashboardMessage.insertFile:
Telemetry.send(TelemetryEvent.insertFileToContent);
MediaHelpers.insertMediaToMarkdown(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
Telemetry.send(TelemetryEvent.updateMediaMetadata);
this.update(msg.data);
+7 -3
View File
@@ -147,12 +147,14 @@ export class DataListener extends BaseListener {
const contentType = ArticleHelper.getContentType(article.data);
const dateFields = contentType.fields.filter((f) => f.type === "datetime");
const imageFields = contentType.fields.filter((f) => f.type === "image" && f.multiple);
const fileFields = contentType.fields.filter((f) => f.type === "file" && f.multiple);
// Support multi-level fields
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
const isDateField = dateFields.some(f => f.name === field);
const isMultiImageField = imageFields.some(f => f.name === field);
const isMultiFileField = fileFields.some(f => f.name === field);
if (isDateField) {
for (const dateField of dateFields) {
@@ -160,9 +162,11 @@ export class DataListener extends BaseListener {
parentObj[field] = Article.formatDate(new Date(value));
}
}
} else if (isMultiImageField) {
for (const imageField of imageFields) {
if (field === imageField.name) {
} else if (isMultiImageField || isMultiFileField) {
const fields = isMultiImageField ? imageFields : fileFields;
for (const crntField of fields) {
if (field === crntField.name) {
// If value is an array, it means it comes from the explorer view itself (deletion)
if (Array.isArray(value)) {
parentObj[field] = value || [];
+3
View File
@@ -22,6 +22,9 @@ export class MediaListener extends BaseListener {
case CommandToCode.selectImage:
this.selectMedia(msg);
break;
case CommandToCode.selectFile:
this.selectMedia(msg);
break;
case CommandToCode.getImageUrl:
this.generateUrl(msg.data);
break;
+4
View File
@@ -19,4 +19,8 @@ export interface ViewData {
parents?: string[];
multiple?: string[];
value?: string;
// File fields
type: "file" | "media";
fileExtensions?: string[];
}
+2 -1
View File
@@ -48,7 +48,7 @@ export interface ContentType {
pageBundle?: boolean;
}
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block";
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file";
export interface Field {
title?: string;
@@ -66,6 +66,7 @@ export interface Field {
fieldGroup?: string | string[];
dataType?: string | string[];
taxonomyLimit?: number;
fileExtensions?: string[];
// Date fields
isPublishDate?: boolean;
+1
View File
@@ -26,6 +26,7 @@ export enum CommandToCode {
updateMetadata = "update-metadata",
openDashboard = "open-dashboard",
selectImage = "select-image",
selectFile = "select-file",
updateCustomTaxonomy = "updateCustomTaxonomy",
addToCustomTaxonomy = "addToCustomTaxonomy",
frameworkCommand = "framework-command",
@@ -0,0 +1,102 @@
import { DocumentIcon, PaperClipIcon, TrashIcon } from '@heroicons/react/outline';
import { basename } from 'path';
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { BlockFieldData } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
export interface IFileFieldProps {
label: string;
fieldName: string;
filePath: string;
multiple?: boolean;
value: string | string[] | null;
fileExtensions?: string[];
parents?: string[];
blockData?: BlockFieldData;
onChange: (value: string | string[] | null) => void;
}
const File = ({ value, onRemove }: { value: string, onRemove: (value: string) => void }) => {
return (
<div className='metadata_field__file__list__item' title={value}>
<div className='metadata_field__file__item__icon'>
<PaperClipIcon style={{ width: "16px", height: "16px" }} />
</div>
<span className='metadata_field__file__item__text' style={{ lineHeight: "16px" }}>{basename(value)}</span>
<button className='metadata_field__file__item__remove' type='button' onClick={() => onRemove(value)}>
<TrashIcon style={{ width: "16px", height: "16px" }} />
<span className='sr-only'>Delete file</span>
</button>
</div>
)
}
export const FileField: React.FunctionComponent<IFileFieldProps> = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange }: React.PropsWithChildren<IFileFieldProps>) => {
const selectFile = useCallback(() => {
MessageHelper.sendMessage(CommandToCode.selectFile, {
filePath,
fieldName,
value,
multiple,
parents,
blockData,
fileExtensions,
type: "file"
});
}, [filePath, fieldName, value, multiple, parents]);
const onRemove = useCallback((crntValue) => {
const newValue = value && Array.isArray(value) ? value.filter(v => v !== crntValue) : null;
onChange(newValue);
} , [value]);
const isEmpty = useMemo(() => {
return !value || (Array.isArray(value) && value.length === 0);
}, [value]);
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>
<div className={`metadata_field__file`}>
{
(isEmpty || multiple) && (
<button className={`metadata_field__file__button ${isEmpty ? "" : "not_empty"}`} title={`Add your ${label?.toLowerCase() || "file"}`} type="button" onClick={selectFile}>
<DocumentIcon />
<span>Add your {label?.toLowerCase() || "file"}</span>
</button>
)
}
{
value && !Array.isArray(value) && (
<div className='metadata_field__file__list'>
<File value={value} onRemove={onRemove} />
</div>
)
}
{
multiple && value && Array.isArray(value) && (
<div className='metadata_field__file__list multiple'>
{
value.map((v, idx) => (
<File key={idx} value={v} onRemove={onRemove} />
))
}
</div>
)
}
</div>
</div>
);
};
@@ -42,7 +42,8 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
multiple,
metadataInsert: true,
parents,
blockData
blockData,
type: "media"
});
}, [filePath, fieldName, value, multiple, parents]);
@@ -64,7 +65,7 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
(!value || multiple) && (
<button className={`metadata_field__preview_image__button`} title={`Add your ${label?.toLowerCase() || "image"}`} type="button" onClick={selectImage}>
<PhotographIcon />
<span className="mt-2 block text-sm font-medium text-gray-900">Add your {label?.toLowerCase() || "image"}</span>
<span>Add your {label?.toLowerCase() || "image"}</span>
</button>
)
}
@@ -19,6 +19,7 @@ import { VsLabel } from '../VscodeComponents';
import { ChoiceField } from './ChoiceField';
import { DateTimeField } from './DateTimeField';
import { DraftField } from './DraftField';
import { FileField } from './FileField';
import { NumberField } from './NumberField';
import { PreviewImageField, PreviewImageValue } from './PreviewImageField';
import { TextField } from './TextField';
@@ -188,6 +189,21 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
onChange={(value) => onSendUpdate(field.name, value, parentFields)} />
</FieldBoundary>
);
} else if (field.type === 'file') {
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<FileField
label={field.title || field.name}
fieldName={field.name}
multiple={field.multiple}
fileExtensions={field.fileExtensions}
filePath={metadata.filePath as string}
value={fieldValue as string | string[] | null}
parents={parentFields}
blockData={blockData}
onChange={(value) => onSendUpdate(field.name, value, parentFields)} />
</FieldBoundary>
);
} else if (field.type === 'choice') {
const choices = field.choices || [];
+74
View File
@@ -253,6 +253,80 @@
display: none;
}
/* File field */
.metadata_field__file__button.not_empty {
display: flex;
align-items: center;
justify-content: center;
}
.metadata_field__file__button.not_empty svg {
height: 16px;
width: 16px;
display: inline-block;
margin: inherit;
}
.metadata_field__file__button.not_empty span {
margin-top: 0;
display: inline-block;
margin: inherit;
margin-left: .5rem;
}
.metadata_field__file__list.multiple {
margin-top: 1rem;
}
.metadata_field__file__list__item {
display: flex;
align-items: center;
padding: .5rem .25rem;
overflow: hidden;
}
.metadata_field__file__list__item:nth-child(odd) {
background-color: rgba(255, 255, 255, .05);
filter: brightness(100%);
}
.metadata_field__file__list__item:hover {
background-color: rgba(255, 255, 255, .1);
filter: brightness(100%);
}
.metadata_field__file__item__icon {
margin-right: .5rem;
display: flex;
align-items: center;
}
.metadata_field__file__item__text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
}
.metadata_field__file__item__remove {
display: none;
background: none;
width: auto;
color: var(--vscode-inputValidation-errorForeground);
padding: 0;
align-items: center;
}
.metadata_field__file__item__remove:hover {
background: var(--vscode-inputValidation-errorBackground);
}
.metadata_field__file__list__item:hover .metadata_field__file__item__remove {
display: flex;
margin: -.5rem -.25rem -.5rem 0;
padding: .5rem;
}
/* Quill changes */
.ql-toolbar.ql-snow,
.ql-container.ql-snow {