From 731212264c6070af7a100a4a9dda9a04b7e7e25d Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 1 Nov 2023 14:41:04 +0100 Subject: [PATCH] New content type relationship field implementation --- .frontmatter/database/pinnedItemsDb.json | 1 + l10n/bundle.l10n.json | 1 + src/localization/localization.enum.ts | 4 + .../components/Fields/ChoiceButton.tsx | 6 +- .../components/Fields/ComboboxField.tsx | 100 ------ .../Fields/ContentTypeRelationshipField.tsx | 286 ++++++++++-------- .../components/Fields/WrapperField.tsx | 14 +- src/panelWebView/hooks/useDropdownStyle.tsx | 4 +- 8 files changed, 181 insertions(+), 235 deletions(-) create mode 100644 .frontmatter/database/pinnedItemsDb.json delete mode 100644 src/panelWebView/components/Fields/ComboboxField.tsx diff --git a/.frontmatter/database/pinnedItemsDb.json b/.frontmatter/database/pinnedItemsDb.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/.frontmatter/database/pinnedItemsDb.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 1f807936..dc967c25 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -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", diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 2591c1be..076205b5 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -111,6 +111,10 @@ export enum LocalizationKey { * Unpin */ commonUnpin = 'common.unpin', + /** + * No results + */ + commonNoResults = 'common.noResults', /** * Common */ diff --git a/src/panelWebView/components/Fields/ChoiceButton.tsx b/src/panelWebView/components/Fields/ChoiceButton.tsx index e8dfadeb..7b2f5bf8 100644 --- a/src/panelWebView/components/Fields/ChoiceButton.tsx +++ b/src/panelWebView/components/Fields/ChoiceButton.tsx @@ -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 = ({ title, value, + className, onClick }: React.PropsWithChildren) => { return ( ); diff --git a/src/panelWebView/components/Fields/ComboboxField.tsx b/src/panelWebView/components/Fields/ComboboxField.tsx deleted file mode 100644 index f661960b..00000000 --- a/src/panelWebView/components/Fields/ComboboxField.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { Combobox, Transition } from '@headlessui/react'; -import * as React from 'react'; -import { BaseFieldProps } from '../../../models'; -import { Fragment, useEffect, useMemo } from 'react'; -import { Page } from '../../../dashboardWebView/models'; -import { messageHandler } from '@estruyf/vscode/dist/client/webview'; -import { CommandToCode } from '../../CommandToCode'; -import { ChevronDownIcon } from '@heroicons/react/outline'; - -export interface IComboboxFieldProps extends BaseFieldProps { - contentTypeName?: string; - contentTypeValue?: string; - multiSelect?: boolean; - onChange: (value: string | string[]) => void; -} - -export const ComboboxField: React.FunctionComponent = ({ - label, - description, - value, - contentTypeName, - contentTypeValue, - multiSelect, - onChange, - required -}: React.PropsWithChildren) => { - const [loading, setLoading] = React.useState(false); - const [choices, setChoices] = React.useState([]); - const [pages, setPages] = React.useState([]); - const [crntSelected, setCrntSelected] = React.useState(value); - - const availableChoices = useMemo(() => { - return pages.filter((page: Page) => { - const value = contentTypeValue === "slug" ? page.slug : page.fmFilePath; - - if (typeof crntSelected === 'string') { - return crntSelected !== `${value}` && !value.includes(crntSelected); - } else if (crntSelected instanceof Array) { - const selected = crntSelected.filter((v) => v === `${value}` || value.includes(v)); - return selected.length === 0; - } - - return true; - }); - }, [choices, crntSelected, multiSelect, contentTypeValue]); - - useEffect(() => { - if (contentTypeName) { - setLoading(true); - messageHandler - .request(CommandToCode.searchByType, contentTypeName) - .then((pages: Page[]) => { - setPages(pages || []); - setChoices((pages || []).map(page => page.title)) - }).finally(() => { - setLoading(false); - }); - } - }, [contentTypeName]); - - return ( - null} - > -
- -
- console.log(e)} /> - - - - -
- - - - {availableChoices.map((choice) => ( - - {choice.title} - - ))} - - -
-
- ) -}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/ContentTypeRelationshipField.tsx b/src/panelWebView/components/Fields/ContentTypeRelationshipField.tsx index da353a8d..79f0762f 100644 --- a/src/panelWebView/components/Fields/ContentTypeRelationshipField.tsx +++ b/src/panelWebView/components/Fields/ContentTypeRelationshipField.tsx @@ -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 { @@ -34,34 +34,25 @@ export const ContentTypeRelationshipField: React.FunctionComponent([]); const [pages, setPages] = React.useState([]); const [crntSelected, setCrntSelected] = React.useState(value); - const dsRef = React.useRef | null>(null); + const [filter, setFilter] = React.useState(undefined); const inputRef = React.useRef(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 { + /** + * 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 { + 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 ) : ( - <> - onValueChange(selected || '')} - itemToString={(item) => (item ? item : '')} - > - {({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => ( -
- + + {({ open }) => ( +
-
    - { - availableChoices.map((choice: Page, index) => ( -
  • - {choice.title || ( - - {l10n.t(LocalizationKey.commonClearValue)} - - )} -
  • - )) - } -
+
+ setFilter(e.target.value)} + value={filter || ""} + placeholder={l10n.t(LocalizationKey.panelFieldsChoiceFieldSelect, label)} + ref={inputRef} /> + + +
- )} - - + setFilter('')} + > + + {availableChoices.map((choice) => ( + `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} + + ))} - {crntSelected instanceof Array - ? crntSelected.map((value: string) => ( - - )) - : crntSelected && ( - - )} - + {availableChoices.length === 0 ? ( +
+ {l10n.t(LocalizationKey.commonNoResults)} +
+ ) : null} +
+
+
+ )} +
+ ) + } + + + + { + pages.length > 0 && ( + crntSelected instanceof Array + ? crntSelected.map((value: string) => ( + + )) + : crntSelected && ( + + ) ) }
- ); -}; + ) +}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 41d58f39..b059a2ae 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -33,7 +33,6 @@ import { fieldWhenClause } from '../../../utils/fieldWhenClause'; import { ContentTypeRelationshipField } from './ContentTypeRelationshipField'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; -import { ComboboxField } from './ComboboxField'; export interface IWrapperFieldProps { field: Field; @@ -486,18 +485,7 @@ export const WrapperField: React.FunctionComponent = ({ } else if (field.type === 'contentRelationship') { return ( - {/* onSendUpdate(field.name, value, parentFields)} - /> */} - - ) { - const bottomStyle = "calc(100% - 38px)"; +export default function useDropdownStyle(inputRef: React.MutableRefObject, inputHeight?: string) { + const bottomStyle = `calc(100% - ${inputHeight || '38px'})`; const listItemHeight = 28; const getDropdownStyle = useCallback((isOpen) => {