From 2e3cf3556f42d6d3900729bd00e21a2ba75ad18c Mon Sep 17 00:00:00 2001 From: Donkoko Date: Fri, 29 Nov 2024 17:35:37 +0200 Subject: [PATCH] add search on the column select dropdowns --- ...vanced-asset-index-filters-and-sorting.tsx | 256 ++++++++++++------ .../advanced-filters/field-selector.tsx | 102 ++++++- .../assets-index/advanced-filters/schema.ts | 1 + .../value.client.validator.tsx | 4 +- package-lock.json | 33 +++ package.json | 1 + 6 files changed, 306 insertions(+), 91 deletions(-) diff --git a/app/components/assets/assets-index/advanced-asset-index-filters-and-sorting.tsx b/app/components/assets/assets-index/advanced-asset-index-filters-and-sorting.tsx index 8f4aa397e..b68d025e6 100644 --- a/app/components/assets/assets-index/advanced-asset-index-filters-and-sorting.tsx +++ b/app/components/assets/assets-index/advanced-asset-index-filters-and-sorting.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import type { CustomField } from "@prisma/client"; import { Popover, @@ -9,6 +9,7 @@ import { import type { SerializeFrom } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { Reorder } from "framer-motion"; +import { Search } from "lucide-react"; import { Switch } from "~/components/forms/switch"; import { ChevronRight, HandleIcon, PlusIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; @@ -104,21 +105,20 @@ function AdvancedFilter() { function addFilter() { setFilters((prev) => { const newCols = [...prev]; - /** We need to make sure the filter we add is not one that already exists */ - const firstColumn = availableColumns[0]; const fieldType = getUIFieldType({ column: firstColumn, }) as FilterFieldType; newCols.push({ - name: firstColumn.name, + name: firstColumn.name, // Keep the name for proper UI rendering operator: operatorsPerType[fieldType][0], value: getDefaultValueForFieldType( firstColumn, customFields as SerializeFrom[] | null ), type: fieldType, + isNew: true, // Mark as new/unselected }); return newCols; }); @@ -184,56 +184,68 @@ function AdvancedFilter() { const column = availableColumns.find( (c) => c.name === name ) as Column; - const fieldType = getUIFieldType({ - column, - }) as FilterFieldType; - - const newFilters = [...prev]; - newFilters[index] = { - ...newFilters[index], - name, - type: fieldType, - operator: operatorsPerType[fieldType][0], - value: getDefaultValueForFieldType( + + // Only proceed with type/operator/value setup if a valid column is selected + if (column) { + const fieldType = getUIFieldType({ column, - customFields as - | SerializeFrom[] - | null - ), // Add default value - }; - return newFilters; - }); - }} - /> - -
- { - // Update filter operator - setFilters((prev) => { - const newFilters = [...prev]; - newFilters[index].operator = operator; - return newFilters; + }) as FilterFieldType; + + const newFilters = [...prev]; + newFilters[index] = { + ...newFilters[index], + name, + type: fieldType, + operator: operatorsPerType[fieldType][0], + value: getDefaultValueForFieldType( + column, + customFields as + | SerializeFrom[] + | null + ), + isNew: false, + }; + return newFilters; + } + return prev; }); }} />
-
- { - setFilters((prev) => { - const newFilters = [...prev]; - newFilters[index].value = value; - return newFilters; - }); - }} - applyFilters={applyFilters} - fieldName={getFieldName(index)} - zormError={getError(index)} - /> -
+ + {/* Only show operator and value fields if a column is selected */} + {filter.name && ( + <> +
+ { + setFilters((prev) => { + const newFilters = [...prev]; + newFilters[index].operator = operator; + return newFilters; + }); + }} + /> +
+
+ { + setFilters((prev) => { + const newFilters = [...prev]; + newFilters[index].value = value; + return newFilters; + }); + }} + applyFilters={applyFilters} + fieldName={getFieldName(index)} + zormError={getError(index)} + /> +
+ + )} + @@ -543,16 +606,43 @@ function PickAColumnToSortBy({ "z-[999999] mt-2 max-h-[400px] w-[250px] overflow-scroll rounded-md border border-gray-200 bg-white" )} > -
- {sortOptions.map((c) => ( +
+ + +
+
+ {filteredOptions.map((option, index) => (
addSort(c)} + id={`sort-option-${index}`} + key={option.name} + className={tw( + "px-4 py-2 text-[14px] text-gray-600 hover:cursor-pointer hover:bg-gray-50", + selectedIndex === index && [ + "bg-gray-50", + "relative", + index !== 0 && + "before:absolute before:inset-x-0 before:top-0 before:border-t before:border-gray-200", + index !== filteredOptions.length - 1 && + "after:absolute after:inset-x-0 after:bottom-0 after:border-b after:border-gray-200", + ] + )} + onClick={() => addSort(option)} > - {parseColumnName(c.name)} + {parseColumnName(option.name)}
))} + {filteredOptions.length === 0 && ( +
+ No columns found +
+ )}
diff --git a/app/components/assets/assets-index/advanced-filters/field-selector.tsx b/app/components/assets/assets-index/advanced-filters/field-selector.tsx index 278d8e1ee..c4a134173 100644 --- a/app/components/assets/assets-index/advanced-filters/field-selector.tsx +++ b/app/components/assets/assets-index/advanced-filters/field-selector.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect } from "react"; +import type { KeyboardEvent } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import { Popover, PopoverTrigger, @@ -6,6 +7,7 @@ import { PopoverContent, } from "@radix-ui/react-popover"; import { useLoaderData } from "@remix-run/react"; +import { Search } from "lucide-react"; import { ChevronRight } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { @@ -22,19 +24,75 @@ export function FieldSelector({ filters, setFilter, }: { - filter: Filter; + filter: Filter & { isNew?: boolean }; filters: Filter[]; setFilter: (name: string) => void; }) { const { settings } = useLoaderData(); const columns = settings.columns as Column[]; const [fieldName, setFieldName] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedIndex, setSelectedIndex] = useState(0); + const searchInputRef = useRef(null); + useEffect(() => { setFieldName(filter.name); }, [filter.name]); - /** Filter out the already existing filters and the columns which are not visible */ - const availableColumns = getAvailableColumns(columns, filters, "filter"); + const baseAvailableColumns = useMemo( + () => getAvailableColumns(columns, filters, "filter"), + [columns, filters] + ); + + const filteredColumns = useMemo(() => { + if (!searchQuery) return baseAvailableColumns; + + return baseAvailableColumns.filter((column) => + parseColumnName(column.name) + .toLowerCase() + .includes(searchQuery.toLowerCase()) + ); + }, [baseAvailableColumns, searchQuery]); + + const handleSearch = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + setSelectedIndex(0); // Reset selection when search changes + }; + + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + setSelectedIndex((prev) => + prev < filteredColumns.length - 1 ? prev + 1 : prev + ); + break; + case "ArrowUp": + event.preventDefault(); + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev)); + break; + case "Enter": + event.preventDefault(); + if (filteredColumns[selectedIndex]) { + setFilter(filteredColumns[selectedIndex].name); + } + break; + } + }; + + // Ensure selected item is visible in viewport + useEffect(() => { + const selectedElement = document.getElementById( + `column-option-${selectedIndex}` + ); + if (selectedElement) { + selectedElement.scrollIntoView({ block: "nearest" }); + } + }, [selectedIndex]); + + const displayText = filter.isNew + ? "Select column" + : parseColumnName(fieldName); return ( @@ -44,7 +102,7 @@ export function FieldSelector({ className="w-[150px] justify-start truncate whitespace-nowrap font-normal [&_span]:max-w-full [&_span]:truncate" > - {parseColumnName(fieldName)}{" "} + {displayText} @@ -54,10 +112,35 @@ export function FieldSelector({ "z-[999999] mt-2 max-h-[400px] overflow-scroll rounded-md border border-gray-200 bg-white" )} > - {availableColumns.map((column, index) => ( +
+ + +
+ {filteredColumns.map((column, index) => (
setFilter(column.name)} > @@ -68,6 +151,11 @@ export function FieldSelector({
))} + {filteredColumns.length === 0 && ( +
+ No columns found +
+ )}
diff --git a/app/components/assets/assets-index/advanced-filters/schema.ts b/app/components/assets/assets-index/advanced-filters/schema.ts index db6ec8635..95cef23c6 100644 --- a/app/components/assets/assets-index/advanced-filters/schema.ts +++ b/app/components/assets/assets-index/advanced-filters/schema.ts @@ -58,6 +58,7 @@ export const filterSchema = z arrayValueSchema, ]), fieldType: z.nativeEnum(CustomFieldType).optional(), + isNew: z.boolean().optional(), }) .refine( (data) => { diff --git a/app/components/assets/assets-index/advanced-filters/value.client.validator.tsx b/app/components/assets/assets-index/advanced-filters/value.client.validator.tsx index cf6f3d19a..d3b3ad23b 100644 --- a/app/components/assets/assets-index/advanced-filters/value.client.validator.tsx +++ b/app/components/assets/assets-index/advanced-filters/value.client.validator.tsx @@ -118,10 +118,12 @@ export function useFilterFormValidation( ); const haveFiltersChanged = JSON.stringify(initialFilters) !== JSON.stringify(filters); + const hasNewFilters = filters.some((filter) => filter.isNew); return { isValid: !hasInvalidFilters, - canApplyFilters: !hasInvalidFilters && haveFiltersChanged, + canApplyFilters: + !hasInvalidFilters && haveFiltersChanged && !hasNewFilters, hasChanges: haveFiltersChanged, }; }; diff --git a/package-lock.json b/package-lock.json index 31a61a108..c7462cc94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@zxing/library": "^0.20.0", "changedpi": "^1.0.4", "chardet": "^2.0.0", + "cmdk": "^1.0.4", "cookie": "^0.7.0", "crisp-sdk-web": "^1.0.21", "csv-parse": "^5.5.3", @@ -52,6 +53,7 @@ "jszip": "^3.10.1", "lodash": "^4.17.21", "lottie-react": "^2.4.0", + "lucide-react": "^0.462.0", "luxon": "^3.4.4", "mapbox-gl": "^3.1.2", "maplibre-gl": "^4.0.0", @@ -9475,6 +9477,21 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -14598,6 +14615,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.462.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", + "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/luxon": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", @@ -21825,6 +21850,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/package.json b/package.json index 34984f252..2db40ef3d 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "jszip": "^3.10.1", "lodash": "^4.17.21", "lottie-react": "^2.4.0", + "lucide-react": "^0.462.0", "luxon": "^3.4.4", "mapbox-gl": "^3.1.2", "maplibre-gl": "^4.0.0",