mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-08 01:42:58 +02:00
Merge branch 'combobox' into dev
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -27,6 +27,7 @@
|
||||
"common.refreshSettings": "Refresh settings",
|
||||
"common.pin": "Pin",
|
||||
"common.unpin": "Unpin",
|
||||
"common.noResults": "No results",
|
||||
|
||||
"settings.view.common": "Common",
|
||||
"settings.view.contentFolders": "Content folders",
|
||||
|
||||
Generated
+25
-5
@@ -12,7 +12,7 @@
|
||||
"@actions/core": "^1.10.0",
|
||||
"@bendera/vscode-webview-elements": "0.6.2",
|
||||
"@estruyf/vscode": "^1.1.0",
|
||||
"@headlessui/react": "1.5.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
@@ -348,9 +348,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.17",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz",
|
||||
"integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "^0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -2960,6 +2964,12 @@
|
||||
"node": ">= 4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/clipboardy": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
|
||||
@@ -13314,9 +13324,13 @@
|
||||
}
|
||||
},
|
||||
"@headlessui/react": {
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.17",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz",
|
||||
"integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"requires": {
|
||||
"client-only": "^0.0.1"
|
||||
}
|
||||
},
|
||||
"@heroicons/react": {
|
||||
"version": "1.0.4",
|
||||
@@ -15272,6 +15286,12 @@
|
||||
"source-map": "~0.6.0"
|
||||
}
|
||||
},
|
||||
"client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"dev": true
|
||||
},
|
||||
"clipboardy": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
|
||||
|
||||
+1
-1
@@ -2526,7 +2526,7 @@
|
||||
"@actions/core": "^1.10.0",
|
||||
"@bendera/vscode-webview-elements": "0.6.2",
|
||||
"@estruyf/vscode": "^1.1.0",
|
||||
"@headlessui/react": "1.5.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
|
||||
@@ -111,6 +111,10 @@ export enum LocalizationKey {
|
||||
* Unpin
|
||||
*/
|
||||
commonUnpin = 'common.unpin',
|
||||
/**
|
||||
* No results
|
||||
*/
|
||||
commonNoResults = 'common.noResults',
|
||||
/**
|
||||
* Common
|
||||
*/
|
||||
|
||||
@@ -6,21 +6,23 @@ import { LocalizationKey } from '../../../localization';
|
||||
export interface IChoiceButtonProps {
|
||||
title: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
onClick: (value: string) => void;
|
||||
}
|
||||
|
||||
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({
|
||||
title,
|
||||
value,
|
||||
className,
|
||||
onClick
|
||||
}: React.PropsWithChildren<IChoiceButtonProps>) => {
|
||||
return (
|
||||
<button
|
||||
title={l10n.t(LocalizationKey.commonRemoveValue, title)}
|
||||
className="metadata_field__choice__button"
|
||||
className={`metadata_field__choice__button text-left ${className || ""}`}
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
{title}
|
||||
<span>{title}</span>
|
||||
<XIcon className={`metadata_field__choice__button_icon`} />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ChevronDownIcon, DocumentAddIcon } from '@heroicons/react/outline';
|
||||
import Downshift from 'downshift';
|
||||
import { Combobox, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { ChoiceButton } from './ChoiceButton';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { FieldMessage } from './FieldMessage';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { Fragment, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Page } from '../../../dashboardWebView/models';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client/webview';
|
||||
import { CommandToCode } from '../../CommandToCode';
|
||||
import { ChevronDownIcon, DocumentAddIcon } from '@heroicons/react/outline';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
import { FieldMessage } from './FieldMessage';
|
||||
import { ChoiceButton } from './ChoiceButton';
|
||||
import useDropdownStyle from '../../hooks/useDropdownStyle';
|
||||
|
||||
export interface IContentTypeRelationshipFieldProps extends BaseFieldProps<string | string[]> {
|
||||
@@ -34,34 +34,25 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
const [choices, setChoices] = React.useState<string[]>([]);
|
||||
const [pages, setPages] = React.useState<Page[]>([]);
|
||||
const [crntSelected, setCrntSelected] = React.useState<string | string[] | null>(value);
|
||||
const dsRef = React.useRef<Downshift<string> | null>(null);
|
||||
const [filter, setFilter] = React.useState<string | undefined>(undefined);
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const { getDropdownStyle } = useDropdownStyle(inputRef as any);
|
||||
const { getDropdownStyle } = useDropdownStyle(inputRef as any, '6px');
|
||||
|
||||
const onValueChange = (txtValue: string) => {
|
||||
if (multiSelect) {
|
||||
const crntValue = typeof crntSelected === 'string' ? [crntSelected] : crntSelected;
|
||||
const newValue = [...((crntValue || []) as string[]), txtValue];
|
||||
const uniqueValues = [...new Set(newValue)];
|
||||
setCrntSelected(uniqueValues);
|
||||
onChange(uniqueValues);
|
||||
} else {
|
||||
setCrntSelected(txtValue);
|
||||
onChange(txtValue);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelected = (txtValue: string) => {
|
||||
if (multiSelect) {
|
||||
const newValue = [...(crntSelected || [])].filter((v) => v !== txtValue);
|
||||
setCrntSelected(newValue);
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setCrntSelected('');
|
||||
onChange('');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Check the required state
|
||||
*/
|
||||
const showRequiredState = useMemo(() => {
|
||||
return (
|
||||
required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected)
|
||||
);
|
||||
}, [required, crntSelected]);
|
||||
|
||||
/**
|
||||
* Retrieve the value based on the field setting
|
||||
* @param value
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
const getValue = (value: Page, type: string = "path") => {
|
||||
if (type === 'path') {
|
||||
return value.fmRelFilePath || value.fmFilePath;
|
||||
@@ -70,7 +61,10 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
return `${value[type]}`;
|
||||
};
|
||||
|
||||
const getChoiceValue = React.useCallback((value: string) => {
|
||||
/**
|
||||
* Retrieve choice value to display
|
||||
*/
|
||||
const getChoiceValue = useCallback((value: string) => {
|
||||
const choice = pages.find(
|
||||
(p: Page) => getValue(p, contentTypeValue) === value
|
||||
);
|
||||
@@ -79,35 +73,80 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
return choice.title;
|
||||
}
|
||||
return '';
|
||||
}, [choices, contentTypeValue]);
|
||||
}, [pages, choices, contentTypeValue]);
|
||||
|
||||
/**
|
||||
* On selecting an option
|
||||
* @param txtValue
|
||||
*/
|
||||
const onSelect = (option: string) => {
|
||||
setFilter(undefined);
|
||||
|
||||
if (multiSelect) {
|
||||
if (option) {
|
||||
const crntValue = typeof crntSelected === 'string' ? [crntSelected] : crntSelected;
|
||||
const newValue = [...((crntValue || []) as string[]), option];
|
||||
const uniqueValues = [...new Set(newValue)];
|
||||
setCrntSelected(uniqueValues);
|
||||
onChange(uniqueValues);
|
||||
}
|
||||
} else if (option) {
|
||||
setCrntSelected(option);
|
||||
onChange(option);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a selected value
|
||||
* @param txtValue
|
||||
*/
|
||||
const removeSelected = useCallback((txtValue: string) => {
|
||||
if (multiSelect) {
|
||||
const newValue = [...(crntSelected || [])].filter((v) => v !== txtValue);
|
||||
setCrntSelected(newValue);
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setCrntSelected('');
|
||||
onChange('');
|
||||
}
|
||||
}, [multiSelect, crntSelected, onChange]);
|
||||
|
||||
/**
|
||||
* Retrieve the available choices
|
||||
*/
|
||||
const availableChoices = useMemo(() => {
|
||||
return pages.filter((page: Page) => {
|
||||
const value = contentTypeValue === "slug" ? page.slug : page.fmFilePath;
|
||||
|
||||
let toShow = true;
|
||||
|
||||
if (typeof crntSelected === 'string') {
|
||||
return crntSelected !== `${value}` && !value.includes(crntSelected);
|
||||
toShow = crntSelected !== `${value}` && !value.includes(crntSelected);
|
||||
} else if (crntSelected instanceof Array) {
|
||||
const selected = crntSelected.filter((v) => v === `${value}` || value.includes(v));
|
||||
return selected.length === 0;
|
||||
toShow = selected.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (toShow && filter) {
|
||||
return page.title.toLowerCase().includes(filter);
|
||||
}
|
||||
|
||||
return toShow;
|
||||
});
|
||||
}, [choices, crntSelected, multiSelect, contentTypeValue]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return (
|
||||
required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected)
|
||||
);
|
||||
}, [required, crntSelected]);
|
||||
}, [choices, crntSelected, multiSelect, contentTypeValue, filter]);
|
||||
|
||||
/**
|
||||
* Retrieve the selected value
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (crntSelected !== value) {
|
||||
setCrntSelected(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
/**
|
||||
* Retrieve the pages based on the content type
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (contentTypeName) {
|
||||
setLoading(true);
|
||||
@@ -137,83 +176,94 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Downshift
|
||||
ref={dsRef}
|
||||
onSelect={(selected) => onValueChange(selected || '')}
|
||||
itemToString={(item) => (item ? item : '')}
|
||||
>
|
||||
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
|
||||
<div
|
||||
{...getRootProps(undefined, { suppressRefError: true })}
|
||||
ref={inputRef}
|
||||
className={`metadata_field__choice`}
|
||||
>
|
||||
<button
|
||||
{...getToggleButtonProps({
|
||||
className: `metadata_field__choice__toggle`,
|
||||
disabled: availableChoices.length === 0
|
||||
})}
|
||||
>
|
||||
<span>{`Select ${label}`}</span>
|
||||
<ChevronDownIcon className="icon" />
|
||||
</button>
|
||||
<Combobox
|
||||
value={crntSelected}
|
||||
onChange={onSelect}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
|
||||
<ul
|
||||
className={`field_dropdown metadata_field__choice_list ${isOpen ? 'open' : 'closed'}`}
|
||||
style={{
|
||||
bottom: getDropdownStyle(isOpen)
|
||||
}}
|
||||
{...getMenuProps()}
|
||||
>
|
||||
{
|
||||
availableChoices.map((choice: Page, index) => (
|
||||
<li
|
||||
{...getItemProps({
|
||||
key: getValue(choice, contentTypeValue),
|
||||
index,
|
||||
item: getValue(choice, contentTypeValue),
|
||||
})}
|
||||
>
|
||||
{choice.title || (
|
||||
<span className={`metadata_field__choice_list__item`}>
|
||||
{l10n.t(LocalizationKey.commonClearValue)}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div className="relative w-full cursor-default overflow-hidden text-left focus:outline-none border border-solid border-[var(--vscode-inputValidation-infoBorder)] rounded">
|
||||
<Combobox.Input
|
||||
className="w-full border-none py-2 pl-3 pr-10 leading-5 focus:ring-0 text-[var(--vscode-input-foreground)]"
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
value={filter || ""}
|
||||
placeholder={l10n.t(LocalizationKey.panelFieldsChoiceFieldSelect, label)}
|
||||
ref={inputRef} />
|
||||
|
||||
<Combobox.Button
|
||||
className="absolute inset-y-0 right-0 flex items-center w-8 bg-inherit rounded-none text-[var(--vscode-input-foreground)] hover:text-[var(--vscode-button-foreground)]">
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Combobox.Button>
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
|
||||
<FieldMessage
|
||||
name={label.toLowerCase()}
|
||||
description={description}
|
||||
showRequired={showRequiredState}
|
||||
/>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setFilter('')}
|
||||
>
|
||||
<Combobox.Options
|
||||
className="field_dropdown absolute max-h-60 w-full shadow-lg overflow-auto py-1 px-0 space-y-1 text-base focus:outline-none z-50 bg-[var(--vscode-dropdown-background)] text-[var(--vscode-dropdown-foreground)] border border-solid border-[var(--vscode-dropdown-border)]"
|
||||
style={{
|
||||
bottom: getDropdownStyle(open)
|
||||
}}
|
||||
>
|
||||
{availableChoices.map((choice) => (
|
||||
<Combobox.Option
|
||||
key={choice.fmFilePath}
|
||||
value={getValue(choice, contentTypeValue)}
|
||||
className={({ active }) => `py-[var(--input-padding-vertical)] px-[var(--input-padding-horizontal)] list-none cursor-pointer hover:text-[var(--vscode-button-foreground)] hover:bg-[var(--vscode-button-hoverBackground)] ${active ? "text-[var(--vscode-button-foreground)] bg-[var(--vscode-button-hoverBackground)] " : ""}`}>
|
||||
{choice.title}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
|
||||
{crntSelected instanceof Array
|
||||
? crntSelected.map((value: string) => (
|
||||
<ChoiceButton
|
||||
key={value}
|
||||
value={value}
|
||||
title={getChoiceValue(value)}
|
||||
onClick={removeSelected}
|
||||
/>
|
||||
))
|
||||
: crntSelected && (
|
||||
<ChoiceButton
|
||||
key={crntSelected}
|
||||
value={crntSelected}
|
||||
title={getChoiceValue(crntSelected)}
|
||||
onClick={removeSelected}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{availableChoices.length === 0 ? (
|
||||
<div className="relative cursor-default select-none text-center">
|
||||
{l10n.t(LocalizationKey.commonNoResults)}
|
||||
</div>
|
||||
) : null}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
<FieldMessage
|
||||
name={label.toLowerCase()}
|
||||
description={description}
|
||||
showRequired={showRequiredState}
|
||||
/>
|
||||
|
||||
{
|
||||
pages.length > 0 && (
|
||||
crntSelected instanceof Array
|
||||
? crntSelected.map((value: string) => (
|
||||
<ChoiceButton
|
||||
key={value}
|
||||
value={value}
|
||||
className='w-full mr-0 flex justify-between'
|
||||
title={getChoiceValue(value)}
|
||||
onClick={removeSelected}
|
||||
/>
|
||||
))
|
||||
: crntSelected && (
|
||||
<ChoiceButton
|
||||
key={crntSelected}
|
||||
value={crntSelected}
|
||||
className='w-full mr-0 flex justify-between'
|
||||
title={getChoiceValue(crntSelected)}
|
||||
onClick={removeSelected}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export default function useDropdownStyle(inputRef: React.MutableRefObject<HTMLInputElement | null>) {
|
||||
const bottomStyle = "calc(100% - 38px)";
|
||||
export default function useDropdownStyle(inputRef: React.MutableRefObject<HTMLInputElement | null>, inputHeight?: string) {
|
||||
const bottomStyle = `calc(100% - ${inputHeight || '38px'})`;
|
||||
const listItemHeight = 28;
|
||||
|
||||
const getDropdownStyle = useCallback((isOpen) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
// require('@vscode/codicons/dist/codicon.css');
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
from {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const defaultConfig = require("./tailwind.config")
|
||||
|
||||
module.exports = {
|
||||
...defaultConfig,
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -31,7 +31,21 @@ const config = [{
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader']
|
||||
use: ['style-loader', 'css-loader', {
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {
|
||||
config: path.resolve(__dirname, '../tailwind.panel.js')
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}]
|
||||
},
|
||||
{
|
||||
test: /\.m?js/,
|
||||
|
||||
Reference in New Issue
Block a user