mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 06:09:56 -05:00
fix: minor adjustments in regards of import redirects and authors
This commit is contained in:
1 parent
589a5f464a
commit
f187b9f25e
9 files changed
+86
-15
No files matched your search
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ImportJob" ADD COLUMN "resultPrintId" TEXT;
|
||||
@@ -210,6 +210,13 @@ model ImportJob {
|
||||
failedCount Int @default(0)
|
||||
errorMessage String?
|
||||
resultCollectionId String?
|
||||
// Set whenever this job finished having created exactly one Print, whatever its type -- a ZIP
|
||||
// job (a single link that needed the zip-entry picker), or a COLLECTION job (MakerWorld/
|
||||
// Thingiverse/Printables) that happened to succeed on just one item despite being a batch import.
|
||||
// See importJobRunner.ts's various runners. ImportJobContext uses it to redirect straight into
|
||||
// that one print's details page; a job with more than one result instead sends the viewer to the
|
||||
// models grid, and a COLLECTION job's own resultCollectionId grouping still applies either way.
|
||||
resultPrintId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -382,6 +382,10 @@ export type ImportJobOut = {
|
||||
failed_count: number;
|
||||
error_message: string | null;
|
||||
result_collection_id: string | null;
|
||||
// Set whenever this job finished having created exactly one Print, whatever its type -- see
|
||||
// importJobRunner.ts. Lets the frontend redirect straight into that print's details page instead
|
||||
// of the models grid.
|
||||
result_print_id: string | null;
|
||||
};
|
||||
|
||||
export function toImportJobOut(job: ImportJob): ImportJobOut {
|
||||
@@ -399,6 +403,7 @@ export function toImportJobOut(job: ImportJob): ImportJobOut {
|
||||
failed_count: job.failedCount,
|
||||
error_message: job.errorMessage,
|
||||
result_collection_id: job.resultCollectionId,
|
||||
result_print_id: job.resultPrintId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ export async function runCollectionImportJob(jobId: string, userId: string, body
|
||||
status: "DONE",
|
||||
sourceLabel: collectionTitle,
|
||||
resultCollectionId,
|
||||
// Only when this is unambiguous -- see resultPrintId's doc comment on the schema. A batch
|
||||
// job that happens to succeed on just one item still deserves "open that one model" over
|
||||
// "open the (now single-item) collection it also landed in".
|
||||
resultPrintId: successPrintIds.length === 1 ? successPrintIds[0] : null,
|
||||
processed,
|
||||
imported,
|
||||
alreadyInLibrary,
|
||||
@@ -239,6 +243,7 @@ async function runThingiverseThingsImportJob(
|
||||
status: "DONE",
|
||||
sourceLabel: collectionTitle,
|
||||
resultCollectionId,
|
||||
resultPrintId: successPrintIds.length === 1 ? successPrintIds[0] : null,
|
||||
processed,
|
||||
imported,
|
||||
alreadyInLibrary,
|
||||
@@ -365,6 +370,7 @@ export async function runPrintablesCollectionImportJob(jobId: string, userId: st
|
||||
status: "DONE",
|
||||
sourceLabel: collectionTitle,
|
||||
resultCollectionId,
|
||||
resultPrintId: successPrintIds.length === 1 ? successPrintIds[0] : null,
|
||||
processed,
|
||||
imported,
|
||||
alreadyInLibrary,
|
||||
@@ -431,6 +437,8 @@ export async function runZipImportJob(jobId: string, userId: string, body: ZipIm
|
||||
processed: body.entries.length,
|
||||
imported: prints.length,
|
||||
failedCount: failed.length,
|
||||
// Only when this is unambiguous -- see resultPrintId's doc comment on the schema.
|
||||
resultPrintId: prints.length === 1 ? prints[0].id : null,
|
||||
});
|
||||
void createLog({
|
||||
userId,
|
||||
|
||||
@@ -47,6 +47,11 @@ export type ImportJob = {
|
||||
failed_count: number;
|
||||
error_message: string | null;
|
||||
result_collection_id: string | null;
|
||||
// Set only for a "ZIP" job (a single link that turned out to need the zip-entry picker) that
|
||||
// finished having created exactly one Print -- see the backend's runZipImportJob. Used to
|
||||
// redirect into that print's edit mode, mirroring what a synchronous single-link import
|
||||
// already does (see useUploadImport's openForEditing).
|
||||
result_print_id: string | null;
|
||||
};
|
||||
|
||||
type ImportLinkPayload = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { importsApi, type ImportJob } from "../../api/imports";
|
||||
import { UnauthorizedError } from "../../api/client";
|
||||
|
||||
@@ -37,6 +38,7 @@ export function ImportJobProvider({
|
||||
onJobCompleted?: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const [activeJob, setActiveJob] = useState<ImportJob | null>(null);
|
||||
const pollRef = useRef<number | null>(null);
|
||||
const onJobCompletedRef = useRef(onJobCompleted);
|
||||
@@ -59,12 +61,29 @@ export function ImportJobProvider({
|
||||
stopPolling();
|
||||
setActiveJob(null);
|
||||
onJobCompletedRef.current?.();
|
||||
// Any finished import lands the viewer somewhere useful: exactly one resulting print (see
|
||||
// the backend's resultPrintId -- set for a "ZIP" job, or a "COLLECTION" batch job that
|
||||
// happened to succeed on just one item) opens straight to its details page, matching
|
||||
// useUploadImport's openForViewing for a synchronous single-link import. Not edit mode: an
|
||||
// import arrives with real metadata already, unlike a plain upload. More than one result
|
||||
// has no single "it" to open, so it goes to the models grid instead -- and nothing goes
|
||||
// anywhere for a job that imported nothing at all (every item failed).
|
||||
if (job.status === "DONE") {
|
||||
if (job.result_print_id) {
|
||||
navigate(`/models/${job.result_print_id}`);
|
||||
} else if (job.imported + job.already_in_library > 1) {
|
||||
// job.imported alone undercounts a batch where some items were dedup hits (already in
|
||||
// the library) rather than newly imported -- those still landed a real print each, and
|
||||
// still count toward "more than one result" the same as a fresh import would.
|
||||
navigate("/models");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
stopPolling();
|
||||
setActiveJob(null);
|
||||
if (err instanceof UnauthorizedError) onUnauthorized?.();
|
||||
}
|
||||
}, [onUnauthorized, stopPolling]);
|
||||
}, [onUnauthorized, stopPolling, navigate]);
|
||||
|
||||
const startPolling = useCallback((jobId: string) => {
|
||||
stopPolling();
|
||||
|
||||
@@ -136,13 +136,19 @@ export function useUploadImport({ onUploaded, categoryId, makerworldCookie, onUn
|
||||
} = useImportJob();
|
||||
const isBusy = uploading || importing || zipPrompt.isOpen || collectionPrompt.isOpen || importModePrompt.isOpen;
|
||||
|
||||
// Sends the user straight into editing a just-created model (per spec: after an upload
|
||||
// finishes, open it and drop into edit mode so the rest of its details can be filled in) --
|
||||
// reuses the same `?edit=<id>` URL param ModelActionsMenu's own "Edit" menu item drives.
|
||||
// Sends the user straight into editing a just-uploaded model (per spec: a plain upload has no
|
||||
// title/notes/tags/category yet, so drop into edit mode immediately to fill them in) -- reuses
|
||||
// the same `?edit=<id>` URL param ModelActionsMenu's own "Edit" menu item drives.
|
||||
const openForEditing = (print: Print) => {
|
||||
navigate(`/models/${print.id}?edit=${print.id}`);
|
||||
};
|
||||
|
||||
// A provider import, by contrast, arrives with real metadata already (title, creator, preview
|
||||
// images, ...) -- just open its details page, not the edit form.
|
||||
const openForViewing = (print: Print) => {
|
||||
navigate(`/models/${print.id}`);
|
||||
};
|
||||
|
||||
const uploadFlatAsMultiplate = async (files: File[]) => {
|
||||
try {
|
||||
const result = await printsApi.upload(files, { category_id: categoryId || undefined, mode: "multiplate" });
|
||||
@@ -386,7 +392,7 @@ export function useUploadImport({ onUploaded, categoryId, makerworldCookie, onUn
|
||||
const imported = await importsApi.fromLink(payload);
|
||||
showToast({ message: t("uploadBar.imported", { name: imported.title || imported.name }) });
|
||||
onUploaded();
|
||||
openForEditing(imported);
|
||||
openForViewing(imported);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -395,7 +401,7 @@ export function useUploadImport({ onUploaded, categoryId, makerworldCookie, onUn
|
||||
const imported = await importsApi.fromLink(payload);
|
||||
showToast({ message: t("uploadBar.imported", { name: imported.title || imported.name }) });
|
||||
onUploaded();
|
||||
openForEditing(imported);
|
||||
openForViewing(imported);
|
||||
return;
|
||||
}
|
||||
setImporting(false);
|
||||
@@ -406,7 +412,7 @@ export function useUploadImport({ onUploaded, categoryId, makerworldCookie, onUn
|
||||
const imported = await importsApi.fromLink(payload);
|
||||
showToast({ message: t("uploadBar.imported", { name: imported.title || imported.name }) });
|
||||
onUploaded();
|
||||
openForEditing(imported);
|
||||
openForViewing(imported);
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedError) {
|
||||
onUnauthorized?.();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
|
||||
// Display info for a Print's import source (Print.source_provider from the backend -- see
|
||||
// importService.ts's identifySourceModel). Same brand colors UserMenu already uses for its
|
||||
// MakerWorld connection chip, kept here so the model card badge and "Open in {Provider}" menu
|
||||
@@ -6,8 +8,9 @@
|
||||
// `color`/`textColor` are plain sx palette-token strings (e.g. "text.primary",
|
||||
// "background.paper") for "thingport", or fixed hex brand colors for everything else -- both
|
||||
// forms resolve the same way through MUI's sx `bgcolor`/`color`, so call sites don't need to
|
||||
// care which kind a given provider uses.
|
||||
export type ImportProviderInfo = { label: string; color: string; textColor?: string };
|
||||
// care which kind a given provider uses. `textColor` may also be an sx-style theme callback,
|
||||
// for "thingport"'s dark-mode override below.
|
||||
export type ImportProviderInfo = { label: string; color: string; textColor?: string | ((theme: Theme) => string) };
|
||||
|
||||
export const IMPORT_PROVIDER_INFO: Record<string, ImportProviderInfo> = {
|
||||
makerworld: { label: "MakerWorld", color: "#00B800" },
|
||||
@@ -17,7 +20,14 @@ export const IMPORT_PROVIDER_INFO: Record<string, ImportProviderInfo> = {
|
||||
// only ever comes from printProviderInfo's fallback below, ID never stored this literally).
|
||||
// Deliberately monochrome (the app's own heading-text/panel colors, inverted) rather than a
|
||||
// brand hue, so it reads as "this is us" instead of competing with the real provider badges.
|
||||
thingport: { label: "Thingport", color: "text.primary", textColor: "background.paper" },
|
||||
// Dark mode overrides to a flat white instead of following background.paper (a dark navy
|
||||
// there) -- text.primary's dark-mode chip background is a muted grey, not the near-black light
|
||||
// mode gets, so paper's own dark tone reads as too low-contrast against it.
|
||||
thingport: {
|
||||
label: "Thingport",
|
||||
color: "text.primary",
|
||||
textColor: (theme) => (theme.palette.mode === "dark" ? "#FFFFFF" : theme.palette.background.paper),
|
||||
},
|
||||
};
|
||||
|
||||
/** Looks up a KNOWN external provider only -- null for anything else, including a plain upload
|
||||
|
||||
@@ -24,7 +24,6 @@ import { useToast } from "../../components/ToastProvider";
|
||||
import { importProviderInfo, printProviderInfo } from "../../constants/importProviders";
|
||||
import { authorProfileUrl } from "../../utils/authorProfileUrl";
|
||||
import { SELF_AUTHOR_ID } from "../../constants/selfAuthor";
|
||||
import { dividerBorderColor } from "../../theme";
|
||||
import ModelCard from "../ModelsPage/ModelCard";
|
||||
|
||||
const PAGE_SIZE = 24;
|
||||
@@ -231,19 +230,29 @@ export default function AuthorPage({ theme, previewMode, onUnauthorized, viewer,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ pl: 3, pr: 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "320px 1fr" },
|
||||
gap: "32px",
|
||||
alignItems: "start",
|
||||
// "stretch" (the grid default, but spelled out since the divider below depends on it)
|
||||
// -- so the shorter profile column's box is exactly as tall as the models grid next to
|
||||
// it, and its border-right runs the full height instead of stopping wherever the
|
||||
// profile column's own (usually much shorter) content ends.
|
||||
alignItems: "stretch",
|
||||
pt: 3,
|
||||
pb: 3
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
borderRight: { xs: "none", md: "1px solid" },
|
||||
borderColor: dividerBorderColor,
|
||||
// Unlike every other light-mode-only border in the app (see dividerBorderColor),
|
||||
// this one stays visible in dark mode too -- the same plain `divider` token the
|
||||
// sidebar's own Download/Administration separator renders unconditionally, rather
|
||||
// than the sidebar's own outer edge (which dividerBorderColor hides in dark mode).
|
||||
borderColor: "divider",
|
||||
pr: { xs: 0, md: 4 },
|
||||
}}
|
||||
>
|
||||
@@ -321,7 +330,7 @@ export default function AuthorPage({ theme, previewMode, onUnauthorized, viewer,
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Box sx={{ minWidth: 0, pt: 3, pb: 3 }}>
|
||||
<Typography variant="h6" fontWeight={700} sx={{ mb: 2 }}>{t("models:pageTitle")}</Typography>
|
||||
{items.length ? (
|
||||
<Stack spacing={2}>
|
||||
|
||||
Reference in new issue
Block a user