From f93bfc7db4de24d07d03d30cfa51d988a87f76da Mon Sep 17 00:00:00 2001 From: Tautvydas Derzinskas Date: Mon, 7 Sep 2026 22:24:24 +0200 Subject: [PATCH] fix: minor fixes in UI --- backend/src/routes/auth.ts | 15 ++++- frontend/index.html | 6 ++ frontend/src/components/Layout/AppLayout.tsx | 1 - frontend/src/components/Layout/TopBar.tsx | 18 ++--- frontend/src/components/Wordmark.tsx | 3 +- frontend/src/i18n/locales/en.json | 5 -- frontend/src/i18n/locales/lt.json | 5 -- .../LibraryPage/Sidebar/FolderTreeRow.tsx | 11 +--- .../src/pages/LibraryPage/Sidebar/index.tsx | 65 +------------------ frontend/src/theme.ts | 11 +++- 10 files changed, 43 insertions(+), 97 deletions(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 7cf1822..3c9c569 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -62,12 +62,25 @@ router.post( asyncHandler(async (req, res) => { const body = parseBody(loginSchema, req.body); const email = body.email.toLowerCase(); - const user = await prisma.user.findUnique({ where: { email } }); + let user = await prisma.user.findUnique({ where: { email } }); // Same generic message whether the email doesn't exist or the password is wrong -- don't // let a login attempt be used to enumerate registered addresses. if (!user || !(await bcrypt.compare(body.password, user.passwordHash))) { throw new HttpError(401, "Invalid email or password"); } + + // Mirrors /register's bootstrap exception: if this account registered as the initial-admin + // address before INITIAL_ADMIN_EMAIL was actually visible to the process (a common ordering + // issue -- e.g. a compose .env added after the container was first created), it would be + // stuck as MEMBER forever since role is otherwise only ever set at creation. Promote it here + // too, under the same "only while no admin exists yet" guard. + if (user.role !== "ADMIN" && Boolean(INITIAL_ADMIN_EMAIL) && email === INITIAL_ADMIN_EMAIL) { + const adminExists = (await prisma.user.count({ where: { role: "ADMIN" } })) > 0; + if (!adminExists) { + user = await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } }); + } + } + const token = createToken(user.id, user.role); res.json({ token, expires_in: AUTH_TOKEN_TTL, user: toUserOut(user) }); }), diff --git a/frontend/index.html b/frontend/index.html index 4b63060..858711a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,6 +5,12 @@ PrintStash + + +
diff --git a/frontend/src/components/Layout/AppLayout.tsx b/frontend/src/components/Layout/AppLayout.tsx index f3f9deb..2cecbca 100644 --- a/frontend/src/components/Layout/AppLayout.tsx +++ b/frontend/src/components/Layout/AppLayout.tsx @@ -102,7 +102,6 @@ export default function AppLayout({ - {showAddMenu && ( - - )} + ; - visibleFolderIds: Set | null; expanded: Set; dropTargetId: string | null; selectedId: string | null | undefined; - query: string; untitledLabel: string; collapseLabel: string; expandLabel: string; @@ -43,13 +41,10 @@ type Props = { /** One row of the recursive folder tree, plus (via a nested Collapse + List) its own * children rendered as more FolderTreeRow instances. */ export default function FolderTreeRow({ folder, depth, ctx }: Props) { - const { visibleFolderIds, childrenMap, expanded, dropTargetId, selectedId, query, untitledLabel } = ctx; - if (visibleFolderIds && !visibleFolderIds.has(folder.id)) return null; - const children = (childrenMap[folder.id] || []).filter( - child => !visibleFolderIds || visibleFolderIds.has(child.id) - ); + const { childrenMap, expanded, dropTargetId, selectedId, untitledLabel } = ctx; + const children = childrenMap[folder.id] || []; const isSelected = selectedId === folder.id; - const isOpen = !!query.trim() || expanded.has(folder.id); + const isOpen = expanded.has(folder.id); const isDropTarget = dropTargetId === folder.id; return ( diff --git a/frontend/src/pages/LibraryPage/Sidebar/index.tsx b/frontend/src/pages/LibraryPage/Sidebar/index.tsx index c4da87d..b0943b8 100644 --- a/frontend/src/pages/LibraryPage/Sidebar/index.tsx +++ b/frontend/src/pages/LibraryPage/Sidebar/index.tsx @@ -5,8 +5,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; -import TextField from "@mui/material/TextField"; -import InputAdornment from "@mui/material/InputAdornment"; import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; @@ -16,8 +14,6 @@ import Divider from "@mui/material/Divider"; import Collapse from "@mui/material/Collapse"; import Tooltip from "@mui/material/Tooltip"; import AddIcon from "@mui/icons-material/Add"; -import SearchIcon from "@mui/icons-material/Search"; -import CloseIcon from "@mui/icons-material/Close"; import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import EditIcon from "@mui/icons-material/Edit"; import LayersIcon from "@mui/icons-material/Layers"; @@ -129,7 +125,6 @@ export default function Sidebar({ const [editParent, setEditParent] = useState(null); const [dropTargetId, setDropTargetId] = useState(null); const [dropUploading, setDropUploading] = useState(false); - const [query, setQuery] = useState(""); const [menuState, setMenuState] = useState<{ folderId: string; anchorEl: HTMLElement } | null>(null); const zipPrompt = useZipImportPrompt(); const importModePrompt = useImportModePrompt(); @@ -452,24 +447,6 @@ export default function Sidebar({ return map; }, [folders]); - const visibleFolderIds = React.useMemo(() => { - const normalizedQuery = query.trim().toLocaleLowerCase(); - if (!normalizedQuery) return null; - - const visible = new Set(); - folders.forEach(folder => { - if (!folderPath(folder).toLocaleLowerCase().includes(normalizedQuery)) return; - let current: Folder | undefined = folder; - const guard = new Set(); - while (current && !guard.has(current.id)) { - visible.add(current.id); - guard.add(current.id); - current = current.parent_id ? folderById[current.parent_id] : undefined; - } - }); - return visible; - }, [folderById, folderPath, folders, query]); - // Default-expand root folders so they are visible useEffect(() => { const roots = childrenMap["__root"] || []; @@ -494,11 +471,9 @@ export default function Sidebar({ const treeCtx: FolderTreeContext = { childrenMap, - visibleFolderIds, expanded, dropTargetId, selectedId, - query, untitledLabel, collapseLabel: t("sidebar.collapse"), expandLabel: t("sidebar.expand"), @@ -587,8 +562,8 @@ export default function Sidebar({ {!collapsed && ( <> - - {t("sidebar.foldersHeadingWithCount", { count: visibleFolderIds ? visibleFolderIds.size : folders.length })} + + {t("sidebar.foldersHeadingWithCount", { count: folders.length })} )} - {!!folders.length && visibleFolderIds?.size === 0 && ( - - - {t("sidebar.noMatchingFolders")} - - - )} diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index 5894bb9..9b1fbf7 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -5,6 +5,12 @@ export type { ResolvedTheme }; export const THEME_IDS: ResolvedTheme[] = ["light", "dark"]; +const BODY_FONT_STACK = + '"Open Sans", "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif'; +/** The brand/display face used for h5/h6 (page and section titles) and the Wordmark -- kept as + * one exported constant so both stay in sync. */ +export const DISPLAY_FONT_STACK = '"PrintStash Display", sans-serif'; + /** * Extra design tokens MUI's Theme doesn't model natively: the page background (a plain color * for light/dark, a layered radial-gradient CSS value for the neon/purple/blue themes) and the @@ -112,9 +118,10 @@ export function buildTheme(id: ResolvedTheme): Theme { shape: { borderRadius: 10 }, typography: { fontSize: 13, + fontFamily: BODY_FONT_STACK, button: { textTransform: "none", fontWeight: 600 }, - h5: { fontFamily: '"PrintStash Display", sans-serif' }, - h6: { fontFamily: '"PrintStash Display", sans-serif' }, + h5: { fontFamily: DISPLAY_FONT_STACK }, + h6: { fontFamily: DISPLAY_FONT_STACK }, }, components: { MuiPaper: { styleOverrides: { root: { backgroundImage: "none", backgroundColor: d.panel } } },