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/package-lock.json b/package-lock.json index 7022a8bf..0ab0edae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 65797d8f..f06be3bd 100644 --- a/package.json +++ b/package.json @@ -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", 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/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/hooks/useDropdownStyle.tsx b/src/panelWebView/hooks/useDropdownStyle.tsx index ff924f63..fb280984 100644 --- a/src/panelWebView/hooks/useDropdownStyle.tsx +++ b/src/panelWebView/hooks/useDropdownStyle.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; -export default function useDropdownStyle(inputRef: React.MutableRefObject) { - 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) => { diff --git a/src/panelWebView/index.tsx b/src/panelWebView/index.tsx index 4875209c..dc8385e9 100644 --- a/src/panelWebView/index.tsx +++ b/src/panelWebView/index.tsx @@ -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'); diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index abbd3175..163bec84 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -1,3 +1,6 @@ +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + /* Animations */ @keyframes spin { from { diff --git a/tailwind.panel.js b/tailwind.panel.js new file mode 100644 index 00000000..bdea1d34 --- /dev/null +++ b/tailwind.panel.js @@ -0,0 +1,8 @@ +const defaultConfig = require("./tailwind.config") + +module.exports = { + ...defaultConfig, + corePlugins: { + preflight: false, + } +} \ No newline at end of file diff --git a/webpack/panel.config.js b/webpack/panel.config.js index 2169d349..3a5ef3a0 100644 --- a/webpack/panel.config.js +++ b/webpack/panel.config.js @@ -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/,