#128 - 🚀 multi image selection support

This commit is contained in:
Elio Struyf
2021-10-04 11:32:52 +02:00
parent 05ce2d3537
commit 6154164b4b
12 changed files with 232 additions and 39 deletions
+1
View File
@@ -16,6 +16,7 @@
- [#124](https://github.com/estruyf/vscode-front-matter/issues/124): Add new `isPreviewImage` property to the content type field to specify custom preview images
- [#126](https://github.com/estruyf/vscode-front-matter/issues/126): Create new content from the available content types
- [#127](https://github.com/estruyf/vscode-front-matter/issues/127): Title bar action added to open the dashboard
- [#128](https://github.com/estruyf/vscode-front-matter/issues/128): Support for multi-select on image fields added
### 🐞 Fixes
+41
View File
@@ -557,12 +557,53 @@ input:checked + .field__toggle__slider:before {
width: auto;
}
.metadata_field__datetime > button:hover {
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__preview_image__button {
background-color: transparent;
border: 2px dashed var(--vscode-button-background);
padding: 1.5rem;
filter: brightness(85%);
}
.metadata_field__preview_image__button:hover {
background-color: rgba(255, 255, 255, .1);
filter: brightness(100%);
}
.metadata_field__preview_image__button svg {
color: var(--vscode-foreground);
display: block;
width: 3rem;
height: 3rem;
margin: 0 auto;
}
.metadata_field__preview_image__button span {
color: var(--vscode-foreground);
display: inline-block;
margin: 0 auto;
margin-top: .5rem;
}
.metadata_field__preview_image__preview {
background-color: var(--vscode-button-secondaryBackground);
}
.metadata_field__preview_image__remove {
background-color: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-inputValidation-errorForeground);
+1 -1
View File
@@ -291,7 +291,7 @@
"type": "boolean",
"description": "Is a single line field"
},
"multiSelect": {
"multiple": {
"type": "boolean",
"description": "Do you allow to select multiple values?"
},
+1 -1
View File
@@ -219,7 +219,7 @@ export class Dashboard {
panel.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image});
panel.updateMetadata({field: data.fieldName, value: data.image });
}
}
}
@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PencilIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname, parse } from 'path';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
@@ -70,6 +70,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
multiple: viewData?.data?.multiple,
value: viewData?.data?.value,
position: viewData?.data?.position || null,
alt: alt || "",
caption: caption || ""
@@ -197,11 +199,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
viewData?.data?.filePath ? (
<>
<button
title={`Insert into your article`}
title={`Insert into your content`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={insertToArticle}>
<CheckCircleIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Insert into your article</span>
<span className={`sr-only`}>Insert into your content</span>
</button>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
+39 -16
View File
@@ -21,9 +21,8 @@ import { Preview } from '../commands/Preview';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { WebviewHelper } from '@estruyf/vscode';
import { Extension } from '../helpers/Extension';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
import { Dashboard } from '../commands/Dashboard';
import { ImageHelper } from '../helpers/ImageHelper';
const FILE_LIMIT = 10;
@@ -245,23 +244,28 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const contentType = ArticleHelper.getContentType(updatedMetadata);
if (contentType) {
const imageFields = contentType.fields.filter((field) => field.type === "image");
for (const field of imageFields) {
if (updatedMetadata[field.name]) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", updatedMetadata[field.name]);
const contentFolderPath = filePath ? join(dirname(filePath), updatedMetadata[field.name]) : null;
const imageData = ImageHelper.allRelToAbs(field, updatedMetadata[field.name])
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (contentFolderPath && existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (imageData) {
if (field.multiple && imageData instanceof Array) {
const preview = imageData.map(preview => preview && preview.absPath ? ({
...preview,
webviewUrl: this.panel?.webview.asWebviewUri(preview.absPath).toString()
}) : null);
if (previewUri) {
const preview = this.panel?.webview.asWebviewUri(previewUri);
updatedMetadata[field.name]= preview?.toString() || "";
updatedMetadata[field.name] = preview || [];
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
updatedMetadata[field.name] = {
...imageData,
webviewUrl: preview ? preview.toString() : null
};
}
} else {
updatedMetadata[field.name] = "";
updatedMetadata[field.name] = field.multiple ? [] : "";
}
}
}
@@ -295,7 +299,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
/**
* Update the metadata of the article
*/
public async updateMetadata({field, value}: { field: string, value: string }) {
public async updateMetadata({field, value }: { field: string, value: any, fieldData?: { multiple: boolean, value: string[] } }) {
if (!field) {
return;
}
@@ -312,14 +316,33 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const contentType = ArticleHelper.getContentType(article.data);
const dateFields = contentType.fields.filter((field) => field.type === "datetime");
const imageFields = contentType.fields.filter((field) => field.type === "image" && field.multiple);
for (const dateField of dateFields) {
if ((field === dateField.name) && value) {
article.data[field] = Article.formatDate(new Date(value));
} else {
} else if (!imageFields.find(f => f.name === field)) {
// Only override the field data if it is not an multiselect image field
article.data[field] = value;
}
}
for (const imageField of imageFields) {
if (field === imageField.name) {
// If value is an array, it means it comes from the explorer view itself (deletion)
if (Array.isArray(value)) {
article.data[field] = value || [];
} else { // Otherwise it is coming from the media dashboard (addition)
let fieldValue = article.data[field];
if (fieldValue && !Array.isArray(fieldValue)) {
fieldValue = [fieldValue];
}
const crntData = Object.assign([], fieldValue);
const allRelPaths = [...(crntData || []), value];
article.data[field] = [...new Set(allRelPaths)].filter(f => f);
}
}
}
ArticleHelper.update(editor, article);
this.pushMetadata(article.data);
+4 -1
View File
@@ -14,7 +14,10 @@ import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
export class ContentType {
/**
* Create content based on content types
* @returns
*/
public static async createContent() {
const selectedContentType = await Questions.SelectContentType();
if (!selectedContentType) {
+80
View File
@@ -0,0 +1,80 @@
import { Uri, window } from 'vscode';
import { dirname, join } from "path";
import { Field } from '../models';
import { existsSync } from 'fs';
import { Folders } from '../commands/Folders';
import { Settings } from './SettingsHelper';
import { SETTINGS_CONTENT_STATIC_FOLDERS } from '../constants';
export class ImageHelper {
/**
* Parse all images to use absolute paths
* @param field
* @param value
* @returns
*/
public static allRelToAbs(field: Field, value: string | string[] | undefined) {
const filePath = window.activeTextEditor?.document.uri.fsPath;
if (!filePath) {
return;
}
let previewUri = null;
if (field.multiple) {
if (Array.isArray(value)) {
previewUri = value.map(v => ({
original: v,
absPath: ImageHelper.relToAbs(filePath, v)
}));
}
} else {
if (typeof value === "string") {
return {
original: value,
absPath: ImageHelper.relToAbs(filePath, value)
};
}
}
return previewUri;
}
/**
* Parse relative path to absolute path
* @param filePath
* @param value
* @returns
*/
public static relToAbs(filePath: string, value: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const staticPath = join(wsFolder?.fsPath || "", staticFolder || "", value);
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
if (existsSync(staticPath)) {
return Uri.file(staticPath);
} else if (contentFolderPath && existsSync(contentFolderPath)) {
return Uri.file(contentFolderPath);
}
}
/**
* Parse absolute path to relative path
* @param imgValue
* @returns
*/
public static absToRel(imgValue: string) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
let relPath = imgValue || "";
if (imgValue) {
relPath = imgValue.split(wsFolder?.fsPath || "").pop() || "";
relPath = imgValue.split(staticFolder || "").pop() || "";
}
return relPath;
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ export interface Field {
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories";
choices?: string[] | Choice[];
single?: boolean;
multiSelect?: boolean;
multiple?: boolean;
isPreviewImage?: boolean;
}
@@ -0,0 +1,17 @@
import * as React from 'react';
import { PreviewImageValue } from './PreviewImageField';
export interface IPreviewImageProps {
value: PreviewImageValue;
onRemove: (value: string) => void;
}
export const PreviewImage: React.FunctionComponent<IPreviewImageProps> = ({ value, onRemove }: React.PropsWithChildren<IPreviewImageProps>) => {
return (
<div className={`metadata_field__preview_image__preview`}>
<img src={value.webviewUrl} />
<button type="button" onClick={() => onRemove(value.original)} className={`metadata_field__preview_image__remove`}>Remove image</button>
</div>
);
};
@@ -3,24 +3,38 @@ import * as React from 'react';
import { MessageHelper } from '../../../helpers/MessageHelper';
import { CommandToCode } from '../../CommandToCode';
import { VsLabel } from '../VscodeComponents';
import { PreviewImage } from './PreviewImage';
export interface PreviewImageValue {
original: string;
webviewUrl: string;
}
export interface IPreviewImageFieldProps {
label: string;
fieldName: string;
value: string | null;
value: PreviewImageValue | PreviewImageValue[] | null;
filePath: string | null;
onChange: (value: string | null) => void;
multiple?: boolean;
onChange: (value: string | string[] | null) => void;
}
export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps> = ({label, fieldName, onChange, value, filePath}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps> = ({label, fieldName, onChange, value, filePath, multiple}: React.PropsWithChildren<IPreviewImageFieldProps>) => {
const selectImage = () => {
MessageHelper.sendMessage(CommandToCode.selectImage, {
filePath,
fieldName
filePath: filePath,
fieldName,
value,
multiple
});
};
const onImageRemove = (imageToRemove: string) => {
const newValue = value && Array.isArray(value) ? value.filter(image => image.original !== imageToRemove).map(i => i.original) : null;
onChange(newValue);
}
return (
<div className={`metadata_field`}>
<VsLabel>
@@ -29,16 +43,27 @@ export const PreviewImageField: React.FunctionComponent<IPreviewImageFieldProps>
</div>
</VsLabel>
<div className={`metadata_field__preview_image`}>
<div className={`metadata_field__preview_image ${multiple && value && (value as PreviewImageValue[]).length > 0 ? `metadata_field__multiple_images` : ''}`}>
{
value ? (
<div>
<img src={value} />
(!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>
</button>
)
}
<button onClick={() => onChange(null)} className={`metadata_field__preview_image__remove`}>Remove image</button>
</div>
) : (
<button onClick={selectImage}>Select image</button>
{
value && !Array.isArray(value) && (
<PreviewImage value={value} onRemove={() => onChange(null)} />
)
}
{
multiple && value && Array.isArray(value) && (
value.map(image => (
<PreviewImage key={image.original} value={image} onRemove={onImageRemove} />
))
)
}
</div>
+4 -3
View File
@@ -12,7 +12,7 @@ import { parseJSON } from 'date-fns';
import { DateTimeField } from './Fields/DateTimeField';
import { TextField } from './Fields/TextField';
import "react-datepicker/dist/react-datepicker.css";
import { PreviewImageField } from './Fields/PreviewImageField';
import { PreviewImageField, PreviewImageValue } from './Fields/PreviewImageField';
import { ListUnorderedIcon } from './Icons/ListUnorderedIcon';
import { NumberField } from './Fields/NumberField';
import { ChoiceField } from './Fields/ChoiceField';
@@ -116,7 +116,8 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
label={field.title || field.name}
fieldName={field.name}
filePath={metadata.filePath as string}
value={metadata[field.name] as string}
value={metadata[field.name] as PreviewImageValue | PreviewImageValue[] | null}
multiple={field.multiple}
onChange={(value => sendUpdate(field.name, value))} />
);
} else if (field.type === 'choice') {
@@ -129,7 +130,7 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
label={field.title || field.name}
selected={choiceValue as string}
choices={choices}
multiSelect={field.multiSelect}
multiSelect={field.multiple}
onChange={(value => sendUpdate(field.name, value))} />
);
} else if (field.type === 'tags') {