diff --git a/.editorconfig b/.editorconfig index 665a74a09a..e6bda814c1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,26 +22,15 @@ indent_size = 4 [*.html] indent_size = 2 -insert_final_newline = false -trim_trailing_whitespace = false -[{*.js,*.jsx,*.mjs,*.ts,*.tsx}] +[{*.js,*.jsx,*.mjs,*.ts,*.tsx,*.mts}] indent_size = 2 [*.css] -# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards. indent_size = 2 [*.{yml,yaml}] -# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices. -indent_size = 2 -insert_final_newline = false -trim_trailing_whitespace = false - -[*.json] -# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting. indent_size = 2 -[*.jsonc] -# JSONC (JSON with comments) files also follow the standard JSON formatting with an indent size of 2 spaces. +[*.{json,jsonc}] indent_size = 2 diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml index 015934030f..b8e15466e5 100644 --- a/.github/workflows/ai-engine.yml +++ b/.github/workflows/ai-engine.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 596be96e8c..e9a27a072d 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -197,7 +197,7 @@ jobs: - name: Install uv if: always() && matrix.flavor == 'saas' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml index 8dc4e560d6..0bd376766e 100644 --- a/.github/workflows/build-enterprise.yml +++ b/.github/workflows/build-enterprise.yml @@ -357,7 +357,7 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/check-generated-models.yml b/.github/workflows/check-generated-models.yml index fafffcc241..4428704b10 100644 --- a/.github/workflows/check-generated-models.yml +++ b/.github/workflows/check-generated-models.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/check_toml.yml b/.github/workflows/check_toml.yml index 1681546e71..9ccfc4af52 100644 --- a/.github/workflows/check_toml.yml +++ b/.github/workflows/check_toml.yml @@ -195,7 +195,7 @@ jobs: core.exportVariable("REFERENCE_FILE", referenceFilePath); - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/coverage-aggregate.yml b/.github/workflows/coverage-aggregate.yml index 61ef8793c4..87fbb24eac 100644 --- a/.github/workflows/coverage-aggregate.yml +++ b/.github/workflows/coverage-aggregate.yml @@ -55,7 +55,7 @@ jobs: distribution: "temurin" - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index 32195ac66e..54f2e6b5f6 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -70,7 +70,7 @@ jobs: sudo chmod +x /usr/local/bin/docker-compose - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 844d26a3d1..29668940c4 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -106,7 +106,7 @@ jobs: fi - name: Install uv if: always() - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/frontend-validation.yml b/.github/workflows/frontend-validation.yml index 133d940b28..0cad599a1c 100644 --- a/.github/workflows/frontend-validation.yml +++ b/.github/workflows/frontend-validation.yml @@ -107,7 +107,7 @@ jobs: } - name: Install uv if: always() - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 006143a2d3..8e1c47b412 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -872,7 +872,7 @@ jobs: # Gate publish on valid updater sigs. Runs after the review upload (so # artifacts survive for debugging) and before action-gh-release. - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0d73fbfa67..42898c4b43 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -154,7 +154,7 @@ jobs: distribution: "temurin" - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index db67ccefe6..d4271c6aad 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/sync_files_v2.yml b/.github/workflows/sync_files_v2.yml index 124c992c71..6b436ed2f6 100644 --- a/.github/workflows/sync_files_v2.yml +++ b/.github/workflows/sync_files_v2.yml @@ -53,7 +53,7 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.imgbotconfig b/.imgbotconfig index 720b938475..6a1b1bf7a2 100644 --- a/.imgbotconfig +++ b/.imgbotconfig @@ -1,5 +1,7 @@ { "ignoredFiles": [ - "frontend/editor/src-tauri/icons/icon.png" + "frontend/editor/src-tauri/icons/macos/*", + "frontend/editor/src-tauri/icons/linux/*", + "frontend/editor/src-tauri/icons/windows/*" ] } diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml index f5325c9ed7..844306824d 100644 --- a/.taskfiles/frontend.yml +++ b/.taskfiles/frontend.yml @@ -375,13 +375,13 @@ tasks: desc: "Auto-fix code formatting" deps: [install] cmds: - - npx prettier --write . + - npx oxfmt --write . format:check: desc: "Check code formatting" deps: [install] cmds: - - npx prettier --check . + - npx oxfmt --check . fix: desc: "Auto-fix lint and format" @@ -554,6 +554,7 @@ tasks: deps: [install, ":backend:swagger"] cmds: - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts + - task: format sources: - editor/scripts/generate-tool-api-types.mts - ../SwaggerDoc.json @@ -563,9 +564,9 @@ tasks: tool-models:check: desc: "Fail if committed tool API types are out of date" - deps: [install, ":backend:swagger"] cmds: - - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts --check + - task: tool-models + - git diff --exit-code -- editor/src/core/types/toolApiTypes.ts editor/src/core/types/toolIO.ts licenses:generate: desc: "Generate frontend license report" diff --git a/app/common/build.gradle b/app/common/build.gradle index 0661a51df7..bb956504cc 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -21,8 +21,8 @@ dependencies { api 'org.snakeyaml:snakeyaml-engine:3.0.1' api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3" // Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage) - api 'org.simplejavamail:simple-java-mail:9.3.1' - api 'org.simplejavamail:outlook-module:9.3.1' // MSG file support + api 'org.simplejavamail:simple-java-mail:9.3.2' + api 'org.simplejavamail:outlook-module:9.3.2' // MSG file support api 'jakarta.mail:jakarta.mail-api:2.1.5' runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5' diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index 7904156262..bfd3f53c67 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -4,7 +4,10 @@ import java.util.regex.Pattern; public class RequestUriUtils { - private static final Pattern SHARE_LINK_PATTERN = Pattern.compile("^/share/[^/]+/?$"); + // Share tokens are 36-char lowercase UUIDs (UUID.randomUUID().toString()); match exactly + private static final Pattern SHARE_LINK_PATTERN = + Pattern.compile( + "^/share/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/?$"); // Invite tokens are 36-char lowercase UUIDs (UUID.randomUUID().toString()); match exactly private static final Pattern INVITE_LINK_PATTERN = Pattern.compile( @@ -73,7 +76,7 @@ public class RequestUriUtils { // cookie, so the server can't authenticate the navigation itself). The // portal gates access via its own auth gate + RequirePortalAccess, and its // data APIs stay protected, so serving the shell pre-auth is safe. - if (normalizedUri.equals("/processor") || normalizedUri.startsWith("/processor/")) { + if ("/processor".equals(normalizedUri) || normalizedUri.startsWith("/processor/")) { return true; } diff --git a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java index 0449514747..72e5eae9a2 100644 --- a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java @@ -206,12 +206,24 @@ class RequestUriUtilsTest { @Test void testIsPublicAuthEndpoint_shareLinkTokenTrailingSlash() { - assertTrue(RequestUriUtils.isPublicAuthEndpoint("/share/abc123/", "")); + assertTrue( + RequestUriUtils.isPublicAuthEndpoint( + "/share/00dcac3a-fc7a-4989-9c4f-97745484d62f/", "")); } @Test void testIsPublicAuthEndpoint_shareLinkWithContextPath() { - assertTrue(RequestUriUtils.isPublicAuthEndpoint("/app/share/abc123", "/app")); + assertTrue( + RequestUriUtils.isPublicAuthEndpoint( + "/app/share/00dcac3a-fc7a-4989-9c4f-97745484d62f", "/app")); + } + + @Test + void testIsPublicAuthEndpoint_shareLinkWithInvalidTokenLength() { + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/share/abc123", "")); + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/share/00dcac3a-fc7a-4989-9c4f-97745484d62fa", "")); } @Test diff --git a/build.gradle b/build.gradle index 8b6dee0262..eb4b568f42 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext { openSamlVersion = "5.2.1" commonmarkVersion = "0.28.0" googleJavaFormatVersion = "1.35.0" - logback = "1.6.1" + logback = "1.6.3" commonsIoVersion = "2.22.0" commonsLang3 = "3.20.0" rhinoVersion = "1.9.1" diff --git a/frontend/.oxfmtrc.json b/frontend/.oxfmtrc.json new file mode 100644 index 0000000000..cb801651b8 --- /dev/null +++ b/frontend/.oxfmtrc.json @@ -0,0 +1,33 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf", + "sortPackageJson": false, + "ignorePatterns": [ + "dist/", + "editor/dist/", + "editor/src-tauri/**/target/", + "editor/src-tauri/gen/", + "node_modules/", + "editor/public/vendor/", + "editor/public/mockServiceWorker.js", + "editor/public/og-metadata.json", + "editor/public/og-metadata.saas.json", + "editor/src/core/data/ogImageMap.json", + "editor/src/portal/generated/docsManifest.json", + "editor/public/pdfjs*/", + "editor/public/js/thirdParty/", + "editor/public/css/cookieconsent.css", + "storybook-static/", + "playwright-report/", + "editor/playwright-report/", + "test-results/", + "editor/test-results/", + "*.min.*", + "*.md", + "*.wxs", + "*.toml", + "editor/src/output.css" + ] +} diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index fa8ab6c502..0000000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,30 +0,0 @@ -dist/ -editor/dist/ -# Tauri/Cargo build output (binary assets named *.js etc. confuse Prettier). -# Match nested target/ dirs too - provisioner/ and thumbnail-handler/ each -# have their own Cargo workspace under src-tauri/. -editor/src-tauri/**/target/ -editor/src-tauri/gen/ -node_modules/ -editor/public/vendor/ -# Auto-generated by MSW (`msw init`); regenerated verbatim, not hand-formatted. -editor/public/mockServiceWorker.js -# Auto-generated OG/social-preview metadata (scripts/generate-og-metadata.mjs); regenerated verbatim. -editor/public/og-metadata.json -editor/public/og-metadata.saas.json -editor/src/core/data/ogImageMap.json -# Auto-generated portal docs manifest (scripts/sync-portal-docs.mts); regenerated verbatim. -editor/src/portal/generated/docsManifest.json -editor/public/pdfjs*/ -editor/public/js/thirdParty/ -editor/public/css/cookieconsent.css -# Build / test artifacts that may exist locally even though they're gitignored -storybook-static/ -playwright-report/ -editor/playwright-report/ -test-results/ -editor/test-results/ -*.min.* -*.md -*.wxs -editor/src/output.css diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 58bc875631..0000000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "endOfLine": "lf" -} diff --git a/frontend/README.md b/frontend/README.md index ea759538e6..a4945ffae1 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -18,7 +18,7 @@ For desktop app development, see the [Tauri](#tauri) section below. `frontend/` is a workspace containing one or more apps. Today it holds the PDF editor under `frontend/editor/`; new apps (the developer portal, etc.) will sit alongside it as siblings. Shared tooling — `package.json`, `node_modules`, -`.storybook/`, oxlint, Prettier — lives at `frontend/` so every app installs +`.storybook/`, oxlint, oxfmt — lives at `frontend/` so every app installs once and lints with the same config. ## Environment Variables diff --git a/frontend/editor/index.html b/frontend/editor/index.html index 427b6d1449..1e782db2de 100644 --- a/frontend/editor/index.html +++ b/frontend/editor/index.html @@ -4,7 +4,10 @@ - + , - outputPath: string, -): Promise { +function renderToolIO(spec: Json, table: Record): string { if (Object.keys(table).length === 0) { throw new Error( `No ${IO_EXTENSION} declarations in the spec. The backend publishes these from @ToolIO; regenerate with 'task backend:swagger'.`, @@ -312,33 +307,12 @@ export function toolIOFor( } `; - const prettierConfig = await prettier.resolveConfig(outputPath); - return prettier.format(body, { ...prettierConfig, parser: "typescript" }); + return body; } -/** In check mode, fail when the committed file is out of date. */ -function writeOrCheck( - outputPath: string, - formatted: string, - check: boolean, - task: string, -): void { - if (check) { - let current = ""; - try { - current = readFileSync(outputPath, "utf-8"); - } catch { - // Missing file counts as out of date. - } - if (current !== formatted) { - throw new Error( - `${outputPath} is out of date. Run '${task}' and commit the result.`, - ); - } - return; - } +function writeOutput(outputPath: string, contents: string): void { mkdirSync(dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, formatted, "utf-8"); + writeFileSync(outputPath, contents, "utf-8"); } async function main(): Promise { @@ -347,12 +321,11 @@ async function main(): Promise { spec: { type: "string" }, output: { type: "string" }, "io-output": { type: "string" }, - check: { type: "boolean", default: false }, }, }); if (!values.spec || !values.output || !values["io-output"]) { throw new Error( - "Usage: generate-tool-api-types.mts --spec --output --io-output [--check]", + "Usage: generate-tool-api-types.mts --spec --output --io-output ", ); } const specPath = resolve(values.spec); @@ -478,14 +451,9 @@ async function main(): Promise { `Dropped ${dropped.length} @ToolIO declaration(s) on paths that are not tool endpoints. Add the namespace to ALLOWED_PATH_PREFIXES if a pipeline can contain these steps:\n ${dropped.join("\n ")}`, ); } - writeOrCheck( - ioOutputPath, - await renderToolIO(spec, ioDeclarations, ioOutputPath), - values.check ?? false, - "task frontend:tool-models", - ); + writeOutput(ioOutputPath, renderToolIO(spec, ioDeclarations)); console.log( - `${values.check ? "Up to date" : "Generated"}: ${Object.keys(ioDeclarations).length} tool I/O declarations.`, + `Generated ${Object.keys(ioDeclarations).length} tool I/O declarations.`, ); // Transitively inline every referenced component into `definitions`, rewriting its refs too. @@ -508,7 +476,6 @@ async function main(): Promise { definitions, fileFieldsByClass, outputPath, - values.check ?? false, skipped, ); } @@ -518,7 +485,6 @@ async function compileAndWrite( definitions: Record, fileFieldsByClass: Record, outputPath: string, - check: boolean, skipped: string[], ): Promise { // json-schema-to-typescript only emits a named, exported interface per schema @@ -597,17 +563,9 @@ async function compileAndWrite( ].join("\n"); const body = `${FILE_HEADER}\n\n${models}\n\n${footer}\n`; - const prettierConfig = await prettier.resolveConfig(outputPath); - const formatted = await prettier.format(body, { - ...prettierConfig, - parser: "typescript", - }); - - writeOrCheck(outputPath, formatted, check, "task frontend:tool-models"); - console.log( - `${check ? "Up to date" : "Generated"}: ${tools.length} tool endpoints.`, - ); - if (!check && skipped.length > 0) { + writeOutput(outputPath, body); + console.log(`Generated ${tools.length} tool endpoints.`); + if (skipped.length > 0) { console.log( `Skipped ${skipped.length} POST endpoint(s) with no request body: ${skipped.join(", ")}`, ); diff --git a/frontend/editor/src-tauri/icons/128x128.png b/frontend/editor/src-tauri/icons/128x128.png deleted file mode 100644 index 9712693d06..0000000000 Binary files a/frontend/editor/src-tauri/icons/128x128.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/16x16.png b/frontend/editor/src-tauri/icons/16x16.png deleted file mode 100644 index 6448ce5424..0000000000 Binary files a/frontend/editor/src-tauri/icons/16x16.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/192x192.png b/frontend/editor/src-tauri/icons/192x192.png deleted file mode 100644 index 55c165df4b..0000000000 Binary files a/frontend/editor/src-tauri/icons/192x192.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/32x32.png b/frontend/editor/src-tauri/icons/32x32.png deleted file mode 100644 index 1844dfceb2..0000000000 Binary files a/frontend/editor/src-tauri/icons/32x32.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/64x64.png b/frontend/editor/src-tauri/icons/64x64.png deleted file mode 100644 index d758a26bf3..0000000000 Binary files a/frontend/editor/src-tauri/icons/64x64.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/icon.icns b/frontend/editor/src-tauri/icons/icon.icns deleted file mode 100644 index 86bad6a2e1..0000000000 Binary files a/frontend/editor/src-tauri/icons/icon.icns and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/icon.png b/frontend/editor/src-tauri/icons/icon.png deleted file mode 100644 index 8abdd8ca3a..0000000000 Binary files a/frontend/editor/src-tauri/icons/icon.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/linux/app-128.png b/frontend/editor/src-tauri/icons/linux/app-128.png new file mode 100644 index 0000000000..9a87d8abb3 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-128.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-16.png b/frontend/editor/src-tauri/icons/linux/app-16.png new file mode 100644 index 0000000000..35fc507cfc Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-16.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-192.png b/frontend/editor/src-tauri/icons/linux/app-192.png new file mode 100644 index 0000000000..36fd70e987 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-192.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-32.png b/frontend/editor/src-tauri/icons/linux/app-32.png new file mode 100644 index 0000000000..c975d69987 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-32.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-512.png b/frontend/editor/src-tauri/icons/linux/app-512.png new file mode 100644 index 0000000000..b20a797a05 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-512.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-64.png b/frontend/editor/src-tauri/icons/linux/app-64.png new file mode 100644 index 0000000000..e6138f71e3 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-64.png differ diff --git a/frontend/editor/src-tauri/icons/macos/app-512.png b/frontend/editor/src-tauri/icons/macos/app-512.png new file mode 100644 index 0000000000..c5638a5270 Binary files /dev/null and b/frontend/editor/src-tauri/icons/macos/app-512.png differ diff --git a/frontend/editor/src-tauri/icons/macos/app.icns b/frontend/editor/src-tauri/icons/macos/app.icns new file mode 100644 index 0000000000..7f6dc2c6e8 Binary files /dev/null and b/frontend/editor/src-tauri/icons/macos/app.icns differ diff --git a/frontend/editor/src-tauri/icons/icon.ico b/frontend/editor/src-tauri/icons/windows/app.ico similarity index 100% rename from frontend/editor/src-tauri/icons/icon.ico rename to frontend/editor/src-tauri/icons/windows/app.ico diff --git a/frontend/editor/src-tauri/tauri.conf.json b/frontend/editor/src-tauri/tauri.conf.json index c308e3ad4e..cdd29e708f 100644 --- a/frontend/editor/src-tauri/tauri.conf.json +++ b/frontend/editor/src-tauri/tauri.conf.json @@ -29,14 +29,9 @@ "publisher": "Stirling PDF Inc.", "targets": ["deb", "rpm", "appimage", "dmg", "app", "msi"], "icon": [ - "icons/icon.png", - "icons/icon.icns", - "icons/icon.ico", - "icons/16x16.png", - "icons/32x32.png", - "icons/64x64.png", - "icons/128x128.png", - "icons/192x192.png" + "icons/windows/app.ico", + "icons/macos/app.icns", + "icons/macos/app-512.png" ], "resources": ["libs/*.jar", "runtime/jre/**/*"], "fileAssociations": [ diff --git a/frontend/editor/src-tauri/tauri.linux.conf.json b/frontend/editor/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000000..54f87a037b --- /dev/null +++ b/frontend/editor/src-tauri/tauri.linux.conf.json @@ -0,0 +1,12 @@ +{ + "bundle": { + "icon": [ + "icons/linux/app-512.png", + "icons/linux/app-192.png", + "icons/linux/app-128.png", + "icons/linux/app-64.png", + "icons/linux/app-32.png", + "icons/linux/app-16.png" + ] + } +} diff --git a/frontend/editor/src/core/components/AppLayout.tsx b/frontend/editor/src/core/components/AppLayout.tsx index 38ab1f1907..48c4cb9149 100644 --- a/frontend/editor/src/core/components/AppLayout.tsx +++ b/frontend/editor/src/core/components/AppLayout.tsx @@ -22,7 +22,7 @@ export function AppLayout({ children }: AppLayoutProps) { } `}
{banner}
{children}
diff --git a/frontend/editor/src/core/components/filesPage/FileGrid.tsx b/frontend/editor/src/core/components/filesPage/FileGrid.tsx index 7fd2eda133..40e17dc502 100644 --- a/frontend/editor/src/core/components/filesPage/FileGrid.tsx +++ b/frontend/editor/src/core/components/filesPage/FileGrid.tsx @@ -978,7 +978,9 @@ function FileCard({ )}
{fileSize} - · + {fileDate}
diff --git a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx index e87b348780..36ffb9c4dc 100644 --- a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx +++ b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx @@ -33,6 +33,10 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; import RefreshIcon from "@mui/icons-material/Refresh"; +import { FilesToolbarBulkMenu } from "@app/components/filesPage/FilesToolbarBulkMenu"; +import { FilesToolbarCount } from "@app/components/filesPage/FilesToolbarCount"; +import { FilesToolbarFilterMenu } from "@app/components/filesPage/FilesToolbarFilterMenu"; +import { FilesToolbarSortMenu } from "@app/components/filesPage/FilesToolbarSortMenu"; import { stripBasePath } from "@app/constants/app"; import { useAuth } from "@app/auth/UseSession"; @@ -893,6 +897,9 @@ export default function FileManagerView() { () => Array.from(selectedFileIds), [selectedFileIds], ); + // A phone with files selected shows a contextual selection bar instead of the + // full toolbar - five bulk buttons plus filters cannot fit the width. + const mobileSelection = isMobile && selectedFiles.length > 0; // Local-only subset of selection; drives Save-to-server visibility. const localOnlySelectedStubs = useMemo( @@ -1210,22 +1217,12 @@ export default function FileManagerView() { })()}
- - {loading - ? t("filesPage.loading", "Loading…") - : t("filesPage.summary", "{{count}} items", { - count: totalCount, - })} - {selectedFiles.length > 0 && ( - - {" "} - ·{" "} - {t("filesPage.selectedCount", "{{count}} selected", { - count: selectedFiles.length, - })} - - )} - + {(() => { // Select all / Clear toggle over visible files. if (visibleFiles.length === 0) return null; @@ -1265,289 +1262,382 @@ export default function FileManagerView() { ); })()}
- {selectedFiles.length > 0 && - (() => { - // Bulk-action labels; CSS collapses to icon-only below 900px. - const addLabel = + {mobileSelection ? ( + handleAddToWorkspace(selectedFiles)} + onSaveToServer={ + localOnlySelectedStubs.length > 0 + ? () => setSaveToServerTarget(localOnlySelectedStubs) + : undefined + } + saveToServerDisabledReason={ + saveToServerDisabledReason ?? undefined + } + onShowDetails={ selectedFiles.length === 1 - ? t("filesPage.addToWorkspace", "Add to workspace") - : t( - "filesPage.addToWorkspaceCount", - "Add {{count}} to workspace", - { count: selectedFiles.length }, - ); - const moveLabel = t("filesPage.moveTo", "Move to…"); - const removeLabel = t("filesPage.remove", "Remove"); - return ( - // wrap="nowrap" keeps the row single-line. - - - - - {/* Save to server; shown whenever local-only files are + ? () => setMobileDetailsOpen(true) + : undefined + } + onMove={() => promptMoveFiles(selectedFiles)} + onRemove={() => handleRemoveFiles(selectedFiles)} + /> + ) : ( + <> + {selectedFiles.length > 0 && + (() => { + // Bulk-action labels; CSS collapses to icon-only below 900px. + const addLabel = + selectedFiles.length === 1 + ? t("filesPage.addToWorkspace", "Add to workspace") + : t( + "filesPage.addToWorkspaceCount", + "Add {{count}} to workspace", + { count: selectedFiles.length }, + ); + const moveLabel = t("filesPage.moveTo", "Move to…"); + const removeLabel = t("filesPage.remove", "Remove"); + return ( + // wrap="nowrap" keeps the row single-line. + + + + + {/* Save to server; shown whenever local-only files are selected. When storage is off it stays visible but disabled, tooltip pointing at the admin. */} - {localOnlySelectedStubs.length > 0 && ( - - + + )} + {/* Show details button on compact viewports. */} + {selectedFiles.length === 1 && + isCompactDetailsViewport && ( + + + )} - > - {t("filesPage.saveToServer", "Save to server")} - - - )} - {/* Show details button on compact viewports. */} - {selectedFiles.length === 1 && - isCompactDetailsViewport && ( - + + + + + + clearSelection()} + aria-label={t( + "filesPage.clearSelection", + "Clear selection", + )} + > + × + + + + ); + })()} + {selectedFiles.length > 0 && ( +
diff --git a/frontend/editor/src/core/components/filesPage/FilesPage.css b/frontend/editor/src/core/components/filesPage/FilesPage.css index aa9d8248aa..6d4070edb6 100644 --- a/frontend/editor/src/core/components/filesPage/FilesPage.css +++ b/frontend/editor/src/core/components/filesPage/FilesPage.css @@ -215,6 +215,17 @@ .files-page-toolbar-actions .mantine-Button-label { display: none; } +/* Exception: the bulk-actions trigger IS its label. Collapsed to an icon it + would read as a bare chevron with nothing to say what it opens. */ +.files-page-toolbar-actions + .files-page-toolbar-bulk-trigger + .mantine-Button-label { + display: inline; +} +.files-page-toolbar-actions .files-page-toolbar-bulk-trigger { + padding-left: 0.75rem; + padding-right: 0.5rem; +} /* Pin the view toggle: never let it clip off the right. flex-shrink:0 keeps its width fixed; the rest of the row shrinks around it. */ .files-page-toolbar-actions .mantine-SegmentedControl-root { @@ -476,6 +487,23 @@ gap: 0.4rem; } +/* Narrow cards: let the values wrap as whole units onto their own lines rather + than breaking mid-value ("239.26 / KB") around a stranded separator. */ +@media (max-width: 64rem) { + .files-page-card-meta { + flex-wrap: wrap; + align-items: baseline; + column-gap: 0.4rem; + row-gap: 0.05rem; + } + .files-page-card-meta > span { + white-space: nowrap; + } + .files-page-card-meta-sep { + display: none; + } +} + /* Parent-folder breadcrumb shown on cards/rows during recursive search so the user can tell which folder each hit lives in without navigating. */ .files-page-card-path { @@ -1295,30 +1323,38 @@ sits next to the Upload button without breaking the action row. */ display: none; } -@media (max-width: 900px) { +@media (max-width: 1024px) { .files-page-toolbar { /* nowrap so "7 items" + "Select all" sit on the same row as the filter dropdowns and view-toggle instead of stacking on three - separate lines. Per-child min-width:0 lets them shrink as needed. - Used to only kick in at ≤640px which left a broken zone where - both side panels were hidden but the toolbar was still wrapping - to multiple rows. */ + separate lines. Runs to the app's mobile breakpoint: capping it at + 900px left 901-1024px wrapping to two rows, which is the band the + mobile layout actually renders in. + + Scrolls rather than clips. With a selection active the bulk-action + strip cannot fit any phone width, and `overflow-x: hidden` put those + buttons permanently out of reach behind the edge. */ flex-wrap: nowrap; gap: 0.35rem; padding: 0.35rem 0.5rem; min-height: auto; - overflow-x: hidden; + overflow-x: auto; + scrollbar-width: none; + } + .files-page-toolbar::-webkit-scrollbar { + display: none; } .files-page-toolbar-info { - /* Was `flex-basis: 100%` which forced a row break. Let it share - the row, shrink hard if needed, and ellipsize so the count line - collapses gracefully (was overlapping the bulk-action buttons - at ~400px because no truncation rule existed). */ - flex: 0 1 auto; + /* The toolbar's only status text. Pinned, because against nowrap + siblings it lost every shrink round and rendered as "3 i". */ + flex-shrink: 0; min-width: 0; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + } + /* Filter and sort collapse to icon triggers here (see FilesToolbar*Menu); + they are the whole control, so they never shrink. */ + .files-page-toolbar-icon-btn { + flex-shrink: 0; } .files-page-toolbar-actions { flex-wrap: nowrap; @@ -1350,41 +1386,48 @@ navigation, so the in-header Home/Apps/Close trio is duplicated and the first to go. Same for "Upload" - the user can use the centre drop overlay. */ -@media (max-width: 640px) { - /* Drop the 3-column grid on phones; flex-wrap lets the search slip onto - * its own row when chrome is too cramped to share. */ +/* ── Mobile + tablet chrome (≤1024px = useIsMobile) ────────────────── + The desktop header is a 3-column grid whose middle track can grow to + 40rem. Below ~1024px that track eats the row: the breadcrumb column + collapsed to ~36px (wrapping "All files" to two lines) and the action + column overflowed, pushing Upload off the right edge. One flex row + instead - breadcrumb and actions keep their intrinsic width and the + search takes whatever is left. Ends at the app's mobile breakpoint so + it matches the layout HomePage is already rendering. */ +@media (max-width: 1024px) { .files-page-header { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; gap: 0.4rem; - padding: 0 0.4rem; + padding: 0.25rem 0.4rem; overflow-x: hidden; } - .files-page-header [data-mobile-hide="true"] { - display: none !important; + .files-page-header-search { + flex: 1 1 auto; + min-width: 0; + justify-content: flex-start; } - .files-page-header [data-desktop-hide="true"] { - display: inline-flex !important; - } - /* Mobile-hide for sub-toolbar create buttons. */ - .files-page-toolbar [data-mobile-hide="true"] { - display: none !important; + /* Undo the fixed 24rem basis so the pill tracks the row's spare width. */ + .files-page-header-search .super-search { + flex: 1 1 auto; + width: 100%; + max-width: none; } .files-page-header-actions { + flex: 0 0 auto; margin-left: auto; - gap: 0.3rem; + gap: 0.25rem; flex-wrap: nowrap; } .files-page-breadcrumbs { + flex: 0 1 auto; font-size: 0.85rem; flex-wrap: nowrap; overflow-x: auto; min-width: 0; } - /* Upload becomes an icon-only square button on mobile so the action - row stops getting clipped. Scoped to `.files-page-header-actions` - so the Back button at the header level keeps its visible "Back" - label (Back has no other on-screen indicator that it's about leaving). */ + /* Icon-only actions: the labels are what pushed Upload past the edge. */ .files-page-header-actions .mantine-Button-root { padding-left: 0.55rem; padding-right: 0.55rem; @@ -1395,6 +1438,9 @@ .files-page-header-actions .mantine-Button-label { display: none; } +} + +@media (max-width: 640px) { /* Grid: single column on very narrow phones; two columns from ~440px */ .files-page-grid { grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); diff --git a/frontend/editor/src/core/components/filesPage/FilesToolbarBulkMenu.tsx b/frontend/editor/src/core/components/filesPage/FilesToolbarBulkMenu.tsx new file mode 100644 index 0000000000..3e344ac7b0 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FilesToolbarBulkMenu.tsx @@ -0,0 +1,104 @@ +import { Menu } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DriveFileMoveIcon from "@mui/icons-material/DriveFileMove"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; + +import { Button } from "@app/ui/Button"; + +interface FilesToolbarBulkMenuProps { + selectedCount: number; + onAddToWorkspace: () => void; + /** Local-only files in the selection; omit when there are none to upload. */ + onSaveToServer?: () => void; + /** Set when storage is off - the item stays listed but disabled. */ + saveToServerDisabledReason?: string; + onShowDetails?: () => void; + onMove: () => void; + onRemove: () => void; +} + +/** + * Bulk actions behind one trigger. The full strip is five buttons wide, which + * no phone can hold alongside the count and the clear control, so rather than + * letting the row scroll them off the edge they collapse into a menu where + * every action keeps its label. + */ +export function FilesToolbarBulkMenu({ + selectedCount, + onAddToWorkspace, + onSaveToServer, + saveToServerDisabledReason, + onShowDetails, + onMove, + onRemove, +}: FilesToolbarBulkMenuProps) { + const { t } = useTranslation(); + + const addLabel = + selectedCount === 1 + ? t("filesPage.addToWorkspace", "Add to workspace") + : t("filesPage.addToWorkspaceCount", "Add {{count}} to workspace", { + count: selectedCount, + }); + + return ( + + + + + + } + onClick={onAddToWorkspace} + > + {addLabel} + + {onSaveToServer && ( + } + disabled={Boolean(saveToServerDisabledReason)} + onClick={onSaveToServer} + > + {t("filesPage.saveToServer", "Save to server")} + + )} + {onShowDetails && ( + } + onClick={onShowDetails} + > + {t("filesPage.showDetails", "Show details")} + + )} + } + onClick={onMove} + > + {t("filesPage.moveTo", "Move to…")} + + + } + onClick={onRemove} + > + {t("filesPage.remove", "Remove")} + + + + ); +} + +export default FilesToolbarBulkMenu; diff --git a/frontend/editor/src/core/components/filesPage/FilesToolbarCount.tsx b/frontend/editor/src/core/components/filesPage/FilesToolbarCount.tsx new file mode 100644 index 0000000000..d808638d63 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FilesToolbarCount.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next"; + +interface FilesToolbarCountProps { + loading: boolean; + totalCount: number; + selectedCount: number; + /** + * Selection-bar mode: report only the selection. A phone spends the room on + * the actions rather than on "3 items · 3 selected". + */ + selectionOnly: boolean; +} + +/** Status text at the head of the files toolbar. */ +export function FilesToolbarCount({ + loading, + totalCount, + selectedCount, + selectionOnly, +}: FilesToolbarCountProps) { + const { t } = useTranslation(); + + const selected = t("filesPage.selectedCount", "{{count}} selected", { + count: selectedCount, + }); + + if (selectionOnly) { + return {selected}; + } + + return ( + + {loading + ? t("filesPage.loading", "Loading…") + : t("filesPage.summary", "{{count}} items", { count: totalCount })} + {selectedCount > 0 && · {selected}} + + ); +} + +export default FilesToolbarCount; diff --git a/frontend/editor/src/core/components/filesPage/FilesToolbarFilterMenu.tsx b/frontend/editor/src/core/components/filesPage/FilesToolbarFilterMenu.tsx new file mode 100644 index 0000000000..81d84daf67 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FilesToolbarFilterMenu.tsx @@ -0,0 +1,149 @@ +import { MultiSelect, Popover, Select, Stack, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import CloseIcon from "@mui/icons-material/Close"; +import SearchIcon from "@mui/icons-material/Search"; +import TuneIcon from "@mui/icons-material/Tune"; + +import { ActionIcon } from "@app/ui/ActionIcon"; +import { Button } from "@app/ui/Button"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import type { FilesPageOriginFilter } from "@app/contexts/FilesPageContext"; + +interface FilesToolbarFilterMenuProps { + originFilter: FilesPageOriginFilter; + onOriginChange: (value: FilesPageOriginFilter) => void; + availableTypes: string[]; + typeFilter: string[]; + onTypeChange: (value: string[]) => void; + search: string; + onSearchChange: (value: string) => void; +} + +/** + * Source, type and name filters collapsed behind one icon. Side by side these + * three need ~480px, so on narrow viewports they were each truncated to + * unreadable stubs ("All sour"). In the popover they get their full width back, + * and a dot on the trigger keeps an active filter discoverable while hidden. + */ +export function FilesToolbarFilterMenu({ + originFilter, + onOriginChange, + availableTypes, + typeFilter, + onTypeChange, + search, + onSearchChange, +}: FilesToolbarFilterMenuProps) { + const { t } = useTranslation(); + + const activeCount = + (originFilter !== "all" ? 1 : 0) + + (typeFilter.length > 0 ? 1 : 0) + + (search.trim() !== "" ? 1 : 0); + const label = t("filesPage.filters.label", "Filters"); + + const clearAll = () => { + onOriginChange("all"); + onTypeChange([]); + onSearchChange(""); + }; + + return ( + + +
+ 0 + ? t( + "filesPage.filters.activeCount", + "{{count}} filters active", + { + count: activeCount, + }, + ) + : label + } + position="bottom" + > + 0 ? "primary" : "tertiary"} + size="sm" + aria-label={label} + className="files-page-toolbar-icon-btn" + > + + + +
+
+ + + onSearchChange(e.currentTarget.value)} + placeholder={t("filesPage.search.placeholder", "Filter files…")} + leftSection={} + rightSection={ + search ? ( + onSearchChange("")} + aria-label={t("filesPage.search.clear", "Clear filter")} + > + + + ) : null + } + aria-label={t("filesPage.search.label", "Filter files by name")} + /> +