mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
attribute sorting
This commit is contained in:
@@ -218,35 +218,67 @@ const normalizeArrayMembership = (entries: AttributeEntry[]): AttributeEntry[] =
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const computeTrackedKeySet = (attributes: string[]): Set<string> =>
|
interface TrackedKeyMetadata {
|
||||||
new Set(
|
set: Set<string>;
|
||||||
attributes
|
order: Map<string, number>;
|
||||||
.map((attribute) => attribute.trim().toLowerCase())
|
}
|
||||||
.filter((attribute) => attribute.length)
|
|
||||||
);
|
|
||||||
|
|
||||||
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) {
|
if (!key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return trackedKeys.has(key.trim().toLowerCase());
|
return metadata.set.has(key.trim().toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortAttributeEntries = (
|
const sortAttributeEntries = (
|
||||||
entries: AttributeEntry[],
|
entries: AttributeEntry[],
|
||||||
trackedKeys: Set<string>
|
trackedMetadata: TrackedKeyMetadata
|
||||||
): AttributeEntry[] => {
|
): AttributeEntry[] => {
|
||||||
const decorated = entries.map((entry, index) => ({
|
const decorated = entries.map((entry, index) => ({
|
||||||
entry,
|
entry,
|
||||||
index,
|
index,
|
||||||
isTracked: isTrackedKey(entry.key, trackedKeys)
|
isTracked: isTrackedKey(entry.key, trackedMetadata)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
decorated.sort((a, b) => {
|
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.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);
|
return decorated.map(({ entry }) => entry);
|
||||||
@@ -254,8 +286,9 @@ const sortAttributeEntries = (
|
|||||||
|
|
||||||
const normalizeAndSortEntries = (
|
const normalizeAndSortEntries = (
|
||||||
entries: AttributeEntry[],
|
entries: AttributeEntry[],
|
||||||
trackedKeys: Set<string>
|
trackedMetadata: TrackedKeyMetadata
|
||||||
): AttributeEntry[] => sortAttributeEntries(normalizeArrayMembership(entries), trackedKeys);
|
): AttributeEntry[] =>
|
||||||
|
sortAttributeEntries(normalizeArrayMembership(entries), trackedMetadata);
|
||||||
|
|
||||||
export function ItemFormModal({
|
export function ItemFormModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -277,18 +310,18 @@ export function ItemFormModal({
|
|||||||
const [attributeEntries, setAttributeEntries] = useState<AttributeEntry[]>([]);
|
const [attributeEntries, setAttributeEntries] = useState<AttributeEntry[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const isFormDisabled = isSubmitting || isInitialDataLoading;
|
const isFormDisabled = isSubmitting || isInitialDataLoading;
|
||||||
const trackedAttributeSet = useMemo(
|
const trackedAttributeMetadata = useMemo(
|
||||||
() => computeTrackedKeySet(projectAttributes),
|
() => computeTrackedKeyMetadata(projectAttributes),
|
||||||
[projectAttributes]
|
[projectAttributes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateAttributeEntries = useCallback(
|
const updateAttributeEntries = useCallback(
|
||||||
(updater: (previous: AttributeEntry[]) => AttributeEntry[]) => {
|
(updater: (previous: AttributeEntry[]) => AttributeEntry[]) => {
|
||||||
setAttributeEntries((previous) =>
|
setAttributeEntries((previous) =>
|
||||||
normalizeAndSortEntries(updater(previous), trackedAttributeSet)
|
normalizeAndSortEntries(updater(previous), trackedAttributeMetadata)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[trackedAttributeSet]
|
[trackedAttributeMetadata]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -336,9 +369,12 @@ export function ItemFormModal({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setAttributeEntries(
|
setAttributeEntries(
|
||||||
normalizeAndSortEntries([...trackedEntries, ...additionalEntries], trackedAttributeSet)
|
normalizeAndSortEntries(
|
||||||
|
[...trackedEntries, ...additionalEntries],
|
||||||
|
trackedAttributeMetadata
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}, [isOpen, initialUrl, initialData, projectAttributes, trackedAttributeSet]);
|
}, [isOpen, initialUrl, initialData, projectAttributes, trackedAttributeMetadata]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@@ -623,7 +659,7 @@ export function ItemFormModal({
|
|||||||
trimmedValue === inference.canonicalText
|
trimmedValue === inference.canonicalText
|
||||||
? null
|
? null
|
||||||
: inference.canonicalText;
|
: inference.canonicalText;
|
||||||
const isTracked = isTrackedKey(attribute.key, trackedAttributeSet);
|
const isTracked = isTrackedKey(attribute.key, trackedAttributeMetadata);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
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 {
|
export interface TrackedAttributesModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
availableAttributes: string[];
|
availableAttributes: string[];
|
||||||
@@ -19,13 +28,15 @@ export function TrackedAttributesModal({
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
errorMessage
|
errorMessage
|
||||||
}: TrackedAttributesModalProps) {
|
}: TrackedAttributesModalProps) {
|
||||||
const [selectedAttributes, setSelectedAttributes] = useState<string[]>(initialSelection);
|
const [selectedAttributes, setSelectedAttributes] = useState<string[]>(() =>
|
||||||
|
normalizeSelection(initialSelection)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedAttributes(initialSelection);
|
setSelectedAttributes(normalizeSelection(initialSelection));
|
||||||
}, [isOpen, initialSelection]);
|
}, [isOpen, initialSelection]);
|
||||||
|
|
||||||
const sortedAttributes = useMemo(
|
const sortedAttributes = useMemo(
|
||||||
@@ -38,21 +49,16 @@ export function TrackedAttributesModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleAttribute = (attribute: string) => {
|
const toggleAttribute = (attribute: string) => {
|
||||||
setSelectedAttributes((current) =>
|
setSelectedAttributes((current) => {
|
||||||
current.includes(attribute)
|
if (current.includes(attribute)) {
|
||||||
? current.filter((value) => value !== attribute)
|
return normalizeSelection(current.filter((value) => value !== attribute));
|
||||||
: [...current, attribute]
|
}
|
||||||
);
|
return normalizeSelection([...current, attribute]);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const normalized = Array.from(
|
const normalized = normalizeSelection(selectedAttributes);
|
||||||
new Set(
|
|
||||||
selectedAttributes
|
|
||||||
.map((attribute) => attribute.trim())
|
|
||||||
.filter((attribute) => attribute.length > 0)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await onSubmit(normalized);
|
await onSubmit(normalized);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,20 @@ export function ProjectDetailPage() {
|
|||||||
|
|
||||||
const project = projectQuery.data;
|
const project = projectQuery.data;
|
||||||
const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.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 availableAttributes = useMemo(() => {
|
||||||
const attributeSet = new Set<string>();
|
const attributeSet = new Set<string>();
|
||||||
|
|
||||||
projectAttributes.forEach((attribute) => {
|
projectAttributes.forEach((attribute) => {
|
||||||
if (attribute.trim().length) {
|
attributeSet.add(attribute);
|
||||||
attributeSet.add(attribute.trim());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
items.forEach((item) => {
|
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]);
|
}, [projectAttributes, items]);
|
||||||
|
|
||||||
const openItemModal = (mode: 'url' | 'manual' | 'edit', initialUrl: string | null) => {
|
const openItemModal = (mode: 'url' | 'manual' | 'edit', initialUrl: string | null) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user