feat: Add 3dfilamentprofiles.com integration (Issue #7)
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
- Backend: Added ExternalSource enum, Filament3DFP model, and transform function - Backend: Updated _sync() to fetch from both SpoolmanDB and 3dfilamentprofiles - Frontend: Added source tabs to FilamentImportModal (All/SpoolmanDB/3DFP) - Frontend: Updated ExternalFilament interface with source and temp range fields - Frontend: Updated filament import handler to use temp range fields Also includes: - Dev mode styling (teal theme, DEV badge and banner) - Translation keys for source filtering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -276,6 +276,8 @@
|
||||
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
|
||||
"import_external": "Import from External",
|
||||
"import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.<br><br>A Manufacturer object will be created automatically when you click Ok, if necessary.",
|
||||
"all_sources": "All Sources",
|
||||
"select_filament": "Select a filament...",
|
||||
"created_success": "Created filament \"{{name}}\"",
|
||||
"vendor_placeholder": "Select a manufacturer",
|
||||
"name_placeholder": "E.g. Matte Black",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Form, Modal, Select } from "antd";
|
||||
import { Form, Modal, Select, Tabs } from "antd";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { formatFilamentLabel } from "../pages/spools/functions";
|
||||
import { searchMatches } from "../utils/filtering";
|
||||
import { ExternalFilament, useGetExternalDBFilaments } from "../utils/queryExternalDB";
|
||||
import { ExternalFilament, ExternalSource, useGetExternalDBFilaments } from "../utils/queryExternalDB";
|
||||
|
||||
export function FilamentImportModal(props: {
|
||||
isOpen: boolean;
|
||||
@@ -12,12 +13,25 @@ export function FilamentImportModal(props: {
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const t = useTranslate();
|
||||
const [selectedSource, setSelectedSource] = useState<ExternalSource | "all">("all");
|
||||
|
||||
const externalFilaments = useGetExternalDBFilaments();
|
||||
const filamentOptions =
|
||||
externalFilaments.data?.map((item) => {
|
||||
|
||||
// Filter and format filaments based on selected source
|
||||
const filamentOptions = useMemo(() => {
|
||||
const filtered = externalFilaments.data?.filter((item) => {
|
||||
if (selectedSource === "all") return true;
|
||||
return item.source === selectedSource;
|
||||
}) ?? [];
|
||||
|
||||
const options = filtered.map((item) => {
|
||||
// Add source indicator to label if showing all
|
||||
const sourceIndicator = selectedSource === "all" && item.source === ExternalSource.FILAMENT_PROFILES_3D
|
||||
? "[3DFP] "
|
||||
: "";
|
||||
|
||||
return {
|
||||
label: formatFilamentLabel(
|
||||
label: sourceIndicator + formatFilamentLabel(
|
||||
item.name,
|
||||
item.diameter,
|
||||
item.manufacturer,
|
||||
@@ -28,8 +42,23 @@ export function FilamentImportModal(props: {
|
||||
value: item.id,
|
||||
item: item,
|
||||
};
|
||||
}) ?? [];
|
||||
filamentOptions.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
||||
});
|
||||
|
||||
options.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
||||
return options;
|
||||
}, [externalFilaments.data, selectedSource]);
|
||||
|
||||
// Count filaments by source
|
||||
const counts = useMemo(() => {
|
||||
const spoolmandb = externalFilaments.data?.filter(f => f.source === ExternalSource.SPOOLMANDB || !f.source).length ?? 0;
|
||||
const threeDFP = externalFilaments.data?.filter(f => f.source === ExternalSource.FILAMENT_PROFILES_3D).length ?? 0;
|
||||
return { spoolmandb, threeDFP, total: spoolmandb + threeDFP };
|
||||
}, [externalFilaments.data]);
|
||||
|
||||
const handleSourceChange = (key: string) => {
|
||||
setSelectedSource(key as ExternalSource | "all");
|
||||
form.resetFields(["filament"]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -37,7 +66,36 @@ export function FilamentImportModal(props: {
|
||||
open={props.isOpen}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={() => props.onClose()}
|
||||
width={600}
|
||||
>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={"filament.form.import_external_description"}
|
||||
components={{
|
||||
br: <br />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Tabs
|
||||
activeKey={selectedSource}
|
||||
onChange={handleSourceChange}
|
||||
items={[
|
||||
{
|
||||
key: "all",
|
||||
label: `${t("filament.form.all_sources")} (${counts.total})`,
|
||||
},
|
||||
{
|
||||
key: ExternalSource.SPOOLMANDB,
|
||||
label: `SpoolmanDB (${counts.spoolmandb})`,
|
||||
},
|
||||
{
|
||||
key: ExternalSource.FILAMENT_PROFILES_3D,
|
||||
label: `3D Filament Profiles (${counts.threeDFP})`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
@@ -49,21 +107,15 @@ export function FilamentImportModal(props: {
|
||||
props.onImport(filament);
|
||||
props.onClose();
|
||||
form.resetFields();
|
||||
setSelectedSource("all");
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={"filament.form.import_external_description"}
|
||||
components={{
|
||||
br: <br />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<Form.Item name="filament" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={filamentOptions}
|
||||
showSearch
|
||||
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
|
||||
placeholder={t("filament.form.select_filament")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ThemedLayout, ThemedSider, ThemedTitle } from "@refinedev/antd";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Button } from "antd";
|
||||
import { Badge, Button } from "antd";
|
||||
import { Footer } from "antd/es/layout/layout";
|
||||
import Logo from "../icon.svg?react";
|
||||
import { getBasePath } from "../utils/url";
|
||||
import { Header } from "./header";
|
||||
import { Version } from "./version";
|
||||
|
||||
// DEV VERSION indicator
|
||||
const IS_DEV = true;
|
||||
|
||||
const SpoolmanFooter: React.FC = () => {
|
||||
const t = useTranslate();
|
||||
|
||||
@@ -50,17 +53,50 @@ interface SpoolmanLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DevTitle: React.FC<{ collapsed: boolean }> = ({ collapsed }) => (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<ThemedTitle collapsed={collapsed} text="Spoolman" icon={<Logo />} />
|
||||
{IS_DEV && !collapsed && (
|
||||
<Badge
|
||||
count="DEV"
|
||||
style={{
|
||||
backgroundColor: "#0891b2",
|
||||
fontSize: 10,
|
||||
fontWeight: "bold",
|
||||
marginLeft: -8,
|
||||
marginTop: -20,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SpoolmanLayout: React.FC<SpoolmanLayoutProps> = ({ children }) => (
|
||||
<ThemedLayout
|
||||
Header={() => <Header sticky />}
|
||||
Sider={() => (
|
||||
<ThemedSider
|
||||
fixed
|
||||
Title={({ collapsed }) => <ThemedTitle collapsed={collapsed} text="Spoolman" icon={<Logo />} />}
|
||||
Title={({ collapsed }) => <DevTitle collapsed={collapsed} />}
|
||||
/>
|
||||
)}
|
||||
Footer={() => <SpoolmanFooter />}
|
||||
>
|
||||
{IS_DEV && (
|
||||
<div
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #0891b2 0%, #06b6d4 50%, #0891b2 100%)",
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
padding: "4px 0",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 1,
|
||||
}}
|
||||
>
|
||||
🚀 DEV BUILD - Connected to Unraid PostgreSQL
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 1400,
|
||||
|
||||
@@ -41,7 +41,9 @@ export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({ children
|
||||
theme={{
|
||||
algorithm: mode === "light" ? defaultAlgorithm : darkAlgorithm,
|
||||
token: {
|
||||
colorPrimary: "#dc7734",
|
||||
// DEV VERSION: Teal/cyan theme to distinguish from production orange
|
||||
colorPrimary: "#0891b2",
|
||||
colorInfo: "#0891b2",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -127,9 +127,9 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
||||
multi_color_hexes: filament.color_hexes?.join(",") || undefined,
|
||||
multi_color_direction: filament.multi_color_direction,
|
||||
settings_extruder_temp_min: filament.extruder_temp || undefined,
|
||||
settings_extruder_temp_max: filament.extruder_temp || undefined,
|
||||
settings_extruder_temp_max: filament.extruder_temp_max || filament.extruder_temp || undefined,
|
||||
settings_bed_temp_min: filament.bed_temp || undefined,
|
||||
settings_bed_temp_max: filament.bed_temp || undefined,
|
||||
settings_bed_temp_max: filament.bed_temp_max || filament.bed_temp || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Create, useForm } from "@refinedev/antd";
|
||||
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
|
||||
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import dayjs from "dayjs";
|
||||
@@ -33,7 +33,6 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.spool);
|
||||
const currency = useCurrency();
|
||||
const invalidate = useInvalidate();
|
||||
const [isFilamentModalOpen, setIsFilamentModalOpen] = useState(false);
|
||||
|
||||
const { form, formProps, formLoading, onFinish, redirect } = useForm<
|
||||
@@ -82,14 +81,12 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
||||
internalSelectOptions,
|
||||
externalSelectOptions,
|
||||
allExternalFilaments,
|
||||
refetch: refetchFilaments,
|
||||
} = useGetFilamentSelectOptions();
|
||||
|
||||
const handleFilamentCreated = async (filament: IFilament) => {
|
||||
// Invalidate filament cache so the select options refresh
|
||||
await invalidate({
|
||||
resource: "filament",
|
||||
invalidates: ["list"],
|
||||
});
|
||||
// Refetch filament list so the select options include the new filament
|
||||
await refetchFilaments();
|
||||
// Select the newly created filament
|
||||
form.setFieldValue("filament_id", filament.id);
|
||||
setIsFilamentModalOpen(false);
|
||||
|
||||
@@ -190,6 +190,7 @@ export function useGetFilamentSelectOptions() {
|
||||
internalSelectOptions: filamentSelectInternal,
|
||||
externalSelectOptions: filamentSelectExternal,
|
||||
allExternalFilaments: externalFilaments.data,
|
||||
refetch: internalFilaments.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,25 +22,33 @@ export enum Pattern {
|
||||
SPARKLE = "sparkle",
|
||||
}
|
||||
|
||||
export enum ExternalSource {
|
||||
SPOOLMANDB = "spoolmandb",
|
||||
FILAMENT_PROFILES_3D = "3dfilamentprofiles",
|
||||
}
|
||||
|
||||
export interface ExternalFilament {
|
||||
id: string;
|
||||
manufacturer: string;
|
||||
name: string;
|
||||
material: string;
|
||||
density: number;
|
||||
weight: number;
|
||||
weight?: number;
|
||||
spool_weight?: number;
|
||||
spool_type?: SpoolType;
|
||||
diameter: number;
|
||||
color_hex?: string;
|
||||
color_hexes?: string[];
|
||||
extruder_temp?: number;
|
||||
extruder_temp_max?: number;
|
||||
bed_temp?: number;
|
||||
bed_temp_max?: number;
|
||||
finish?: Finish;
|
||||
multi_color_direction?: MultiColorDirection;
|
||||
pattern?: Pattern;
|
||||
translucent: boolean;
|
||||
glow: boolean;
|
||||
source?: ExternalSource;
|
||||
}
|
||||
|
||||
export interface ExternalMaterial {
|
||||
|
||||
Reference in New Issue
Block a user