Compare commits

...

20 Commits

Author SHA1 Message Date
c3b0c64d6c messy env stuff 2024-08-15 11:43:55 +10:00
84521b28f3 Lee: Tests are broken because auth skip needs to happen 2024-08-14 16:23:29 -04:00
fd3e2c7665 Fix login (requires new @kittycad/lib WHICH IS NOT INCLUDED HERE) 2024-08-14 16:05:51 -04:00
26715a0af0 Fix settings#projectDir link on home 2024-08-14 11:17:39 -04:00
72d6234f30 Work without a web server 2024-08-14 11:15:14 -04:00
1b01e10eed Remove a layer of indirection 2024-08-14 11:15:14 -04:00
c6517ff8f4 Update flake.nix to support Electron 2024-08-14 10:18:10 -04:00
ead493684a Fix up paths 2024-08-14 09:43:54 -04:00
cc9aedd1ac Rename and delete projects, also spam arrow keys when renaming #3364 #3365 #3259 2024-08-14 22:24:58 +10:00
634fd5f2fc add aria labels to icons 2024-08-14 21:13:48 +10:00
5b3ad376af pressing delete on home screen should do nothing #3387 2024-08-14 20:03:11 +10:00
338f46ff5c Merge remote-tracking branch 'origin' into lf94/tauri-to-electron 2024-08-14 19:04:11 +10:00
e82b39d72d File in the file pane should open with a single click #3385 2024-08-14 19:04:01 +10:00
7028ceb05d Merge branch 'main' into lf94/tauri-to-electron 2024-08-14 15:37:14 +10:00
26d1410588 can sort projects #3362 2024-08-14 15:34:52 +10:00
52eb41c7c7 move files 2024-08-14 14:18:13 +10:00
e6d0ce6fb1 put kcl files together 2024-08-14 14:14:43 +10:00
7e0efaa254 add test #3375 and #3420 2024-08-14 14:05:22 +10:00
c17f0ab04f Merge Frank test setup work (#3418)
* Working window.electron.getPath

* Loading project-specific settings in electron tests

* Simplify test until we can get snapshots/traces working in electron tests

* test tweaks

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-08-14 13:01:01 +10:00
6ba050727a Get electron building something at all 2024-08-14 06:19:25 +10:00
157 changed files with 6707 additions and 5193 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas,.yarn.lock,**/yarn.lock

View File

@ -0,0 +1,404 @@
name: build-test-publish-apps
on:
pull_request:
push:
release:
types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
prepare-json-files:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
# TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with:
path: |
package.json
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-app-macos:
needs: [prepare-json-files]
runs-on: macos-14
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
# TODO: sign the app (and updater bundle potentially)
- name: Add signing certificate
if: ${{ env.BUILD_RELEASE == 'true' }}
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
- name: Build the app for arm64
run: "yarn electron-forge make"
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: List artifacts
run: "ls -R out/make"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*/*"
build-test-app-windows:
needs: [prepare-json-files]
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: Build the app for arm64
run: "yarn electron-forge make --arch arm64"
- name: List artifacts
run: "ls -R out/make"
- name: Sign using Signtool
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D"
X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe"
ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe"
run: |
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}"
signtool.exe verify /v /pa "${{ env.X64_FILE }}"
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}"
signtool.exe verify /v /pa "${{ env.ARM64_FILE }}"
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*"
# TODO: Run e2e tests
build-test-app-ubuntu:
needs: [prepare-json-files]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Build the app for arm64
run: "yarn electron-forge make --arch arm64"
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: List artifacts
run: "ls -R out/make"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
# TODO: sign the app (and updater bundle potentially)
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*"
publish-apps-release:
runs-on: ubuntu-22.04
permissions:
contents: write
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"windows-x86_64": {
"signature": $windows_x86_64_sig,
"url": $windows_x86_64_url
},
"windows-aarch64": {
"signature": $windows_aarch64_sig,
"url": $windows_aarch64_url
}
}
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_x86_64_url
},
"msi-aarch64": {
"url": $windows_aarch64_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.3'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
with:
path: artifact
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'artifact/*/Zoo*'
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-22.04
if: github.event_name == 'release'
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

76
.github/workflows/build-test-web.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: build-test-web
on:
pull_request:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-22.04'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
- run: yarn test:nowatch

View File

@ -1,586 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- main
release:
types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-format:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn playwright install chromium --with-deps
- name: run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn test:nowatch
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when webkit works on ubuntu remove the os part of the commit message
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
git push
git push origin ${{ github.head_ref }}
prepare-json-files:
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
echo "$(jq --arg id 'dev.zoo.modeling-app-nightly' \
'.identifier=$id' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- name: Set updater test version
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with:
path: |
package.json
src-tauri/tauri.conf.json
src-tauri/tauri.release.conf.json
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-apps:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-14, ubuntu-latest, windows-latest]
env:
# Specific Apple Universal target for macos
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
# Only build executable on linux (no appimage or deb)
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Update WebView2 on Windows
if: matrix.os == 'windows-latest'
# Workaround needed to build the tauri windows app with matching edge version.
# From https://github.com/actions/runner-images/issues/9538
run: |
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Run vite build (build:both)
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
- name: Fix format
run: yarn fmt
- name: Install x86 target for Universal builds (MacOS only)
if: matrix.os == 'macos-14'
run: |
rustup target add x86_64-apple-darwin
- name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug)
if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- name: Build for Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
shell: bash
run: |
unset APPLE_SIGNING_IDENTITY
unset APPLE_CERTIFICATE
sign_app="3rd Party Mac Developer Application: KittyCAD Inc (${APPLE_TEAM_ID})"
sign_install="3rd Party Mac Developer Installer: KittyCAD Inc (${APPLE_TEAM_ID})"
profile="src-tauri/entitlements/Mac_App_Distribution.provisionprofile"
mkdir -p src-tauri/entitlements
echo -n "${APPLE_STORE_PROVISIONING_PROFILE}" | base64 --decode -o "${profile}"
echo -n "${APPLE_STORE_DISTRIBUTION_CERT}" | base64 --decode -o "dist.cer"
echo -n "${APPLE_STORE_INSTALLER_CERT}" | base64 --decode -o "installer.cer"
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD="password"
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import "dist.cer" -P "$APPLE_STORE_P12_PASSWORD" -k $KEYCHAIN_PATH -f pkcs12 -t cert -A
security import "installer.cer" -P "$APPLE_STORE_P12_PASSWORD" -k $KEYCHAIN_PATH -f pkcs12 -t cert -A
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
target="universal-apple-darwin"
# Turn off the default target
# We don't want to install the updater for the apple store build
sed -i.bu "s/default =/# default =/" src-tauri/Cargo.toml
rm src-tauri/Cargo.toml.bu
git diff src-tauri/Cargo.toml
yarn tauri build --target "${target}" --verbose --config src-tauri/tauri.app-store.conf.json
app_path="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app"
build_name="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.pkg"
cp_dir="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app/Contents/embedded.provisionprofile"
entitlements="src-tauri/entitlements/app-store.entitlements"
cp "${profile}" "${cp_dir}"
codesign --deep --force -s "${sign_app}" --entitlements "${entitlements}" "${app_path}"
productbuild --component "${app_path}" /Applications/ --sign "${sign_install}" "${build_name}"
# Undo the changes to the Cargo.toml
git checkout src-tauri/Cargo.toml
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_STORE_PROVISIONING_PROFILE: ${{ secrets.APPLE_STORE_PROVISIONING_PROFILE }}
APPLE_STORE_DISTRIBUTION_CERT: ${{ secrets.APPLE_STORE_DISTRIBUTION_CERT }}
APPLE_STORE_INSTALLER_CERT: ${{ secrets.APPLE_STORE_INSTALLER_CERT }}
APPLE_STORE_P12_PASSWORD: ${{ secrets.APPLE_STORE_P12_PASSWORD }}
- name: 'Upload to Mac TestFlight (nightly)'
uses: apple-actions/upload-testflight-build@v1
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
with:
app-path: 'src-tauri/target/universal-apple-darwin/release/bundle/macos/Zoo Modeling App.pkg'
issuer-id: ${{ secrets.APPLE_STORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPLE_STORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPLE_STORE_API_PRIVATE_KEY }}
app-type: osx
- name: Clean up after Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
shell: bash
run: |
git status
# remove our target builds because we want to make sure the later build
# includes the updater, and that anything we changed with the target
# does not persist
rm -rf src-tauri/target
# Lets get rid of the info.plist for the normal mac builds since its
# being sketchy.
rm src-tauri/Info.plist
# We do this after the apple store because the apple store build is
# specific and we want to overwrite it with the this new build after and
# not upload the apple store build to the public bucket
- name: Build the app (release) and sign
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
- name: Run e2e tests (linux only)
if: ${{ matrix.os == 'ubuntu-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
cargo install tauri-driver --force
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run e2e tests (windows only)
if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
cargo install tauri-driver --force
yarn wdio run wdio.conf.ts
env:
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
E2E_TAURI_ENABLED: true
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
- uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
TAURI_BUNDLE_ARGS: "-b ${{ matrix.os == 'windows-latest' && 'msi' || 'dmg' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}"
name: updater-test
publish-apps-release:
permissions:
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
}
}
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.3'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: artifact
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'artifact/*/Zoo*'
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

View File

@ -171,7 +171,7 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert="@snapshot|@electron" || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
@ -186,7 +186,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --last-failed --grep-invert=@snapshot || true
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --last-failed --grep-invert="@snapshot|@electron" || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -233,6 +233,7 @@ jobs:
retention-days: 30
overwrite: true
playwright-macos:
timeout-minutes: 30
runs-on: macos-14
@ -325,7 +326,7 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert="@snapshot|@electron" || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
@ -340,7 +341,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --last-failed --grep-invert=@snapshot || true
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --last-failed --grep-invert="@snapshot|@electron" || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -381,3 +382,156 @@ jobs:
path: playwright-report/
retention-days: 30
overwrite: true
playwright-electron:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
run: yarn playwright install chromium --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm
- name: build web
run: yarn build:local
- uses: actions/download-artifact@v4
if: always()
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
path: test-results/
- name: run electron
run: |
yarn electron:start > electron.log 2>&1 &
while ! grep -q "built in" electron.log; do
sleep 1
done
- name: Run ubuntu/chrome flow (with retries)
id: retry
if: always()
run: |
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" --grep=@electron || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed --grep=@electron || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: send to axiom
if: always()
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-electron-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-electron-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true

7
.gitignore vendored
View File

@ -62,3 +62,10 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo
venv
.vite/
# electron
out/
src-tauri/target
electron-test-projects-dir

View File

@ -89,26 +89,19 @@ enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Tauri
## Desktop
To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
```
yarn tauri dev
yarn electron:start
```
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writing they can conflict.
This will start the application and hot-reload on changed.
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access to the devtools.
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
![image](https://github.com/KittyCAD/modeling-app/assets/29681384/a08762c5-8d16-42d8-a02f-a5efc9ae5551)
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
To build, run `yarn electron:package`.
## Checking out commits / Bisecting

24
add-osx-cert.sh Normal file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env sh
# From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
# Recreate the certificate from the secure environment variable
echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12
#create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12

View File

@ -0,0 +1,48 @@
import test, { _electron } from '@playwright/test'
import { TEST_SETTINGS_KEY } from './storageStates'
import { _electron as electron } from '@playwright/test'
import * as TOML from '@iarna/toml'
import fs from 'node:fs'
import { secrets } from './secrets'
test('Electron setup', { tag: '@electron' }, async () => {
// create or otherwise clear the folder ./electron-test-projects-dir
const fileName = './electron-test-projects-dir'
try {
fs.rmSync(fileName, { recursive: true })
} catch (e) {
console.error(e)
}
fs.mkdirSync(fileName)
// get full path for ./electron-test-projects-dir
const fullPath = fs.realpathSync(fileName)
const electronApp = await electron.launch({
args: ['.'],
})
const page = await electronApp.firstWindow()
// Set local storage directly using evaluate
await page.evaluate(
(token) => localStorage.setItem('TOKEN_PERSIST_KEY', token),
secrets.token
)
// Override settings with electron temporary project directory
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: {
app: { projectDirectory: fullPath },
},
}),
}
)
})

View File

@ -0,0 +1,591 @@
import { test, expect } from '@playwright/test'
import { getUtils, setupElectron, tearDown } from './test-utils'
import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test(
'Rename and delete projects, also spam arrow keys when renaming',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
)
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
)
await fsp.mkdir(`${dir}/lego`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/lego.kcl',
`${dir}/lego/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await page.waitForTimeout(1_000)
await test.step('rename a project clicking buttons checking left and right arrow does not impact the text', async () => {
const routerTemplate = page.getByText('router-template-slate')
await routerTemplate.hover()
await routerTemplate.focus()
await expect(page.getByLabel('sketch').last()).toBeVisible()
await page.getByLabel('sketch').last().click()
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
expect(selectedText).toBe('router-template-slate')
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.type('updated project name')
for (let i = 0; i < 10; i++) {
await page.keyboard.press('ArrowRight')
}
for (let i = 0; i < 30; i++) {
await page.keyboard.press('ArrowLeft')
}
await page.getByLabel('checkmark').last().click()
await expect(page.getByText('Successfully renamed')).toBeVisible()
await expect(page.getByText('Successfully renamed')).not.toBeVisible()
await expect(page.getByText('updated project name')).toBeVisible()
})
await test.step('update a project by hitting enter', async () => {
const project = page.getByText('updated project name')
await project.hover()
await project.focus()
await expect(page.getByLabel('sketch').last()).toBeVisible()
await page.getByLabel('sketch').last().click()
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
expect(selectedText).toBe('updated project name')
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.type('updated name again')
await page.keyboard.press('Enter')
await expect(page.getByText('Successfully renamed')).toBeVisible()
await expect(page.getByText('Successfully renamed')).not.toBeVisible()
await expect(page.getByText('updated name again')).toBeVisible()
})
await test.step('Cancel and edit by clicking the x button', async () => {
const project = page.getByText('updated name again')
await project.hover()
await project.focus()
await expect(page.getByLabel('sketch').last()).toBeVisible()
await page.getByLabel('sketch').last().click()
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
expect(selectedText).toBe('updated name again')
await page.keyboard.press('Backspace')
await page.keyboard.type('dismiss this text')
await page.getByLabel('close').last().click()
await expect(page.getByText('updated name again')).toBeVisible()
})
await test.step('Cancel and edit by pressing esc', async () => {
const project = page.getByText('updated name again')
await project.hover()
await project.focus()
await expect(page.getByLabel('sketch').last()).toBeVisible()
await page.getByLabel('sketch').last().click()
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
expect(selectedText).toBe('updated name again')
await page.keyboard.press('Backspace')
await page.keyboard.type('dismiss this text')
await page.keyboard.press('Escape')
await expect(page.getByText('updated name again')).toBeVisible()
})
await test.step('delete a project by clicking the trash button', async () => {
const project = page.getByText('updated name again')
await project.hover()
await project.focus()
await expect(page.getByLabel('trash').last()).toBeVisible()
await page.getByLabel('trash').last().click()
await expect(page.getByText('This will permanently delete')).toBeVisible()
await page.getByTestId('delete-confirmation').click()
await expect(page.getByText('Successfully deleted')).toBeVisible()
await expect(page.getByText('Successfully deleted')).not.toBeVisible()
await expect(page.getByText('updated name again')).not.toBeVisible()
})
await test.step('rename a project to an empty string should make the field complain', async () => {
const routerTemplate = page.getByText('bracket')
await routerTemplate.hover()
await routerTemplate.focus()
await expect(page.getByLabel('sketch').last()).toBeVisible()
await page.getByLabel('sketch').last().click()
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
expect(selectedText).toBe('bracket')
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Escape')
// expect the name not to have changed
await expect(page.getByText('bracket')).toBeVisible()
})
await electronApp.close()
}
)
test(
'pressing "delete" on home screen should do nothing',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible()
await page.keyboard.press('Delete')
await page.waitForTimeout(200)
await page.keyboard.press('Delete')
// expect to still be on the home page
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible()
await electronApp.close()
}
)
test.fixme(
'File in the file pane should open with a single click',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
)
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/router-template-slate/otherThingToClickOn.kcl`
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText('routerDiameter')
await expect(u.codeLocator).toContainText('templateGap')
await expect(u.codeLocator).toContainText('minClampingDistance')
await page.getByRole('button', { name: 'Project Files' }).click()
const file = page.getByRole('button', { name: 'otherThingToClickOn.kcl' })
await expect(file).toBeVisible()
await file.click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
)
await electronApp.close()
}
)
test(
'Can sort projects on home page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
)
// wait more than a second so the timestamp is different
await new Promise((r) => setTimeout(r, 1_200))
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
)
// wait more than a second so the timestamp is different
await new Promise((r) => setTimeout(r, 1_200))
await fsp.mkdir(`${dir}/lego`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/lego.kcl',
`${dir}/lego/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const getAllProjects = () => page.getByTestId('project-link').all()
page.on('console', console.log)
await test.step('should be shorted by modified initially', async () => {
const lastModifiedButton = page.getByRole('button', {
name: 'Last Modified',
})
await expect(lastModifiedButton).toBeVisible()
await expect(lastModifiedButton.getByLabel('arrow down')).toBeVisible()
})
const projectNamesOrderedByModified = [
'lego',
'bracket',
'router-template-slate',
]
await test.step('Check the order of the projects is correct', async () => {
for (const [index, projectLink] of (await getAllProjects()).entries()) {
await expect(projectLink).toContainText(
projectNamesOrderedByModified[index]
)
}
})
await test.step('Reverse modified order', async () => {
const lastModifiedButton = page.getByRole('button', {
name: 'Last Modified',
})
await lastModifiedButton.click()
await expect(lastModifiedButton).toBeVisible()
await expect(lastModifiedButton.getByLabel('arrow up')).toBeVisible()
})
await test.step('Check the order of the projects is has reversed', async () => {
for (const [index, projectLink] of (await getAllProjects()).entries()) {
await expect(projectLink).toContainText(
[...projectNamesOrderedByModified].reverse()[index]
)
}
})
await test.step('Change order to by name', async () => {
const nameButton = page.getByRole('button', {
name: 'Name',
})
await nameButton.click()
await expect(nameButton).toBeVisible()
await expect(nameButton.getByLabel('arrow down')).toBeVisible()
})
const projectNamesOrderedByName = [
'bracket',
'lego',
'router-template-slate',
]
await test.step('Check the order of the projects is by name', async () => {
for (const [index, projectLink] of (await getAllProjects()).entries()) {
await expect(projectLink).toContainText(
projectNamesOrderedByName[index]
)
}
})
await test.step('Reverse name order', async () => {
const nameButton = page.getByRole('button', {
name: 'Name',
})
await nameButton.click()
await expect(nameButton).toBeVisible()
await expect(nameButton.getByLabel('arrow up')).toBeVisible()
})
await test.step('Check the order of the projects is by name reversed', async () => {
for (const [index, projectLink] of (await getAllProjects()).entries()) {
await expect(projectLink).toContainText(
[...projectNamesOrderedByName].reverse()[index]
)
}
})
await electronApp.close()
}
)
test(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({ testInfo })
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText('project-000')).toBeVisible()
await page.getByText('project-000').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await page.locator('.cm-content')
.fill(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %)
|> line([324.07, 27.199], %, $seg01)
|> line([118.328, -291.754], %)
|> line([-180.04, -202.08], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(200, sketch001)`)
const pointOnModel = { x: 660, y: 250 }
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000,
})
.toBeLessThan(10)
await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 })
await page.mouse.move(pointOnModel.x, pointOnModel.y, { steps: 5 })
await page.mouse.click(pointOnModel.x, pointOnModel.y)
// check user can interact with model by checking it turns yellow
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
.toBeLessThan(10)
}).toPass({ timeout: 40_000, intervals: [1_000] })
await page.getByTestId('app-logo').click()
await expect(
page.getByRole('button', { name: 'New project' })
).toBeVisible()
const createProject = async (projectNum: number) => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
const projectNumStr = projectNum.toString().padStart(3, '0')
await expect(page.getByText(`project-${projectNumStr}`)).toBeVisible()
}
for (let i = 1; i <= 10; i++) {
await createProject(i)
}
await electronApp.close()
}
)
test(
'Check you can go home with two different methods, and that switching between projects does not harm the stream',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
fsp.mkdir(`${dir}/bracket`, { recursive: true }),
])
await Promise.all([
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
),
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
),
])
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
timeout: 10_000,
})
.toBeLessThan(10)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('Opening the router-template project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('router-template-slate')).toBeVisible()
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000,
})
.toBeLessThan(10)
})
await test.step('Opening the router-template project should load the stream', async () => {
await page.getByTestId('project-sidebar-toggle').click()
await expect(
page.getByRole('button', { name: 'Go to Home' })
).toBeVisible()
await page.getByRole('button', { name: 'Go to Home' }).click()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await electronApp.close()
}
)

View File

@ -4,19 +4,24 @@ import {
Download,
TestInfo,
BrowserContext,
_electron as electron,
} from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph'
import os from 'os'
import fsp from 'fs/promises'
import fsSync from 'fs'
import { join } from 'path'
import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Protocol } from 'playwright-core/types/protocol'
import type { Models } from '@kittycad/lib'
import { APP_NAME } from 'lib/constants'
import { APP_NAME, TEST_SETTINGS_FILE_KEY } from 'lib/constants'
import waitOn from 'wait-on'
import { secrets } from './secrets'
import { TEST_SETTINGS_KEY, TEST_SETTINGS } from './storageStates'
import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
type TestColor = [number, number, number]
export const TEST_COLORS = {
@ -623,26 +628,96 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
await page.waitForTimeout(3000)
}
export async function setup(context: BrowserContext, page: Page) {
// wait for Vite preview server to be up
await waitOn({
resources: ['tcp:3000'],
timeout: 5000,
})
// settingsOverrides may need to be augmented to take more generic items,
// but we'll be strict for now
export async function setup(
context: BrowserContext,
page: Page,
overrideDirectory?: string
) {
await context.addInitScript(
async ({ token, settingsKey, settings }) => {
async ({
token,
settingsKey,
settings,
// appSettingsFileKey,
// appSettingsFileContent,
}) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(settingsKey, settings)
// localStorage.setItem(appSettingsFileKey, appSettingsFileContent)
localStorage.setItem('playwright', 'true')
},
{
token: secrets.token,
// appSettingsFileKey: TEST_SETTINGS_FILE_KEY,
// appSettingsFileContent:
// overrideDirectory || TEST_SETTINGS.app.projectDirectory,
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS }),
settings: TOML.stringify({
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.projects,
projectDirectory:
overrideDirectory || TEST_SETTINGS.app.projectDirectory,
},
} as Partial<SaveSettingsPayload>),
}
)
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
}
export async function setupElectron({
testInfo,
folderSetupFn,
overrideDirectory,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
overrideDirectory?: string
}) {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
try {
if (fsSync.existsSync(projectDirName)) {
await fsp.rm(projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
await fsp.mkdir(projectDirName)
const electronApp = await electron.launch({
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY:
overrideDirectory || TEST_SETTINGS.app.projectDirectory,
},
})
const context = electronApp.context()
const page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify({
...TEST_SETTINGS,
settings: {
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
},
},
})
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
await folderSetupFn?.(projectDirName)
await setup(context, page, projectDirName)
return { electronApp, page }
}

View File

@ -1,155 +0,0 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
import { click, setDatasetValue } from '../utils'
const isWin32 = os.platform() === 'win32'
const documentsDir = path.join(os.homedir(), 'Documents')
const userSettingsDir = path.join(
os.homedir(),
'.config',
'dev.zoo.modeling-app'
)
const defaultProjectDir = path.join(documentsDir, 'zoo-modeling-app-projects')
const newProjectDir = path.join(documentsDir, 'a-different-directory')
const tmp = process.env.TEMP || '/tmp'
const userCodeDir = path.join(tmp, 'kittycad_user_code')
describe('ZMA sign in flow', () => {
before(async () => {
// Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultProjectDir, { force: true, recursive: true })
await fs.rm(newProjectDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true })
await fs.rm(userSettingsDir, { force: true, recursive: true })
await fs.mkdir(defaultProjectDir, { recursive: true })
await fs.mkdir(newProjectDir, { recursive: true })
})
it('opens the auth page and signs in', async () => {
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
await click(signInButton)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs
const userCode = await (await fs.readFile(userCodeDir)).toString()
console.log(`Found user code ${userCode}`)
// Device flow: verify
const token = process.env.KITTYCAD_API_TOKEN
const headers = {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
const apiBaseUrl = process.env.VITE_KC_API_BASE_URL
const verifyUrl = `${apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
console.log(`GET ${verifyUrl}`)
const vr = await fetch(verifyUrl, { headers })
console.log(vr.status)
// Device flow: confirm
const confirmUrl = `${apiBaseUrl}/oauth2/device/confirm`
const data = JSON.stringify({ user_code: userCode })
console.log(`POST ${confirmUrl} ${data}`)
const cr = await fetch(confirmUrl, {
headers,
method: 'POST',
body: data,
})
console.log(cr.status)
// Now should be signed in
await new Promise((resolve) => setTimeout(resolve, 10000))
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New project')
})
})
describe('ZMA authorized user flows', () => {
// Note: each flow below is intended to start *and* end from the home page
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const settingsButton = await $('[data-testid="user-settings"]')
await click(settingsButton)
const projectDirInput = await $('[data-testid="project-directory-input"]')
expect(await projectDirInput.getValue()).toEqual(defaultProjectDir)
/*
* We've set up the project directory input (in initialSettings.tsx)
* to be able to skip the folder selection dialog if data-testValue
* has a value, allowing us to test the input otherwise works.
*/
// TODO: understand why we need to force double \ on Windows
await setDatasetValue(
projectDirInput,
'testValue',
isWin32 ? newProjectDir.replaceAll('\\', '\\\\') : newProjectDir
)
const projectDirButton = await $('[data-testid="project-directory-button"]')
await click(projectDirButton)
await new Promise((resolve) => setTimeout(resolve, 500))
// This line is broken. I need a different way to grab the toast
await expect(await $('div*=Set project directory to')).toBeDisplayed()
const nameInput = await $('[data-testid="projects-defaultProjectName"]')
expect(await nameInput.getValue()).toEqual('project-$nnn')
// Setting it back (for back to back local tests)
await new Promise((resolve) => setTimeout(resolve, 5000))
await setDatasetValue(
projectDirInput,
'testValue',
isWin32 ? defaultProjectDir.replaceAll('\\', '\\\\') : newProjectDir
)
await click(projectDirButton)
const closeButton = await $('[data-testid="settings-close-button"]')
await click(closeButton)
})
it('checks that no file exists, creates a new file', async () => {
const homeSection = await $('[data-testid="home-section"]')
expect(await homeSection.getText()).toContain('No Projects found')
const newFileButton = await $('[data-testid="home-new-file"]')
await click(newFileButton)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await homeSection.getText()).toContain('project-000')
})
it('opens the new file and expects a loading stream', async () => {
const projectLink = await $('[data-testid="project-link"]')
await click(projectLink)
if (isWin32) {
// TODO: actually do something to check that the stream is up
await new Promise((resolve) => setTimeout(resolve, 5000))
} else {
const errorText = await $('[data-testid="unexpected-error"]')
expect(await errorText.getText()).toContain('unexpected error')
}
const base = isWin32 ? 'http://tauri.localhost' : 'tauri://localhost'
await browser.execute(`window.location.href = "${base}/home"`)
})
})
describe('ZMA sign out flow', () => {
it('signs out', async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
await click(signoutButton)
const newSignInButton = await $('[data-testid="sign-in-button"]')
expect(await newSignInButton.getText()).toEqual('Sign in')
})
})

View File

@ -1,18 +0,0 @@
import { browser } from '@wdio/globals'
export async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
await browser.execute('arguments[0].click();', element)
}
/* Shoutout to @Sheap on Github for a great workaround utility:
* https://github.com/tauri-apps/tauri/issues/6541#issue-1638944060
*/
export async function setDatasetValue(
field: WebdriverIO.Element,
property: string,
value: string
) {
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
}

View File

@ -58,6 +58,7 @@
nodejs_22
yarn
electron
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [
libiconv
darwin.apple_sdk.frameworks.Security
@ -65,6 +66,7 @@
TARGET_CC = "${pkgs.stdenv.cc}/bin/${pkgs.stdenv.cc.targetPrefix}cc";
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
ELECTRON_OVERRIDE_DIST_PATH = "${pkgs.electron}/bin/";
};
});
};

66
forge.config.ts Normal file
View File

@ -0,0 +1,66 @@
import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite'
import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses'
const config: ForgeConfig = {
packagerConfig: {
asar: true,
osxSign: (process.env.BUILD_RELEASE === 'true' && {}) || undefined,
osxNotarize:
(process.env.BUILD_RELEASE === 'true' && {
appleId: process.env.APPLE_ID || '',
appleIdPassword: process.env.APPLE_PASSWORD || '',
teamId: process.env.APPLE_TEAM_ID || '',
}) ||
undefined,
executableName: 'zoo-modeling-app',
},
rebuildConfig: {},
makers: [
new MakerSquirrel({}),
new MakerZIP({}, ['darwin']),
new MakerRpm({}),
new MakerDeb({}),
],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: 'src/main.ts',
config: 'vite.main.config.ts',
},
{
entry: 'src/preload.ts',
config: 'vite.preload.config.ts',
},
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
}
export default config

35
forge.env.d.ts vendored Normal file
View File

@ -0,0 +1,35 @@
export {} // Make this a module
declare global {
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
// whether you're running in development or production).
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string
const MAIN_WINDOW_VITE_NAME: string
namespace NodeJS {
interface Process {
// Used for hot reload after preload scripts.
viteDevServers: Record<string, import('vite').ViteDevServer>
}
}
type VitePluginConfig = ConstructorParameters<
typeof import('@electron-forge/plugin-vite').VitePlugin
>[0]
interface VitePluginRuntimeKeys {
VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`
VITE_NAME: `${string}_VITE_NAME`
}
}
declare module 'vite' {
interface ConfigEnv<
K extends keyof VitePluginConfig = keyof VitePluginConfig
> {
root: string
forgeConfig: VitePluginConfig
forgeConfigSelf: VitePluginConfig[K][number]
}
}

View File

@ -2,6 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- PERPETUAL TODO reconsider this option.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
-->
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

45
interface.d.ts vendored Normal file
View File

@ -0,0 +1,45 @@
import fs from 'node:fs/promises'
import path from 'path'
import { dialog, shell } from 'electron'
import { MachinesListing } from 'lib/machineManager'
export interface IElectronAPI {
open: typeof dialog.showOpenDialog
save: typeof dialog.showSaveDialog
openExternal: typeof shell.openExternal
showInFolder: typeof shell.showItemInFolder
login: (host: string) => Promise<string>
platform: typeof process.env.platform
arch: typeof process.env.arch
version: typeof process.env.version
readFile: (path: string) => ReturnType<fs.readFile>
writeFile: (
path: string,
data: string | Uint8Array
) => ReturnType<fs.writeFile>
readdir: (path: string) => ReturnType<fs.readdir>
getPath: (name: string) => Promise<string>
rm: typeof fs.rm
stat: (path: string) => ReturnType<fs.stat>
statIsDirectory: (path: string) => Promise<boolean>
path: typeof path
mkdir: typeof fs.mkdir
rename: (prev: string, next: string) => typeof fs.rename
packageJson: {
name: string
}
process: {
env: {
BASE_URL: (value?: string) => string
}
}
kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
}
declare global {
interface Window {
electron: IElectronAPI
}
}

View File

@ -57,9 +57,8 @@ echo "New version number without 'v': $new_version_number"
git checkout -b "cut-release-$new_version"
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
echo "$(jq --arg v "$new_version_number" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
git add package.json src-tauri/tauri.conf.json
git add package.json
git commit -m "Cut release $new_version"
echo ""

View File

@ -1,7 +1,16 @@
{
"name": "untitled-app",
"version": "0.24.10",
"private": true,
"name": "zoo-modeling-app",
"productName": "Zoo Modeling App",
"version": "0.24.10",
"author": {
"name": "Zoo Corporation",
"email": "info@zoo.dev",
"url": "https://zoo.dev"
},
"description": "Edit CAD visually or with code",
"main": ".vite/build/main.js",
"license": "none",
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0",
@ -17,29 +26,24 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.70",
"@kittycad/lib": "2",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",
"@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "^2.0.0-beta.14",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.6",
"@tauri-apps/plugin-fs": "^2.0.0-beta.6",
"@tauri-apps/plugin-http": "^2.0.0-beta.7",
"@tauri-apps/plugin-os": "^2.0.0-beta.6",
"@tauri-apps/plugin-process": "^2.0.0-beta.6",
"@tauri-apps/plugin-shell": "^2.0.0-beta.7",
"@tauri-apps/plugin-updater": "^2.0.0-beta.6",
"@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2",
"bonjour-service": "^1.2.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.5",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",
"openid-client": "^5.6.5",
"re-resizable": "^6.9.11",
"react": "^18.3.1",
"react-dom": "^18.2.0",
@ -51,7 +55,6 @@
"react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4",
"three": "^0.166.1",
"typescript": "^5.4.5",
"ua-parser-js": "^1.0.37",
"uuid": "^9.0.1",
"vscode-jsonrpc": "^8.2.1",
@ -72,8 +75,6 @@
"test": "vitest --mode development",
"test:nowatch": "vitest run --mode development",
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
@ -88,7 +89,11 @@
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts"
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"electron:start": "electron-forge start",
"electron:package": "electron-forge package",
"electron:make": "electron-forge make",
"electron:publish": "electron-forge publish"
},
"prettier": {
"trailingComma": "es5",
@ -110,14 +115,23 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.0",
"@babel/preset-env": "^7.24.3",
"@electron-forge/cli": "^7.4.0",
"@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/maker-rpm": "^7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-zip": "^7.4.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.45.1",
"@tauri-apps/cli": "==2.0.0-beta.13",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
"@types/electron": "^1.6.10",
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.31",
"@types/pixelmatch": "^5.2.6",
@ -131,19 +145,17 @@
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0",
"@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.36.0",
"@wdio/local-runner": "^8.36.0",
"@wdio/mocha-framework": "^8.36.0",
"@wdio/spec-reporter": "^8.36.0",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"eslint": "^8.57.0",
"electron": "^31.2.1",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.25.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",
@ -156,7 +168,9 @@
"prettier": "^2.8.8",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1",
"vite": "^5.3.3",
"ts-node": "^10.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.12",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -0,0 +1,36 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 120_000, // override the default 30s timeout
testDir: './e2e/playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Do not retry */
retries: process.env.CI ? 0 : 0,
/* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 1 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
[process.env.CI ? 'dot' : 'list'],
['json', { outputFile: './test-results/report.json' }],
['html'],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'retain-on-failure',
actionTimeout: 15000,
screenshot: 'only-on-failure',
},
})

7
public/logo-blue.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="97" height="32" viewBox="0 0 97 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3583 5.5893V0.690826H0.00311715V7.59893H17.5057L0.00625718 26.5774H0.00311715V26.5805L-2.28882e-05 26.5837L0.00311715 26.5868V31.2278H4.48709L9.00246 26.3293V31.2278H28.3576V24.3197H10.8582L28.3576 5.3381V0.690826L23.8705 0.697107L19.3583 5.5893Z" fill="#3C73FF"/>
<path d="M36.8417 16.0017C36.8417 10.987 40.9206 6.90811 45.9353 6.90811C47.7 6.90811 49.3485 7.41365 50.7427 8.28659L55.4904 3.17459C52.8214 1.18066 49.5149 0 45.9353 0C37.1118 0 29.9336 7.17815 29.9336 16.0017C29.9336 20.0021 31.4095 23.6665 33.8524 26.4769L38.6001 21.3649C37.4949 19.8608 36.8417 18.005 36.8417 16.0017Z" fill="#3C73FF"/>
<path d="M53.2739 10.6351C54.376 12.1423 55.0291 13.9981 55.0291 16.0014C55.0291 21.013 50.9502 25.0919 45.9356 25.0919C44.1709 25.0919 42.5255 24.5863 41.1314 23.7134L36.3805 28.8285C39.0495 30.8193 42.356 32 45.9356 32C54.7591 32 61.9372 24.8218 61.9372 16.0014C61.9372 11.9979 60.4614 8.33345 58.0185 5.5231L53.2739 10.6351Z" fill="#3C73FF"/>
<path d="M92.4988 5.5231L87.7542 10.6351C88.8564 12.1423 89.5095 13.9981 89.5095 16.0014C89.5095 21.013 85.4306 25.0919 80.416 25.0919C78.6513 25.0919 77.0059 24.5863 75.6117 23.7134L70.8608 28.8285C73.5299 30.8193 76.8363 32 80.416 32C89.2395 32 96.4176 24.8218 96.4176 16.0014C96.4176 11.9979 94.9418 8.33345 92.4988 5.5231Z" fill="#3C73FF"/>
<path d="M71.3225 16.0017C71.3225 10.987 75.4014 6.90811 80.416 6.90811C82.1807 6.90811 83.8292 7.41365 85.2234 8.28659L89.9711 3.17459C87.3021 1.18066 83.9957 0 80.416 0C71.5925 0 64.4144 7.17815 64.4144 16.0017C64.4144 20.0021 65.8902 23.6665 68.3332 26.4769L73.0809 21.3649C71.9756 19.8608 71.3225 18.005 71.3225 16.0017Z" fill="#3C73FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

7
public/logo-white.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="97" height="32" viewBox="0 0 97 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3583 5.5893V0.690826H0.00310952V7.59893H17.5057L0.00624955 26.5774H0.00310952V26.5805L-3.05176e-05 26.5837L0.00310952 26.5868V31.2278H4.48708L9.00245 26.3293V31.2278H28.3576V24.3197H10.8582L28.3576 5.3381V0.690826L23.8705 0.697107L19.3583 5.5893Z" fill="#FCFCFC"/>
<path d="M36.8417 16.0017C36.8417 10.987 40.9206 6.90811 45.9353 6.90811C47.7 6.90811 49.3485 7.41365 50.7427 8.28659L55.4904 3.17459C52.8214 1.18066 49.5149 0 45.9353 0C37.1118 0 29.9337 7.17815 29.9337 16.0017C29.9337 20.0021 31.4095 23.6665 33.8524 26.4769L38.6002 21.3649C37.4949 19.8608 36.8417 18.005 36.8417 16.0017Z" fill="#FCFCFC"/>
<path d="M53.2739 10.6351C54.376 12.1423 55.0291 13.9981 55.0291 16.0014C55.0291 21.013 50.9502 25.0919 45.9356 25.0919C44.1709 25.0919 42.5255 24.5863 41.1314 23.7134L36.3805 28.8285C39.0495 30.8193 42.356 32 45.9356 32C54.7591 32 61.9372 24.8218 61.9372 16.0014C61.9372 11.9979 60.4614 8.33345 58.0185 5.5231L53.2739 10.6351Z" fill="#FCFCFC"/>
<path d="M92.4988 5.5231L87.7542 10.6351C88.8564 12.1423 89.5095 13.9981 89.5095 16.0014C89.5095 21.013 85.4306 25.0919 80.416 25.0919C78.6513 25.0919 77.0059 24.5863 75.6117 23.7134L70.8608 28.8285C73.5299 30.8193 76.8363 32 80.416 32C89.2395 32 96.4176 24.8218 96.4176 16.0014C96.4176 11.9979 94.9418 8.33345 92.4988 5.5231Z" fill="#FCFCFC"/>
<path d="M71.3225 16.0017C71.3225 10.987 75.4014 6.90811 80.416 6.90811C82.1807 6.90811 83.8292 7.41365 85.2234 8.28659L89.9711 3.17459C87.3021 1.18066 83.9957 0 80.416 0C71.5925 0 64.4144 7.17815 64.4144 16.0017C64.4144 20.0021 65.8902 23.6665 68.3332 26.4769L73.0809 21.3649C71.9756 19.8608 71.3225 18.005 71.3225 16.0017Z" fill="#FCFCFC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,3 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View File

@ -1,376 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSDesktopFolderUsageDescription</key>
<string>Zoo Modeling App accesses the Desktop to load and save your project files and/or exported files here</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>Zoo Modeling App accesses the Documents folder to load and save your project files and/or exported files here</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>Zoo Modeling App accesses the Downloads folder to load and save your project files and/or exported files here</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>DTXcode</key>
<string>1501</string>
<key>DTXcodeBuild</key>
<string>15A507</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>dev.zoo.modeling-app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>zoo-modeling-app</string>
<string>zoo</string>
</array>
</dict>
</array>
<key>LSFileQuarantineEnabled</key>
<false/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.kcl</string>
</array>
<key>CFBundleTypeName</key>
<string>KCL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.toml</string>
</array>
<key>CFBundleTypeName</key>
<string>TOML</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.gltf</string>
</array>
<key>CFBundleTypeName</key>
<string>glTF</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.glb</string>
</array>
<key>CFBundleTypeName</key>
<string>glb</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.step</string>
</array>
<key>CFBundleTypeName</key>
<string>STEP</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.fbx</string>
</array>
<key>CFBundleTypeName</key>
<string>FBX</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.sldprt</string>
</array>
<key>CFBundleTypeName</key>
<string>Solidworks Part</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.geometry-definition-format</string>
</array>
<key>CFBundleTypeName</key>
<string>OBJ</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.polygon-file-format</string>
</array>
<key>CFBundleTypeName</key>
<string>PLY</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.standard-tesselated-geometry-format</string>
</array>
<key>CFBundleTypeName</key>
<string>STL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.folder</string>
</array>
<key>CFBundleTypeName</key>
<string>Folders</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.kcl</string>
<key>UTTypeReferenceURL</key>
<string>https://zoo.dev/docs/kcl</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.source-code</string>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.script</string>
</array>
<key>UTTypeDescription</key>
<string>KCL (KittyCAD Language) document</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.zoo.kcl</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.gltf</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF)</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gltf</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf+json</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.glb</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF) binary</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>glb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf-binary</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.step</string>
<key>UTTypeReferenceURL</key>
<string>https://www.loc.gov/preservation/digital/formats/fdd/fdd000448.shtml</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>STEP-file, ISO 10303-21</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>step</string>
<string>stp</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/step</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.sldprt</string>
<key>UTTypeReferenceURL</key>
<string>https://docs.fileformat.com/cad/sldprt/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Solidworks Part</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>sldprt</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.solidworks.sldprt</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.fbx</string>
<key>UTTypeReferenceURL</key>
<string>https://en.wikipedia.org/wiki/FBX</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Autodesk Filmbox (FBX) format</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>fbx</string>
<string>fbxb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.autodesk.fbx</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.toml</string>
<key>UTTypeReferenceURL</key>
<string>https://toml.io/en/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Tom's Obvious Minimal Language</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/toml</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,127 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"context": "local",
"windows": [
"main"
],
"permissions": [
"cli:default",
"deep-link:default",
"log:default",
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"fs:allow-create",
"fs:allow-read-file",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$TEMP"
},
{
"path": "$TEMP/**/*"
},
{
"path": "$HOME"
},
{
"path": "$HOME/**/*"
},
{
"path": "$HOME/.config"
},
{
"path": "$HOME/.config/**/*"
},
{
"path": "$APPCONFIG"
},
{
"path": "$APPCONFIG/**/*"
},
{
"path": "$DOCUMENT"
},
{
"path": "$DOCUMENT/**/*"
}
]
},
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "open",
"cmd": "open",
"args": [
"-R",
{
"validator": "\\S+"
}
],
"sidecar": false
},
{
"name": "explorer",
"cmd": "explorer",
"args": [
"/select",
{
"validator": "\\S+"
}
],
"sidecar": false
}
]
},
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
{
"identifier": "http:default",
"allow": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"process:allow-restart",
"updater:default"
],
"platforms": [
"linux",
"macOS",
"windows"
]
}

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.application-identifier</key>
<string>92H8YB3B95.dev.zoo.modeling-app</string>
<key>com.apple.developer.team-identifier</key>
<string>92H8YB3B95</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.zoo.dev</string>
</array>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -1,6 +0,0 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,603 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub(crate) mod state;
use std::{
env,
path::{Path, PathBuf},
};
use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
Configuration,
};
use oauth2::TokenResponse;
use tauri::{ipc::InvokeError, Manager};
use tauri_plugin_cli::CliExt;
use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.zoo.dev";
const SETTINGS_FILE_NAME: &str = "settings.toml";
const PROJECT_SETTINGS_FILE_NAME: &str = "project.toml";
const PROJECT_FOLDER: &str = "zoo-modeling-app-projects";
#[tauri::command]
async fn rename_project_directory(project_path: &str, new_name: &str) -> Result<PathBuf, InvokeError> {
let project_dir = std::path::Path::new(project_path);
kcl_lib::settings::types::file::rename_project_directory(project_dir, new_name)
.await
.map_err(InvokeError::from_anyhow)
}
#[tauri::command]
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let dir = match app.path().document_dir() {
Ok(dir) => dir,
Err(_) => {
// for headless Linux (eg. Github Actions)
let home_dir = app.path().home_dir()?;
home_dir.join("Documents")
}
};
Ok(dir.join(PROJECT_FOLDER))
}
#[tauri::command]
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
let store = app.state::<state::Store>();
Ok(store.get().await)
}
#[tauri::command]
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
let store = app.state::<state::Store>();
store.set(state).await;
Ok(())
}
async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let app_config_dir = app.path().app_config_dir()?;
// Ensure this directory exists.
if !app_config_dir.exists() {
tokio::fs::create_dir_all(&app_config_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
}
#[tauri::command]
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
let mut settings_path = get_app_settings_file_path(&app).await?;
let mut needs_migration = false;
// Check if this file exists.
if !settings_path.exists() {
// Try the backwards compatible path.
// TODO: Remove this after a few releases.
let app_config_dir = app.path().app_config_dir()?;
settings_path = format!(
"{}user.toml",
app_config_dir.display().to_string().trim_end_matches('/')
)
.into();
needs_migration = true;
// Check if this path exists.
if !settings_path.exists() {
let mut default = Configuration::default();
default.settings.project.directory = get_initial_default_dir(app.clone())?;
// Return the default configuration.
return Ok(default);
}
}
let contents = tokio::fs::read_to_string(&settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
if parsed.settings.project.directory == PathBuf::new() {
parsed.settings.project.directory = get_initial_default_dir(app.clone())?;
}
// TODO: Remove this after a few releases.
if needs_migration {
write_app_settings_file(app, parsed.clone()).await?;
// Delete the old file.
tokio::fs::remove_file(settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(parsed)
}
#[tauri::command]
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
async fn get_project_settings_file_path(project_path: &str) -> Result<PathBuf, InvokeError> {
let project_dir = std::path::Path::new(project_path);
if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
}
#[tauri::command]
async fn read_project_settings_file(project_path: &str) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(project_path).await?;
// Check if this file exists.
if !settings_path.exists() {
// Return the default configuration.
return Ok(ProjectConfiguration::default());
}
let contents = tokio::fs::read_to_string(&settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
Ok(parsed)
}
#[tauri::command]
async fn write_project_settings_file(
project_path: &str,
configuration: ProjectConfiguration,
) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(project_path).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
/// Initialize the directory that holds all the projects.
#[tauri::command]
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
configuration
.ensure_project_directory_exists()
.await
.map_err(InvokeError::from_anyhow)
}
/// Create a new project directory.
#[tauri::command]
async fn create_new_project_directory(
configuration: Configuration,
project_name: &str,
initial_code: Option<&str>,
) -> Result<Project, InvokeError> {
configuration
.create_new_project_directory(project_name, initial_code)
.await
.map_err(InvokeError::from_anyhow)
}
/// List all the projects in the project directory.
#[tauri::command]
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
}
/// Get information about a project.
#[tauri::command]
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_path)
.await
.map_err(InvokeError::from_anyhow)
}
/// Parse the project route.
#[tauri::command]
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
}
#[tauri::command]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
kcl_lib::settings::utils::walk_dir(Path::new(path).to_path_buf())
.await
.map_err(InvokeError::from_anyhow)
}
/// This command instantiates a new window with auth.
/// The string returned from this method is the access token.
#[tauri::command]
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
log::debug!("Logging in...");
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
// We can hardcode the client ID.
// This value is safe to be embedded in version control.
// This is the client ID of the KittyCAD app.
let client_id = "2af127fb-e14e-400a-9c57-a9ed08d1a5b7".to_string();
let auth_client = oauth2::basic::BasicClient::new(
oauth2::ClientId::new(client_id),
None,
oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?,
Some(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
),
)
.set_auth_type(oauth2::AuthType::RequestBody)
.set_device_authorization_url(device_auth_url);
let details: oauth2::devicecode::StandardDeviceAuthorizationResponse = auth_client
.exchange_device_code()
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.request_async(oauth2::reqwest::async_http_client)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let Some(auth_uri) = details.verification_uri_complete() else {
return Err(InvokeError::from("getting the verification uri failed"));
};
// Open the system browser with the auth_uri.
// We do this in the browser and not a separate window because we want 1password and
// other crap to work well.
// TODO: find a better way to share this value with tauri e2e tests
// Here we're using an env var to enable the /tmp file (windows not supported for now)
// and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled {
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
let mut temp = String::from("/tmp");
// Overwrite with Windows variable
match env::var("TEMP") {
Ok(val) => temp = val,
Err(_e) => println!("Fallback to default /tmp"),
}
let path = Path::new(&temp).join("kittycad_user_code");
println!("Writing to {}", path.to_string_lossy());
tokio::fs::write(path, details.user_code().secret())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
} else {
app.shell()
.open(auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
// Wait for the user to login.
let token = auth_client
.exchange_device_access_token(&details)
.request_async(oauth2::reqwest::async_http_client, tokio::time::sleep, None)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.access_token()
.secret()
.to_string();
Ok(token)
}
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
// Use the host passed in if it's set.
// Otherwise, use the default host.
let host = if hostname.is_empty() {
DEFAULT_HOST.to_string()
} else {
hostname.to_string()
};
// Change the baseURL to the one we want.
let mut baseurl = host.to_string();
if !host.starts_with("http://") && !host.starts_with("https://") {
baseurl = format!("https://{host}");
if host.starts_with("localhost") {
baseurl = format!("http://{host}")
}
}
log::debug!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token);
if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl);
}
let user_info: kittycad::types::User = client
.users()
.get_self()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(user_info)
}
/// Open the selected path in the system file manager.
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
/// But with the Linux support removed since we don't need it for now.
#[tauri::command]
fn show_in_folder(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError> {
// Check if the file exists.
// If it doesn't, return an error.
if !Path::new(path).exists() {
return Err(InvokeError::from_anyhow(anyhow::anyhow!(
"The file `{}` does not exist",
path
)));
}
#[cfg(not(unix))]
{
app.shell()
.command("explorer")
.args(["/select,", path]) // The comma after select is not a typo
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
#[cfg(unix)]
{
app.shell()
.command("open")
.args(["-R", path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(())
}
const SERVICE_NAME: &str = "_machine-api._tcp.local.";
async fn find_machine_api() -> Result<Option<String>> {
println!("Looking for machine API...");
// Timeout if no response is received after 5 seconds.
let timeout_duration = std::time::Duration::from_secs(5);
let mdns = mdns_sd::ServiceDaemon::new()?;
// Browse for a service type.
let receiver = mdns.browse(SERVICE_NAME)?;
let resp = tokio::time::timeout(
timeout_duration,
tokio::spawn(async move {
while let Ok(event) = receiver.recv() {
if let mdns_sd::ServiceEvent::ServiceResolved(info) = event {
if let Some(addr) = info.get_addresses().iter().next() {
return Some(format!("{}:{}", addr, info.get_port()));
}
}
}
None
}),
)
.await;
// Shut down.
mdns.shutdown()?;
let Ok(Ok(Some(addr))) = resp else {
return Ok(None);
};
Ok(Some(addr))
}
#[tauri::command]
async fn get_machine_api_ip() -> Result<Option<String>, InvokeError> {
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
Ok(machine_api)
}
#[tauri::command]
async fn list_machines() -> Result<String, InvokeError> {
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
let Some(machine_api) = machine_api else {
// Empty array.
return Ok("[]".to_string());
};
let client = reqwest::Client::new();
let response = client
.get(format!("http://{}/machines", machine_api))
.send()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let text = response.text().await.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(text)
}
#[allow(dead_code)]
fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
log::debug!("Opening URL: {:?}", url);
let cloned_url = url.clone();
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> = tauri::async_runtime::spawn(async move {
let url_str = cloned_url.path().to_string();
log::debug!("Opening URL path : {}", url_str);
let path = Path::new(url_str.as_str());
ProjectState::new_from_path(path.to_path_buf()).await
});
// Block on the handle.
match tauri::async_runtime::block_on(runner) {
Ok(Ok(store)) => {
// Create a state object to hold the project.
app.manage(state::Store::new(store));
}
Err(e) => {
log::warn!("Error opening URL:{} {:?}", url, e);
}
Ok(Err(e)) => {
log::warn!("Error opening URL:{} {:?}", url, e);
}
}
}
fn main() -> Result<()> {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_state,
set_state,
get_initial_default_dir,
initialize_project_directory,
create_new_project_directory,
list_projects,
get_project_info,
parse_project_route,
get_user,
login,
read_dir_recursive,
show_in_folder,
read_app_settings_file,
write_app_settings_file,
read_project_settings_file,
write_project_settings_file,
rename_project_directory,
get_machine_api_ip,
list_machines
])
.plugin(tauri_plugin_cli::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(
tauri_plugin_log::Builder::new()
.targets([
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
])
.level(log::LevelFilter::Debug)
.build(),
)
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_persisted_scope::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.setup(|app| {
// Do update things.
#[cfg(debug_assertions)]
{
app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
#[cfg(feature = "updater")]
{
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
let mut verbose = false;
let mut source_path: Option<PathBuf> = None;
match app.cli().matches() {
// `matches` here is a Struct with { args, subcommand }.
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
Ok(matches) => {
if let Some(verbose_flag) = matches.args.get("verbose") {
let Some(value) = verbose_flag.value.as_bool() else {
return Err(
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
);
};
verbose = value;
}
// Get the path we are trying to open.
if let Some(source_arg) = matches.args.get("source") {
// We don't do an else here because this can be null.
if let Some(value) = source_arg.value.as_str() {
log::info!("Got path in cli argument: {}", value);
source_path = Some(Path::new(value).to_path_buf());
}
}
}
Err(err) => {
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
}
}
if verbose {
log::debug!("Verbose mode enabled.");
}
// If we have a source path to open, make sure it exists.
let Some(source_path) = source_path else {
// The user didn't provide a source path to open.
// Run the app as normal.
app.manage(state::Store::default());
return Ok(());
};
if !source_path.exists() {
return Err(anyhow::anyhow!(
"Error: the path `{}` you are trying to open does not exist",
source_path.display()
)
.into());
}
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
tauri::async_runtime::spawn(async move { ProjectState::new_from_path(source_path).await });
// Block on the handle.
let store = tauri::async_runtime::block_on(runner)??;
// Create a state object to hold the project.
app.manage(state::Store::new(store));
// Listen on the deep links.
app.listen("deep-link://new-url", |event| {
log::info!("got deep-link url: {:?}", event);
// TODO: open_url_sync(app.handle(), event.url);
});
Ok(())
})
.build(tauri::generate_context!())?
.run(
#[allow(unused_variables)]
|app, event| {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let tauri::RunEvent::Opened { urls } = event {
log::info!("Opened URLs: {:?}", urls);
// Handle the first URL.
// TODO: do we want to handle more than one URL?
// Under what conditions would we even have more than one?
if let Some(url) = urls.first() {
open_url_sync(app, url);
}
}
},
);
Ok(())
}

View File

@ -1,21 +0,0 @@
//! State management for the application.
use kcl_lib::settings::types::file::ProjectState;
use tokio::sync::Mutex;
#[derive(Debug, Default)]
pub struct Store(Mutex<Option<ProjectState>>);
impl Store {
pub fn new(p: ProjectState) -> Self {
Self(Mutex::new(Some(p)))
}
pub async fn get(&self) -> Option<ProjectState> {
self.0.lock().await.clone()
}
pub async fn set(&self, p: Option<ProjectState>) {
*self.0.lock().await = p;
}
}

View File

@ -1,8 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"macOS": {
"entitlements": "entitlements/app-store.entitlements"
}
}
}

View File

@ -1,84 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"security": {
"csp": null
},
"windows": [
{
"fullscreen": false,
"height": 1200,
"resizable": true,
"title": "Zoo Modeling App",
"width": 1800
}
]
},
"build": {
"beforeDevCommand": "yarn start",
"devUrl": "http://localhost:3000",
"frontendDist": "../build"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"deb": {
"depends": []
}
},
"longDescription": "",
"macOS": {},
"resources": [],
"shortDescription": "",
"targets": "all"
},
"identifier": "dev.zoo.modeling-app",
"plugins": {
"cli": {
"description": "Zoo Modeling App CLI",
"args": [
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
},
{
"name": "source",
"description": "The file or directory to open",
"required": false,
"index": 1,
"takesValue": true
}
],
"subcommands": {}
},
"deep-link": {
"mobile": [
{
"host": "app.zoo.dev"
}
],
"desktop": {
"schemes": [
"zoo",
"zoo-modeling-app"
]
}
},
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.24.10"
}

View File

@ -1,57 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
},
"fileAssociations": [
{
"ext": ["kcl"],
"mimeType": "text/vnd.zoo.kcl"
},
{
"ext": ["obj"],
"mimeType": "model/obj"
},
{
"ext": ["gltf"],
"mimeType": "model/gltf+json"
},
{
"ext": ["glb"],
"mimeType": "model/gltf+binary"
},
{
"ext": ["fbx", "fbxb"],
"mimeType": "model/fbx"
},
{
"ext": ["stl"],
"mimeType": "model/stl"
},
{
"ext": ["ply"],
"mimeType": "model/ply"
},
{
"ext": ["step", "stp"],
"mimeType": "model/step"
},
{
"ext": ["sldprt"],
"mimeType": "model/sldprt"
}
]
},
"plugins": {
"updater": {
"active": true,
"endpoints": [
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
}
}
}

View File

@ -16,7 +16,7 @@ import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubsc
import { engineCommandManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
@ -28,8 +28,8 @@ import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu'
export function App() {
useRefreshSettings(PATHS.FILE + 'SETTINGS')
const { project, file } = useLoaderData() as IndexLoaderData
useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
@ -62,7 +62,7 @@ export function App() {
e.preventDefault()
})
useHotkeyWrapper(
[isTauri() ? 'mod + ,' : 'shift + mod + ,'],
[isDesktop() ? 'mod + ,' : 'shift + mod + ,'],
() => navigate(filePath + PATHS.SETTINGS),
{
splitKey: '|',

View File

@ -1,6 +1,7 @@
import { App } from './App'
import {
createBrowserRouter,
createHashRouter,
Outlet,
redirect,
RouterProvider,
@ -10,7 +11,7 @@ import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
import SignIn from './routes/SignIn'
import { Auth } from './Auth'
import { isTauri } from './lib/isTauri'
import { isDesktop } from './lib/isDesktop'
import Home from './routes/Home'
import { NetworkContext } from './hooks/useNetworkContext'
import { useNetworkStatus } from './hooks/useNetworkStatus'
@ -32,7 +33,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri'
import { getState, setState } from 'lib/desktop'
import { CoreDumpManager } from 'lib/coredump'
import { engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -42,7 +43,9 @@ import { coreDump } from 'lang/wasm'
import { useMemo } from 'react'
import { AppStateProvider } from 'AppState'
const router = createBrowserRouter([
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
const router = createRouter([
{
loader: settingsLoader,
id: PATHS.INDEX,
@ -66,8 +69,8 @@ const router = createBrowserRouter([
{
path: PATHS.INDEX,
loader: async () => {
const inTauri = isTauri()
if (inTauri) {
const onDesktop = isDesktop()
if (onDesktop) {
const appState = await getState()
if (appState) {
@ -84,7 +87,7 @@ const router = createBrowserRouter([
}
}
return inTauri
return onDesktop
? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
},
@ -101,7 +104,10 @@ const router = createBrowserRouter([
<Outlet />
<App />
<CommandBar />
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
{
// @ts-ignore
!isDesktop() && import.meta.env.PROD && <DownloadAppBanner />
}
</ModelingMachineProvider>
<WasmErrBanner />
</FileMachineProvider>

View File

@ -591,7 +591,7 @@ export class SceneEntities {
const sg = kclManager.programMemory.get(
variableDeclarationName
) as SketchGroup
const lastSeg = sg.value.slice(-1)[0] || sg.start
const lastSeg = sg?.value?.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`

View File

@ -309,7 +309,7 @@ export class SceneInfra {
const textureLoader = new TextureLoader()
this.extraSegmentTexture = textureLoader.load(
'/clientSideSceneAssets/extra-segment-texture.png'
'./clientSideSceneAssets/extra-segment-texture.png'
)
this.extraSegmentTexture.anisotropy =
this.renderer?.capabilities?.getMaxAnisotropy?.()

View File

@ -1,4 +1,5 @@
import { ActionIcon, ActionIconProps } from './ActionIcon'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import React, { ForwardedRef, forwardRef } from 'react'
import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom'
@ -107,6 +108,7 @@ export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
ref={ref as ForwardedRef<HTMLAnchorElement>}
to={to || PATHS.INDEX}
className={classNames}
onClick={openExternalBrowserIfDesktop(to as string)}
{...rest}
target="_blank"
>

View File

@ -2,7 +2,12 @@ import { cloneElement } from 'react'
const CustomIconMap = {
arc: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="arc"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -12,7 +17,12 @@ const CustomIconMap = {
</svg>
),
angle: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="angle"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -22,7 +32,12 @@ const CustomIconMap = {
</svg>
),
arrowDown: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="arrow down"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -32,7 +47,12 @@ const CustomIconMap = {
</svg>
),
arrowLeft: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="arrow left"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -42,7 +62,12 @@ const CustomIconMap = {
</svg>
),
arrowRight: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="arrow right"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -52,7 +77,12 @@ const CustomIconMap = {
</svg>
),
arrowRotateRight: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="arrow rotate right"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -62,7 +92,12 @@ const CustomIconMap = {
</svg>
),
arrowUp: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="arrow up"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -72,7 +107,12 @@ const CustomIconMap = {
</svg>
),
booleanExclude: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="boolean exclude"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -82,7 +122,12 @@ const CustomIconMap = {
</svg>
),
booleanIntersect: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="boolean intersect"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -92,7 +137,12 @@ const CustomIconMap = {
</svg>
),
booleanSubtract: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="boolean subtract"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -102,7 +152,12 @@ const CustomIconMap = {
</svg>
),
booleanUnion: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="boolean union"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -112,7 +167,12 @@ const CustomIconMap = {
</svg>
),
bug: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="bug"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -122,7 +182,12 @@ const CustomIconMap = {
</svg>
),
checkmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="checkmark"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -132,7 +197,12 @@ const CustomIconMap = {
</svg>
),
caretDown: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="caret down"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -142,7 +212,12 @@ const CustomIconMap = {
</svg>
),
chamfer3d: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="chamfer 3d"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -152,7 +227,12 @@ const CustomIconMap = {
</svg>
),
circle: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="circle"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -162,7 +242,12 @@ const CustomIconMap = {
</svg>
),
clipboardCheckmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="clipboard checkmark"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -172,7 +257,12 @@ const CustomIconMap = {
</svg>
),
clipboardPlus: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="clipboard plus"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -182,7 +272,12 @@ const CustomIconMap = {
</svg>
),
close: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="close"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -192,7 +287,12 @@ const CustomIconMap = {
</svg>
),
code: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="code"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -202,7 +302,12 @@ const CustomIconMap = {
</svg>
),
dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="dimension"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -212,7 +317,12 @@ const CustomIconMap = {
</svg>
),
equal: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="equal"
>
<path
d="M5 8.78V7H14.52V8.78H5ZM5 13.02V11.24H14.52V13.02H5Z"
fill="currentColor"
@ -220,7 +330,12 @@ const CustomIconMap = {
</svg>
),
exclamationMark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="exclamation mark"
>
<path
d="M9.76391 11.6597L9.3633 7.91112V5.00671H10.6224V7.91112L10.2217 11.6597H9.76391ZM9.99283 15.1221C9.60176 15.1221 9.32515 15.041 9.163 14.8788C9.01039 14.7167 8.93408 14.5116 8.93408 14.2636V14.0061C8.93408 13.7581 9.01039 13.553 9.163 13.3909C9.32515 13.2287 9.60176 13.1476 9.99283 13.1476C10.3839 13.1476 10.6557 13.2287 10.8084 13.3909C10.9705 13.553 11.0516 13.7581 11.0516 14.0061V14.2636C11.0516 14.5116 10.9705 14.7167 10.8084 14.8788C10.6557 15.041 10.3839 15.1221 9.99283 15.1221Z"
fill="currentColor"
@ -228,7 +343,12 @@ const CustomIconMap = {
</svg>
),
exportFile: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="export file"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -238,7 +358,12 @@ const CustomIconMap = {
</svg>
),
extrude: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="extrude"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -248,7 +373,12 @@ const CustomIconMap = {
</svg>
),
fillet: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="fillet"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -264,7 +394,12 @@ const CustomIconMap = {
</svg>
),
fillet3d: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="fillet 3d"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -274,7 +409,12 @@ const CustomIconMap = {
</svg>
),
file: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="file"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -284,7 +424,12 @@ const CustomIconMap = {
</svg>
),
filePlus: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="file plus"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -294,7 +439,12 @@ const CustomIconMap = {
</svg>
),
floppyDiskArrow: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="floppy disk arrow"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -304,7 +454,12 @@ const CustomIconMap = {
</svg>
),
folder: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="folder"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -314,7 +469,12 @@ const CustomIconMap = {
</svg>
),
folderPlus: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="folder plus"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -324,7 +484,12 @@ const CustomIconMap = {
</svg>
),
gear: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="gear"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -334,7 +499,12 @@ const CustomIconMap = {
</svg>
),
hole: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="hole"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -344,7 +514,12 @@ const CustomIconMap = {
</svg>
),
horizontal: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="horizontal"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -354,7 +529,12 @@ const CustomIconMap = {
</svg>
),
horizontalDash: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="horizontal Dash"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -364,7 +544,12 @@ const CustomIconMap = {
</svg>
),
'intersection-offset': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="intersection offset"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -374,7 +559,12 @@ const CustomIconMap = {
</svg>
),
kcl: (
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="kcl"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -384,7 +574,12 @@ const CustomIconMap = {
</svg>
),
keyboard: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="keyboard"
>
<path
d="M16 12V15H13.5M16 12V9M16 12H13.5M4 12V15H6.5M4 12V9M4 12H6.5M4 9V6H6.5M4 9H6.5M16 9V6H13.5M16 9H13.5M6.5 12V15M6.5 12H7.5M6.5 15H13.5M13.5 15V12M13.5 12H12.5M7.5 12V9M7.5 12H10M7.5 9H8.75M7.5 9H6.5M10 12V9M10 12H12.5M10 9H11.25M10 9H8.75M12.5 12V9M12.5 9H13.5M12.5 9H11.25M13.5 9V6M13.5 6H11.25M11.25 9V6M11.25 6H8.75M8.75 9V6M8.75 6H6.5M6.5 9V6"
stroke="currentColor"
@ -392,7 +587,12 @@ const CustomIconMap = {
</svg>
),
line: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="line"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -402,7 +602,12 @@ const CustomIconMap = {
</svg>
),
link: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="link"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -412,7 +617,12 @@ const CustomIconMap = {
</svg>
),
lockClosed: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="lock closed"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -422,7 +632,12 @@ const CustomIconMap = {
</svg>
),
lockOpen: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="lock open"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -432,7 +647,12 @@ const CustomIconMap = {
</svg>
),
loft: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="loft"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -442,7 +662,12 @@ const CustomIconMap = {
</svg>
),
'make-variable': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="make variable"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -452,7 +677,12 @@ const CustomIconMap = {
</svg>
),
menu: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="menu"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -462,7 +692,12 @@ const CustomIconMap = {
</svg>
),
mirror: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="mirror"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -472,7 +707,12 @@ const CustomIconMap = {
</svg>
),
move: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="move"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -482,7 +722,12 @@ const CustomIconMap = {
</svg>
),
network: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="network"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -492,7 +737,12 @@ const CustomIconMap = {
</svg>
),
networkCrossedOut: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="network crossed out"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -502,7 +752,12 @@ const CustomIconMap = {
</svg>
),
parallel: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="parallel"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -512,7 +767,12 @@ const CustomIconMap = {
</svg>
),
person: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="person"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -522,7 +782,12 @@ const CustomIconMap = {
</svg>
),
plane: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="plane"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -532,7 +797,12 @@ const CustomIconMap = {
</svg>
),
plus: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="plus"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -542,7 +812,12 @@ const CustomIconMap = {
</svg>
),
printer3d: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="3D printer"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -552,7 +827,12 @@ const CustomIconMap = {
</svg>
),
polygon: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="polygon"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -562,7 +842,12 @@ const CustomIconMap = {
</svg>
),
questionMark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="question mark"
>
<path
d="M9.12005 11.9172V9.67093C9.94034 9.63278 10.5842 9.45632 11.0515 9.14156C11.5189 8.81725 11.7526 8.3308 11.7526 7.6822V7.48189C11.7526 6.94775 11.5905 6.54714 11.2662 6.28007C10.9514 6.013 10.5174 5.87946 9.96419 5.87946C9.39189 5.87946 8.93405 6.03685 8.59067 6.35161C8.2473 6.66637 8.00884 7.06698 7.8753 7.55343L6.80225 7.15282C6.89763 6.83806 7.03116 6.54237 7.20285 6.26576C7.37454 5.97962 7.58915 5.73162 7.84669 5.52178C8.11376 5.31194 8.42375 5.14502 8.77667 5.02102C9.13912 4.89702 9.54927 4.83502 10.0071 4.83502C10.4649 4.83502 10.8751 4.90179 11.2375 5.03533C11.6095 5.15932 11.9243 5.34055 12.1818 5.57901C12.4394 5.80793 12.6397 6.08931 12.7827 6.42315C12.9258 6.75699 12.9974 7.12898 12.9974 7.53912C12.9974 7.98742 12.9163 8.38326 12.7541 8.72664C12.592 9.06048 12.3821 9.34663 12.1246 9.58508C11.8671 9.82354 11.5714 10.0191 11.2375 10.1717C10.9132 10.3148 10.5842 10.4149 10.2503 10.4721V11.9172H9.12005ZM9.73527 15.1221C9.3442 15.1221 9.06759 15.041 8.90544 14.8788C8.75282 14.7167 8.67652 14.5116 8.67652 14.2636V14.0061C8.67652 13.7581 8.75282 13.553 8.90544 13.3909C9.06759 13.2287 9.3442 13.1476 9.73527 13.1476C10.1263 13.1476 10.3982 13.2287 10.5508 13.3909C10.7129 13.553 10.794 13.7581 10.794 14.0061V14.2636C10.794 14.5116 10.7129 14.7167 10.5508 14.8788C10.3982 15.041 10.1263 15.1221 9.73527 15.1221Z"
fill="currentColor"
@ -570,7 +855,12 @@ const CustomIconMap = {
</svg>
),
rectangle: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="rectangle"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -580,7 +870,12 @@ const CustomIconMap = {
</svg>
),
refresh: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="refresh"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -596,6 +891,7 @@ const CustomIconMap = {
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="revolve"
>
<path
fillRule="evenodd"
@ -606,7 +902,12 @@ const CustomIconMap = {
</svg>
),
search: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="search"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -616,7 +917,12 @@ const CustomIconMap = {
</svg>
),
settings: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="settings"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -626,7 +932,12 @@ const CustomIconMap = {
</svg>
),
shell: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="shell"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -636,7 +947,12 @@ const CustomIconMap = {
</svg>
),
sketch: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="sketch"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -646,7 +962,12 @@ const CustomIconMap = {
</svg>
),
spline: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="spline"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -662,6 +983,7 @@ const CustomIconMap = {
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="sweep"
>
<path
fillRule="evenodd"
@ -672,7 +994,12 @@ const CustomIconMap = {
</svg>
),
tangent: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="tangent"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -682,7 +1009,12 @@ const CustomIconMap = {
</svg>
),
text: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="text"
>
<path
d="M12.3107 14.9933L11.5524 12.3321H8.37616L7.61786 14.9933H5.98682L8.90553 5.00671H11.0946L14.0133 14.9933H12.3107ZM10.0215 6.62345H9.90705L8.67661 11.0015H11.2519L10.0215 6.62345Z"
fill="currentColor"
@ -690,7 +1022,12 @@ const CustomIconMap = {
</svg>
),
'three-dots': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="three-dots"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -700,7 +1037,12 @@ const CustomIconMap = {
</svg>
),
trash: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="trash"
>
<path
d="M8.5 6H5V8H6M8.5 6V4H11.5V6M8.5 6H11.5M11.5 6H15V8H14M6 8V15.5H8M6 8H14M14 8V15.5H12M8 15.5V10M8 15.5H10M12 15.5V10M12 15.5H10M10 15.5V12"
stroke="currentColor"
@ -708,7 +1050,12 @@ const CustomIconMap = {
</svg>
),
vertical: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="vertical"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -718,7 +1065,12 @@ const CustomIconMap = {
</svg>
),
xAbsolute: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="x-absolute"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -728,7 +1080,12 @@ const CustomIconMap = {
</svg>
),
xRelative: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="x-relative"
>
<path
d="M8.75069 6.82599C8.97469 6.78866 9.20803 6.79799 9.45069 6.85399C9.91736 6.95666 10.2627 7.17132 10.4867 7.49799C10.524 7.55399 10.552 7.58199 10.5707 7.58199L10.6547 7.46999C10.8787 7.20866 11.1447 7.01732 11.4527 6.89599C11.8074 6.76532 12.162 6.77932 12.5167 6.93799C12.7874 7.07799 12.9787 7.26466 13.0907 7.49799C13.2774 7.87132 13.282 8.26332 13.1047 8.67399C13.03 8.83266 12.9367 8.95399 12.8247 9.03799C12.4887 9.28066 12.1714 9.32732 11.8727 9.17799C11.8167 9.14999 11.77 9.11732 11.7327 9.07999C11.5927 8.93999 11.5367 8.76266 11.5647 8.54799C11.6207 8.12799 11.8354 7.85266 12.2087 7.72199L12.3207 7.67999L12.2647 7.62399C12.0967 7.47466 11.8914 7.43266 11.6487 7.49799C11.63 7.49799 11.6114 7.50266 11.5927 7.51199C11.3127 7.61466 11.0887 7.88532 10.9207 8.32399C10.8274 8.55732 10.58 9.54199 10.1787 11.278C10.132 11.4367 10.1087 11.5347 10.1087 11.572C10.062 11.8613 10.0667 12.0573 10.1227 12.16C10.1974 12.3 10.314 12.398 10.4727 12.454C10.5194 12.4727 10.6174 12.482 10.7667 12.482C10.9067 12.482 11.0094 12.4727 11.0747 12.454C11.3174 12.3793 11.56 12.23 11.8027 12.006C12.092 11.7073 12.2834 11.3807 12.3767 11.026C12.4047 10.9327 12.442 10.8813 12.4887 10.872C12.526 10.8627 12.61 10.858 12.7407 10.858C12.918 10.858 13.0207 10.8673 13.0487 10.886C13.0674 10.9047 13.0767 10.9373 13.0767 10.984C13.0767 11.18 12.9554 11.474 12.7127 11.866C12.7034 11.894 12.6894 11.9173 12.6707 11.936C12.2507 12.58 11.6954 12.9767 11.0047 13.126C10.79 13.1633 10.5474 13.1633 10.2767 13.126C9.80069 13.0233 9.44603 12.8133 9.21269 12.496L9.12869 12.37L9.08669 12.412C8.72269 12.8787 8.31203 13.126 7.85469 13.154C7.32269 13.182 6.92603 12.986 6.66469 12.566C6.60869 12.4913 6.56669 12.4073 6.53869 12.314C6.47336 12.1087 6.45469 11.8893 6.48269 11.656C6.54803 11.2547 6.73469 10.9793 7.04269 10.83C7.34136 10.69 7.60736 10.6853 7.84069 10.816C8.05536 10.9187 8.15336 11.1053 8.13469 11.376C8.10669 11.7493 7.93403 12.02 7.61669 12.188C7.57003 12.216 7.50469 12.244 7.42069 12.272L7.36469 12.286L7.40669 12.328C7.53736 12.4307 7.68669 12.482 7.85469 12.482C7.97603 12.4913 8.10203 12.4587 8.23269 12.384C8.47536 12.216 8.65736 11.9593 8.77869 11.614L9.54869 8.53399C9.61403 8.19799 9.62336 7.96466 9.57669 7.83399C9.53003 7.68466 9.40869 7.57732 9.21269 7.51199C9.02603 7.45599 8.83469 7.45599 8.63869 7.51199C8.36803 7.58666 8.13003 7.72666 7.92469 7.93199C7.65403 8.21199 7.45803 8.52466 7.33669 8.86999C7.30869 8.98199 7.28069 9.05199 7.25269 9.07999C7.23403 9.09866 7.13603 9.10799 6.95869 9.10799H6.69269L6.65069 9.06599C6.62269 9.03799 6.60869 9.00066 6.60869 8.95399C6.63669 8.81399 6.69269 8.65532 6.77669 8.47799C6.86069 8.28199 6.96803 8.09532 7.09869 7.91799C7.24803 7.69399 7.45336 7.48399 7.71469 7.28799C7.72403 7.27866 7.73803 7.26932 7.75669 7.25999C8.06469 7.04532 8.39603 6.90066 8.75069 6.82599Z"
fill="currentColor"
@ -736,7 +1093,12 @@ const CustomIconMap = {
</svg>
),
yAbsolute: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="y-absolute"
>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -746,7 +1108,12 @@ const CustomIconMap = {
</svg>
),
yRelative: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="y-relative"
>
<path
d="M7.92463 6.83998C8.1113 6.79332 8.33063 6.79798 8.58263 6.85398C9.05863 6.98465 9.36197 7.26932 9.49263 7.70798C9.52997 7.86665 9.52997 8.06732 9.49263 8.30998C9.47397 8.37532 9.40397 8.56665 9.28263 8.88399C8.94663 9.78932 8.7273 10.4847 8.62463 10.97C8.52197 11.5953 8.57797 12.0293 8.79263 12.272C8.9513 12.4587 9.1753 12.5287 9.46463 12.482C9.88463 12.426 10.2533 12.174 10.5706 11.726L10.6546 11.6L11.1586 9.54198C11.5133 8.15132 11.7046 7.43265 11.7326 7.38598C11.882 7.11532 12.106 6.97532 12.4046 6.96598C12.554 6.96598 12.68 7.00798 12.7826 7.09198C12.8293 7.11065 12.8713 7.16198 12.9086 7.24598C12.9553 7.33932 12.9646 7.45598 12.9366 7.59598C12.9086 7.73598 12.6846 8.65532 12.2646 10.354C11.742 12.3887 11.49 13.3733 11.5086 13.308C11.4153 13.6067 11.2753 13.882 11.0886 14.134C10.566 14.9273 9.8753 15.4593 9.01663 15.73C8.60597 15.87 8.20463 15.912 7.81263 15.856C7.11263 15.7533 6.67397 15.436 6.49663 14.904C6.4593 14.6987 6.46397 14.5073 6.51063 14.33C6.56663 14.0967 6.6833 13.91 6.86063 13.77C7.19663 13.5273 7.51397 13.4807 7.81263 13.63C7.92463 13.6953 8.00863 13.784 8.06463 13.896C8.1673 14.12 8.14863 14.3627 8.00863 14.624C7.9153 14.8013 7.7893 14.932 7.63063 15.016L7.54663 15.072L7.61663 15.1C7.98063 15.2587 8.3633 15.2587 8.76463 15.1C9.26863 14.8947 9.68863 14.442 10.0246 13.742C10.09 13.5927 10.1506 13.4433 10.2066 13.294C10.3186 12.9673 10.3653 12.7993 10.3466 12.79C10.3373 12.79 10.3046 12.8087 10.2486 12.846C10.1086 12.93 9.9593 13 9.80063 13.056C9.2873 13.2333 8.75997 13.2007 8.21863 12.958C8.08797 12.902 7.9713 12.832 7.86863 12.748C7.44863 12.412 7.26663 11.8987 7.32263 11.208C7.35997 10.7507 7.5793 9.98065 7.98063 8.89798C7.98997 8.86065 7.9993 8.82798 8.00863 8.79998C8.1393 8.47332 8.2093 8.27732 8.21863 8.21198C8.33063 7.89465 8.34463 7.67532 8.26063 7.55398C8.2233 7.49798 8.14863 7.46998 8.03663 7.46998C7.6353 7.50732 7.30397 7.84798 7.04263 8.49198C6.99597 8.60398 6.95397 8.72532 6.91663 8.85598C6.86997 9.00532 6.83263 9.08465 6.80463 9.09398C6.7953 9.10332 6.6973 9.10798 6.51063 9.10798H6.25863L6.21663 9.06598C6.16997 9.02865 6.17463 8.93065 6.23063 8.77198C6.40797 8.17465 6.67863 7.70332 7.04263 7.35798C7.30397 7.08732 7.59797 6.91465 7.92463 6.83998Z"
fill="currentColor"

View File

@ -1,4 +1,4 @@
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
import { ActionButton } from './ActionButton'
import {
@ -25,7 +25,7 @@ export const ErrorPage = () => {
</p>
)}
<div className="flex justify-between gap-2 mt-6">
{isTauri() && (
{isDesktop() && (
<ActionButton
Element="link"
to={'/'}

View File

@ -15,11 +15,9 @@ import {
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path'
import { isDesktop } from 'lib/isDesktop'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri'
import { getProjectInfo } from 'lib/desktop'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -51,7 +49,9 @@ export const FileMachineProvider = ({
commandBarSend({ type: 'Close' })
navigate(
`${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory + sep() + event.data.name
context.selectedDirectory +
window.electron.path.sep +
event.data.name
)}`
)
} else if (
@ -86,7 +86,7 @@ export const FileMachineProvider = ({
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
const newFiles = isDesktop()
? (await getProjectInfo(context.project.path)).children
: []
return {
@ -99,15 +99,18 @@ export const FileMachineProvider = ({
let createdPath: string
if (event.data.makeDir) {
createdPath = await join(context.selectedDirectory.path, createdName)
await mkdir(createdPath)
createdPath = window.electron.path.join(
context.selectedDirectory.path,
createdName
)
await window.electron.mkdir(createdPath)
} else {
createdPath =
context.selectedDirectory.path +
sep() +
window.electron.path.sep +
createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath)
await window.electron.writeFile(createdPath, '')
}
return {
@ -121,14 +124,25 @@ export const FileMachineProvider = ({
) => {
const { oldName, newName, isDir } = event.data
const name = newName ? newName : DEFAULT_FILE_NAME
const oldPath = await join(context.selectedDirectory.path, oldName)
const newDirPath = await join(context.selectedDirectory.path, name)
const oldPath = window.electron.path.join(
context.selectedDirectory.path,
oldName
)
const newDirPath = window.electron.path.join(
context.selectedDirectory.path,
name
)
const newPath =
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
await rename(oldPath, newPath, {})
await window.electron.rename(oldPath, newPath)
if (oldPath === file?.path && project?.path) {
if (!file) {
return Promise.reject(new Error('file is not defined'))
}
const currentFilePath = window.electron.path.join(file.path, file.name)
if (oldPath === currentFilePath && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(PATHS.FILE + '/' + encodeURIComponent(newPath))
} else if (file?.path.includes(oldPath)) {
@ -153,13 +167,15 @@ export const FileMachineProvider = ({
const isDir = !!event.data.children
if (isDir) {
await remove(event.data.path, {
await window.electron
.rm(event.data.path, {
recursive: true,
}).catch((e) => console.error('Error deleting directory', e))
})
.catch((e) => console.error('Error deleting directory', e))
} else {
await remove(event.data.path).catch((e) =>
console.error('Error deleting file', e)
)
await window.electron
.rm(event.data.path)
.catch((e) => console.error('Error deleting file', e))
}
// If we just deleted the current file or one of its parent directories,

View File

@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
@ -44,7 +44,7 @@ function RenameForm({
data: {
oldName: fileOrDir.name || '',
newName: inputRef.current?.value || fileOrDir.name || '',
isDir: fileOrDir.children !== undefined,
isDir: fileOrDir.children !== null,
},
})
}
@ -90,7 +90,7 @@ function DeleteFileTreeItemDialog({
const { send } = useFileContext()
return (
<DeleteConfirmationDialog
title={`Delete ${fileOrDir.children !== undefined ? 'folder' : 'file'}`}
title={`Delete ${fileOrDir.children !== null ? 'folder' : 'file'}`}
onDismiss={() => setIsOpen(false)}
onConfirm={() => {
send({ type: 'Delete file', data: fileOrDir })
@ -99,7 +99,7 @@ function DeleteFileTreeItemDialog({
>
<p className="my-4">
This will permanently delete "{fileOrDir.name || 'this file'}"
{fileOrDir.children !== undefined ? ' and all of its contents. ' : '. '}
{fileOrDir.children !== null ? ' and all of its contents. ' : '. '}
</p>
<p className="my-4">
Are you sure you want to delete "{fileOrDir.name || 'this file'}
@ -165,7 +165,7 @@ const FileTreeItem = ({
}
function handleClick() {
if (fileOrDir.children !== undefined) return // Don't open directories
if (fileOrDir.children !== null) return // Don't open directories
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
// Import non-kcl files
@ -194,7 +194,7 @@ const FileTreeItem = ({
return (
<div className="contents" ref={itemRef}>
{fileOrDir.children === undefined ? (
{fileOrDir.children === null ? (
<li
className={
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +

View File

@ -3,10 +3,11 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CustomIcon } from './CustomIcon'
import { useLocation, useNavigate } from 'react-router-dom'
import { createAndOpenNewProject } from 'lib/tauriFS'
import { PATHS } from 'lib/paths'
import { createAndOpenNewProject } from 'lib/desktopFS'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -141,6 +142,9 @@ function HelpMenuItem({
{as === 'a' ? (
<a
{...(props as React.ComponentProps<'a'>)}
onClick={openExternalBrowserIfDesktop(
(props as React.ComponentProps<'a'>).href
)}
className={`no-underline text-inherit ${baseClassName} ${className}`}
>
{children}

View File

@ -26,8 +26,12 @@ export function LowerRightControls({
const isPlayWright = window?.localStorage.getItem('playwright') === 'true'
async function reportbug(event: { preventDefault: () => void }) {
async function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
}) {
event?.preventDefault()
event?.stopPropagation()
if (!coreDumpManager) {
// open default reporting option
@ -88,7 +92,7 @@ export function LowerRightControls({
<Link
to={
location.pathname.includes(PATHS.FILE)
? filePath + PATHS.SETTINGS_PROJECT
? filePath + PATHS.SETTINGS + '?tab=project'
: PATHS.HOME + PATHS.SETTINGS
}
>

View File

@ -25,7 +25,7 @@ import {
import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
@ -125,7 +125,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
])
useMemo(() => {
if (!isTauri() && isKclLspReady && kclLspClient && codeManager.code) {
if (!isDesktop() && isKclLspReady && kclLspClient && codeManager.code) {
kclLspClient.textDocumentDidOpen({
textDocument: {
uri: `file:///${PROJECT_ENTRYPOINT}`,

View File

@ -7,6 +7,7 @@ import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './KclEditorPane'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -60,6 +61,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
href="https://zoo.dev/docs/kcl"
target="_blank"
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
<span>Read the KCL docs</span>
<small>
@ -78,6 +80,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
href="https://zoo.dev/docs/kcl-samples"
target="_blank"
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
<span>KCL samples</span>
<small>

View File

@ -7,7 +7,7 @@ import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useModelingContext } from 'hooks/useModelingContext'
import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
@ -71,7 +71,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
(action) =>
(!action.hide || (action.hide instanceof Function && !action.hide())) &&
(!action.hideOnPlatform ||
(isTauri()
(isDesktop()
? action.hideOnPlatform === 'web'
: action.hideOnPlatform === 'desktop'))
)
@ -86,7 +86,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
(isDesktop()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
),

View File

@ -1,7 +1,7 @@
import { Popover } from '@headlessui/react'
import Tooltip from './Tooltip'
import { machineManager } from 'lib/machineManager'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon'
export const NetworkMachineIndicator = ({
@ -10,7 +10,7 @@ export const NetworkMachineIndicator = ({
className?: string
}) => {
const machineCount = Object.keys(machineManager.machines).length
return isTauri() ? (
return isDesktop() ? (
<Popover className="relative">
<Popover.Button
className={

View File

@ -32,6 +32,7 @@ export function DeleteConfirmationDialog({
iconClassName: '!text-destroy-80 dark:!text-destroy-20',
}}
className="hover:border-destroy-40 dark:hover:border-destroy-40 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
data-testid="delete-confirmation"
>
Delete
</ActionButton>

View File

@ -36,8 +36,8 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
function getDisplayedTime(dateTimeMs: number) {
const date = new Date(dateTimeMs)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
@ -52,7 +52,7 @@ function ProjectCard({
}
// async function setupImageUrl() {
// const projectImagePath = await join(project.path, PROJECT_IMAGE_NAME)
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
// if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' })
@ -113,8 +113,8 @@ function ProjectCard({
</span>
<span className="px-2 text-chalkboard-60 text-xs">
Edited{' '}
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
{project.metadata && project.metadata.modified
? getDisplayedTime(parseInt(project.metadata.modified))
: 'never'}
</span>
</div>

Some files were not shown because too many files have changed in this diff Show More