feat: Add support for product export in UI (#8281)
* feat: Add support for product export in UI * fix:Return the backend URL for private files of local file provider
This commit is contained in:
@@ -41,10 +41,15 @@ export type Filter = {
|
||||
|
||||
type DataTableFilterProps = {
|
||||
filters: Filter[]
|
||||
readonly?: boolean
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
export const DataTableFilter = ({
|
||||
filters,
|
||||
readonly,
|
||||
prefix,
|
||||
}: DataTableFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchParams] = useSearchParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -127,6 +132,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
key={filter.key}
|
||||
filter={filter}
|
||||
prefix={prefix}
|
||||
readonly={readonly}
|
||||
options={filter.options}
|
||||
multiple={filter.multiple}
|
||||
searchable={filter.searchable}
|
||||
@@ -139,6 +145,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
key={filter.key}
|
||||
filter={filter}
|
||||
prefix={prefix}
|
||||
readonly={readonly}
|
||||
openOnMount={filter.openOnMount}
|
||||
/>
|
||||
)
|
||||
@@ -148,6 +155,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
key={filter.key}
|
||||
filter={filter}
|
||||
prefix={prefix}
|
||||
readonly={readonly}
|
||||
openOnMount={filter.openOnMount}
|
||||
/>
|
||||
)
|
||||
@@ -157,6 +165,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
key={filter.key}
|
||||
filter={filter}
|
||||
prefix={prefix}
|
||||
readonly={readonly}
|
||||
openOnMount={filter.openOnMount}
|
||||
/>
|
||||
)
|
||||
@@ -164,7 +173,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
break
|
||||
}
|
||||
})}
|
||||
{availableFilters.length > 0 && (
|
||||
{!readonly && availableFilters.length > 0 && (
|
||||
<Popover.Root modal open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild id="filters_menu_trigger">
|
||||
<Button size="small" variant="secondary">
|
||||
@@ -208,7 +217,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
)}
|
||||
{activeFilters.length > 0 && (
|
||||
{!readonly && activeFilters.length > 0 && (
|
||||
<ClearAllFilters filters={filters} prefix={prefix} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ type DateComparisonOperator = {
|
||||
export const DateFilter = ({
|
||||
filter,
|
||||
prefix,
|
||||
readonly,
|
||||
openOnMount,
|
||||
}: DateFilterProps) => {
|
||||
const [open, setOpen] = useState(openOnMount)
|
||||
@@ -118,112 +119,119 @@ export const DateFilter = ({
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<DateDisplay label={label} value={displayValue} onRemove={handleRemove} />
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
data-name="date_filter_content"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={24}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout h-full max-h-[var(--radix-popper-available-height)] w-[300px] overflow-auto rounded-lg"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
<DateDisplay
|
||||
label={label}
|
||||
value={displayValue}
|
||||
onRemove={handleRemove}
|
||||
readonly={readonly}
|
||||
/>
|
||||
{!readonly && (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
data-name="date_filter_content"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={24}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout h-full max-h-[var(--radix-popper-available-height)] w-[300px] overflow-auto rounded-lg"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ul className="w-full p-1">
|
||||
{presets.map((preset) => {
|
||||
const isSelected = selectedParams
|
||||
.get()
|
||||
.includes(JSON.stringify(preset.value))
|
||||
return (
|
||||
<li key={preset.label}>
|
||||
<button
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleSelectPreset(preset.value)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||
{
|
||||
"[&_svg]:invisible": !isSelected,
|
||||
}
|
||||
)}
|
||||
}}
|
||||
>
|
||||
<ul className="w-full p-1">
|
||||
{presets.map((preset) => {
|
||||
const isSelected = selectedParams
|
||||
.get()
|
||||
.includes(JSON.stringify(preset.value))
|
||||
return (
|
||||
<li key={preset.label}>
|
||||
<button
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleSelectPreset(preset.value)
|
||||
}}
|
||||
>
|
||||
<EllipseMiniSolid />
|
||||
</div>
|
||||
{preset.label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li>
|
||||
<button
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||
type="button"
|
||||
onClick={handleSelectCustom}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||
{
|
||||
"[&_svg]:invisible": !showCustom,
|
||||
}
|
||||
)}
|
||||
<div
|
||||
className={clx(
|
||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||
{
|
||||
"[&_svg]:invisible": !isSelected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EllipseMiniSolid />
|
||||
</div>
|
||||
{preset.label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li>
|
||||
<button
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||
type="button"
|
||||
onClick={handleSelectCustom}
|
||||
>
|
||||
<EllipseMiniSolid />
|
||||
<div
|
||||
className={clx(
|
||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||
{
|
||||
"[&_svg]:invisible": !showCustom,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EllipseMiniSolid />
|
||||
</div>
|
||||
{t("filters.date.custom")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{showCustom && (
|
||||
<div className="border-t px-1 pb-3 pt-1">
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<Text size="xsmall" leading="compact" weight="plus">
|
||||
{t("filters.date.from")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2 py-1">
|
||||
<DatePicker
|
||||
maxValue={customEndValue}
|
||||
value={customStartValue}
|
||||
onChange={(d) => handleCustomDateChange(d, "start")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{t("filters.date.custom")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{showCustom && (
|
||||
<div className="border-t px-1 pb-3 pt-1">
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<Text size="xsmall" leading="compact" weight="plus">
|
||||
{t("filters.date.from")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2 py-1">
|
||||
<DatePicker
|
||||
maxValue={customEndValue}
|
||||
value={customStartValue}
|
||||
onChange={(d) => handleCustomDateChange(d, "start")}
|
||||
/>
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<Text size="xsmall" leading="compact" weight="plus">
|
||||
{t("filters.date.to")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2 py-1">
|
||||
<DatePicker
|
||||
minValue={customStartValue}
|
||||
value={customEndValue || undefined}
|
||||
onChange={(d) => {
|
||||
handleCustomDateChange(d, "end")
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<Text size="xsmall" leading="compact" weight="plus">
|
||||
{t("filters.date.to")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2 py-1">
|
||||
<DatePicker
|
||||
minValue={customStartValue}
|
||||
value={customEndValue || undefined}
|
||||
onChange={(d) => {
|
||||
handleCustomDateChange(d, "end")
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)}
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)}
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
@@ -231,10 +239,16 @@ export const DateFilter = ({
|
||||
type DateDisplayProps = {
|
||||
label: string
|
||||
value?: string
|
||||
readonly?: boolean
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
|
||||
const DateDisplay = ({
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
onRemove,
|
||||
}: DateDisplayProps) => {
|
||||
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
@@ -245,8 +259,10 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
|
||||
asChild
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"data-[state=open]:bg-ui-bg-field-hover"
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
@@ -268,7 +284,7 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{value && (
|
||||
{!readonly && value && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
|
||||
@@ -24,6 +24,7 @@ type Operator = "lt" | "gt" | "eq"
|
||||
export const NumberFilter = ({
|
||||
filter,
|
||||
prefix,
|
||||
readonly,
|
||||
openOnMount,
|
||||
}: NumberFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -136,104 +137,107 @@ export const NumberFilter = ({
|
||||
label={label}
|
||||
value={currentValue}
|
||||
onRemove={handleRemove}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
data-name="number_filter_content"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={24}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] w-[300px] divide-y overflow-y-auto rounded-lg outline-none"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="p-1">
|
||||
<RadioGroup.Root
|
||||
value={operator}
|
||||
onValueChange={(val) => setOperator(val as Comparison)}
|
||||
className="flex flex-col items-start"
|
||||
orientation="vertical"
|
||||
autoFocus
|
||||
>
|
||||
{operators.map((o) => (
|
||||
<RadioGroup.Item
|
||||
key={o.operator}
|
||||
value={o.operator}
|
||||
className="txt-compact-small hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed transition-fg grid w-full grid-cols-[20px_1fr] gap-2 rounded-[4px] px-2 py-1.5 text-left outline-none"
|
||||
>
|
||||
<div className="size-5">
|
||||
<RadioGroup.Indicator>
|
||||
<EllipseMiniSolid />
|
||||
</RadioGroup.Indicator>
|
||||
</div>
|
||||
<span className="w-full">{o.label}</span>
|
||||
</RadioGroup.Item>
|
||||
))}
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<div>
|
||||
{operator === "range" ? (
|
||||
<div className="px-1 pb-3 pt-1" key="range">
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={GT_KEY}>
|
||||
{t("filters.compare.greaterThan")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={GT_KEY}
|
||||
size="small"
|
||||
type="number"
|
||||
defaultValue={getValue(currentValue, "gt")}
|
||||
onChange={(e) => debouncedOnChange(e, "gt")}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={LT_KEY}>
|
||||
{t("filters.compare.lessThan")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={LT_KEY}
|
||||
size="small"
|
||||
type="number"
|
||||
defaultValue={getValue(currentValue, "lt")}
|
||||
onChange={(e) => debouncedOnChange(e, "lt")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-1 pb-3 pt-1" key="exact">
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={EQ_KEY}>
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={EQ_KEY}
|
||||
size="small"
|
||||
type="number"
|
||||
defaultValue={getValue(currentValue, "eq")}
|
||||
onChange={(e) => debouncedOnChange(e, "eq")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
data-name="number_filter_content"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={24}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] w-[300px] divide-y overflow-y-auto rounded-lg outline-none"
|
||||
)}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="p-1">
|
||||
<RadioGroup.Root
|
||||
value={operator}
|
||||
onValueChange={(val) => setOperator(val as Comparison)}
|
||||
className="flex flex-col items-start"
|
||||
orientation="vertical"
|
||||
autoFocus
|
||||
>
|
||||
{operators.map((o) => (
|
||||
<RadioGroup.Item
|
||||
key={o.operator}
|
||||
value={o.operator}
|
||||
className="txt-compact-small hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed transition-fg grid w-full grid-cols-[20px_1fr] gap-2 rounded-[4px] px-2 py-1.5 text-left outline-none"
|
||||
>
|
||||
<div className="size-5">
|
||||
<RadioGroup.Indicator>
|
||||
<EllipseMiniSolid />
|
||||
</RadioGroup.Indicator>
|
||||
</div>
|
||||
<span className="w-full">{o.label}</span>
|
||||
</RadioGroup.Item>
|
||||
))}
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<div>
|
||||
{operator === "range" ? (
|
||||
<div className="px-1 pb-3 pt-1" key="range">
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={GT_KEY}>
|
||||
{t("filters.compare.greaterThan")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={GT_KEY}
|
||||
size="small"
|
||||
type="number"
|
||||
defaultValue={getValue(currentValue, "gt")}
|
||||
onChange={(e) => debouncedOnChange(e, "gt")}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={LT_KEY}>
|
||||
{t("filters.compare.lessThan")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={LT_KEY}
|
||||
size="small"
|
||||
type="number"
|
||||
defaultValue={getValue(currentValue, "lt")}
|
||||
onChange={(e) => debouncedOnChange(e, "lt")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-1 pb-3 pt-1" key="exact">
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={EQ_KEY}>
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={EQ_KEY}
|
||||
size="small"
|
||||
type="number"
|
||||
defaultValue={getValue(currentValue, "eq")}
|
||||
onChange={(e) => debouncedOnChange(e, "eq")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)}
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
@@ -241,10 +245,12 @@ export const NumberFilter = ({
|
||||
const NumberDisplay = ({
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
onRemove,
|
||||
}: {
|
||||
label: string
|
||||
value?: string[]
|
||||
readonly?: boolean
|
||||
onRemove: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -282,8 +288,10 @@ const NumberDisplay = ({
|
||||
asChild
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"data-[state=open]:bg-ui-bg-field-hover"
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
@@ -317,7 +325,7 @@ const NumberDisplay = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{value && (
|
||||
{!readonly && value && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { IFilter } from "./types"
|
||||
|
||||
interface SelectFilterProps extends IFilter {
|
||||
options: { label: string; value: unknown }[]
|
||||
readonly?: boolean
|
||||
multiple?: boolean
|
||||
searchable?: boolean
|
||||
}
|
||||
@@ -18,6 +19,7 @@ interface SelectFilterProps extends IFilter {
|
||||
export const SelectFilter = ({
|
||||
filter,
|
||||
prefix,
|
||||
readonly,
|
||||
multiple,
|
||||
searchable,
|
||||
options,
|
||||
@@ -80,103 +82,107 @@ export const SelectFilter = ({
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<SelectDisplay
|
||||
readonly={readonly}
|
||||
label={label}
|
||||
value={labelValues}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={8}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
{!readonly && (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={8}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Command className="h-full">
|
||||
{searchable && (
|
||||
<div className="border-b p-1">
|
||||
<div className="grid grid-cols-[1fr_20px] gap-x-2 rounded-md px-2 py-1">
|
||||
<Command.Input
|
||||
ref={setSearchRef}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
className="txt-compact-small placeholder:text-ui-fg-muted bg-transparent outline-none"
|
||||
placeholder="Search"
|
||||
/>
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<button
|
||||
disabled={!search}
|
||||
onClick={handleClearSearch}
|
||||
className={clx(
|
||||
"transition-fg text-ui-fg-muted focus-visible:bg-ui-bg-base-pressed rounded-md outline-none",
|
||||
{
|
||||
invisible: !search,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
}}
|
||||
>
|
||||
<Command className="h-full">
|
||||
{searchable && (
|
||||
<div className="border-b p-1">
|
||||
<div className="grid grid-cols-[1fr_20px] gap-x-2 rounded-md px-2 py-1">
|
||||
<Command.Input
|
||||
ref={setSearchRef}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
className="txt-compact-small placeholder:text-ui-fg-muted bg-transparent outline-none"
|
||||
placeholder="Search"
|
||||
/>
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<button
|
||||
disabled={!search}
|
||||
onClick={handleClearSearch}
|
||||
className={clx(
|
||||
"transition-fg text-ui-fg-muted focus-visible:bg-ui-bg-base-pressed rounded-md outline-none",
|
||||
{
|
||||
invisible: !search,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Command.Empty className="txt-compact-small flex items-center justify-center p-1">
|
||||
<span className="w-full px-2 py-1 text-center">
|
||||
{t("general.noResultsTitle")}
|
||||
</span>
|
||||
</Command.Empty>
|
||||
<Command.List className="h-full max-h-[163px] min-h-[0] overflow-auto p-1 outline-none">
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedParams
|
||||
.get()
|
||||
.includes(String(option.value))
|
||||
)}
|
||||
<Command.Empty className="txt-compact-small flex items-center justify-center p-1">
|
||||
<span className="w-full px-2 py-1 text-center">
|
||||
{t("general.noResultsTitle")}
|
||||
</span>
|
||||
</Command.Empty>
|
||||
<Command.List className="h-full max-h-[163px] min-h-[0] overflow-auto p-1 outline-none">
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedParams
|
||||
.get()
|
||||
.includes(String(option.value))
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
key={String(option.value)}
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover aria-selected:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex cursor-pointer select-none items-center gap-x-2 rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||
value={option.label}
|
||||
onSelect={() => {
|
||||
handleSelect(option.value)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||
{
|
||||
"[&_svg]:invisible": !isSelected,
|
||||
}
|
||||
)}
|
||||
return (
|
||||
<Command.Item
|
||||
key={String(option.value)}
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover aria-selected:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex cursor-pointer select-none items-center gap-x-2 rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||
value={option.label}
|
||||
onSelect={() => {
|
||||
handleSelect(option.value)
|
||||
}}
|
||||
>
|
||||
{multiple ? <CheckMini /> : <EllipseMiniSolid />}
|
||||
</div>
|
||||
{option.label}
|
||||
</Command.Item>
|
||||
)
|
||||
})}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
<div
|
||||
className={clx(
|
||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||
{
|
||||
"[&_svg]:invisible": !isSelected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{multiple ? <CheckMini /> : <EllipseMiniSolid />}
|
||||
</div>
|
||||
{option.label}
|
||||
</Command.Item>
|
||||
)
|
||||
})}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)}
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectDisplayProps = {
|
||||
label: string
|
||||
readonly?: boolean
|
||||
value?: string | string[]
|
||||
onRemove: () => void
|
||||
}
|
||||
@@ -185,6 +191,7 @@ export const SelectDisplay = ({
|
||||
label,
|
||||
value,
|
||||
onRemove,
|
||||
readonly,
|
||||
}: SelectDisplayProps) => {
|
||||
const { t } = useTranslation()
|
||||
const v = value ? (Array.isArray(value) ? value : [value]) : null
|
||||
@@ -200,8 +207,10 @@ export const SelectDisplay = ({
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden rounded-md",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"data-[state=open]:bg-ui-bg-field-hover"
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -242,7 +251,7 @@ export const SelectDisplay = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{v && v.length > 0 && (
|
||||
{!readonly && v && v.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
|
||||
@@ -13,6 +13,7 @@ type StringFilterProps = IFilter
|
||||
export const StringFilter = ({
|
||||
filter,
|
||||
prefix,
|
||||
readonly,
|
||||
openOnMount,
|
||||
}: StringFilterProps) => {
|
||||
const [open, setOpen] = useState(openOnMount)
|
||||
@@ -67,45 +68,52 @@ export const StringFilter = ({
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<StringDisplay label={label} value={query?.[0]} onRemove={handleRemove} />
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={8}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
<StringDisplay
|
||||
label={label}
|
||||
value={query?.[0]}
|
||||
onRemove={handleRemove}
|
||||
readonly={readonly}
|
||||
/>
|
||||
{!readonly && (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={8}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (
|
||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||
"filters_menu_content"
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="px-1 pb-3 pt-1">
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={key}>
|
||||
{label}
|
||||
</Label>
|
||||
}}
|
||||
>
|
||||
<div className="px-1 pb-3 pt-1">
|
||||
<div className="px-2 py-1.5">
|
||||
<Label size="xsmall" weight="plus" htmlFor={key}>
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={key}
|
||||
size="small"
|
||||
defaultValue={query?.[0] || undefined}
|
||||
onChange={debouncedOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 py-0.5">
|
||||
<Input
|
||||
name={key}
|
||||
size="small"
|
||||
defaultValue={query?.[0] || undefined}
|
||||
onChange={debouncedOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)}
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
@@ -113,10 +121,12 @@ export const StringFilter = ({
|
||||
const StringDisplay = ({
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
onRemove,
|
||||
}: {
|
||||
label: string
|
||||
value?: string
|
||||
readonly?: boolean
|
||||
onRemove: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -126,8 +136,10 @@ const StringDisplay = ({
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden rounded-md",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"data-[state=open]:bg-ui-bg-field-hover"
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -168,7 +180,7 @@ const StringDisplay = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!!value && (
|
||||
{!readonly && !!value && (
|
||||
<div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface IFilter {
|
||||
key: string
|
||||
label: string
|
||||
}
|
||||
readonly?: boolean
|
||||
openOnMount?: boolean
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user