Skip to content

Commit 992a00e

Browse files
committed
fix: unified search param for filament modal + Ruff violations
- Use single debounced ?search= call, consistent with spool modal and PR846 - Remove 4-field parallel fetch loop, getAPIURL import, allSearchResults state - Fix Ruff violations in vendor.py and vendor_logos.py
1 parent 6dbb827 commit 992a00e

4 files changed

Lines changed: 35 additions & 51 deletions

File tree

client/src/pages/printing/filamentSelectModal.tsx

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@ function collapseFilament(element: IFilament): IFilamentCollapsed {
2424
return { ...element, "vendor.name": element.vendor?.name ?? null };
2525
}
2626

27-
// Keep the quick search local to the currently loaded page instead of changing the server-side query contract.
28-
function matchesSearch(filament: IFilamentCollapsed, searchTerm: string): boolean {
29-
const needle = searchTerm.trim().toLowerCase();
30-
if (needle.length === 0) {
31-
return true;
32-
}
33-
34-
const haystacks = [String(filament.id), filament["vendor.name"] ?? "", filament.name ?? "", filament.material ?? ""];
35-
return haystacks.some((value) => value.toLowerCase().includes(needle));
36-
}
37-
3827
const MIN_TABLE_SCROLL_Y = 180;
3928
const TABLE_BOTTOM_GAP = 16;
4029

@@ -43,9 +32,9 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
4332
const [selectedItems, setSelectedItems] = useState<number[]>([]);
4433
const [messageApi, contextHolder] = message.useMessage();
4534
const navigate = useNavigate();
46-
const [searchValue, setSearchValue] = useState("");
47-
const [serverSearchValue, setServerSearchValue] = useState("");
4835
const [tableScrollY, setTableScrollY] = useState<number>(300);
36+
const [searchTerm, setSearchTerm] = useState("");
37+
const [debouncedSearch, setDebouncedSearch] = useState("");
4938
const rootRef = useRef<HTMLDivElement | null>(null);
5039
const tableContainerRef = useRef<HTMLDivElement | null>(null);
5140

@@ -54,7 +43,7 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
5443
resource: "filament",
5544
meta: {
5645
queryParams: {
57-
...(serverSearchValue.trim().length > 0 ? { search: serverSearchValue.trim() } : {}),
46+
...(debouncedSearch.length > 0 ? { search: debouncedSearch } : {}),
5847
},
5948
},
6049
syncWithLocation: false,
@@ -85,16 +74,9 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
8574
pagination: { currentPage, pageSize },
8675
};
8776

88-
const dataSource: IFilamentCollapsed[] = useMemo(
89-
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
90-
[tableProps.dataSource],
91-
);
92-
// Keep typing responsive by narrowing the current page immediately even while the backend query is in flight.
93-
const visibleDataSource = useMemo(
94-
() => dataSource.filter((filament) => matchesSearch(filament, searchValue)),
95-
[dataSource, searchValue],
96-
);
77+
const dataSource = [...(tableProps.dataSource ?? [])];
9778
const selectedSet = useMemo(() => new Set(selectedItems), [selectedItems]);
79+
const paginationTotal = tableProps.pagination ? (tableProps.pagination.total ?? 0) : 0;
9880

9981
useEffect(() => {
10082
const computeScrollHeight = () => {
@@ -130,8 +112,6 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
130112
resizeObserver?.disconnect();
131113
};
132114
}, []);
133-
134-
const paginationTotal = tableProps.pagination ? (tableProps.pagination.total ?? 0) : 0;
135115
const handlePageChange = useCallback(
136116
(page: number, nextPageSize?: number) => {
137117
if (typeof nextPageSize === "number" && nextPageSize !== pageSize) {
@@ -145,17 +125,22 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
145125
setPageSize(size);
146126
setCurrentPage(1);
147127
}, []);
148-
const applySearchFilter = useCallback((nextSearch: string) => {
149-
setServerSearchValue(nextSearch.trim());
150-
setCurrentPage(1);
151-
}, []);
152128

153-
// Bulk toggles only touch the rows currently visible after search and paging.
129+
// Debounce search input to avoid excessive API calls while typing
130+
useEffect(() => {
131+
const timer = setTimeout(() => {
132+
setDebouncedSearch(searchTerm.trim());
133+
setCurrentPage(1);
134+
}, 300);
135+
return () => clearTimeout(timer);
136+
}, [searchTerm, setCurrentPage]);
137+
138+
// Bulk toggles only touch the rows currently visible after paging and server-side filtering.
154139
const selectUnselectFiltered = useCallback(
155140
(select: boolean) => {
156141
setSelectedItems((prevSelected) => {
157142
const nextSelected = new Set(prevSelected);
158-
visibleDataSource.forEach((filament) => {
143+
dataSource.forEach((filament) => {
159144
if (select) {
160145
nextSelected.add(filament.id);
161146
} else {
@@ -165,7 +150,7 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
165150
return Array.from(nextSelected);
166151
});
167152
},
168-
[visibleDataSource],
153+
[dataSource],
169154
);
170155

171156
const handleSelectItem = useCallback((item: number) => {
@@ -174,10 +159,9 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
174159
);
175160
}, []);
176161

177-
const isAllFilteredSelected =
178-
visibleDataSource.length > 0 && visibleDataSource.every((filament) => selectedSet.has(filament.id));
162+
const isAllFilteredSelected = dataSource.length > 0 && dataSource.every((filament) => selectedSet.has(filament.id));
179163
const isSomeButNotAllFilteredSelected =
180-
visibleDataSource.some((filament) => selectedSet.has(filament.id)) && !isAllFilteredSelected;
164+
dataSource.some((filament) => selectedSet.has(filament.id)) && !isAllFilteredSelected;
181165

182166
const commonProps = {
183167
t,
@@ -227,17 +211,14 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
227211
<Col xs={24} md={12}>
228212
<Input.Search
229213
placeholder={resolvedSearchPlaceholder}
230-
value={searchValue}
214+
value={searchTerm}
231215
allowClear
232216
enterButton
233217
onChange={(event) => {
234-
const value = event.target.value;
235-
setSearchValue(value);
236-
applySearchFilter(value);
218+
setSearchTerm(event.target.value);
237219
}}
238220
onSearch={(value) => {
239-
setSearchValue(value);
240-
applySearchFilter(value);
221+
setSearchTerm(value);
241222
}}
242223
/>
243224
</Col>
@@ -246,8 +227,7 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
246227
<Col flex="none">
247228
<Button
248229
onClick={() => {
249-
setSearchValue("");
250-
setServerSearchValue("");
230+
setSearchTerm("");
251231
setFilters([], "replace");
252232
setCurrentPage(1);
253233
}}
@@ -303,7 +283,7 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
303283
rowKey="id"
304284
tableLayout="fixed"
305285
pagination={false}
306-
dataSource={visibleDataSource}
286+
dataSource={dataSource}
307287
scroll={{ y: tableScrollY, x: "max-content" }}
308288
columns={removeUndefined([
309289
{

spoolman/api/v1/vendor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class VendorLogoConvertRequest(BaseModel):
7575
@field_validator("logo_url")
7676
@classmethod
7777
def validate_logo_url(cls: type["VendorLogoConvertRequest"], value: str) -> str:
78+
"""Validate and strip the logo URL."""
7879
trimmed = value.strip()
7980
if trimmed == "":
8081
raise ValueError("Logo URL is required.")

spoolman/database/filament.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament:
9292
return filament
9393

9494

95-
async def find(
95+
async def find( # noqa: C901, PLR0912
9696
*,
9797
db: AsyncSession,
9898
ids: list[int] | None = None,

spoolman/vendor_logos.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919

2020
def get_runtime_vendor_logo_dir() -> Path:
21+
"""Return the runtime vendor logo directory, creating it if needed."""
2122
target_dir = env.get_data_dir() / "vendor-logos"
2223
target_dir.mkdir(parents=True, exist_ok=True)
2324
return target_dir
2425

2526

2627
def get_bundled_vendor_logo_dir() -> Path:
28+
"""Return the bundled vendor logo directory from the built client assets."""
2729
project_root = Path(__file__).resolve().parent.parent
2830
return project_root / "client" / "dist" / "vendor-logos"
2931

@@ -95,9 +97,7 @@ def _normalize_logo_asset_path(logo_url: str) -> str:
9597
if base_path and path.startswith(base_path + "/"):
9698
path = path[len(base_path) + 1 :]
9799

98-
normalized = path.lstrip("/")
99-
normalized = normalized.removeprefix("vendor-logos/")
100-
return normalized
100+
return path.lstrip("/").removeprefix("vendor-logos/")
101101

102102

103103
def _load_logo_source_bytes(logo_url: str) -> bytes:
@@ -106,7 +106,7 @@ def _load_logo_source_bytes(logo_url: str) -> bytes:
106106
raise ValueError("Logo URL is required.")
107107

108108
if value.startswith(("http://", "https://")):
109-
request = Request(value, headers={"User-Agent": "spoolman-vendor-logo-convert"})
109+
request = Request(value, headers={"User-Agent": "spoolman-vendor-logo-convert"}) # noqa: S310
110110
with urlopen(request, timeout=60) as response: # noqa: S310
111111
return response.read()
112112

@@ -148,6 +148,9 @@ def _update_runtime_manifest_with_generated_print_logo(print_logo_url: str) -> N
148148
json.dump(runtime_manifest, file, indent=2)
149149

150150

151+
_MONOCHROME_THRESHOLD = 180
152+
153+
151154
def convert_web_logo_to_print_logo(logo_url: str, vendor_name: str | None = None) -> str:
152155
"""Convert a web logo to grayscale PNG and store it in runtime print logo directory."""
153156
source_bytes = _load_logo_source_bytes(logo_url)
@@ -161,13 +164,13 @@ def convert_web_logo_to_print_logo(logo_url: str, vendor_name: str | None = None
161164
# Preserve transparency from the source asset while forcing the printable pixels to pure black or white.
162165
alpha = rgba.getchannel("A")
163166
grayscale = ImageOps.grayscale(rgba.convert("RGB"))
164-
monochrome = grayscale.point(lambda value: 0 if value < 180 else 255, mode="L")
167+
monochrome = grayscale.point(lambda value: 0 if value < _MONOCHROME_THRESHOLD else 255, mode="L")
165168
converted = Image.merge("RGBA", (monochrome, monochrome, monochrome, alpha))
166169

167170
parsed_logo_path = urlparse(logo_url).path if logo_url.startswith(("http://", "https://")) else logo_url
168171
source_stem = Path(parsed_logo_path).stem.replace("-web", "")
169172
slug = slugify_vendor_name(vendor_name) or slugify_vendor_name(source_stem) or f"vendor-{uuid4().hex[:8]}"
170-
source_hash = hashlib.sha1(source_bytes).hexdigest()[:10]
173+
source_hash = hashlib.sha1(source_bytes).hexdigest()[:10] # noqa: S324
171174

172175
runtime_dir = get_runtime_vendor_logo_dir()
173176
print_dir = runtime_dir / "print"

0 commit comments

Comments
 (0)