Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions client/src/components/column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface Action {

interface BaseColumnProps<Obj extends Entity> {
id: string | string[];
dataId?: keyof Obj & string;
dataId?: keyof Obj & string | string; // Allow string values for custom fields
i18ncat?: string;
i18nkey?: string;
title?: string;
Expand Down Expand Up @@ -389,13 +389,56 @@ export function NumberRangeColumn<Obj extends Entity>(props: NumberColumnProps<O
});
}

// Helper function to create filter items for custom fields
function createCustomFieldFilters(field: Field): ColumnFilterItem[] {
const filters: ColumnFilterItem[] = [];

// For choice fields, add each choice as a filter option
if (field.field_type === FieldType.choice && field.choices) {
field.choices.forEach(choice => {
filters.push({
text: choice,
value: `"${choice}"`, // Exact match
});
});
}

// For boolean fields, add true/false options
if (field.field_type === FieldType.boolean) {
filters.push(
{ text: "Yes", value: "true" },
{ text: "No", value: "false" }
);
}

// Add empty option for all field types
filters.push({
text: "<empty>",
value: "<empty>",
});

return filters;
}

export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProps<Obj>, "id"> & { field: Field }) {
const field = props.field;
const fieldId = `extra.${field.key}`;

// Get filtered values for this field
const typedFilters = typeFilters<Obj>(props.tableState.filters);
const filteredValue = getFiltersForField(typedFilters, fieldId);

// Create filters based on field type
const filters = createCustomFieldFilters(field);

const commonProps = {
...props,
id: ["extra", field.key],
title: field.name,
sorter: false,
sorter: true, // Enable sorting for custom fields
dataId: fieldId, // Set the dataId for sorting
filters: filters, // Add filters
filteredValue: filteredValue, // Set filtered values
transform: (value: unknown) => {
if (value === null || value === undefined) {
return undefined;
Expand Down
21 changes: 21 additions & 0 deletions client/src/components/dataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { DataProvider } from "@refinedev/core";
import { axiosInstance } from "@refinedev/simple-rest";
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
import { getCustomFieldFilters } from "../utils/filtering";
import { isCustomField } from "../utils/queryFields";
import { getCustomFieldSorters, isCustomFieldSorter } from "../utils/sorting";

type MethodTypes = "get" | "delete" | "head" | "options";
type MethodTypesWithBody = "post" | "put" | "patch";
Expand All @@ -25,20 +28,30 @@ const dataProvider = (
}

if (sorters && sorters.length > 0) {
// Map all sorters, including custom field sorters
queryParams["sort"] = sorters
.map((sort) => {
const field = sort.field;
// Custom field sorters are already in the correct format (extra.field_key)
return `${field}:${sort.order}`;
})
.join(",");
}

if (filters && filters.length > 0) {
// Process regular filters
filters.forEach((filter) => {
if (!("field" in filter)) {
throw Error("Filter must be a LogicalFilter.");
}

const field = filter.field;

// Skip custom fields, they'll be handled separately
if (typeof field === 'string' && isCustomField(field)) {
return;
}

if (filter.value.length > 0) {
const filterValueArray = Array.isArray(filter.value) ? filter.value : [filter.value];

Expand All @@ -54,6 +67,14 @@ const dataProvider = (
queryParams[field] = filterValue;
}
});

// Process custom field filters
const customFieldFilters = getCustomFieldFilters(filters);
Object.entries(customFieldFilters).forEach(([key, values]) => {
if (values.length > 0) {
queryParams[`extra.${key}`] = values.join(",");
}
});
}

const { data, headers } = await httpClient[requestMethod](`${url}`, {
Expand Down
81 changes: 78 additions & 3 deletions client/src/utils/filtering.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { CrudFilter, CrudOperators } from "@refinedev/core";
import { Field, FieldType, getCustomFieldKey, isCustomField } from "./queryFields";

interface TypedCrudFilter<Obj> {
field: keyof Obj;
field: keyof Obj | string;
operator: Exclude<CrudOperators, "or" | "and">;
value: string[];
}
Expand All @@ -16,9 +17,9 @@ export function typeFilters<Obj>(filters: CrudFilter[]): TypedCrudFilter<Obj>[]
* @param field The field to get the filter values for.
* @returns An array of filter values for the given field.
*/
export function getFiltersForField<Obj, Field extends keyof Obj>(
export function getFiltersForField<Obj>(
filters: TypedCrudFilter<Obj>[],
field: Field,
field: Field | string,
): string[] {
const filterValues: string[] = [];
filters.forEach((filter) => {
Expand All @@ -29,6 +30,80 @@ export function getFiltersForField<Obj, Field extends keyof Obj>(
return filterValues;
}

/**
* Creates a filter value for a custom field based on its type
* @param field The custom field definition
* @param value The value to filter by
* @returns The formatted filter value
*/
export function formatCustomFieldFilterValue(field: Field, value: any): string {
switch (field.field_type) {
case FieldType.text:
case FieldType.choice:
// For text and choice fields, we can use the value directly
// If it's an exact match, surround with quotes
if (typeof value === "string" && !value.startsWith('"') && !value.endsWith('"')) {
// Check if we need an exact match (no wildcards)
if (!value.includes("*") && !value.includes("?")) {
return `"${value}"`;
}
}
return value;

case FieldType.integer:
case FieldType.float:
// For numeric fields, we can use the value directly
return value.toString();

case FieldType.boolean:
// For boolean fields, convert to "true" or "false"
return value ? "true" : "false";

case FieldType.datetime:
// For datetime fields, format as ISO string
if (value instanceof Date) {
return value.toISOString();
}
return value;

case FieldType.integer_range:
case FieldType.float_range:
// For range fields, format as min:max
if (Array.isArray(value) && value.length === 2) {
return `${value[0] ?? ""}:${value[1] ?? ""}`;
}
return value;

default:
return value;
}
}

/**
* Extracts all custom field filters from a list of filters
* @param filters The list of filters
* @returns An object with custom field keys and their filter values
*/
export function getCustomFieldFilters<Obj = any>(
filters: CrudFilter[] | TypedCrudFilter<Obj>[]
): Record<string, string[]> {
const customFieldFilters: Record<string, string[]> = {};

filters.forEach((filter) => {
if (!("field" in filter)) {
return; // Skip non-field filters
}

const field = filter.field.toString();
if (isCustomField(field)) {
const key = getCustomFieldKey(field);
customFieldFilters[key] = filter.value as string[];
}
});

return customFieldFilters;
}

/**
* Function that returns an array with all undefined values removed.
*/
Expand Down
18 changes: 18 additions & 0 deletions client/src/utils/queryFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ export function useSetField(entity_type: EntityType) {
});
}

/**
* Checks if a field is a custom field (starts with "extra.")
* @param field The field to check
* @returns True if the field is a custom field
*/
export function isCustomField(field: string): boolean {
return field.startsWith("extra.");
}

/**
* Extracts the key from a custom field (removes the "extra." prefix)
* @param field The custom field
* @returns The key of the custom field
*/
export function getCustomFieldKey(field: string): string {
return field.substring(6); // Remove "extra." prefix
}

export function useDeleteField(entity_type: EntityType) {
const queryClient = useQueryClient();

Expand Down
37 changes: 34 additions & 3 deletions client/src/utils/sorting.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { CrudSort } from "@refinedev/core";
import { SortOrder } from "antd/es/table/interface";
import { getCustomFieldKey, isCustomField } from "./queryFields";

interface TypedCrudSort<Obj> {
field: keyof Obj;
field: keyof Obj | string;
order: "asc" | "desc";
}

Expand All @@ -12,9 +13,9 @@ interface TypedCrudSort<Obj> {
* @param field The field to get the sort order for.
* @returns The sort order for the given field, or undefined if the field is not being sorted.
*/
export function getSortOrderForField<Obj, Field extends keyof Obj>(
export function getSortOrderForField<Obj>(
sorters: TypedCrudSort<Obj>[],
field: Field,
field: Field | string,
): SortOrder | undefined {
const sorter = sorters.find((s) => s.field === field);
if (sorter) {
Expand All @@ -26,3 +27,33 @@ export function getSortOrderForField<Obj, Field extends keyof Obj>(
export function typeSorters<Obj>(sorters: CrudSort[]): TypedCrudSort<Obj>[] {
return sorters as TypedCrudSort<Obj>[]; // <-- Unsafe cast
}

/**
* Checks if a sorter is for a custom field
* @param sorter The sorter to check
* @returns True if the sorter is for a custom field
*/
export function isCustomFieldSorter<Obj = any>(sorter: TypedCrudSort<Obj> | CrudSort): boolean {
return typeof sorter.field === 'string' && isCustomField(sorter.field);
}

/**
* Extracts all custom field sorters from a list of sorters
* @param sorters The list of sorters
* @returns An object with custom field keys and their sort orders
*/
export function getCustomFieldSorters<Obj = any>(
sorters: TypedCrudSort<Obj>[] | CrudSort[]
): Record<string, "asc" | "desc"> {
const customFieldSorters: Record<string, "asc" | "desc"> = {};

sorters.forEach((sorter) => {
if (isCustomFieldSorter(sorter)) {
const field = sorter.field.toString();
const key = getCustomFieldKey(field);
customFieldSorters[key] = sorter.order;
}
});

return customFieldSorters;
}
12 changes: 11 additions & 1 deletion spoolman/api/v1/filament.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from typing import Annotated

from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
from fastapi import APIRouter, Depends, Query, Request, WebSocket, WebSocketDisconnect
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, field_validator, model_validator
Expand Down Expand Up @@ -201,6 +201,7 @@ def prevent_none(cls: type["FilamentUpdateParameters"], v: float | None) -> floa
)
async def find(
*,
request: Request,
db: Annotated[AsyncSession, Depends(get_db_session)],
vendor_name_old: Annotated[
str | None,
Expand Down Expand Up @@ -342,6 +343,14 @@ async def find(
else:
filter_by_ids = None

# Extract custom field filters from query parameters
extra_field_filters = {}
query_params = request.query_params
for key, value in query_params.items():
if key.startswith("extra."):
field_key = key[6:] # Remove "extra." prefix
extra_field_filters[field_key] = value

db_items, total_count = await filament.find(
db=db,
ids=filter_by_ids,
Expand All @@ -351,6 +360,7 @@ async def find(
material=material,
article_number=article_number,
external_id=external_id,
extra_field_filters=extra_field_filters if extra_field_filters else None,
sort_by=sort_by,
limit=limit,
offset=offset,
Expand Down
12 changes: 11 additions & 1 deletion spoolman/api/v1/spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime
from typing import Annotated

from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
from fastapi import APIRouter, Depends, Query, Request, WebSocket, WebSocketDisconnect
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, field_validator
Expand Down Expand Up @@ -127,6 +127,7 @@ class SpoolMeasureParameters(BaseModel):
)
async def find(
*,
request: Request,
db: Annotated[AsyncSession, Depends(get_db_session)],
filament_name_old: Annotated[
str | None,
Expand Down Expand Up @@ -285,6 +286,14 @@ async def find(
else:
filament_vendor_ids = None

# Extract custom field filters from query parameters
extra_field_filters = {}
query_params = request.query_params
for key, value in query_params.items():
if key.startswith("extra."):
field_key = key[6:] # Remove "extra." prefix
extra_field_filters[field_key] = value

db_items, total_count = await spool.find(
db=db,
filament_name=filament_name if filament_name is not None else filament_name_old,
Expand All @@ -295,6 +304,7 @@ async def find(
location=location,
lot_nr=lot_nr,
allow_archived=allow_archived,
extra_field_filters=extra_field_filters if extra_field_filters else None,
sort_by=sort_by,
limit=limit,
offset=offset,
Expand Down
Loading
Loading