feat: Add color column with filter to spool list
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Add optional color column to spool list showing filament color swatches with multi-select filtering capability. Backend: - GET /api/v1/color endpoint to list distinct filament colors - filament.color_hex query param on spool list for filtering Frontend: - ColorFilterColumn component renders color swatches - useSpoolmanColors() hook fetches available colors - Filter dropdown shows swatches with hex codes - Column hidden by default (enable via column selector) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,7 @@
|
||||
"filament_internal": "Internal",
|
||||
"filament_external": "External",
|
||||
"price": "Price",
|
||||
"color": "Color",
|
||||
"material": "Material",
|
||||
"weight_to_use": "Weight",
|
||||
"used_weight": "Used Weight",
|
||||
|
||||
@@ -220,6 +220,52 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
|
||||
return Column({ ...props, filters, filteredValue, onFilterDropdownOpen, loadingFilters: query.isLoading });
|
||||
}
|
||||
|
||||
interface ColorFilterColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
|
||||
filterValueQuery: UseQueryResult<ColumnFilterItem[], unknown>;
|
||||
colorGetter: (record: Obj) => string | undefined;
|
||||
}
|
||||
|
||||
export function ColorFilterColumn<Obj extends Entity>(props: ColorFilterColumnProps<Obj>) {
|
||||
const query = props.filterValueQuery;
|
||||
|
||||
let filters: ColumnFilterItem[] = [];
|
||||
if (query.data) {
|
||||
filters = query.data;
|
||||
}
|
||||
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? (props.id as keyof Obj));
|
||||
|
||||
const onFilterDropdownOpen = () => {
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
return Column({
|
||||
...props,
|
||||
filters,
|
||||
filteredValue,
|
||||
onFilterDropdownOpen,
|
||||
loadingFilters: query.isLoading,
|
||||
allowMultipleFilters: true,
|
||||
render: (_: unknown, record: Obj) => {
|
||||
const colorHex = props.colorGetter(record);
|
||||
if (!colorHex) return null;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "1.5em",
|
||||
height: "1.5em",
|
||||
backgroundColor: "#" + colorHex,
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--swatch-border-color, rgba(68, 68, 68, 0.3))",
|
||||
}}
|
||||
title={"#" + colorHex}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
|
||||
unit: string;
|
||||
maxDecimals?: number;
|
||||
|
||||
@@ -203,3 +203,36 @@ export function useSpoolmanLocations(enabled: boolean = false) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanColors(enabled: boolean = false) {
|
||||
return useQuery<string[], unknown, ColumnFilterItem[]>({
|
||||
enabled: enabled,
|
||||
queryKey: ["colors"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(getAPIURL() + "/color");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
return data.map((colorHex) => ({
|
||||
text: (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
backgroundColor: "#" + colorHex,
|
||||
borderRadius: "2px",
|
||||
border: "1px solid var(--swatch-border-color, rgba(68, 68, 68, 0.3))",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontFamily: "monospace", fontSize: "0.85em" }}>#{colorHex}</span>
|
||||
</div>
|
||||
),
|
||||
value: colorHex,
|
||||
}));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useNavigate } from "react-router";
|
||||
import {
|
||||
Action,
|
||||
ActionsColumn,
|
||||
ColorFilterColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
FilteredQueryColumn,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
} from "../../components/column";
|
||||
import { useLiveify } from "../../components/liveify";
|
||||
import {
|
||||
useSpoolmanColors,
|
||||
useSpoolmanFilamentFilter,
|
||||
useSpoolmanLocations,
|
||||
useSpoolmanLotNumbers,
|
||||
@@ -48,6 +50,7 @@ interface ISpoolCollapsed extends ISpool {
|
||||
"filament.combined_name": string; // Eg. "Prusa - PLA Red"
|
||||
"filament.id": number;
|
||||
"filament.material"?: string;
|
||||
"filament.color_hex"?: string;
|
||||
}
|
||||
|
||||
function collapseSpool(element: ISpool): ISpoolCollapsed {
|
||||
@@ -65,6 +68,7 @@ function collapseSpool(element: ISpool): ISpoolCollapsed {
|
||||
"filament.combined_name": filament_name,
|
||||
"filament.id": element.filament.id,
|
||||
"filament.material": element.filament.material,
|
||||
"filament.color_hex": element.filament.color_hex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,6 +76,7 @@ function translateColumnI18nKey(columnName: string): string {
|
||||
columnName = columnName.replace(".", "_");
|
||||
if (columnName === "filament_combined_name") columnName = "filament_name";
|
||||
else if (columnName === "filament_material") columnName = "material";
|
||||
else if (columnName === "filament_color_hex") columnName = "color";
|
||||
return `spool.fields.${columnName}`;
|
||||
}
|
||||
|
||||
@@ -80,6 +85,7 @@ const namespace = "spoolList-v2";
|
||||
const allColumns: (keyof ISpoolCollapsed & string)[] = [
|
||||
"id",
|
||||
"filament.combined_name",
|
||||
"filament.color_hex",
|
||||
"filament.material",
|
||||
"price",
|
||||
"used_weight",
|
||||
@@ -94,7 +100,7 @@ const allColumns: (keyof ISpoolCollapsed & string)[] = [
|
||||
"comment",
|
||||
];
|
||||
const defaultColumns = allColumns.filter(
|
||||
(column_id) => ["registered", "used_length", "remaining_length", "lot_nr"].indexOf(column_id) === -1
|
||||
(column_id) => ["registered", "used_length", "remaining_length", "lot_nr", "filament.color_hex"].indexOf(column_id) === -1
|
||||
);
|
||||
|
||||
export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
@@ -367,6 +373,14 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
dataId: "filament.combined_name",
|
||||
filterValueQuery: useSpoolmanFilamentFilter(),
|
||||
}),
|
||||
ColorFilterColumn({
|
||||
...commonProps,
|
||||
id: "filament.color_hex",
|
||||
i18nkey: "spool.fields.color",
|
||||
filterValueQuery: useSpoolmanColors(),
|
||||
colorGetter: (record: ISpoolCollapsed) => record.filament.color_hex,
|
||||
width: 60,
|
||||
}),
|
||||
FilteredQueryColumn({
|
||||
...commonProps,
|
||||
id: "filament.material",
|
||||
|
||||
Reference in New Issue
Block a user