mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
feat: Add option to filter content relationship options by active locale
This commit is contained in:
@@ -1543,6 +1543,11 @@
|
||||
"default": "path",
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.contentTypeValue.description%"
|
||||
},
|
||||
"sameContentLocale": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.sameContentLocale.description%"
|
||||
},
|
||||
"when": {
|
||||
"type": "object",
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.description%",
|
||||
|
||||
@@ -228,6 +228,7 @@
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.required.description": "Specify if the field is required",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.contentTypeName.description": "Specify the content type name to filter content for the contentRelationship field",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.contentTypeValue.description": "Specify the value to insert for the contentRelationship field",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.sameContentLocale.description": "Specify if you only want to show the content with the same locale",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.description": "Specify the conditions to show the field",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.fieldRef.description": "The field ID to use",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.operator.description": "The operator to use",
|
||||
|
||||
@@ -257,7 +257,7 @@ export class PagesListener extends BaseListener {
|
||||
*/
|
||||
private static async createSearchIndex(pages: Page[]) {
|
||||
const pagesIndex = Fuse.createIndex(
|
||||
['title', 'slug', 'description', 'fmBody', 'type', 'fmContentType'],
|
||||
['title', 'slug', 'description', 'fmBody', 'type', 'fmContentType', 'fmLocale.locale'],
|
||||
pages
|
||||
);
|
||||
await Extension.getInstance().setState(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { i18n } from '../../commands';
|
||||
import { ExtensionState } from '../../constants';
|
||||
import { Page } from '../../dashboardWebView/models';
|
||||
import { Extension } from '../../helpers';
|
||||
@@ -29,15 +30,28 @@ export class FieldsListener extends BaseListener {
|
||||
* @param payload
|
||||
* @returns
|
||||
*/
|
||||
private static async searchByType(command: string, requestId?: string, type?: string) {
|
||||
if (!type || !requestId) {
|
||||
private static async searchByType(
|
||||
command: string,
|
||||
requestId?: string,
|
||||
data?: { type?: string; sameLocale?: boolean; activePath?: string }
|
||||
) {
|
||||
if (!data?.type || !data?.activePath || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeLocale = await i18n.getLocale(data.activePath);
|
||||
if (!activeLocale?.locale) {
|
||||
return;
|
||||
}
|
||||
|
||||
PagesListener.getPagesData(false, async (pages) => {
|
||||
const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
keys: [{ name: 'fmContentType', weight: 1 }],
|
||||
threshold: 0,
|
||||
keys: [
|
||||
{ name: 'fmContentType', weight: 1 },
|
||||
...(data.sameLocale ? [{ name: 'fmLocale.locale', weight: 1 }] : [])
|
||||
],
|
||||
findAllMatches: true,
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const pagesIndex = await Extension.getInstance().getState<Fuse.FuseIndex<Page>>(
|
||||
@@ -48,9 +62,8 @@ export class FieldsListener extends BaseListener {
|
||||
const fuse = new Fuse(pages || [], fuseOptions, fuseIndex);
|
||||
const results = fuse.search({
|
||||
$and: [
|
||||
{
|
||||
fmContentType: type
|
||||
}
|
||||
{ fmContentType: data.type! },
|
||||
...(data.sameLocale ? [{ 'fmLocale.locale': activeLocale.locale }] : [])
|
||||
]
|
||||
});
|
||||
const pageResults = results.map((page) => page.item);
|
||||
|
||||
@@ -135,6 +135,7 @@ export interface Field {
|
||||
// Content relationship
|
||||
contentTypeName?: string;
|
||||
contentTypeValue?: 'path' | 'slug';
|
||||
sameContentLocale?: boolean;
|
||||
|
||||
// Custom field
|
||||
customType?: string;
|
||||
|
||||
@@ -12,20 +12,25 @@ import { FieldTitle } from './FieldTitle';
|
||||
import { FieldMessage } from './FieldMessage';
|
||||
import { ChoiceButton } from './ChoiceButton';
|
||||
import useDropdownStyle from '../../hooks/useDropdownStyle';
|
||||
import useMessages from '../../hooks/useMessages';
|
||||
|
||||
export interface IContentTypeRelationshipFieldProps extends BaseFieldProps<string | string[]> {
|
||||
contentTypeName?: string;
|
||||
contentTypeValue?: string;
|
||||
sameContentLocale?: boolean;
|
||||
multiSelect?: boolean;
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeRelationshipFieldProps> = ({
|
||||
export const ContentTypeRelationshipField: React.FunctionComponent<
|
||||
IContentTypeRelationshipFieldProps
|
||||
> = ({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
contentTypeName,
|
||||
contentTypeValue,
|
||||
sameContentLocale,
|
||||
multiSelect,
|
||||
onChange,
|
||||
required
|
||||
@@ -37,6 +42,7 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
const [filter, setFilter] = React.useState<string | undefined>(undefined);
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const { getDropdownStyle } = useDropdownStyle(inputRef as any, '6px');
|
||||
const { metadata } = useMessages();
|
||||
|
||||
/**
|
||||
* Check the required state
|
||||
@@ -49,11 +55,11 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
|
||||
/**
|
||||
* Retrieve the value based on the field setting
|
||||
* @param value
|
||||
* @param type
|
||||
* @returns
|
||||
* @param value
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
const getValue = (value: Page, type: string = "path") => {
|
||||
const getValue = (value: Page, type: string = 'path') => {
|
||||
if (type === 'path') {
|
||||
return value.fmRelFilePath || value.fmFilePath;
|
||||
}
|
||||
@@ -64,20 +70,21 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
/**
|
||||
* Retrieve choice value to display
|
||||
*/
|
||||
const getChoiceValue = useCallback((value: string) => {
|
||||
const choice = pages.find(
|
||||
(p: Page) => getValue(p, contentTypeValue) === value
|
||||
);
|
||||
const getChoiceValue = useCallback(
|
||||
(value: string) => {
|
||||
const choice = pages.find((p: Page) => getValue(p, contentTypeValue) === value);
|
||||
|
||||
if (choice) {
|
||||
return choice.title;
|
||||
}
|
||||
return '';
|
||||
}, [pages, choices, contentTypeValue]);
|
||||
if (choice) {
|
||||
return choice.title;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[pages, choices, contentTypeValue]
|
||||
);
|
||||
|
||||
/**
|
||||
* On selecting an option
|
||||
* @param txtValue
|
||||
* @param txtValue
|
||||
*/
|
||||
const onSelect = (option: string) => {
|
||||
setFilter(undefined);
|
||||
@@ -98,25 +105,28 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
|
||||
/**
|
||||
* Remove a selected value
|
||||
* @param txtValue
|
||||
* @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]);
|
||||
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;
|
||||
const value = contentTypeValue === 'slug' ? page.slug : page.fmFilePath;
|
||||
|
||||
let toShow = true;
|
||||
|
||||
@@ -148,93 +158,93 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
* Retrieve the pages based on the content type
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (contentTypeName) {
|
||||
if (contentTypeName && metadata?.filePath) {
|
||||
setLoading(true);
|
||||
messageHandler
|
||||
.request<Page[]>(CommandToCode.searchByType, contentTypeName)
|
||||
.request<Page[]>(CommandToCode.searchByType, {
|
||||
type: contentTypeName,
|
||||
sameLocale: sameContentLocale ?? true,
|
||||
activePath: metadata?.filePath
|
||||
})
|
||||
.then((pages: Page[]) => {
|
||||
setPages(pages || []);
|
||||
setChoices((pages || []).map(page => page.title))
|
||||
}).finally(() => {
|
||||
setChoices((pages || []).map((page) => page.title));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [contentTypeName]);
|
||||
}, [contentTypeName, sameContentLocale, metadata?.filePath]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field ${showRequiredState ? 'required' : ''}`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<DocumentPlusIcon />}
|
||||
required={required} />
|
||||
<FieldTitle label={label} icon={<DocumentPlusIcon />} required={required} />
|
||||
|
||||
{
|
||||
loading ? (
|
||||
<div className='metadata_field__wrapper'>
|
||||
<div className='metadata_field__loading'>
|
||||
{l10n.t(LocalizationKey.panelFieldsContentTypeRelationshipFieldLoading)}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="metadata_field__wrapper">
|
||||
<div className="metadata_field__loading">
|
||||
{l10n.t(LocalizationKey.panelFieldsContentTypeRelationshipFieldLoading)}
|
||||
</div>
|
||||
) : (
|
||||
<Combobox
|
||||
value={crntSelected}
|
||||
onChange={onSelect}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
</div>
|
||||
) : (
|
||||
<Combobox value={crntSelected} onChange={onSelect}>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<div className="relative w-full cursor-default overflow-hidden rounded border border-solid border-[var(--vscode-inputValidation-infoBorder)] text-left focus:outline-none">
|
||||
<Combobox.Input
|
||||
className="w-full border-none py-2 pl-3 pr-10 leading-5 text-[var(--vscode-input-foreground)] focus:ring-0"
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
value={filter || ''}
|
||||
placeholder={l10n.t(LocalizationKey.panelFieldsChoiceFieldSelect, label)}
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<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}
|
||||
<div className='text-xs opacity-60 mt-0.5'>{choice.slug}</div>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
|
||||
{availableChoices.length === 0 ? (
|
||||
<div className="relative cursor-default select-none text-center">
|
||||
{l10n.t(LocalizationKey.commonNoResults)}
|
||||
</div>
|
||||
) : null}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex w-8 items-center rounded-none bg-inherit text-[var(--vscode-input-foreground)] hover:text-[var(--vscode-button-foreground)]">
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Combobox.Button>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setFilter('')}
|
||||
>
|
||||
<Combobox.Options
|
||||
className="field_dropdown absolute z-50 max-h-60 w-full space-y-1 overflow-auto border border-solid border-[var(--vscode-dropdown-border)] bg-[var(--vscode-dropdown-background)] px-0 py-1 text-base text-[var(--vscode-dropdown-foreground)] shadow-lg focus:outline-none"
|
||||
style={{
|
||||
bottom: getDropdownStyle(open)
|
||||
}}
|
||||
>
|
||||
{availableChoices.map((choice) => (
|
||||
<Combobox.Option
|
||||
key={choice.fmFilePath}
|
||||
value={getValue(choice, contentTypeValue)}
|
||||
className={({ active }) =>
|
||||
`cursor-pointer list-none px-[var(--input-padding-horizontal)] py-[var(--input-padding-vertical)] hover:bg-[var(--vscode-button-hoverBackground)] hover:text-[var(--vscode-button-foreground)] ${
|
||||
active
|
||||
? 'bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] '
|
||||
: ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
{choice.title}
|
||||
<div className="mt-0.5 text-xs opacity-60">{choice.slug}</div>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
|
||||
{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()}
|
||||
@@ -242,29 +252,26 @@ export const ContentTypeRelationshipField: React.FunctionComponent<IContentTypeR
|
||||
showRequired={showRequiredState}
|
||||
/>
|
||||
|
||||
{
|
||||
pages.length > 0 && (
|
||||
crntSelected instanceof Array
|
||||
? crntSelected.map((value: string) => (
|
||||
{pages.length > 0 &&
|
||||
(crntSelected instanceof Array
|
||||
? crntSelected.map((value: string) => (
|
||||
<ChoiceButton
|
||||
key={value}
|
||||
value={value}
|
||||
className='w-full mr-0 flex justify-between'
|
||||
className="mr-0 flex w-full justify-between"
|
||||
title={getChoiceValue(value)}
|
||||
onClick={removeSelected}
|
||||
/>
|
||||
))
|
||||
: crntSelected && (
|
||||
: crntSelected && (
|
||||
<ChoiceButton
|
||||
key={crntSelected}
|
||||
value={crntSelected}
|
||||
className='w-full mr-0 flex justify-between'
|
||||
className="mr-0 flex w-full justify-between"
|
||||
title={getChoiceValue(crntSelected)}
|
||||
onClick={removeSelected}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
);
|
||||
};
|
||||
|
||||
@@ -497,6 +497,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
required={!!field.required}
|
||||
contentTypeName={field.contentTypeName}
|
||||
contentTypeValue={field.contentTypeValue}
|
||||
sameContentLocale={field.sameContentLocale}
|
||||
multiSelect={field.multiple}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user