attribute sorting

This commit is contained in:
Jurgis Sakalauskas
2025-10-15 09:51:18 +03:00
parent 3f81dc3e94
commit 5caeb68f3e
3 changed files with 88 additions and 40 deletions
+57 -21
View File
@@ -218,35 +218,67 @@ const normalizeArrayMembership = (entries: AttributeEntry[]): AttributeEntry[] =
});
};
const computeTrackedKeySet = (attributes: string[]): Set<string> =>
new Set(
attributes
.map((attribute) => attribute.trim().toLowerCase())
.filter((attribute) => attribute.length)
);
interface TrackedKeyMetadata {
set: Set<string>;
order: Map<string, number>;
}
const isTrackedKey = (key: string, trackedKeys: Set<string>): boolean => {
const computeTrackedKeyMetadata = (attributes: string[]): TrackedKeyMetadata => {
const order = new Map<string, number>();
attributes
.map((attribute) => attribute.trim())
.filter((attribute) => attribute.length)
.forEach((attribute, index) => {
const normalized = attribute.toLowerCase();
if (!order.has(normalized)) {
order.set(normalized, index);
}
});
return {
set: new Set(order.keys()),
order
};
};
const isTrackedKey = (key: string, metadata: TrackedKeyMetadata): boolean => {
if (!key) {
return false;
}
return trackedKeys.has(key.trim().toLowerCase());
return metadata.set.has(key.trim().toLowerCase());
};
const sortAttributeEntries = (
entries: AttributeEntry[],
trackedKeys: Set<string>
trackedMetadata: TrackedKeyMetadata
): AttributeEntry[] => {
const decorated = entries.map((entry, index) => ({
entry,
index,
isTracked: isTrackedKey(entry.key, trackedKeys)
isTracked: isTrackedKey(entry.key, trackedMetadata)
}));
decorated.sort((a, b) => {
if (a.isTracked === b.isTracked) {
if (a.isTracked && b.isTracked) {
const keyA = a.entry.key.trim().toLowerCase();
const keyB = b.entry.key.trim().toLowerCase();
const orderA = trackedMetadata.order.get(keyA);
const orderB = trackedMetadata.order.get(keyB);
if (orderA !== undefined && orderB !== undefined && orderA !== orderB) {
return orderA - orderB;
}
const keyComparison = keyA.localeCompare(keyB);
if (keyComparison !== 0) {
return keyComparison;
}
return a.index - b.index;
}
return a.isTracked ? -1 : 1;
if (a.isTracked) {
return -1;
}
if (b.isTracked) {
return 1;
}
return a.index - b.index;
});
return decorated.map(({ entry }) => entry);
@@ -254,8 +286,9 @@ const sortAttributeEntries = (
const normalizeAndSortEntries = (
entries: AttributeEntry[],
trackedKeys: Set<string>
): AttributeEntry[] => sortAttributeEntries(normalizeArrayMembership(entries), trackedKeys);
trackedMetadata: TrackedKeyMetadata
): AttributeEntry[] =>
sortAttributeEntries(normalizeArrayMembership(entries), trackedMetadata);
export function ItemFormModal({
isOpen,
@@ -277,18 +310,18 @@ export function ItemFormModal({
const [attributeEntries, setAttributeEntries] = useState<AttributeEntry[]>([]);
const [error, setError] = useState<string | null>(null);
const isFormDisabled = isSubmitting || isInitialDataLoading;
const trackedAttributeSet = useMemo(
() => computeTrackedKeySet(projectAttributes),
const trackedAttributeMetadata = useMemo(
() => computeTrackedKeyMetadata(projectAttributes),
[projectAttributes]
);
const updateAttributeEntries = useCallback(
(updater: (previous: AttributeEntry[]) => AttributeEntry[]) => {
setAttributeEntries((previous) =>
normalizeAndSortEntries(updater(previous), trackedAttributeSet)
normalizeAndSortEntries(updater(previous), trackedAttributeMetadata)
);
},
[trackedAttributeSet]
[trackedAttributeMetadata]
);
useEffect(() => {
@@ -336,9 +369,12 @@ export function ItemFormModal({
});
setAttributeEntries(
normalizeAndSortEntries([...trackedEntries, ...additionalEntries], trackedAttributeSet)
normalizeAndSortEntries(
[...trackedEntries, ...additionalEntries],
trackedAttributeMetadata
)
);
}, [isOpen, initialUrl, initialData, projectAttributes, trackedAttributeSet]);
}, [isOpen, initialUrl, initialData, projectAttributes, trackedAttributeMetadata]);
useEffect(() => {
if (!isOpen) {
@@ -623,7 +659,7 @@ export function ItemFormModal({
trimmedValue === inference.canonicalText
? null
: inference.canonicalText;
const isTracked = isTrackedKey(attribute.key, trackedAttributeSet);
const isTracked = isTrackedKey(attribute.key, trackedAttributeMetadata);
return (
<div
@@ -1,5 +1,14 @@
import { useEffect, useMemo, useState } from 'react';
const normalizeSelection = (attributes: string[]): string[] =>
Array.from(
new Set(
attributes
.map((attribute) => attribute.trim())
.filter((attribute) => attribute.length > 0)
)
).sort((a, b) => a.localeCompare(b));
export interface TrackedAttributesModalProps {
isOpen: boolean;
availableAttributes: string[];
@@ -19,13 +28,15 @@ export function TrackedAttributesModal({
isSubmitting,
errorMessage
}: TrackedAttributesModalProps) {
const [selectedAttributes, setSelectedAttributes] = useState<string[]>(initialSelection);
const [selectedAttributes, setSelectedAttributes] = useState<string[]>(() =>
normalizeSelection(initialSelection)
);
useEffect(() => {
if (!isOpen) {
return;
}
setSelectedAttributes(initialSelection);
setSelectedAttributes(normalizeSelection(initialSelection));
}, [isOpen, initialSelection]);
const sortedAttributes = useMemo(
@@ -38,21 +49,16 @@ export function TrackedAttributesModal({
}
const toggleAttribute = (attribute: string) => {
setSelectedAttributes((current) =>
current.includes(attribute)
? current.filter((value) => value !== attribute)
: [...current, attribute]
);
setSelectedAttributes((current) => {
if (current.includes(attribute)) {
return normalizeSelection(current.filter((value) => value !== attribute));
}
return normalizeSelection([...current, attribute]);
});
};
const handleSubmit = async () => {
const normalized = Array.from(
new Set(
selectedAttributes
.map((attribute) => attribute.trim())
.filter((attribute) => attribute.length > 0)
)
);
const normalized = normalizeSelection(selectedAttributes);
await onSubmit(normalized);
};
@@ -41,14 +41,20 @@ export function ProjectDetailPage() {
const project = projectQuery.data;
const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]);
const projectAttributes = useMemo(() => project?.attributes ?? [], [project]);
const projectAttributes = useMemo(() => {
if (!project?.attributes) {
return [];
}
const normalized = project.attributes
.map((attribute) => attribute.trim())
.filter((attribute) => attribute.length);
return Array.from(new Set(normalized)).sort((a, b) => a.localeCompare(b));
}, [project]);
const availableAttributes = useMemo(() => {
const attributeSet = new Set<string>();
projectAttributes.forEach((attribute) => {
if (attribute.trim().length) {
attributeSet.add(attribute.trim());
}
attributeSet.add(attribute);
});
items.forEach((item) => {
@@ -61,7 +67,7 @@ export function ProjectDetailPage() {
});
});
return Array.from(attributeSet);
return Array.from(attributeSet).sort((a, b) => a.localeCompare(b));
}, [projectAttributes, items]);
const openItemModal = (mode: 'url' | 'manual' | 'edit', initialUrl: string | null) => {