Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 014648807f | |||
| 1014881551 | |||
| 84dd40702c | |||
| 28c545ff6e |
11 changed files with 205 additions and 8 deletions
81
SOURCE.txt
Normal file
81
SOURCE.txt
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
PCBJam — self-hosted fork (CMMS embed)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
This repository is a self-hosted fork of PCBJam (browser-based KiCad compiled to
|
||||||
|
WebAssembly), maintained for embedding the editor in the tas-tech.net CMMS
|
||||||
|
(TT-DB) at /vendor/pcbjam/.
|
||||||
|
|
||||||
|
Upstream
|
||||||
|
--------
|
||||||
|
https://github.com/emergence-engineering/pcbjam
|
||||||
|
tracked here as git remote `upstream`.
|
||||||
|
|
||||||
|
Forked at upstream commit:
|
||||||
|
766e2f409fa8b098555226479742ec694709ea48 (branch main, 2026-09-01)
|
||||||
|
|
||||||
|
Branches
|
||||||
|
--------
|
||||||
|
main pristine mirror of upstream @ 766e2f4 (no local changes)
|
||||||
|
build/host-networking docker/docker-compose.yml: host networking on the build
|
||||||
|
host (its dockerd runs bridge:none + iptables:false for
|
||||||
|
a VPN killswitch, so the image build + compile container
|
||||||
|
use the host network namespace). 1 commit (28c545f).
|
||||||
|
cmms-embed build/host-networking + sub-path mount support so the
|
||||||
|
standalone editor serves under /vendor/pcbjam/ with no
|
||||||
|
change to KiCad/wxWidgets or @pcbjam/shared. See the
|
||||||
|
commit message for the file-by-file breakdown. A pure
|
||||||
|
no-op when built without `--base` (root deploy).
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
Pinned by the superproject gitlinks (nothing in scripts/ overrides them —
|
||||||
|
`KICAD_COMMIT` in scripts/common/versions.sh is unused/informational):
|
||||||
|
|
||||||
|
kicad 56678639eea1b3c0358f384fa47ef7fab78bd09b
|
||||||
|
upstream https://github.com/PCBJam/kicad-source-mirror (branch wasm-port)
|
||||||
|
wxwidgets 4e6cc5a441e46ac07b242caa3adacfd8e7c7ec50
|
||||||
|
upstream https://github.com/PCBJam/wxWidgets (branch wasm-port)
|
||||||
|
web/pcbjam-shared a4984c626d847bd07ce66148d6ec96fde8ee8e42
|
||||||
|
upstream https://github.com/PCBJam/pcbjam-shared (branch main)
|
||||||
|
|
||||||
|
.gitmodules still points at PCBJam's GitHub forks — these are NOT mirrored into
|
||||||
|
Forgejo. A checkout needs `git submodule update --init --recursive` against GitHub.
|
||||||
|
|
||||||
|
Toolchain / dependency pins (scripts/common/versions.sh)
|
||||||
|
-------------------------------------------------------
|
||||||
|
Emscripten SDK 6.0.6 (installed from emsdk inside docker/Dockerfile;
|
||||||
|
base image ubuntu:22.04)
|
||||||
|
KiCad 8.99
|
||||||
|
OpenCASCADE 7.8.0
|
||||||
|
wxWidgets 3.3.1
|
||||||
|
Boost 1.84.0
|
||||||
|
Protobuf 3.21.12
|
||||||
|
Python 3.11.5
|
||||||
|
ngspice 46
|
||||||
|
FreeType 2.13.2
|
||||||
|
HarfBuzz 8.3.0
|
||||||
|
Cairo 1.18.0
|
||||||
|
Pixman 0.42.2
|
||||||
|
Zstd 1.5.5
|
||||||
|
GLM 0.9.9.8
|
||||||
|
(every download in versions.sh also carries a SHA256 pin)
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
KiCad WASM: ./docker/build.sh kicad_editor -j 4 --build-deps (debug)
|
||||||
|
./docker/build.sh kicad_editor -j 4 --full --release (ship)
|
||||||
|
Editor UI: cd web && pnpm install --frozen-lockfile
|
||||||
|
pnpm --filter @pcbjam/standalone exec vite build --base=/vendor/pcbjam/
|
||||||
|
(env: web/standalone/.env.production on the cmms-embed branch)
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
KiCad and wxWidgets are GPLv3. PCBJam's own code carries its upstream licenses
|
||||||
|
(see LICENSE and the per-package headers; @pcbjam/shared is MIT). This fork is
|
||||||
|
distributed under the same terms.
|
||||||
|
|
||||||
|
The corresponding source for any deployed WASM build IS this repository at the
|
||||||
|
commit the build was made from, together with the submodule revisions its
|
||||||
|
gitlinks pin (listed above). Repository:
|
||||||
|
|
||||||
|
https://repo.tas-tech.net/AI_Assistant/pcbjam
|
||||||
|
|
@ -3,6 +3,9 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
|
# Build host runs dockerd with bridge:none + iptables:false (PIA killswitch,
|
||||||
|
# no Docker NAT). Image-build RUN steps (emsdk download) use the host netns.
|
||||||
|
network: host
|
||||||
args:
|
args:
|
||||||
# Single source of truth: scripts/common/versions.sh, exported into the env by the scripts
|
# Single source of truth: scripts/common/versions.sh, exported into the env by the scripts
|
||||||
# that drive compose (docker/build.sh, docker/shell.sh). No default -> the Dockerfile fails
|
# that drive compose (docker/build.sh, docker/shell.sh). No default -> the Dockerfile fails
|
||||||
|
|
@ -10,6 +13,10 @@ services:
|
||||||
EMSCRIPTEN_VERSION: ${EMSCRIPTEN_VERSION:?source scripts/common/versions.sh before docker compose}
|
EMSCRIPTEN_VERSION: ${EMSCRIPTEN_VERSION:?source scripts/common/versions.sh before docker compose}
|
||||||
# Container name is auto-generated with project prefix (set in build.sh)
|
# Container name is auto-generated with project prefix (set in build.sh)
|
||||||
|
|
||||||
|
# The compile container shares the host network namespace so it inherits the
|
||||||
|
# PIA killswitch directly — the build host's dockerd has no bridge/NAT.
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
# Resource limits. Defaults are sized for a dev Mac (Docker Desktop VM).
|
# Resource limits. Defaults are sized for a dev Mac (Docker Desktop VM).
|
||||||
# CI overrides via env: the 32-core Hetzner runner sets KICAD_DOCKER_CPUS=$(nproc)
|
# CI overrides via env: the 32-core Hetzner runner sets KICAD_DOCKER_CPUS=$(nproc)
|
||||||
# and KICAD_DOCKER_MEM=110G — with the 10-CPU default, run 27226030304 compiled
|
# and KICAD_DOCKER_MEM=110G — with the 10-CPU default, run 27226030304 compiled
|
||||||
|
|
|
||||||
33
web/standalone/.env.production
Normal file
33
web/standalone/.env.production
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# CMMS embed (cmms-embed branch). Consumed by `vite build` (mode=production).
|
||||||
|
# Pair with `vite build --base=/vendor/pcbjam/`. All values are public (they ship
|
||||||
|
# in the bundle) — no secrets here.
|
||||||
|
|
||||||
|
# The PHP adapter. Relative so it resolves against the CMMS origin. The adapter
|
||||||
|
# handles PATH_INFO, so /api/scopes/... and /api/me land as
|
||||||
|
# /pcb-project-api.php/api/scopes/... and /pcb-project-api.php/api/me.
|
||||||
|
VITE_API_BASE_URL=/pcb-project-api.php
|
||||||
|
|
||||||
|
# Projects come from the REST adapter above; file bytes fetched + saves PUT back.
|
||||||
|
VITE_PROJECT_SOURCE=remote
|
||||||
|
VITE_DOC_SOURCE=api
|
||||||
|
|
||||||
|
# No library backend for v1. `static` (not `off`): `off` leaves `libsSource` null so boot.ts
|
||||||
|
# never calls installLibsProvider → `window.kicadLibs` is never installed → any C++ path that
|
||||||
|
# resolves a symbol/footprint through the libs bridge on schematic open (park site K1,
|
||||||
|
# sch_io_pcbjam_lib) calls a method on undefined → throws inside a suspending import →
|
||||||
|
# SuspendError → abort. `static` installs the bridge backed by 2 built-in example symbols; it
|
||||||
|
# answers "not found" gracefully for everything else. No backend, no manifest, self-contained.
|
||||||
|
VITE_LIBS_SOURCE=static
|
||||||
|
|
||||||
|
# Flat WASM layout, served static by Apache under the mount (no per-tool subdir,
|
||||||
|
# VITE_WASM_MANIFEST intentionally unset). Files: kicad_editor.{js,wasm}, wx.js,
|
||||||
|
# wx-dom.js, images.tar.gz.
|
||||||
|
VITE_WASM_ROOT=/vendor/pcbjam/wasm
|
||||||
|
|
||||||
|
# Cross-tab only; no collab server.
|
||||||
|
VITE_YJS_PROVIDER=broadcastchannel
|
||||||
|
|
||||||
|
# VITE_APP_URL deliberately UNSET — with it set, every non-editor route would
|
||||||
|
# redirect away from the mount (lib/redirect.ts).
|
||||||
|
# VITE_USER / VITE_SCOPE deliberately UNSET — identity comes from
|
||||||
|
# /pcb-project-api.php/api/me, scope from the URL path segment after the mount.
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from "@pcbjam/shared";
|
} from "@pcbjam/shared";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { uploadFileBytes } from "@/lib/api";
|
import { uploadFileBytes } from "@/lib/api";
|
||||||
|
import { hardNav } from "@/lib/base-path";
|
||||||
import { currentScope } from "@/lib/config";
|
import { currentScope } from "@/lib/config";
|
||||||
import { localProjectStore } from "@/lib/project-source";
|
import { localProjectStore } from "@/lib/project-source";
|
||||||
import { useLocalProjects } from "@/lib/api";
|
import { useLocalProjects } from "@/lib/api";
|
||||||
|
|
@ -124,7 +125,7 @@ export function NewFileDialog({
|
||||||
// A home-created project is browser-local (@local scope); an in-project new
|
// A home-created project is browser-local (@local scope); an in-project new
|
||||||
// file keeps the current scope. The tool is inferred from the file's ext.
|
// file keeps the current scope. The tool is inferred from the file's ext.
|
||||||
const scope = homeMode ? LOCAL_SCOPE : currentScope();
|
const scope = homeMode ? LOCAL_SCOPE : currentScope();
|
||||||
window.location.assign(projectPath(scope, slug, finalName));
|
hardNav(projectPath(scope, slug, finalName));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setError(e instanceof Error ? e.message : String(e));
|
setError(e instanceof Error ? e.message : String(e));
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
yjsProviderConfig,
|
yjsProviderConfig,
|
||||||
type DocSource,
|
type DocSource,
|
||||||
} from "@/lib/config";
|
} from "@/lib/config";
|
||||||
|
import { stripBase } from "@/lib/base-path";
|
||||||
import { redirectTargetFor } from "@/lib/redirect";
|
import { redirectTargetFor } from "@/lib/redirect";
|
||||||
import { loadSessionIdentity, seedSessionIdentity } from "@/lib/session-identity";
|
import { loadSessionIdentity, seedSessionIdentity } from "@/lib/session-identity";
|
||||||
import { useThemeValue } from "@/lib/theme";
|
import { useThemeValue } from "@/lib/theme";
|
||||||
|
|
@ -629,7 +630,10 @@ export function WasmTool({
|
||||||
// non-editor surfaces: on a backed deploy (APP_URL set) they belong to the
|
// non-editor surfaces: on a backed deploy (APP_URL set) they belong to the
|
||||||
// management app, so quit goes straight there (one hop instead of letting
|
// management app, so quit goes straight there (one hop instead of letting
|
||||||
// App.tsx's 0006 redirect bounce it).
|
// App.tsx's 0006 redirect bounce it).
|
||||||
const segments = win.location.pathname.split("/").filter(Boolean);
|
// stripBase: segments are relative to the mount, so segments[1] is
|
||||||
|
// "projects"/"libs" even under /vendor/pcbjam/. exitPath stays app-absolute
|
||||||
|
// (no mount prefix) — installQuitHook's hardNav() re-adds it.
|
||||||
|
const segments = stripBase(win.location.pathname).split("/").filter(Boolean);
|
||||||
const exitPath =
|
const exitPath =
|
||||||
segments[1] === "libs" ? "/" : projectPath(currentScope(), slug);
|
segments[1] === "libs" ? "/" : projectPath(currentScope(), slug);
|
||||||
const removeQuitHook = installQuitHook(win, {
|
const removeQuitHook = installQuitHook(win, {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
// the page itself unloads (app.cpp UnloadCallback), so the dispatcher latches
|
// the page itself unloads (app.cpp UnloadCallback), so the dispatcher latches
|
||||||
// off as soon as any unload/navigation is under way.
|
// off as soon as any unload/navigation is under way.
|
||||||
|
|
||||||
|
import { hardNav } from "@/lib/base-path";
|
||||||
|
|
||||||
let activeQuitHook: (() => void) | undefined;
|
let activeQuitHook: (() => void) | undefined;
|
||||||
let quitHandled = false;
|
let quitHandled = false;
|
||||||
|
|
||||||
|
|
@ -93,7 +95,9 @@ export function installQuitHook(
|
||||||
// wasm stack has unwound.
|
// wasm stack has unwound.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
opts.log(`[quit] editor closed — going to ${opts.exitUrl}`);
|
opts.log(`[quit] editor closed — going to ${opts.exitUrl}`);
|
||||||
win.location.assign(opts.exitUrl);
|
// hardNav: re-add the CMMS mount prefix to the app-absolute exit path
|
||||||
|
// (a full APP_URL redirect target passes through untouched).
|
||||||
|
hardNav(opts.exitUrl, win);
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
toolSchema,
|
toolSchema,
|
||||||
type Tool,
|
type Tool,
|
||||||
} from "@pcbjam/shared";
|
} from "@pcbjam/shared";
|
||||||
|
import { hardNav } from "@/lib/base-path";
|
||||||
import { currentScope } from "@/lib/config";
|
import { currentScope } from "@/lib/config";
|
||||||
import { defaultFileName, newFileTemplate, withExtension } from "@/lib/new-file";
|
import { defaultFileName, newFileTemplate, withExtension } from "@/lib/new-file";
|
||||||
import { memfsProjectDir } from "@/wasm/constants";
|
import { memfsProjectDir } from "@/wasm/constants";
|
||||||
|
|
@ -175,7 +176,7 @@ export function installToolNavigationHook(
|
||||||
await createFile(relPath, bytes);
|
await createFile(relPath, bytes);
|
||||||
opts.log(`[nav] created missing ${nextTool} file ${relPath} -> ${url}`);
|
opts.log(`[nav] created missing ${nextTool} file ${relPath} -> ${url}`);
|
||||||
markDeliberateNavigation();
|
markDeliberateNavigation();
|
||||||
win.location.assign(url);
|
hardNav(url, win);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pendingCreate = null;
|
pendingCreate = null;
|
||||||
opts.log(
|
opts.log(
|
||||||
|
|
@ -196,7 +197,7 @@ export function installToolNavigationHook(
|
||||||
|
|
||||||
opts.log(`[nav] ${rawToolName} ${rawFileName || "(no file)"} -> ${url}`);
|
opts.log(`[nav] ${rawToolName} ${rawFileName || "(no file)"} -> ${url}`);
|
||||||
markDeliberateNavigation();
|
markDeliberateNavigation();
|
||||||
win.location.assign(url);
|
hardNav(url, win);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
57
web/standalone/src/lib/base-path.ts
Normal file
57
web/standalone/src/lib/base-path.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Sub-path mount support (cmms-embed fork).
|
||||||
|
*
|
||||||
|
* The CMMS serves this SPA under `/vendor/pcbjam/`, built with
|
||||||
|
* `vite build --base=/vendor/pcbjam/` — so `import.meta.env.BASE_URL` is that
|
||||||
|
* prefix (always a leading + trailing slash; "/" for a root deploy).
|
||||||
|
*
|
||||||
|
* `<BrowserRouter basename={import.meta.env.BASE_URL}>` already makes
|
||||||
|
* `<Link>` / `navigate()` / `useLocation()` mount-relative. This module covers
|
||||||
|
* the two things the router can't:
|
||||||
|
* 1. code that reads `window.location.pathname` raw (currentScope, WasmTool's
|
||||||
|
* quit-path), and
|
||||||
|
* 2. the hard cross-document `window.location.assign()` navigations — the
|
||||||
|
* WASM runtime is process-global, so a tool switch / File→Quit / new file
|
||||||
|
* is a full page load, not a router transition, and the app paths handed
|
||||||
|
* to it (`@pcbjam/shared` `projectPath()` etc.) are origin-absolute.
|
||||||
|
*
|
||||||
|
* Nothing here touches `@pcbjam/shared` — it also compiles for the backend,
|
||||||
|
* where `import.meta` does not exist.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** `import.meta.env.BASE_URL` without its trailing slash ("" for a root deploy). */
|
||||||
|
export const BASE_PREFIX = import.meta.env.BASE_URL.replace(/\/$/, "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `pathname` with the mount prefix removed; the result always starts with "/".
|
||||||
|
* A pathname outside the mount (or a root deploy) is returned unchanged.
|
||||||
|
*/
|
||||||
|
export function stripBase(pathname: string): string {
|
||||||
|
if (
|
||||||
|
BASE_PREFIX &&
|
||||||
|
(pathname === BASE_PREFIX || pathname.startsWith(`${BASE_PREFIX}/`))
|
||||||
|
) {
|
||||||
|
const rest = pathname.slice(BASE_PREFIX.length);
|
||||||
|
return rest === "" ? "/" : rest;
|
||||||
|
}
|
||||||
|
return pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `window.location.assign()` to an app-absolute path (e.g. from
|
||||||
|
* `projectPath()`), re-adding the mount prefix. A full URL (has a scheme or is
|
||||||
|
* protocol-relative) passes through untouched — so an `APP_URL` redirect target
|
||||||
|
* is never corrupted.
|
||||||
|
*
|
||||||
|
* `win` is injectable for the same reason the tool-navigation / quit hooks take
|
||||||
|
* a window: test seams and the wx-port's frame window.
|
||||||
|
*/
|
||||||
|
export function hardNav(
|
||||||
|
appPath: string,
|
||||||
|
win: { location: { assign(url: string): void } } = window,
|
||||||
|
): void {
|
||||||
|
const url = /^([a-z][a-z0-9+.-]*:)?\/\//i.test(appPath)
|
||||||
|
? appPath
|
||||||
|
: BASE_PREFIX + appPath;
|
||||||
|
win.location.assign(url);
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,7 @@ export const LOCAL_PROJECTS_ENABLED =
|
||||||
import.meta.env.VITE_LOCAL_PROJECTS === "idb";
|
import.meta.env.VITE_LOCAL_PROJECTS === "idb";
|
||||||
|
|
||||||
import { colorForUser, type PresenceUser } from "@pcbjam/shared";
|
import { colorForUser, type PresenceUser } from "@pcbjam/shared";
|
||||||
|
import { stripBase } from "@/lib/base-path";
|
||||||
import { sessionIdentity } from "@/lib/session-identity";
|
import { sessionIdentity } from "@/lib/session-identity";
|
||||||
import type { ProviderConfig, ProviderKind } from "@/wasm/collab";
|
import type { ProviderConfig, ProviderKind } from "@/wasm/collab";
|
||||||
import { cdnLibsSource } from "@/wasm/libs/cdn-source";
|
import { cdnLibsSource } from "@/wasm/libs/cdn-source";
|
||||||
|
|
@ -282,7 +283,11 @@ export const PRESENCE_TUNER_ENABLED = import.meta.env.VITE_PRESENCE_TUNER === "1
|
||||||
*/
|
*/
|
||||||
export function currentScope(): string {
|
export function currentScope(): string {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const seg = window.location.pathname.split("/").filter(Boolean)[0];
|
// stripBase: under the CMMS sub-path mount the scope is the first segment
|
||||||
|
// AFTER /vendor/pcbjam/, not "vendor". No-op for a root deploy.
|
||||||
|
const seg = stripBase(window.location.pathname)
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean)[0];
|
||||||
if (seg && seg !== "projects" && seg !== "libs") {
|
if (seg && seg !== "projects" && seg !== "libs") {
|
||||||
return decodeURIComponent(seg);
|
return decodeURIComponent(seg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,10 @@ const queryClient = new QueryClient({
|
||||||
// instantiate exactly once per navigation.
|
// instantiate exactly once per navigation.
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
{/* basename = the vite `base` (import.meta.env.BASE_URL) so the CMMS
|
||||||
|
sub-path mount (/vendor/pcbjam/) is transparent to <Link>/navigate();
|
||||||
|
the hard location.assign() navs use hardNav() (lib/base-path.ts). */}
|
||||||
|
<BrowserRouter basename={import.meta.env.BASE_URL}>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { fetchFileBytes, useProject, useSourceDescriptor } from "@/lib/api";
|
import { fetchFileBytes, useProject, useSourceDescriptor } from "@/lib/api";
|
||||||
|
import { hardNav } from "@/lib/base-path";
|
||||||
import { downloadBytes } from "@/lib/download";
|
import { downloadBytes } from "@/lib/download";
|
||||||
import { localProjectStore } from "@/lib/project-source";
|
import { localProjectStore } from "@/lib/project-source";
|
||||||
import { zipFiles } from "@/lib/zip";
|
import { zipFiles } from "@/lib/zip";
|
||||||
|
|
@ -108,7 +109,7 @@ export function ProjectView() {
|
||||||
// scoped with a full reload; document tools create a templated file first.
|
// scoped with a full reload; document tools create a templated file first.
|
||||||
const launchTool = (tool: Tool) => {
|
const launchTool = (tool: Tool) => {
|
||||||
if (FILELESS_TOOLS.has(tool)) {
|
if (FILELESS_TOOLS.has(tool)) {
|
||||||
window.location.assign(projectToolPath(scope, slug, tool));
|
hardNav(projectToolPath(scope, slug, tool));
|
||||||
} else {
|
} else {
|
||||||
setNewFileTool(tool);
|
setNewFileTool(tool);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue