Compare commits
55 Commits
kurt-speed
...
v0.20.1
Author | SHA1 | Date | |
---|---|---|---|
6959036688 | |||
570d0473c6 | |||
44f0d7c25c | |||
3ccb04c4e7 | |||
00058f699a | |||
5a478fe0b3 | |||
723cf4f746 | |||
3950de0a4d | |||
901d474986 | |||
e7ab645267 | |||
cf830f9895 | |||
2c1f53f0f0 | |||
d39e2502d0 | |||
51fed9c541 | |||
b3a09abe01 | |||
cd3a2fea07 | |||
c29c4a8567 | |||
39ccd94884 | |||
d99ab22b56 | |||
20a8f2aa6a | |||
93266a9819 | |||
a9c7a7cb13 | |||
8dd9b8d192 | |||
23181d8144 | |||
834967df6a | |||
deacaac33a | |||
c55603853b | |||
93f652647e | |||
67cea620a6 | |||
ed0c7d038d | |||
d3aa789761 | |||
cd68f80b71 | |||
d341681c0d | |||
0578e9d2a1 | |||
b413538e9e | |||
c4e7754fc5 | |||
94515b5490 | |||
aa52407fda | |||
e45be831d0 | |||
005944f3a3 | |||
755ef8ce7f | |||
005d1f0ca7 | |||
e158f6f513 | |||
879d7ec4f4 | |||
f6838b9b14 | |||
cb75c47631 | |||
9b95ec1083 | |||
a3eeff65c8 | |||
fab3d2b130 | |||
0a96dc6fd2 | |||
e123a00d4b | |||
b950cc0583 | |||
c89780a489 | |||
1afed68dd7 | |||
dcbed4f06f |
@ -3,3 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
|||||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
|
VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||||
|
33
.github/workflows/build-and-store-wasm.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Build and Store WASM
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-upload:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Cache wasm
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
- name: build wasm
|
||||||
|
run: yarn build:wasm
|
||||||
|
|
||||||
|
|
||||||
|
# Upload the WASM bundle as an artifact
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wasm-bundle
|
||||||
|
path: src/wasm-lib/pkg
|
17
.github/workflows/cargo-clippy.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib']
|
dir: ['src/wasm-lib', 'src-tauri']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
@ -31,9 +31,22 @@ jobs:
|
|||||||
|
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
if: matrix.dir == 'src-tauri'
|
if: matrix.dir == 'src-tauri'
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
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
|
||||||
|
yarn install
|
||||||
|
yarn build:wasm
|
||||||
|
yarn build:local
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
|
57
.github/workflows/cargo-test-tauri.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src-tauri/**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-test-tauri.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src-tauri/**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-test-tauri.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions: read-all
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
name: cargo test of tauri
|
||||||
|
jobs:
|
||||||
|
cargotest:
|
||||||
|
name: cargo test
|
||||||
|
runs-on: ubuntu-latest-8-cores
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dir: ['src-tauri']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install latest rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- name: install dependencies
|
||||||
|
if: matrix.dir == 'src-tauri'
|
||||||
|
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: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
- name: cargo test
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
cargo test --all
|
135
.github/workflows/ci.yml
vendored
@ -13,7 +13,7 @@ on:
|
|||||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || 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:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
@ -50,7 +50,7 @@ jobs:
|
|||||||
- run: yarn tsc
|
- run: yarn tsc
|
||||||
|
|
||||||
|
|
||||||
check-typos:
|
check-typos:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -98,7 +98,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Set nightly version
|
- name: Set nightly version
|
||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
run: |
|
run: |
|
||||||
@ -130,7 +130,9 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [macos-14, ubuntu-latest, windows-latest]
|
os: [macos-14, ubuntu-latest, windows-latest]
|
||||||
env:
|
env:
|
||||||
|
# Specific Apple Universal target for macos
|
||||||
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
|
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' || '' }}
|
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -143,21 +145,21 @@ jobs:
|
|||||||
ls -l artifact
|
ls -l artifact
|
||||||
cp artifact/package.json package.json
|
cp artifact/package.json package.json
|
||||||
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.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
|
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
- name: Install ubuntu system dependencies
|
- name: Install ubuntu system dependencies
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: >
|
run: |
|
||||||
sudo apt-get update &&
|
sudo apt-get update
|
||||||
sudo apt-get install -y
|
sudo apt-get install -y \
|
||||||
libgtk-3-dev
|
libgtk-3-dev \
|
||||||
libayatana-appindicator3-dev
|
libayatana-appindicator3-dev \
|
||||||
webkit2gtk-driver
|
webkit2gtk-driver \
|
||||||
libsoup-3.0-dev
|
libsoup-3.0-dev \
|
||||||
libjavascriptcoregtk-4.1-dev
|
libjavascriptcoregtk-4.1-dev \
|
||||||
libwebkit2gtk-4.1-dev
|
libwebkit2gtk-4.1-dev \
|
||||||
at-spi2-core
|
at-spi2-core \
|
||||||
xvfb
|
xvfb
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@ -237,6 +239,96 @@ jobs:
|
|||||||
includeDebug: true
|
includeDebug: true
|
||||||
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||||
|
|
||||||
|
- name: Mac App Store
|
||||||
|
if: ${{ env.BUILD_RELEASE == 'true' && 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 app to TestFlight'
|
||||||
|
uses: apple-actions/upload-testflight-build@v1
|
||||||
|
if: ${{ env.BUILD_RELEASE == 'true' && 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 App Store
|
||||||
|
if: ${{ env.BUILD_RELEASE == 'true' && 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
|
- name: Build the app (release) and sign
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||||
@ -261,11 +353,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||||
|
|
||||||
# TODO: re-enable linux e2e tests when possible
|
|
||||||
- name: Run e2e tests (linux only)
|
- name: Run e2e tests (linux only)
|
||||||
if: false
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
cargo install tauri-driver
|
cargo install tauri-driver --force
|
||||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||||
export VITE_KC_API_BASE_URL
|
export VITE_KC_API_BASE_URL
|
||||||
xvfb-run yarn test:e2e:tauri
|
xvfb-run yarn test:e2e:tauri
|
||||||
@ -383,7 +474,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: 'artifact/*/Zoo*'
|
files: 'artifact/*/Zoo*'
|
||||||
|
|
||||||
announce_release:
|
announce_release:
|
||||||
needs: [publish-apps-release]
|
needs: [publish-apps-release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -391,17 +482,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install requests
|
pip install requests
|
||||||
|
|
||||||
- name: Announce Release
|
- name: Announce Release
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
37
.github/workflows/create-release.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Create Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: read
|
||||||
|
if: contains(github.event.head_commit.message, 'Cut release v')
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v7
|
||||||
|
name: Read Cut release PR info and create release
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo
|
||||||
|
const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
commit_sha: context.sha,
|
||||||
|
})
|
||||||
|
const { title, body } = pulls.data[0]
|
||||||
|
const version = title.split('Cut release ')[1]
|
||||||
|
|
||||||
|
const result = await github.rest.repos.createRelease({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
body,
|
||||||
|
tag_name: version,
|
||||||
|
name: version,
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
console.log(result)
|
79
.github/workflows/playwright.yml
vendored
@ -12,11 +12,31 @@ concurrency:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
|
check-rust-changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
rust-changed: ${{ steps.filter.outputs.rust }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- id: filter
|
||||||
|
name: Check for Rust changes
|
||||||
|
uses: dorny/paths-filter@v3
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
rust:
|
||||||
|
- 'src/wasm-lib/**'
|
||||||
|
|
||||||
playwright-ubuntu:
|
playwright-ubuntu:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest-8-cores
|
runs-on: ubuntu-latest-8-cores
|
||||||
|
needs: check-rust-changes
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@ -28,13 +48,38 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Download Wasm Cache
|
||||||
|
id: download-wasm
|
||||||
|
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||||
|
uses: dawidd6/action-download-artifact@v3
|
||||||
|
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
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cache wasm
|
- name: Cache Wasm (because rust diff)
|
||||||
|
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src/wasm-lib'
|
workspaces: './src/wasm-lib'
|
||||||
- name: build wasm
|
- 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: 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
|
run: yarn build:wasm
|
||||||
- name: build web
|
- name: build web
|
||||||
run: yarn build:local
|
run: yarn build:local
|
||||||
@ -89,6 +134,7 @@ jobs:
|
|||||||
playwright-macos:
|
playwright-macos:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
|
needs: check-rust-changes
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@ -99,13 +145,38 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Download Wasm Cache
|
||||||
|
id: download-wasm
|
||||||
|
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||||
|
uses: dawidd6/action-download-artifact@v3
|
||||||
|
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
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cache wasm
|
- name: Cache Wasm (because rust diff)
|
||||||
|
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src/wasm-lib'
|
workspaces: './src/wasm-lib'
|
||||||
- name: build wasm
|
- 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: 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
|
run: yarn build:wasm
|
||||||
- name: build web
|
- name: build web
|
||||||
run: yarn build:local
|
run: yarn build:local
|
||||||
|
1
.gitignore
vendored
@ -54,3 +54,4 @@ src/**/*.typegen.ts
|
|||||||
src-tauri/gen
|
src-tauri/gen
|
||||||
|
|
||||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||||
|
Mac_App_Distribution.provisionprofile
|
||||||
|
12
README.md
@ -59,6 +59,10 @@ followed by:
|
|||||||
```
|
```
|
||||||
yarn build:wasm-dev
|
yarn build:wasm-dev
|
||||||
```
|
```
|
||||||
|
or if you have the gh cli installed
|
||||||
|
```
|
||||||
|
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
|
||||||
|
```
|
||||||
|
|
||||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||||
|
|
||||||
@ -68,7 +72,13 @@ finally, to run the web app only, run:
|
|||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Developing in Chrome
|
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens ofcourse, then navigate to localhost:3000 again. Note that navigating to localhost:3000/signin removes your token so you will need to set the token again.
|
||||||
|
|
||||||
|
### Development environment variables
|
||||||
|
|
||||||
|
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
|
||||||
|
|
||||||
|
### Developing in Chrome
|
||||||
|
|
||||||
Chrome is in the process of rolling out a new default which
|
Chrome is in the process of rolling out a new default which
|
||||||
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
|
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
|
||||||
|
@ -1068,7 +1068,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthX('seg01', 10, %),\n 5\n ], %)\n |> close(%)"
|
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthX('seg01', 10, %),\n 5\n ], %)\n |> close(%)\n |> extrude(5, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2074,7 +2074,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthY('seg01', 10, %),\n 5\n ], %)\n |> close(%)"
|
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthY('seg01', 10, %),\n 5\n ], %)\n |> close(%)\n |> extrude(5, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -22380,8 +22380,8 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)",
|
"startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)",
|
||||||
"startSketchOn('YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%, \"edge1\")"
|
"startSketchOn('YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%, \"edge1\")\n |> extrude(10, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -43695,7 +43695,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"fn rectShape = (pos, w, l) => {\n const rr = startSketchOn('YZ')\n |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)\n |> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, \"edge1\")\n |> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, \"edge2\")\n |> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, \"edge3\")\n |> close(%, \"edge4\")\n return rr\n}\n\n// Create the mounting plate extrusion, holes, and fillets\nconst part = rectShape([0, 0], 20, 20)"
|
"fn rectShape = (pos, w, l) => {\n const rr = startSketchOn('YZ')\n |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)\n |> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, \"edge1\")\n |> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, \"edge2\")\n |> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, \"edge3\")\n |> close(%, \"edge4\")\n return rr\n}\n\n// Create the mounting plate extrusion, holes, and fillets\nconst part = rectShape([0, 0], 20, 20)\n |> extrude(10, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -45900,7 +45900,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternCircular2d({\n center: [20, 20],\n repetitions: 12,\n arcDegrees: 210,\n rotateDuplicates: true\n }, %)"
|
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternCircular2d({\n center: [20, 20],\n repetitions: 12,\n arcDegrees: 210,\n rotateDuplicates: true\n }, %)\n |> extrude(1, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50459,7 +50459,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternLinear2d({\n axis: [0, 1],\n repetitions: 12,\n distance: 2\n }, %)"
|
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternLinear2d({\n axis: [0, 1],\n repetitions: 12,\n distance: 2\n }, %)\n |> extrude(1, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -59318,7 +59318,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)"
|
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -60312,7 +60312,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchAt([0, 0])\n |> line([10, 10], %)"
|
"startSketchAt([0, 0])\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")\n |> extrude(10, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -61585,7 +61585,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")",
|
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")\n |> extrude(10, %)",
|
||||||
"fn cube = (pos, scale) => {\n const sg = startSketchOn('XY')\n |> startProfileAt(pos, %)\n |> line([0, scale], %)\n |> line([scale, 0], %)\n |> line([0, -scale], %)\n |> close(%)\n |> extrude(scale, %)\n\n return sg\n}\n\nconst box = cube([0, 0], 20)\n\nconst part001 = startSketchOn(box, \"start\")\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%)\n |> extrude(20, %)"
|
"fn cube = (pos, scale) => {\n const sg = startSketchOn('XY')\n |> startProfileAt(pos, %)\n |> line([0, scale], %)\n |> line([scale, 0], %)\n |> line([0, -scale], %)\n |> close(%)\n |> extrude(scale, %)\n\n return sg\n}\n\nconst box = cube([0, 0], 20)\n\nconst part001 = startSketchOn(box, \"start\")\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%)\n |> extrude(20, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -65584,7 +65584,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('-YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %, \"edge0\")\n |> tangentialArcTo([10, 0], %)\n |> close(%)"
|
"startSketchOn('-YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %, \"edge0\")\n |> tangentialArcTo([10, 0], %)\n |> close(%)\n |> extrude(10, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { getUtils } from './test-utils'
|
import { makeTemplate, getUtils } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
@ -8,7 +8,8 @@ import {
|
|||||||
TEST_SETTINGS,
|
TEST_SETTINGS,
|
||||||
TEST_SETTINGS_KEY,
|
TEST_SETTINGS_KEY,
|
||||||
TEST_SETTINGS_CORRUPTED,
|
TEST_SETTINGS_CORRUPTED,
|
||||||
TEST_SETTINGS_ONBOARDING,
|
TEST_SETTINGS_ONBOARDING_EXPORT,
|
||||||
|
TEST_SETTINGS_ONBOARDING_START,
|
||||||
} from './storageStates'
|
} from './storageStates'
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
|
|
||||||
@ -278,7 +279,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
|||||||
const bottomAng = 25
|
const bottomAng = 25
|
||||||
*/
|
*/
|
||||||
await page.click('.cm-content')
|
await page.click('.cm-content')
|
||||||
await page.keyboard.type('# error')
|
await page.keyboard.type('$ error')
|
||||||
|
|
||||||
// press arrows to clear autocomplete
|
// press arrows to clear autocomplete
|
||||||
await page.keyboard.press('ArrowLeft')
|
await page.keyboard.press('ArrowLeft')
|
||||||
@ -295,10 +296,10 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
|||||||
|
|
||||||
// error text on hover
|
// error text on hover
|
||||||
await page.hover('.cm-lint-marker-error')
|
await page.hover('.cm-lint-marker-error')
|
||||||
await expect(page.getByText("found unknown token '#'")).toBeVisible()
|
await expect(page.getByText("found unknown token '$'")).toBeVisible()
|
||||||
|
|
||||||
// select the line that's causing the error and delete it
|
// select the line that's causing the error and delete it
|
||||||
await page.getByText('# error').click()
|
await page.getByText('$ error').click()
|
||||||
await page.keyboard.press('End')
|
await page.keyboard.press('End')
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.keyboard.press('Home')
|
await page.keyboard.press('Home')
|
||||||
@ -596,13 +597,12 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
|
|
||||||
test('Stored settings are validated and fall back to defaults', async ({
|
test('Stored settings are validated and fall back to defaults', async ({
|
||||||
page,
|
page,
|
||||||
context,
|
|
||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
// Override beforeEach test setup
|
// Override beforeEach test setup
|
||||||
// with corrupted settings
|
// with corrupted settings
|
||||||
await context.addInitScript(
|
await page.addInitScript(
|
||||||
async ({ settingsKey, settings }) => {
|
async ({ settingsKey, settings }) => {
|
||||||
localStorage.setItem(settingsKey, settings)
|
localStorage.setItem(settingsKey, settings)
|
||||||
},
|
},
|
||||||
@ -619,18 +619,18 @@ test('Stored settings are validated and fall back to defaults', async ({
|
|||||||
// Check the settings were reset
|
// Check the settings were reset
|
||||||
const storedSettings = TOML.parse(
|
const storedSettings = TOML.parse(
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
|
||||||
{ settingsKey: TEST_SETTINGS_KEY }
|
{ settingsKey: TEST_SETTINGS_KEY }
|
||||||
)
|
)
|
||||||
) as { settings: SaveSettingsPayload }
|
) as { settings: SaveSettingsPayload }
|
||||||
|
|
||||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
expect(storedSettings.settings?.app?.theme).toBe(undefined)
|
||||||
|
|
||||||
// Check that the invalid settings were removed
|
// Check that the invalid settings were removed
|
||||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
|
||||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
|
||||||
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
|
||||||
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Project settings can be set and override user settings', async ({
|
test('Project settings can be set and override user settings', async ({
|
||||||
@ -681,6 +681,45 @@ test('Project settings can be set and override user settings', async ({
|
|||||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
|
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Click through each onboarding step', async ({ page }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
|
||||||
|
// Override beforeEach test setup
|
||||||
|
await page.addInitScript(
|
||||||
|
async ({ settingsKey, settings }) => {
|
||||||
|
// Give no initial code, so that the onboarding start is shown immediately
|
||||||
|
localStorage.setItem('persistCode', '')
|
||||||
|
localStorage.setItem(settingsKey, settings)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingsKey: TEST_SETTINGS_KEY,
|
||||||
|
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 1080 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// Test that the onboarding pane loaded
|
||||||
|
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
|
||||||
|
|
||||||
|
const nextButton = page.getByTestId('onboarding-next')
|
||||||
|
|
||||||
|
while ((await nextButton.innerText()) !== 'Finish') {
|
||||||
|
await expect(nextButton).toBeVisible()
|
||||||
|
await nextButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish the onboarding
|
||||||
|
await expect(nextButton).toBeVisible()
|
||||||
|
await nextButton.click()
|
||||||
|
|
||||||
|
// Test that the onboarding pane is gone
|
||||||
|
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
||||||
|
await expect(page.url()).not.toContain('onboarding')
|
||||||
|
})
|
||||||
|
|
||||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
@ -693,7 +732,7 @@ test('Onboarding redirects and code updating', async ({ page }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
settingsKey: TEST_SETTINGS_KEY,
|
settingsKey: TEST_SETTINGS_KEY,
|
||||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING }),
|
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1650,14 +1689,13 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
const result = makeTemplate`const part002 = startSketchOn(part001, 'seg01')
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
|> startProfileAt([-12.83, 6.7], %)
|
||||||
|> startProfileAt([-12.83, 6.7], %)
|
|> line([${[2.28, 2.35]}, -${0.07}], %)
|
||||||
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
|
|> line([-3.05, -1.47], %)
|
||||||
process?.env?.CI ? 0.07 : 0.07
|
|> close(%)`
|
||||||
}], %)
|
|
||||||
|> line([-3.05, -1.47], %)
|
await expect(page.locator('.cm-content')).toHaveText(result.regExp)
|
||||||
|> close(%)`)
|
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -1676,15 +1714,9 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
const result2 = result.genNext`
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
|> extrude(${[5, 5]} + 7, %)`
|
||||||
|> startProfileAt([-12.83, 6.7], %)
|
await expect(page.locator('.cm-content')).toHaveText(result2.regExp)
|
||||||
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
|
|
||||||
process?.env?.CI ? 0.07 : 0.07
|
|
||||||
}], %)
|
|
||||||
|> line([-3.05, -1.47], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(5 + 7, %)`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can code mod a line length', async ({ page }) => {
|
test('Can code mod a line length', async ({ page }) => {
|
||||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@ -1,7 +1,7 @@
|
|||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
|
||||||
export const TEST_SETTINGS_KEY = '/user.toml'
|
export const TEST_SETTINGS_KEY = '/settings.toml'
|
||||||
export const TEST_SETTINGS = {
|
export const TEST_SETTINGS = {
|
||||||
app: {
|
app: {
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
@ -22,9 +22,14 @@ export const TEST_SETTINGS = {
|
|||||||
},
|
},
|
||||||
} satisfies Partial<SaveSettingsPayload>
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
|
|
||||||
export const TEST_SETTINGS_ONBOARDING = {
|
export const TEST_SETTINGS_ONBOARDING_EXPORT = {
|
||||||
...TEST_SETTINGS,
|
...TEST_SETTINGS,
|
||||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
|
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
|
||||||
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
|
|
||||||
|
export const TEST_SETTINGS_ONBOARDING_START = {
|
||||||
|
...TEST_SETTINGS,
|
||||||
|
app: { ...TEST_SETTINGS.app, onboardingStatus: '' },
|
||||||
} satisfies Partial<SaveSettingsPayload>
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
|
|
||||||
export const TEST_SETTINGS_CORRUPTED = {
|
export const TEST_SETTINGS_CORRUPTED = {
|
||||||
|
@ -182,3 +182,76 @@ export function getUtils(page: Page) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TemplateOptions = Array<number | Array<number>>
|
||||||
|
|
||||||
|
type makeTemplateReturn = {
|
||||||
|
regExp: RegExp
|
||||||
|
genNext: (
|
||||||
|
templateParts: TemplateStringsArray,
|
||||||
|
...options: TemplateOptions
|
||||||
|
) => makeTemplateReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeRegExp = (string: string) => {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
const _makeTemplate = (
|
||||||
|
templateParts: TemplateStringsArray,
|
||||||
|
...options: TemplateOptions
|
||||||
|
) => {
|
||||||
|
const length = Math.max(...options.map((a) => (Array.isArray(a) ? a[0] : 0)))
|
||||||
|
let reExpTemplate = ''
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const currentStr = templateParts.map((str, index) => {
|
||||||
|
const currentOptions = options[index]
|
||||||
|
return (
|
||||||
|
escapeRegExp(str) +
|
||||||
|
String(
|
||||||
|
Array.isArray(currentOptions)
|
||||||
|
? currentOptions[i]
|
||||||
|
: typeof currentOptions === 'number'
|
||||||
|
? currentOptions
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
reExpTemplate += '|' + currentStr.join('')
|
||||||
|
}
|
||||||
|
return new RegExp(reExpTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool for making templates to match code snippets in the editor with some fudge factor,
|
||||||
|
* as there's some level of non-determinism.
|
||||||
|
*
|
||||||
|
* Usage is as such:
|
||||||
|
* ```typescript
|
||||||
|
* const result = makeTemplate`const myVar = aFunc(${[1, 2, 3]})`
|
||||||
|
* await expect(page.locator('.cm-content')).toHaveText(result.regExp)
|
||||||
|
* ```
|
||||||
|
* Where the value `1`, `2` or `3` are all valid and should make the test pass.
|
||||||
|
*
|
||||||
|
* The function also has a `genNext` function that allows you to chain multiple templates
|
||||||
|
* together without having to repeat previous parts of the template.
|
||||||
|
* ```typescript
|
||||||
|
* const result2 = result.genNext`const myVar2 = aFunc(${[4, 5, 6]})`
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const makeTemplate: (
|
||||||
|
templateParts: TemplateStringsArray,
|
||||||
|
...values: TemplateOptions
|
||||||
|
) => makeTemplateReturn = (templateParts, ...options) => {
|
||||||
|
return {
|
||||||
|
regExp: _makeTemplate(templateParts, ...options),
|
||||||
|
genNext: (
|
||||||
|
nextTemplateParts: TemplateStringsArray,
|
||||||
|
...nextOptions: TemplateOptions
|
||||||
|
) =>
|
||||||
|
makeTemplate(
|
||||||
|
[...templateParts, ...nextTemplateParts] as any as TemplateStringsArray,
|
||||||
|
[...options, ...nextOptions] as any
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { browser, $, expect } from '@wdio/globals'
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
const documentsDir = `${process.env.HOME}/Documents`
|
const documentsDir = `${process.env.HOME}/Documents`
|
||||||
const userSettingsFile = `${process.env.HOME}/.config/dev.zoo.modeling-app/user.toml`
|
const userSettingsDir = `${process.env.HOME}/.config/dev.zoo.modeling-app`
|
||||||
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
|
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
|
||||||
const newProjectDir = `${documentsDir}/a-different-directory`
|
const newProjectDir = `${documentsDir}/a-different-directory`
|
||||||
const userCodeDir = '/tmp/kittycad_user_code'
|
const userCodeDir = '/tmp/kittycad_user_code'
|
||||||
@ -29,8 +29,10 @@ describe('ZMA (Tauri, Linux)', () => {
|
|||||||
// Clean up filesystem from previous tests
|
// Clean up filesystem from previous tests
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
await fs.rm(defaultProjectDir, { force: true, recursive: true })
|
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(userCodeDir, { force: true })
|
||||||
await fs.rm(userSettingsFile, { force: true })
|
await fs.rm(userSettingsDir, { force: true, recursive: true })
|
||||||
|
await fs.mkdir(defaultProjectDir, { recursive: true })
|
||||||
await fs.mkdir(newProjectDir, { recursive: true })
|
await fs.mkdir(newProjectDir, { recursive: true })
|
||||||
|
|
||||||
const signInButton = await $('[data-testid="sign-in-button"]')
|
const signInButton = await $('[data-testid="sign-in-button"]')
|
||||||
@ -70,8 +72,9 @@ describe('ZMA (Tauri, Linux)', () => {
|
|||||||
console.log(cr.status)
|
console.log(cr.status)
|
||||||
|
|
||||||
// Now should be signed in
|
// Now should be signed in
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10000))
|
||||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||||
expect(await newFileButton.getText()).toEqual('New file')
|
expect(await newFileButton.getText()).toEqual('New project')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||||
@ -117,8 +120,8 @@ describe('ZMA (Tauri, Linux)', () => {
|
|||||||
it('opens the new file and expects a loading stream', async () => {
|
it('opens the new file and expects a loading stream', async () => {
|
||||||
const projectLink = await $('[data-testid="project-link"]')
|
const projectLink = await $('[data-testid="project-link"]')
|
||||||
await click(projectLink)
|
await click(projectLink)
|
||||||
const loadingText = await $('[data-testid="loading-stream"]')
|
const errorText = await $('[data-testid="unexpected-error"]')
|
||||||
expect(await loadingText.getText()).toContain('Loading stream...')
|
expect(await errorText.getText()).toContain('unexpected error')
|
||||||
await browser.execute('window.location.href = "tauri://localhost/home"')
|
await browser.execute('window.location.href = "tauri://localhost/home"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
24
get-latest-wasm-bundle.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Set the repository owner and name
|
||||||
|
REPO_OWNER="KittyCAD"
|
||||||
|
REPO_NAME="modeling-app"
|
||||||
|
WORKFLOW_NAME="build-and-store-wasm.yml"
|
||||||
|
ARTIFACT_NAME="wasm-bundle"
|
||||||
|
|
||||||
|
# Fetch the latest completed workflow run ID for the specified workflow
|
||||||
|
# RUN_ID=$(gh api repos/$REPO_OWNER/$REPO_NAME/actions/workflows/$WORKFLOW_NAME/runs --paginate --jq '.workflow_runs[] | select(.status=="completed") | .id' | head -n 1)
|
||||||
|
RUN_ID=$(gh api repos/$REPO_OWNER/$REPO_NAME/actions/workflows/$WORKFLOW_NAME/runs --paginate --jq '.workflow_runs[] | select(.status=="completed" and .conclusion=="success") | .id' | head -n 1)
|
||||||
|
|
||||||
|
echo $RUN_ID
|
||||||
|
|
||||||
|
# Check if a valid RUN_ID was found
|
||||||
|
if [ -z "$RUN_ID" ]; then
|
||||||
|
echo "Failed to find a workflow run for $WORKFLOW_NAME."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh run download $RUN_ID --repo $REPO_OWNER/$REPO_NAME --name $ARTIFACT_NAME --dir ./src/wasm-lib/pkg
|
||||||
|
|
||||||
|
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||||
|
echo "latest wasm copied to public folder"
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.18.1",
|
"version": "0.20.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.16.0",
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.19",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.58",
|
"@kittycad/lib": "^0.0.60",
|
||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
@ -86,6 +86,7 @@
|
|||||||
"simpleserver": "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",
|
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
||||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
||||||
|
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||||
@ -122,6 +123,7 @@
|
|||||||
"@tauri-apps/cli": "^2.0.0-beta.13",
|
"@tauri-apps/cli": "^2.0.0-beta.13",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/debounce-promise": "^3.1.9",
|
"@types/debounce-promise": "^3.1.9",
|
||||||
|
"@types/mocha": "^10.0.6",
|
||||||
"@types/pixelmatch": "^5.2.6",
|
"@types/pixelmatch": "^5.2.6",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/react-modal": "^3.16.3",
|
"@types/react-modal": "^3.16.3",
|
||||||
|
@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 3 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 2 : 1,
|
workers: process.env.CI ? 1 : 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
@ -27,7 +27,7 @@ export default defineConfig({
|
|||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
|
15
public/.well-known/apple-app-site-association
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appIDs": ["92H8YB3B95.dev.zoo.modeling-app"],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"/": "/file/*",
|
||||||
|
"comment": "Matches any URL whose path starts with /file/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
public/onboarding-bracket-dimensions-dark.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
public/onboarding-bracket-dimensions.png
Normal file
After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 867 KiB |
2728
src-tauri/Cargo.lock
generated
@ -1,14 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "The Zoo Modeling App"
|
||||||
authors = ["you"]
|
authors = ["Zoo Engineers <eng@zoo.dev>"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
default-run = "app"
|
default-run = "app"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@ -16,23 +15,30 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
|
||||||
kittycad = "0.3.0"
|
kittycad = "0.3.0"
|
||||||
|
log = "0.4.21"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||||
|
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
||||||
|
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
|
||||||
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||||
|
tauri-plugin-log = { version = "2.0.0-beta.4" }
|
||||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||||
tokio = { version = "1.37.0", features = ["time"] }
|
tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.2"
|
||||||
|
url = "2.5.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["updater"]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||||
# DO NOT REMOVE!!
|
# DO NOT REMOVE!!
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
updater = []
|
||||||
|
376
src-tauri/Info.plist
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
<?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>
|
@ -7,6 +7,9 @@
|
|||||||
"main"
|
"main"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"cli:default",
|
||||||
|
"deep-link:default",
|
||||||
|
"log:default",
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
"window:default",
|
"window:default",
|
||||||
@ -23,7 +26,6 @@
|
|||||||
"fs:allow-copy-file",
|
"fs:allow-copy-file",
|
||||||
"fs:allow-mkdir",
|
"fs:allow-mkdir",
|
||||||
"fs:allow-remove",
|
"fs:allow-remove",
|
||||||
"fs:allow-remove",
|
|
||||||
"fs:allow-rename",
|
"fs:allow-rename",
|
||||||
"fs:allow-exists",
|
"fs:allow-exists",
|
||||||
"fs:allow-stat",
|
"fs:allow-stat",
|
||||||
|
24
src-tauri/entitlements/app-store.entitlements
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?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>
|
6
src-tauri/rustfmt.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
max_width = 120
|
||||||
|
edition = "2018"
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
format_strings = false
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
group_imports = "StdExternalCrate"
|
@ -1,98 +1,232 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use std::env;
|
pub(crate) mod state;
|
||||||
use std::fs;
|
|
||||||
use std::io::Read;
|
use std::{
|
||||||
use std::path::Path;
|
env,
|
||||||
use std::path::PathBuf;
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use kcl_lib::settings::types::{
|
||||||
|
file::{FileEntry, Project, ProjectRoute, ProjectState},
|
||||||
|
project::ProjectConfiguration,
|
||||||
|
Configuration,
|
||||||
|
};
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
use serde::Serialize;
|
use tauri::{ipc::InvokeError, Manager};
|
||||||
use std::process::Command;
|
use tauri_plugin_cli::CliExt;
|
||||||
use tauri::ipc::InvokeError;
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
/// This command returns the a json string parse from a toml file at the path.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_toml(path: &str) -> Result<String, InvokeError> {
|
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||||
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
let dir = match app.path().document_dir() {
|
||||||
let mut contents = String::new();
|
Ok(dir) => dir,
|
||||||
file.read_to_string(&mut contents)
|
Err(_) => {
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
// for headless Linux (eg. Github Actions)
|
||||||
let value =
|
let home_dir = app.path().home_dir()?;
|
||||||
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
home_dir.join("Documents")
|
||||||
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
}
|
||||||
Ok(value)
|
};
|
||||||
|
|
||||||
|
Ok(dir.join(PROJECT_FOLDER))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
||||||
/// Removed from tauri v2
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct DiskEntry {
|
|
||||||
/// The path to the entry.
|
|
||||||
pub path: PathBuf,
|
|
||||||
/// The name of the entry (file name with extension or directory name).
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// The children of this entry if it's a directory.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub children: Option<Vec<DiskEntry>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
||||||
/// Removed from tauri v2
|
|
||||||
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
|
|
||||||
std::fs::metadata(path)
|
|
||||||
.map(|md| md.is_dir())
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
||||||
/// Removed from tauri v2
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
|
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
|
||||||
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
let store = app.state::<state::Store>();
|
||||||
// let path = path.as_ref();
|
Ok(store.get().await)
|
||||||
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
|
}
|
||||||
let path = entry
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
||||||
.path();
|
|
||||||
|
|
||||||
if let Ok(flag) = is_dir(&path) {
|
#[tauri::command]
|
||||||
files_and_dirs.push(DiskEntry {
|
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
|
||||||
path: path.clone(),
|
let store = app.state::<state::Store>();
|
||||||
children: if flag {
|
store.set(state).await;
|
||||||
Some(read_dir_recursive(path.to_str().expect("No path"))?)
|
Ok(())
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
},
|
async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||||
name: path
|
let app_config_dir = app.path().app_config_dir()?;
|
||||||
.file_name()
|
|
||||||
.map(|name| name.to_string_lossy())
|
// Ensure this directory exists.
|
||||||
.map(|name| name.to_string()),
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(files_and_dirs)
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This command returns a string that is the contents of a file at the path.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
|
||||||
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
let settings_path = get_app_settings_file_path(&app).await?;
|
||||||
let mut contents = String::new();
|
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
file.read_to_string(&mut contents)
|
tokio::fs::write(settings_path, contents.as_bytes())
|
||||||
|
.await
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
Ok(contents)
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_project_settings_file_path(
|
||||||
|
app_settings: Configuration,
|
||||||
|
project_name: &str,
|
||||||
|
) -> Result<PathBuf, InvokeError> {
|
||||||
|
let project_dir = app_settings.settings.project.directory.join(project_name);
|
||||||
|
|
||||||
|
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(
|
||||||
|
app_settings: Configuration,
|
||||||
|
project_name: &str,
|
||||||
|
) -> Result<ProjectConfiguration, InvokeError> {
|
||||||
|
let settings_path = get_project_settings_file_path(app_settings, project_name).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(
|
||||||
|
app_settings: Configuration,
|
||||||
|
project_name: &str,
|
||||||
|
configuration: ProjectConfiguration,
|
||||||
|
) -> Result<(), InvokeError> {
|
||||||
|
let settings_path = get_project_settings_file_path(app_settings, project_name).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.
|
/// This command instantiates a new window with auth.
|
||||||
/// The string returned from this method is the access token.
|
/// The string returned from this method is the access token.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
|
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
|
||||||
println!("Logging in...");
|
log::debug!("Logging in...");
|
||||||
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
// 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"))
|
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
@ -103,8 +237,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
let auth_client = oauth2::basic::BasicClient::new(
|
let auth_client = oauth2::basic::BasicClient::new(
|
||||||
oauth2::ClientId::new(client_id),
|
oauth2::ClientId::new(client_id),
|
||||||
None,
|
None,
|
||||||
oauth2::AuthUrl::new(format!("{host}/authorize"))
|
oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
|
||||||
Some(
|
Some(
|
||||||
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
@ -132,12 +265,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
// and bypass the shell::open call as it fails on GitHub Actions.
|
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||||
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||||
if e2e_tauri_enabled {
|
if e2e_tauri_enabled {
|
||||||
println!(
|
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
||||||
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||||
auth_uri.secret()
|
.await
|
||||||
);
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
|
||||||
.expect("Unable to write /tmp/kittycad_user_code file");
|
|
||||||
} else {
|
} else {
|
||||||
app.shell()
|
app.shell()
|
||||||
.open(auth_uri.secret(), None)
|
.open(auth_uri.secret(), None)
|
||||||
@ -160,10 +291,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
///This command returns the KittyCAD user info given a 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.
|
/// The string returned from this method is the user info as a json string.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_user(
|
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
|
||||||
token: Option<String>,
|
|
||||||
hostname: &str,
|
|
||||||
) -> Result<kittycad::types::User, InvokeError> {
|
|
||||||
// Use the host passed in if it's set.
|
// Use the host passed in if it's set.
|
||||||
// Otherwise, use the default host.
|
// Otherwise, use the default host.
|
||||||
let host = if hostname.is_empty() {
|
let host = if hostname.is_empty() {
|
||||||
@ -180,10 +308,10 @@ async fn get_user(
|
|||||||
baseurl = format!("http://{host}")
|
baseurl = format!("http://{host}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Getting user info...");
|
log::debug!("Getting user info...");
|
||||||
|
|
||||||
// use kittycad library to fetch the user info from /user/me
|
// use kittycad library to fetch the user info from /user/me
|
||||||
let mut client = kittycad::Client::new(token.unwrap());
|
let mut client = kittycad::Client::new(token);
|
||||||
|
|
||||||
if baseurl != DEFAULT_HOST {
|
if baseurl != DEFAULT_HOST {
|
||||||
client.set_base_url(&baseurl);
|
client.set_base_url(&baseurl);
|
||||||
@ -202,50 +330,186 @@ async fn get_user(
|
|||||||
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
/// 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.
|
/// But with the Linux support removed since we don't need it for now.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn show_in_folder(path: String) {
|
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
Command::new("explorer")
|
Command::new("explorer")
|
||||||
.args(["/select,", &path]) // The comma after select is not a typo
|
.args(["/select,", path]) // The comma after select is not a typo
|
||||||
.spawn()
|
.spawn()
|
||||||
.unwrap();
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
Command::new("open")
|
||||||
|
.args(["-R", path])
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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() {
|
fn main() -> Result<()> {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|_app| {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
use tauri::Manager;
|
|
||||||
_app.get_webview("main").unwrap().open_devtools();
|
|
||||||
}
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
{
|
|
||||||
_app.handle()
|
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
.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,
|
get_user,
|
||||||
login,
|
login,
|
||||||
read_toml,
|
|
||||||
read_txt_file,
|
|
||||||
read_dir_recursive,
|
read_dir_recursive,
|
||||||
show_in_folder,
|
show_in_folder,
|
||||||
|
read_app_settings_file,
|
||||||
|
write_app_settings_file,
|
||||||
|
read_project_settings_file,
|
||||||
|
write_project_settings_file,
|
||||||
])
|
])
|
||||||
|
.plugin(tauri_plugin_cli::init())
|
||||||
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_http::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_os::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.run(tauri::generate_context!())
|
.setup(|app| {
|
||||||
.expect("error while running tauri application");
|
// 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(())
|
||||||
}
|
}
|
||||||
|
21
src-tauri/src/state.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//! 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;
|
||||||
|
}
|
||||||
|
}
|
8
src-tauri/tauri.app-store.conf.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"bundle": {
|
||||||
|
"macOS": {
|
||||||
|
"entitlements": "entitlements/app-store.entitlements"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,23 +37,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {},
|
||||||
"entitlements": null,
|
|
||||||
"exceptionDomain": "",
|
|
||||||
"frameworks": [],
|
|
||||||
"providerShortName": null,
|
|
||||||
"signingIdentity": null
|
|
||||||
},
|
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"targets": "all"
|
"targets": "all"
|
||||||
},
|
},
|
||||||
"identifier": "dev.zoo.modeling-app",
|
"identifier": "dev.zoo.modeling-app",
|
||||||
"plugins": {
|
"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": {
|
||||||
|
"domains": [
|
||||||
|
{
|
||||||
|
"host": "app.zoo.dev"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.18.1"
|
"version": "0.20.1"
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,45 @@
|
|||||||
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": "http://timestamp.digicert.com"
|
"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": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
|
@ -30,6 +30,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
|||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
|
import { getState, setState } from 'lib/tauri'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -52,10 +53,29 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: () =>
|
loader: async () => {
|
||||||
isTauri()
|
const inTauri = isTauri()
|
||||||
|
if (inTauri) {
|
||||||
|
const appState = await getState()
|
||||||
|
|
||||||
|
if (appState) {
|
||||||
|
// Reset the state.
|
||||||
|
// We do this so that we load the initial state from the cli but everything
|
||||||
|
// else we can ignore.
|
||||||
|
await setState(undefined)
|
||||||
|
// Redirect to the file if we have a file path.
|
||||||
|
if (appState.current_file) {
|
||||||
|
return redirect(
|
||||||
|
paths.FILE + '/' + encodeURIComponent(appState.current_file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inTauri
|
||||||
? redirect(paths.HOME)
|
? redirect(paths.HOME)
|
||||||
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
|
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: fileLoader,
|
loader: fileLoader,
|
||||||
|
@ -16,7 +16,7 @@ export const ErrorPage = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
<section className="max-w-full xl:max-w-4xl mx-auto">
|
<section className="max-w-full xl:max-w-4xl mx-auto">
|
||||||
<h1 className="text-4xl mb-8 font-bold">
|
<h1 className="text-4xl mb-8 font-bold" data-testid="unexpected-error">
|
||||||
An unexpected error occurred
|
An unexpected error occurred
|
||||||
</h1>
|
</h1>
|
||||||
{isRouteErrorResponse(error) && (
|
{isRouteErrorResponse(error) && (
|
||||||
@ -26,7 +26,12 @@ export const ErrorPage = () => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex justify-between gap-2 mt-6">
|
<div className="flex justify-between gap-2 mt-6">
|
||||||
{isTauri() && (
|
{isTauri() && (
|
||||||
<ActionButton Element="link" to={'/'} icon={{ icon: faHome }}>
|
<ActionButton
|
||||||
|
Element="link"
|
||||||
|
to={'/'}
|
||||||
|
icon={{ icon: faHome }}
|
||||||
|
data-testid="unexpected-error-home"
|
||||||
|
>
|
||||||
Go Home
|
Go Home
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
@ -15,10 +15,10 @@ import {
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
||||||
import { readProject } from 'lib/tauriFS'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { join, sep } from '@tauri-apps/api/path'
|
import { join, sep } from '@tauri-apps/api/path'
|
||||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||||
|
import { getProjectInfo } from 'lib/tauri'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
|
|||||||
services: {
|
services: {
|
||||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||||
const newFiles = isTauri()
|
const newFiles = isTauri()
|
||||||
? await readProject(context.project.path)
|
? (await getProjectInfo(context.project.path)).children
|
||||||
: []
|
: []
|
||||||
return {
|
return {
|
||||||
...context.project,
|
...context.project,
|
||||||
|
@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { Dispatch, useEffect, useRef, useState } from 'react'
|
import { Dispatch, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
import { Dialog, Disclosure } from '@headlessui/react'
|
import { Dialog, Disclosure } from '@headlessui/react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -133,18 +133,13 @@ const FileTreeItem = ({
|
|||||||
project,
|
project,
|
||||||
currentFile,
|
currentFile,
|
||||||
fileOrDir,
|
fileOrDir,
|
||||||
closePanel,
|
onDoubleClick,
|
||||||
level = 0,
|
level = 0,
|
||||||
}: {
|
}: {
|
||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
currentFile?: IndexLoaderData['file']
|
currentFile?: IndexLoaderData['file']
|
||||||
fileOrDir: FileEntry
|
fileOrDir: FileEntry
|
||||||
closePanel: (
|
onDoubleClick?: () => void
|
||||||
focusableElement?:
|
|
||||||
| HTMLElement
|
|
||||||
| React.MutableRefObject<HTMLElement | null>
|
|
||||||
| undefined
|
|
||||||
) => void
|
|
||||||
level?: number
|
level?: number
|
||||||
}) => {
|
}) => {
|
||||||
const { send, context } = useFileContext()
|
const { send, context } = useFileContext()
|
||||||
@ -186,7 +181,7 @@ const FileTreeItem = ({
|
|||||||
// Open kcl files
|
// Open kcl files
|
||||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||||
}
|
}
|
||||||
closePanel()
|
onDoubleClick?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -194,8 +189,10 @@ const FileTreeItem = ({
|
|||||||
{fileOrDir.children === undefined ? (
|
{fileOrDir.children === undefined ? (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' +
|
'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 ' +
|
||||||
(isCurrentFile ? '!bg-primary/10 !text-primary' : '')
|
(isCurrentFile
|
||||||
|
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
||||||
|
: '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
@ -227,9 +224,9 @@ const FileTreeItem = ({
|
|||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
className={
|
className={
|
||||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5' +
|
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
||||||
(context.selectedDirectory.path.includes(fileOrDir.path)
|
(context.selectedDirectory.path.includes(fileOrDir.path)
|
||||||
? ' ui-open:text-primary'
|
? ' ui-open:bg-primary/10'
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
@ -293,7 +290,7 @@ const FileTreeItem = ({
|
|||||||
fileOrDir={child}
|
fileOrDir={child}
|
||||||
project={project}
|
project={project}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
closePanel={closePanel}
|
onDoubleClick={onDoubleClick}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
key={level + '-' + child.path}
|
key={level + '-' + child.path}
|
||||||
/>
|
/>
|
||||||
@ -325,20 +322,8 @@ interface FileTreeProps {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileTree = ({
|
export const FileTreeMenu = () => {
|
||||||
className = '',
|
const { send } = useFileContext()
|
||||||
file,
|
|
||||||
closePanel,
|
|
||||||
}: FileTreeProps) => {
|
|
||||||
const { send, context } = useFileContext()
|
|
||||||
const docuemntHasFocus = useDocumentHasFocus()
|
|
||||||
useHotkeys('meta + n', createFile)
|
|
||||||
useHotkeys('meta + shift + n', createFolder)
|
|
||||||
|
|
||||||
// Refresh the file tree when the document gets focus
|
|
||||||
useEffect(() => {
|
|
||||||
send({ type: 'Refresh' })
|
|
||||||
}, [docuemntHasFocus])
|
|
||||||
|
|
||||||
async function createFile() {
|
async function createFile() {
|
||||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||||
@ -348,58 +333,88 @@ export const FileTree = ({
|
|||||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys('meta + n', createFile)
|
||||||
|
useHotkeys('meta + shift + n', createFolder)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'filePlus',
|
||||||
|
iconClassName: '!text-current',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
|
onClick={createFile}
|
||||||
|
>
|
||||||
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
|
Create file
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'folderPlus',
|
||||||
|
iconClassName: '!text-current',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
|
onClick={createFolder}
|
||||||
|
>
|
||||||
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
|
Create folder
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||||
<ActionButton
|
<FileTreeMenu />
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'filePlus',
|
|
||||||
iconClassName: '!text-current',
|
|
||||||
bgClassName: 'bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
|
||||||
onClick={createFile}
|
|
||||||
>
|
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
|
||||||
Create file
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'folderPlus',
|
|
||||||
iconClassName: '!text-current',
|
|
||||||
bgClassName: 'bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
|
||||||
onClick={createFolder}
|
|
||||||
>
|
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
|
||||||
Create folder
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto max-h-full pb-12">
|
|
||||||
<ul
|
|
||||||
className="m-0 p-0 text-sm"
|
|
||||||
onClickCapture={(e) => {
|
|
||||||
send({ type: 'Set selected directory', data: context.project })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
|
||||||
<FileTreeItem
|
|
||||||
project={context.project}
|
|
||||||
currentFile={file}
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
closePanel={closePanel}
|
|
||||||
key={fileOrDir.path}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<FileTreeInner onDoubleClick={closePanel} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileTreeInner = ({
|
||||||
|
onDoubleClick,
|
||||||
|
}: {
|
||||||
|
onDoubleClick?: () => void
|
||||||
|
}) => {
|
||||||
|
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
|
const { send, context } = useFileContext()
|
||||||
|
const documentHasFocus = useDocumentHasFocus()
|
||||||
|
|
||||||
|
// Refresh the file tree when the document gets focus
|
||||||
|
useEffect(() => {
|
||||||
|
send({ type: 'Refresh' })
|
||||||
|
}, [documentHasFocus])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto max-h-full pb-12">
|
||||||
|
<ul
|
||||||
|
className="m-0 p-0 text-sm"
|
||||||
|
onClickCapture={(e) => {
|
||||||
|
send({ type: 'Set selected directory', data: context.project })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||||
|
<FileTreeItem
|
||||||
|
project={context.project}
|
||||||
|
currentFile={loaderData?.file}
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
key={fileOrDir.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -94,10 +94,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
if (isInProject) {
|
if (isInProject) {
|
||||||
navigate('onboarding')
|
navigate('onboarding')
|
||||||
} else {
|
} else {
|
||||||
createAndOpenNewProject(
|
createAndOpenNewProject(navigate)
|
||||||
settings.context.app.projectDirectory.current,
|
|
||||||
navigate
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -3,7 +3,7 @@ import type * as LSP from 'vscode-languageserver-protocol'
|
|||||||
import React, { createContext, useMemo, useEffect, useContext } from 'react'
|
import React, { createContext, useMemo, useEffect, useContext } from 'react'
|
||||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||||
import Client from '../editor/plugins/lsp/client'
|
import Client from '../editor/plugins/lsp/client'
|
||||||
import { DEV, TEST } from 'env'
|
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
||||||
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
||||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
@ -85,7 +85,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkStatus()
|
||||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||||
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
baseUnit: defaultUnit.current,
|
baseUnit: defaultUnit.current,
|
||||||
devMode: DEV,
|
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
worker: LspWorker.Kcl,
|
worker: LspWorker.Kcl,
|
||||||
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const initEvent: CopilotWorkerOptions = {
|
const initEvent: CopilotWorkerOptions = {
|
||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
devMode: DEV,
|
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
worker: LspWorker.Copilot,
|
worker: LspWorker.Copilot,
|
||||||
|
@ -56,6 +56,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { EditorSelection } from '@uiw/react-codemirror'
|
import { EditorSelection } from '@uiw/react-codemirror'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
@ -84,7 +85,12 @@ export const ModelingMachineProvider = ({
|
|||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
let [searchParams] = useSearchParams()
|
||||||
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
useSetupEngineManager(streamRef, token, {
|
useSetupEngineManager(streamRef, token, {
|
||||||
|
pool: pool,
|
||||||
theme: theme.current,
|
theme: theme.current,
|
||||||
highlightEdges: highlightEdges.current,
|
highlightEdges: highlightEdges.current,
|
||||||
enableSSAO: enableSSAO.current,
|
enableSSAO: enableSSAO.current,
|
||||||
|
@ -24,6 +24,7 @@ export const ModelingPaneHeader = ({
|
|||||||
|
|
||||||
export const ModelingPane = ({
|
export const ModelingPane = ({
|
||||||
title,
|
title,
|
||||||
|
id,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
Menu,
|
Menu,
|
||||||
@ -43,6 +44,7 @@ export const ModelingPane = ({
|
|||||||
<section
|
<section
|
||||||
{...props}
|
{...props}
|
||||||
data-testid={detailsTestId}
|
data-testid={detailsTestId}
|
||||||
|
id={id}
|
||||||
className={
|
className={
|
||||||
pointerEventsCssClass + styles.panel + ' group ' + (className || '')
|
pointerEventsCssClass + styles.panel + ' group ' + (className || '')
|
||||||
}
|
}
|
||||||
|
@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import type { PaneType } from 'useStore'
|
|
||||||
import { MemoryPane } from './MemoryPane'
|
import { MemoryPane } from './MemoryPane'
|
||||||
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
||||||
import { DebugPane } from './DebugPane'
|
import { DebugPane } from './DebugPane'
|
||||||
|
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
|
||||||
|
|
||||||
export type Pane = {
|
export type SidebarType =
|
||||||
id: PaneType
|
| 'code'
|
||||||
|
| 'debug'
|
||||||
|
| 'export'
|
||||||
|
| 'files'
|
||||||
|
| 'kclErrors'
|
||||||
|
| 'logs'
|
||||||
|
| 'lspMessages'
|
||||||
|
| 'variables'
|
||||||
|
|
||||||
|
export type SidebarPane = {
|
||||||
|
id: SidebarType
|
||||||
title: string
|
title: string
|
||||||
icon: CustomIconName | IconDefinition
|
icon: CustomIconName | IconDefinition
|
||||||
|
keybinding: string
|
||||||
Content: ReactNode | React.FC
|
Content: ReactNode | React.FC
|
||||||
Menu?: ReactNode | React.FC
|
Menu?: ReactNode | React.FC
|
||||||
keybinding: string
|
hideOnPlatform?: 'desktop' | 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const topPanes: Pane[] = [
|
export const topPanes: SidebarPane[] = [
|
||||||
{
|
{
|
||||||
id: 'code',
|
id: 'code',
|
||||||
title: 'KCL Code',
|
title: 'KCL Code',
|
||||||
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
|
|||||||
keybinding: 'shift + c',
|
keybinding: 'shift + c',
|
||||||
Menu: KclEditorMenu,
|
Menu: KclEditorMenu,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
title: 'Project Files',
|
||||||
|
icon: 'folder',
|
||||||
|
Content: FileTreeInner,
|
||||||
|
keybinding: 'shift + f',
|
||||||
|
Menu: FileTreeMenu,
|
||||||
|
hideOnPlatform: 'web',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const bottomPanes: Pane[] = [
|
export const bottomPanes: SidebarPane[] = [
|
||||||
{
|
{
|
||||||
id: 'variables',
|
id: 'variables',
|
||||||
title: 'Variables',
|
title: 'Variables',
|
||||||
|
@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { PaneType, useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { Tab } from '@headlessui/react'
|
import { Tab } from '@headlessui/react'
|
||||||
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
|
import {
|
||||||
|
SidebarPane,
|
||||||
|
SidebarType,
|
||||||
|
bottomPanes,
|
||||||
|
topPanes,
|
||||||
|
} from './ModelingPanes'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import styles from './ModelingSidebar.module.css'
|
import styles from './ModelingSidebar.module.css'
|
||||||
import { ModelingPane } from './ModelingPane'
|
import { ModelingPane } from './ModelingPane'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingSidebarSectionProps {
|
interface ModelingSidebarSectionProps {
|
||||||
panes: Pane[]
|
panes: SidebarPane[]
|
||||||
alignButtons?: 'start' | 'end'
|
alignButtons?: 'start' | 'end'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,11 +75,11 @@ function ModelingSidebarSection({
|
|||||||
}))
|
}))
|
||||||
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
||||||
const [currentPane, setCurrentPane] = useState(
|
const [currentPane, setCurrentPane] = useState(
|
||||||
foundOpenPane || ('none' as PaneType | 'none')
|
foundOpenPane || ('none' as SidebarType | 'none')
|
||||||
)
|
)
|
||||||
|
|
||||||
const togglePane = useCallback(
|
const togglePane = useCallback(
|
||||||
(newPane: PaneType | 'none') => {
|
(newPane: SidebarType | 'none') => {
|
||||||
if (newPane === 'none') {
|
if (newPane === 'none') {
|
||||||
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
||||||
setCurrentPane('none')
|
setCurrentPane('none')
|
||||||
@ -90,9 +96,15 @@ function ModelingSidebarSection({
|
|||||||
|
|
||||||
// Filter out the debug panel if it's not supposed to be shown
|
// Filter out the debug panel if it's not supposed to be shown
|
||||||
// TODO: abstract out for allowing user to configure which panes to show
|
// TODO: abstract out for allowing user to configure which panes to show
|
||||||
const filteredPanes = showDebugPanel.current
|
const filteredPanes = (
|
||||||
? panes
|
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
|
||||||
: panes.filter((pane) => pane.id !== 'debug')
|
).filter(
|
||||||
|
(pane) =>
|
||||||
|
!pane.hideOnPlatform ||
|
||||||
|
(isTauri()
|
||||||
|
? pane.hideOnPlatform === 'web'
|
||||||
|
: pane.hideOnPlatform === 'desktop')
|
||||||
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!showDebugPanel.current &&
|
!showDebugPanel.current &&
|
||||||
@ -153,7 +165,11 @@ function ModelingSidebarSection({
|
|||||||
<Tab.Panel key="none" />
|
<Tab.Panel key="none" />
|
||||||
{filteredPanes.map((pane) => (
|
{filteredPanes.map((pane) => (
|
||||||
<Tab.Panel key={pane.id} className="h-full">
|
<Tab.Panel key={pane.id} className="h-full">
|
||||||
<ModelingPane title={pane.title} Menu={pane.Menu}>
|
<ModelingPane
|
||||||
|
id={`${pane.id}-pane`}
|
||||||
|
title={pane.title}
|
||||||
|
Menu={pane.Menu}
|
||||||
|
>
|
||||||
{pane.Content instanceof Function ? (
|
{pane.Content instanceof Function ? (
|
||||||
<pane.Content />
|
<pane.Content />
|
||||||
) : (
|
) : (
|
||||||
@ -168,8 +184,8 @@ function ModelingSidebarSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingPaneButtonProps {
|
interface ModelingPaneButtonProps {
|
||||||
paneConfig: Pane
|
paneConfig: SidebarPane
|
||||||
currentPane: PaneType | 'none'
|
currentPane: SidebarType | 'none'
|
||||||
togglePane: () => void
|
togglePane: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
@ -9,11 +8,11 @@ import {
|
|||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faX,
|
faX,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { getPartsCount, readProject } from '../lib/tauriFS'
|
|
||||||
import { FILE_EXT } from 'lib/constants'
|
import { FILE_EXT } from 'lib/constants'
|
||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
function ProjectCard({
|
function ProjectCard({
|
||||||
project,
|
project,
|
||||||
@ -21,17 +20,17 @@ function ProjectCard({
|
|||||||
handleDeleteProject,
|
handleDeleteProject,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
project: ProjectWithEntryPointMetadata
|
project: Project
|
||||||
handleRenameProject: (
|
handleRenameProject: (
|
||||||
e: FormEvent<HTMLFormElement>,
|
e: FormEvent<HTMLFormElement>,
|
||||||
f: ProjectWithEntryPointMetadata
|
f: Project
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
|
handleDeleteProject: (f: Project) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
useHotkeys('esc', () => setIsEditing(false))
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
const [numberOfParts, setNumberOfParts] = useState(1)
|
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
||||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||||
|
|
||||||
let inputRef = useRef<HTMLInputElement>(null)
|
let inputRef = useRef<HTMLInputElement>(null)
|
||||||
@ -41,7 +40,8 @@ function ProjectCard({
|
|||||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayedTime(date: Date) {
|
function getDisplayedTime(dateStr: string) {
|
||||||
|
const date = new Date(dateStr)
|
||||||
const startOfToday = new Date()
|
const startOfToday = new Date()
|
||||||
startOfToday.setHours(0, 0, 0, 0)
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
return date.getTime() < startOfToday.getTime()
|
return date.getTime() < startOfToday.getTime()
|
||||||
@ -50,15 +50,12 @@ function ProjectCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getNumberOfParts() {
|
async function getNumberOfFiles() {
|
||||||
const { kclFileCount, kclDirCount } = getPartsCount(
|
setNumberOfFiles(project.kcl_file_count)
|
||||||
await readProject(project.path)
|
setNumberOfFolders(project.directory_count)
|
||||||
)
|
|
||||||
setNumberOfParts(kclFileCount)
|
|
||||||
setNumberOfFolders(kclDirCount)
|
|
||||||
}
|
}
|
||||||
void getNumberOfParts()
|
void getNumberOfFiles()
|
||||||
}, [project.path])
|
}, [project.kcl_file_count, project.directory_count])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@ -129,7 +126,7 @@ function ProjectCard({
|
|||||||
{project.name?.replace(FILE_EXT, '')}
|
{project.name?.replace(FILE_EXT, '')}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
||||||
{numberOfFolders > 0 &&
|
{numberOfFolders > 0 &&
|
||||||
`/ ${numberOfFolders} folder${
|
`/ ${numberOfFolders} folder${
|
||||||
numberOfFolders === 1 ? '' : 's'
|
numberOfFolders === 1 ? '' : 's'
|
||||||
@ -137,8 +134,8 @@ function ProjectCard({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
Edited{' '}
|
Edited{' '}
|
||||||
{project.entrypointMetadata.mtime
|
{project.metadata && project.metadata?.modified
|
||||||
? getDisplayedTime(project.entrypointMetadata.mtime)
|
? getDisplayedTime(project.metadata.modified)
|
||||||
: 'never'}
|
: 'never'}
|
||||||
</span>
|
</span>
|
||||||
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -14,29 +14,17 @@ const projectWellFormed = {
|
|||||||
{
|
{
|
||||||
name: 'main.kcl',
|
name: 'main.kcl',
|
||||||
path: '/some/path/Simple Box/main.kcl',
|
path: '/some/path/Simple Box/main.kcl',
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
entrypointMetadata: {
|
metadata: {
|
||||||
atime: now,
|
created: now.toISOString(),
|
||||||
blksize: 32,
|
modified: now.toISOString(),
|
||||||
blocks: 32,
|
|
||||||
birthtime: now,
|
|
||||||
dev: 1,
|
|
||||||
gid: 1,
|
|
||||||
ino: 1,
|
|
||||||
isDirectory: false,
|
|
||||||
isFile: true,
|
|
||||||
isSymlink: false,
|
|
||||||
mode: 1,
|
|
||||||
mtime: now,
|
|
||||||
nlink: 1,
|
|
||||||
readonly: false,
|
|
||||||
rdev: 1,
|
|
||||||
size: 32,
|
size: 32,
|
||||||
uid: 1,
|
|
||||||
fileAttributes: null,
|
|
||||||
},
|
},
|
||||||
} satisfies ProjectWithEntryPointMetadata
|
kcl_file_count: 1,
|
||||||
|
directory_count: 0,
|
||||||
|
} satisfies Project
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
|
@ -133,13 +133,13 @@ function ProjectMenuPopover({
|
|||||||
<p className="m-0 text-mono" data-testid="projectName">
|
<p className="m-0 text-mono" data-testid="projectName">
|
||||||
{project?.name ? project.name : APP_NAME}
|
{project?.name ? project.name : APP_NAME}
|
||||||
</p>
|
</p>
|
||||||
{project?.entrypointMetadata && (
|
{project?.metadata && project.metadata.created && (
|
||||||
<p
|
<p
|
||||||
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
||||||
data-testid="createdAt"
|
data-testid="createdAt"
|
||||||
>
|
>
|
||||||
Created{' '}
|
Created{' '}
|
||||||
{project.entrypointMetadata.birthtime?.toLocaleDateString()}
|
{new Date(project.metadata.created).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
152
src/components/Settings/SettingsFieldInput.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { Toggle } from 'components/Toggle/Toggle'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
|
import {
|
||||||
|
SetEventTypes,
|
||||||
|
SettingsLevel,
|
||||||
|
WildcardSetEvent,
|
||||||
|
} from 'lib/settings/settingsTypes'
|
||||||
|
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Event } from 'xstate'
|
||||||
|
|
||||||
|
interface SettingsFieldInputProps {
|
||||||
|
// We don't need the fancy types here,
|
||||||
|
// it doesn't help us with autocomplete or anything
|
||||||
|
category: string
|
||||||
|
settingName: string
|
||||||
|
settingsLevel: SettingsLevel
|
||||||
|
setting: Setting<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsFieldInput({
|
||||||
|
category,
|
||||||
|
settingName,
|
||||||
|
settingsLevel,
|
||||||
|
setting,
|
||||||
|
}: SettingsFieldInputProps) {
|
||||||
|
const {
|
||||||
|
settings: { context, send },
|
||||||
|
} = useSettingsAuthContext()
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return setting.commandConfig &&
|
||||||
|
'options' in setting.commandConfig &&
|
||||||
|
setting.commandConfig.options
|
||||||
|
? setting.commandConfig.options instanceof Array
|
||||||
|
? setting.commandConfig.options
|
||||||
|
: setting.commandConfig.options(
|
||||||
|
{
|
||||||
|
argumentsToSubmit: {
|
||||||
|
level: settingsLevel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
}, [setting, settingsLevel, context])
|
||||||
|
const inputType = getSettingInputType(setting)
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case 'component':
|
||||||
|
return (
|
||||||
|
setting.Component && (
|
||||||
|
<setting.Component
|
||||||
|
value={setting[settingsLevel] || setting.getFallback(settingsLevel)}
|
||||||
|
updateValue={(newValue) => {
|
||||||
|
send({
|
||||||
|
type: `set.${category}.${settingName}`,
|
||||||
|
data: {
|
||||||
|
level: settingsLevel,
|
||||||
|
value: newValue,
|
||||||
|
},
|
||||||
|
} as unknown as Event<WildcardSetEvent>)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<Toggle
|
||||||
|
offLabel="Off"
|
||||||
|
onLabel="On"
|
||||||
|
onChange={(e) =>
|
||||||
|
send({
|
||||||
|
type: `set.${category}.${settingName}`,
|
||||||
|
data: {
|
||||||
|
level: settingsLevel,
|
||||||
|
value: Boolean(e.target.checked),
|
||||||
|
},
|
||||||
|
} as SetEventTypes)
|
||||||
|
}
|
||||||
|
checked={Boolean(
|
||||||
|
setting[settingsLevel] !== undefined
|
||||||
|
? setting[settingsLevel]
|
||||||
|
: setting.getFallback(settingsLevel)
|
||||||
|
)}
|
||||||
|
name={`${category}-${settingName}`}
|
||||||
|
data-testid={`${category}-${settingName}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'options':
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
name={`${category}-${settingName}`}
|
||||||
|
data-testid={`${category}-${settingName}`}
|
||||||
|
className="p-1 bg-transparent border rounded-sm border-chalkboard-30 w-full"
|
||||||
|
value={String(
|
||||||
|
setting[settingsLevel] || setting.getFallback(settingsLevel)
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
send({
|
||||||
|
type: `set.${category}.${settingName}`,
|
||||||
|
data: {
|
||||||
|
level: settingsLevel,
|
||||||
|
value: e.target.value,
|
||||||
|
},
|
||||||
|
} as unknown as Event<WildcardSetEvent>)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{options &&
|
||||||
|
options.length > 0 &&
|
||||||
|
options.map((option) => (
|
||||||
|
<option key={option.name} value={String(option.value)}>
|
||||||
|
{option.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
case 'string':
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
name={`${category}-${settingName}`}
|
||||||
|
data-testid={`${category}-${settingName}`}
|
||||||
|
type="text"
|
||||||
|
className="p-1 bg-transparent border rounded-sm border-chalkboard-30 w-full"
|
||||||
|
defaultValue={String(
|
||||||
|
setting[settingsLevel] || setting.getFallback(settingsLevel)
|
||||||
|
)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (
|
||||||
|
setting[settingsLevel] === undefined
|
||||||
|
? setting.getFallback(settingsLevel) !== e.target.value
|
||||||
|
: setting[settingsLevel] !== e.target.value
|
||||||
|
) {
|
||||||
|
send({
|
||||||
|
type: `set.${category}.${settingName}`,
|
||||||
|
data: {
|
||||||
|
level: settingsLevel,
|
||||||
|
value: e.target.value,
|
||||||
|
},
|
||||||
|
} as unknown as Event<WildcardSetEvent>)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<p className="text-destroy-70 dark:text-destroy-20">
|
||||||
|
No component or input type found for setting {settingName} in category{' '}
|
||||||
|
{category}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
110
src/components/Settings/SettingsSearchBar.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Combobox } from '@headlessui/react'
|
||||||
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
|
import decamelize from 'decamelize'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export function SettingsSearchBar() {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
useHotkeys(
|
||||||
|
'Ctrl+.',
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
inputRef.current?.focus()
|
||||||
|
},
|
||||||
|
{ enableOnFormTags: true }
|
||||||
|
)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
const settingsAsSearchable = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.entries(settings.state.context).flatMap(
|
||||||
|
([category, categorySettings]) =>
|
||||||
|
Object.entries(categorySettings).flatMap(([settingName, setting]) => {
|
||||||
|
const s = setting as Setting
|
||||||
|
return ['project', 'user']
|
||||||
|
.filter((l) => s.hideOnLevel !== l)
|
||||||
|
.map((l) => ({
|
||||||
|
category: decamelize(category, { separator: ' ' }),
|
||||||
|
settingName: settingName,
|
||||||
|
settingNameDisplay: decamelize(settingName, { separator: ' ' }),
|
||||||
|
setting: s,
|
||||||
|
level: l,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
),
|
||||||
|
[settings.state.context]
|
||||||
|
)
|
||||||
|
const [searchResults, setSearchResults] = useState(settingsAsSearchable)
|
||||||
|
|
||||||
|
const fuse = new Fuse(settingsAsSearchable, {
|
||||||
|
keys: ['category', 'settingNameDisplay', 'setting.description'],
|
||||||
|
includeScore: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
|
setSearchResults(query.length > 0 ? results : settingsAsSearchable)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
function handleSelection({
|
||||||
|
level,
|
||||||
|
settingName,
|
||||||
|
}: {
|
||||||
|
category: string
|
||||||
|
settingName: string
|
||||||
|
setting: Setting<unknown>
|
||||||
|
level: string
|
||||||
|
}) {
|
||||||
|
navigate(`?tab=${level}#${settingName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox onChange={handleSelection}>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="flex items-center gap-2 py-0.5 pr-1 pl-2 rounded border-solid border border-primary/10 dark:border-chalkboard-80 focus-within:border-primary dark:focus-within:border-chalkboard-30">
|
||||||
|
<Combobox.Input
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
|
||||||
|
placeholder="Search settings (^.)"
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<CustomIcon
|
||||||
|
name="search"
|
||||||
|
className="w-5 h-5 rounded-sm bg-primary/10 text-primary group-focus-within:bg-primary group-focus-within:text-chalkboard-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options className="absolute top-full mt-2 right-0 w-80 overflow-y-auto z-50 max-h-96 cursor-pointer bg-chalkboard-10 dark:bg-chalkboard-100 border border-solid border-primary dark:border-chalkboard-30 rounded">
|
||||||
|
{searchResults?.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={`${option.category}-${option.settingName}-${option.level}`}
|
||||||
|
value={option}
|
||||||
|
className="flex flex-col items-start gap-2 px-4 py-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
|
||||||
|
>
|
||||||
|
<p className="flex-grow text-base capitalize m-0 leading-none">
|
||||||
|
{option.level} ·{' '}
|
||||||
|
{decamelize(option.category, { separator: ' ' })} ·{' '}
|
||||||
|
{option.settingNameDisplay}
|
||||||
|
</p>
|
||||||
|
{option.setting.description && (
|
||||||
|
<p className="text-xs leading-tight text-chalkboard-70 dark:text-chalkboard-50">
|
||||||
|
{option.setting.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
)
|
||||||
|
}
|
60
src/components/Settings/SettingsSection.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
|
|
||||||
|
interface SettingsSectionProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
parentLevel?: SettingsLevel | 'default'
|
||||||
|
onFallback?: () => void
|
||||||
|
settingHasChanged?: boolean
|
||||||
|
headingClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSection({
|
||||||
|
title,
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
parentLevel,
|
||||||
|
settingHasChanged,
|
||||||
|
onFallback,
|
||||||
|
headingClassName = 'text-lg font-normal capitalize tracking-wide',
|
||||||
|
}: SettingsSectionProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id={id}
|
||||||
|
className={
|
||||||
|
'group p-2 pl-0 grid grid-cols-2 gap-6 items-start ' +
|
||||||
|
className +
|
||||||
|
(settingHasChanged ? ' border-0 border-l-2 -ml-0.5 border-primary' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="ml-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className={headingClassName}>{title}</h2>
|
||||||
|
{onFallback && parentLevel && settingHasChanged && (
|
||||||
|
<button
|
||||||
|
onClick={onFallback}
|
||||||
|
className="hidden group-hover:block group-focus-within:block border-none p-0 hover:bg-warn-10 dark:hover:bg-warn-80 focus:bg-warn-10 dark:focus:bg-warn-80 focus:outline-none"
|
||||||
|
>
|
||||||
|
<CustomIcon name="refresh" className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Roll back {title}</span>
|
||||||
|
<Tooltip position="right">
|
||||||
|
Roll back to match {parentLevel}
|
||||||
|
</Tooltip>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-2 text-xs text-chalkboard-80 dark:text-chalkboard-30">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
28
src/components/Settings/SettingsTabButton.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||||
|
|
||||||
|
interface SettingsTabButtonProps {
|
||||||
|
checked: boolean
|
||||||
|
icon: CustomIconName
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsTabButton(props: SettingsTabButtonProps) {
|
||||||
|
const { checked, icon, text } = props
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`cursor-pointer select-none flex items-center gap-1 p-1 pr-2 -mb-[1px] border-0 border-b ${
|
||||||
|
checked
|
||||||
|
? 'border-primary'
|
||||||
|
: 'border-chalkboard-20 dark:border-chalkboard-30 hover:bg-primary/20 dark:hover:bg-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CustomIcon
|
||||||
|
name={icon}
|
||||||
|
className={
|
||||||
|
'w-5 h-5 ' + (checked ? 'bg-primary !text-chalkboard-10' : '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
39
src/components/Settings/SettingsTabs.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { RadioGroup } from '@headlessui/react'
|
||||||
|
import { SettingsTabButton } from './SettingsTabButton'
|
||||||
|
|
||||||
|
interface SettingsTabButtonProps {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
showProjectTab: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsTabs({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
showProjectTab,
|
||||||
|
}: SettingsTabButtonProps) {
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className="flex justify-start pl-4 pr-5 gap-5 border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-90"
|
||||||
|
>
|
||||||
|
<RadioGroup.Option value="user">
|
||||||
|
{({ checked }) => (
|
||||||
|
<SettingsTabButton checked={checked} icon="person" text="User" />
|
||||||
|
)}
|
||||||
|
</RadioGroup.Option>
|
||||||
|
{showProjectTab && (
|
||||||
|
<RadioGroup.Option value="project">
|
||||||
|
{({ checked }) => (
|
||||||
|
<SettingsTabButton
|
||||||
|
checked={checked}
|
||||||
|
icon="folder"
|
||||||
|
text="This project"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RadioGroup.Option>
|
||||||
|
)}
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
@ -168,7 +168,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
'Execute AST': () => kclManager.executeCode(true),
|
'Execute AST': () => kclManager.executeCode(true),
|
||||||
persistSettings: (context) =>
|
persistSettings: (context) =>
|
||||||
saveSettings(context, loadedProject?.project?.path),
|
saveSettings(context, loadedProject?.project?.name),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
.toggle {
|
.toggle {
|
||||||
@apply flex items-center gap-2 w-fit;
|
@apply flex items-center gap-2 w-fit;
|
||||||
--toggle-size: 1.25rem;
|
@apply text-chalkboard-110;
|
||||||
|
--toggle-size: 0.75rem;
|
||||||
--padding: 0.25rem;
|
--padding: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toggle {
|
||||||
|
@apply text-chalkboard-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle:focus-within > span {
|
.toggle:focus-within > span {
|
||||||
@ -13,9 +19,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle > span {
|
.toggle > span {
|
||||||
@apply relative rounded border border-chalkboard-110 hover:border-chalkboard-100 cursor-pointer;
|
@apply relative rounded border border-chalkboard-70 hover:border-chalkboard-80 cursor-pointer;
|
||||||
width: calc(2 * (var(--toggle-size) + var(--padding)));
|
border-width: var(--border);
|
||||||
height: calc(var(--toggle-size) + var(--padding));
|
width: calc(
|
||||||
|
2 * (var(--toggle-size) + var(--padding) * 2 - var(--border) * 2)
|
||||||
|
);
|
||||||
|
height: calc(var(--toggle-size) + var(--padding) * 2 - var(--border) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .toggle > span {
|
:global(.dark) .toggle > span {
|
||||||
@ -23,18 +32,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle > span::after {
|
.toggle > span::after {
|
||||||
|
width: var(--toggle-size);
|
||||||
|
height: var(--toggle-size);
|
||||||
|
border-radius: calc(var(--toggle-size) / 8);
|
||||||
content: '';
|
content: '';
|
||||||
@apply absolute w-4 h-4 rounded-sm bg-chalkboard-110;
|
@apply absolute bg-chalkboard-70;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
translate: calc(-100% - var(--padding)) -50%;
|
translate: calc(-100% - var(--padding) + var(--border)) -50%;
|
||||||
transition: translate 0.08s ease-out;
|
transition: translate 0.08s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .toggle > span::after {
|
:global(.dark) .toggle > span::after {
|
||||||
@apply bg-chalkboard-10;
|
@apply bg-chalkboard-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle input:checked + span::after {
|
.toggle input:checked + span::after {
|
||||||
translate: calc(50% - var(--padding)) -50%;
|
translate: calc(50% - var(--padding) + var(--border)) -50%;
|
||||||
|
@apply bg-chalkboard-110;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toggle input:checked + span::after {
|
||||||
|
@apply bg-chalkboard-10;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,11 @@ export const Toggle = ({
|
|||||||
}: ToggleProps) => {
|
}: ToggleProps) => {
|
||||||
return (
|
return (
|
||||||
<label className={`${styles.toggle} ${className}`}>
|
<label className={`${styles.toggle} ${className}`}>
|
||||||
{offLabel}
|
<p
|
||||||
|
className={checked ? 'text-chalkboard-70 dark:text-chalkboard-50' : ''}
|
||||||
|
>
|
||||||
|
{offLabel}
|
||||||
|
</p>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={name}
|
name={name}
|
||||||
@ -28,7 +32,11 @@ export const Toggle = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<span></span>
|
<span></span>
|
||||||
{onLabel}
|
<p
|
||||||
|
className={!checked ? 'text-chalkboard-70 dark:text-chalkboard-50' : ''}
|
||||||
|
>
|
||||||
|
{onLabel}
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -167,6 +167,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
if (pos === null) return null
|
if (pos === null) return null
|
||||||
const dom = document.createElement('div')
|
const dom = document.createElement('div')
|
||||||
dom.classList.add('documentation')
|
dom.classList.add('documentation')
|
||||||
|
dom.style.zIndex = '99999999'
|
||||||
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
||||||
else dom.textContent = formatContents(contents)
|
else dom.textContent = formatContents(contents)
|
||||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
return { pos, end, create: (view) => ({ dom }), above: true }
|
||||||
|
@ -8,13 +8,13 @@ export interface KclWorkerOptions {
|
|||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
baseUnit: UnitLength
|
baseUnit: UnitLength
|
||||||
devMode: boolean
|
apiBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotWorkerOptions {
|
export interface CopilotWorkerOptions {
|
||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
devMode: boolean
|
apiBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LspWorkerEventType {
|
export enum LspWorkerEventType {
|
||||||
|
@ -28,11 +28,11 @@ const initialise = async (wasmUrl: string) => {
|
|||||||
export async function copilotLspRun(
|
export async function copilotLspRun(
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
token: string,
|
token: string,
|
||||||
devMode: boolean = false
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('starting copilot lsp')
|
console.log('starting copilot lsp')
|
||||||
await copilot_lsp_run(config, token, devMode)
|
await copilot_lsp_run(config, token, baseUrl)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('copilot lsp failed', e)
|
console.log('copilot lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -44,11 +44,11 @@ export async function kclLspRun(
|
|||||||
engineCommandManager: EngineCommandManager | null,
|
engineCommandManager: EngineCommandManager | null,
|
||||||
token: string,
|
token: string,
|
||||||
baseUnit: string,
|
baseUnit: string,
|
||||||
devMode: boolean = false
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('start kcl lsp')
|
console.log('start kcl lsp')
|
||||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode)
|
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('kcl lsp failed', e)
|
console.log('kcl lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -80,12 +80,12 @@ onmessage = function (event) {
|
|||||||
null,
|
null,
|
||||||
kclData.token,
|
kclData.token,
|
||||||
kclData.baseUnit,
|
kclData.baseUnit,
|
||||||
kclData.devMode
|
kclData.apiBaseUrl
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
copilotLspRun(config, copilotData.token, copilotData.devMode)
|
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -7,5 +7,8 @@ export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
|||||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
|
export const VITE_KC_DEV_TOKEN = import.meta.env.VITE_KC_DEV_TOKEN as
|
||||||
|
| string
|
||||||
|
| undefined
|
||||||
export const TEST = import.meta.env.TEST
|
export const TEST = import.meta.env.TEST
|
||||||
export const DEV = import.meta.env.DEV
|
export const DEV = import.meta.env.DEV
|
||||||
|
@ -9,10 +9,12 @@ export function useSetupEngineManager(
|
|||||||
streamRef: React.RefObject<HTMLDivElement>,
|
streamRef: React.RefObject<HTMLDivElement>,
|
||||||
token?: string,
|
token?: string,
|
||||||
settings = {
|
settings = {
|
||||||
|
pool: null,
|
||||||
theme: Themes.System,
|
theme: Themes.System,
|
||||||
highlightEdges: true,
|
highlightEdges: true,
|
||||||
enableSSAO: true,
|
enableSSAO: true,
|
||||||
} as {
|
} as {
|
||||||
|
pool: string | null
|
||||||
theme: Themes
|
theme: Themes
|
||||||
highlightEdges: boolean
|
highlightEdges: boolean
|
||||||
enableSSAO: boolean
|
enableSSAO: boolean
|
||||||
@ -35,6 +37,12 @@ export function useSetupEngineManager(
|
|||||||
|
|
||||||
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
||||||
|
|
||||||
|
if (settings.pool) {
|
||||||
|
// override the pool param (?pool=) to request a specific engine instance
|
||||||
|
// from a particular pool.
|
||||||
|
engineCommandManager.pool = settings.pool
|
||||||
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Load the engine command manager once with the initial width and height,
|
// Load the engine command manager once with the initial width and height,
|
||||||
// then we do not want to reload it.
|
// then we do not want to reload it.
|
||||||
|
@ -335,7 +335,9 @@ class EngineConnection {
|
|||||||
// Information on the connect transaction
|
// Information on the connect transaction
|
||||||
|
|
||||||
const createPeerConnection = () => {
|
const createPeerConnection = () => {
|
||||||
this.pc = new RTCPeerConnection()
|
this.pc = new RTCPeerConnection({
|
||||||
|
bundlePolicy: 'max-bundle',
|
||||||
|
})
|
||||||
|
|
||||||
// Data channels MUST BE specified before SDP offers because requesting
|
// Data channels MUST BE specified before SDP offers because requesting
|
||||||
// them affects what our needs are!
|
// them affects what our needs are!
|
||||||
@ -652,7 +654,9 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
// No ICE servers can be valid in a local dev. env.
|
// No ICE servers can be valid in a local dev. env.
|
||||||
if (ice_servers?.length === 0) {
|
if (ice_servers?.length === 0) {
|
||||||
console.warn('No ICE servers')
|
console.warn('No ICE servers')
|
||||||
this.pc?.setConfiguration({})
|
this.pc?.setConfiguration({
|
||||||
|
bundlePolicy: 'max-bundle',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// When we set the Configuration, we want to always force
|
// When we set the Configuration, we want to always force
|
||||||
// iceTransportPolicy to 'relay', since we know the topology
|
// iceTransportPolicy to 'relay', since we know the topology
|
||||||
@ -660,6 +664,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
// talk to the engine in any configuration /other/ than relay
|
// talk to the engine in any configuration /other/ than relay
|
||||||
// from a infra POV.
|
// from a infra POV.
|
||||||
this.pc?.setConfiguration({
|
this.pc?.setConfiguration({
|
||||||
|
bundlePolicy: 'max-bundle',
|
||||||
iceServers: ice_servers,
|
iceServers: ice_servers,
|
||||||
iceTransportPolicy: 'relay',
|
iceTransportPolicy: 'relay',
|
||||||
})
|
})
|
||||||
@ -888,6 +893,7 @@ export class EngineCommandManager {
|
|||||||
sceneCommandArtifacts: ArtifactMap = {}
|
sceneCommandArtifacts: ArtifactMap = {}
|
||||||
outSequence = 1
|
outSequence = 1
|
||||||
inSequence = 1
|
inSequence = 1
|
||||||
|
pool?: string
|
||||||
engineConnection?: EngineConnection
|
engineConnection?: EngineConnection
|
||||||
defaultPlanes: DefaultPlanes | null = null
|
defaultPlanes: DefaultPlanes | null = null
|
||||||
commandLogs: CommandLog[] = []
|
commandLogs: CommandLog[] = []
|
||||||
@ -914,8 +920,9 @@ export class EngineCommandManager {
|
|||||||
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
||||||
[]
|
[]
|
||||||
|
|
||||||
constructor() {
|
constructor(pool?: string) {
|
||||||
this.engineConnection = undefined
|
this.engineConnection = undefined
|
||||||
|
this.pool = pool
|
||||||
}
|
}
|
||||||
|
|
||||||
private _camControlsCameraChange = () => {}
|
private _camControlsCameraChange = () => {}
|
||||||
@ -972,7 +979,8 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
|
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
|
||||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}`
|
const pool = this.pool === undefined ? '' : `&pool=${this.pool}`
|
||||||
|
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
|
||||||
this.engineConnection = new EngineConnection({
|
this.engineConnection = new EngineConnection({
|
||||||
engineCommandManager: this,
|
engineCommandManager: this,
|
||||||
url,
|
url,
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
|
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { join } from '@tauri-apps/api/path'
|
import { join } from '@tauri-apps/api/path'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { readDirRecursive } from 'lib/tauri'
|
||||||
import { FileEntry } from 'lib/types'
|
|
||||||
|
|
||||||
/// FileSystemManager is a class that provides a way to read files from the local file system.
|
/// FileSystemManager is a class that provides a way to read files from the local file system.
|
||||||
/// It assumes that you are in a project since it is solely used by the std lib
|
/// It assumes that you are in a project since it is solely used by the std lib
|
||||||
@ -69,9 +68,7 @@ class FileSystemManager {
|
|||||||
throw new Error(`Error joining dir: ${error}`)
|
throw new Error(`Error joining dir: ${error}`)
|
||||||
})
|
})
|
||||||
.then((p) => {
|
.then((p) => {
|
||||||
invoke<FileEntry[]>('read_dir_recursive', {
|
readDirRecursive(p)
|
||||||
path: p,
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`Error reading dir: ${error}`)
|
throw new Error(`Error reading dir: ${error}`)
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,11 @@ import init, {
|
|||||||
make_default_planes,
|
make_default_planes,
|
||||||
coredump,
|
coredump,
|
||||||
toml_stringify,
|
toml_stringify,
|
||||||
toml_parse,
|
default_app_settings,
|
||||||
|
parse_app_settings,
|
||||||
|
parse_project_settings,
|
||||||
|
default_project_settings,
|
||||||
|
parse_project_route,
|
||||||
} from '../wasm-lib/pkg/wasm_lib'
|
} from '../wasm-lib/pkg/wasm_lib'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
@ -26,6 +30,9 @@ import { CoreDumpManager } from 'lib/coredump'
|
|||||||
import openWindow from 'lib/openWindow'
|
import openWindow from 'lib/openWindow'
|
||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
|
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||||
|
|
||||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||||
@ -349,11 +356,53 @@ export function tomlStringify(toml: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tomlParse(toml: string): any {
|
export function defaultAppSettings(): Configuration {
|
||||||
try {
|
try {
|
||||||
const parsed: any = toml_parse(toml)
|
const settings: Configuration = default_app_settings()
|
||||||
return parsed
|
return settings
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new Error(`Error parsing toml: ${e}`)
|
throw new Error(`Error getting default app settings: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAppSettings(toml: string): Configuration {
|
||||||
|
try {
|
||||||
|
const settings: Configuration = parse_app_settings(toml)
|
||||||
|
return settings
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Error parsing app settings: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultProjectSettings(): ProjectConfiguration {
|
||||||
|
try {
|
||||||
|
const settings: ProjectConfiguration = default_project_settings()
|
||||||
|
return settings
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Error getting default project settings: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseProjectSettings(toml: string): ProjectConfiguration {
|
||||||
|
try {
|
||||||
|
const settings: ProjectConfiguration = parse_project_settings(toml)
|
||||||
|
return settings
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Error parsing project settings: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseProjectRoute(
|
||||||
|
configuration: Configuration,
|
||||||
|
route_str: string
|
||||||
|
): ProjectRoute {
|
||||||
|
try {
|
||||||
|
const route: ProjectRoute = parse_project_route(
|
||||||
|
JSON.stringify(configuration),
|
||||||
|
route_str
|
||||||
|
)
|
||||||
|
return route
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Error parsing project route: ${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
|
||||||
|
|
||||||
const noModifiersPressed = (e: React.MouseEvent) =>
|
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||||
|
|
||||||
@ -20,6 +22,29 @@ export const cameraSystems: CameraSystem[] = [
|
|||||||
'AutoCAD',
|
'AutoCAD',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export function mouseControlsToCameraSystem(
|
||||||
|
mouseControl: MouseControlType | undefined
|
||||||
|
): CameraSystem | undefined {
|
||||||
|
switch (mouseControl) {
|
||||||
|
case 'kitty_cad':
|
||||||
|
return 'KittyCAD'
|
||||||
|
case 'on_shape':
|
||||||
|
return 'OnShape'
|
||||||
|
case 'trackpad_friendly':
|
||||||
|
return 'Trackpad Friendly'
|
||||||
|
case 'solidworks':
|
||||||
|
return 'Solidworks'
|
||||||
|
case 'nx':
|
||||||
|
return 'NX'
|
||||||
|
case 'creo':
|
||||||
|
return 'Creo'
|
||||||
|
case 'auto_cad':
|
||||||
|
return 'AutoCAD'
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface MouseGuardHandler {
|
interface MouseGuardHandler {
|
||||||
description: string
|
description: string
|
||||||
callback: (e: React.MouseEvent) => boolean
|
callback: (e: React.MouseEvent) => boolean
|
||||||
|
@ -8,8 +8,6 @@ export const MAX_PADDING = 7
|
|||||||
* This is available for users to edit as a setting.
|
* This is available for users to edit as a setting.
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||||
/** The file name for settings files, both at the user and project level */
|
|
||||||
export const SETTINGS_FILE_EXT = '.toml'
|
|
||||||
/** Name given the temporary "project" in the browser version of the app */
|
/** Name given the temporary "project" in the browser version of the app */
|
||||||
export const BROWSER_PROJECT_NAME = 'browser'
|
export const BROWSER_PROJECT_NAME = 'browser'
|
||||||
/** Name given the temporary file in the browser version of the app */
|
/** Name given the temporary file in the browser version of the app */
|
||||||
|
@ -49,6 +49,11 @@ export class CoreDumpManager {
|
|||||||
return APP_VERSION
|
return APP_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the backend pool we've requested.
|
||||||
|
pool(): string {
|
||||||
|
return this.engineCommandManager.pool || ''
|
||||||
|
}
|
||||||
|
|
||||||
// Get the os information.
|
// Get the os information.
|
||||||
getOsInfo(): Promise<string> {
|
getOsInfo(): Promise<string> {
|
||||||
if (this.isTauri()) {
|
if (this.isTauri()) {
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
export const bracket = `// Shelf Bracket
|
export const bracket = `// Shelf Bracket
|
||||||
// This is a shelf bracket made out of 6061-T6 aluminum sheet metal. The required thickness is calculated based on a point load of 300 lbs applied to the end of the shelf. There are two brackets holding up the shelf, so the moment experienced is divided by 2. The shelf is 1 foot long from the wall.
|
// This is a shelf bracket made out of 6061-T6 aluminum sheet metal. The required thickness is calculated based on a point load of 300 lbs applied to the end of the shelf. There are two brackets holding up the shelf, so the moment experienced is divided by 2. The shelf is 1 foot long from the wall.
|
||||||
|
|
||||||
|
// Define our bracket feet lengths
|
||||||
|
const shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
|
||||||
|
const wallMountL = 6 // the length of the bracket
|
||||||
|
|
||||||
|
// Define constants required to calculate the thickness needed to support 300 lbs
|
||||||
const sigmaAllow = 35000 // psi
|
const sigmaAllow = 35000 // psi
|
||||||
const width = 6 // inch
|
const width = 6 // inch
|
||||||
const p = 300 // Force on shelf - lbs
|
const p = 300 // Force on shelf - lbs
|
||||||
const distance = 12 // inches
|
const L = 12 // inches
|
||||||
const M = 12 * 300 / 2 // Moment experienced at fixed end of bracket
|
const M = L * p / 2 // Moment experienced at fixed end of bracket
|
||||||
const FOS = 2 // Factor of safety of 2
|
const FOS = 2 // Factor of safety of 2 to be conservative
|
||||||
const shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
|
|
||||||
const wallMountL = 8 // the length of the bracket
|
|
||||||
|
|
||||||
|
// Calculate the thickness off the bending stress and factor of safety
|
||||||
// Calculate the thickness off the allowable bending stress and factor of safety
|
|
||||||
const thickness = sqrt(6 * M * FOS / (width * sigmaAllow))
|
const thickness = sqrt(6 * M * FOS / (width * sigmaAllow))
|
||||||
|
|
||||||
// 0.25 inch fillet radius
|
// 0.25 inch fillet radius
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { sep } from '@tauri-apps/api/path'
|
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
|
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||||
|
import { parseProjectRoute, readAppSettingsFile } from './tauri'
|
||||||
|
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
|
||||||
|
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -25,28 +29,23 @@ export const paths = {
|
|||||||
} as const
|
} as const
|
||||||
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
||||||
|
|
||||||
export function getProjectMetaByRouteId(id?: string, defaultDir = '') {
|
export async function getProjectMetaByRouteId(
|
||||||
|
id?: string,
|
||||||
|
configuration?: Configuration
|
||||||
|
): Promise<ProjectRoute | undefined> {
|
||||||
if (!id) return undefined
|
if (!id) return undefined
|
||||||
const s = isTauri() ? sep() : '/'
|
|
||||||
|
|
||||||
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash
|
const inTauri = isTauri()
|
||||||
const projectAndFile =
|
|
||||||
defaultDir === '/'
|
|
||||||
? decodedId.replace(defaultDir, '')
|
|
||||||
: decodedId.replace(defaultDir + s, '')
|
|
||||||
const filePathParts = projectAndFile.split(s)
|
|
||||||
const projectName = filePathParts[0]
|
|
||||||
const projectPath =
|
|
||||||
(defaultDir === '/' ? defaultDir : defaultDir + s) + projectName
|
|
||||||
const lastPathPart = filePathParts[filePathParts.length - 1]
|
|
||||||
const currentFileName =
|
|
||||||
lastPathPart === projectName ? undefined : lastPathPart
|
|
||||||
const currentFilePath = lastPathPart === projectName ? undefined : decodedId
|
|
||||||
|
|
||||||
return {
|
if (!configuration) {
|
||||||
projectName,
|
configuration = inTauri
|
||||||
projectPath,
|
? await readAppSettingsFile()
|
||||||
currentFileName,
|
: readLocalStorageAppSettingsFile()
|
||||||
currentFilePath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const route = inTauri
|
||||||
|
? await parseProjectRoute(configuration, id)
|
||||||
|
: parseProjectRouteWasm(configuration, id)
|
||||||
|
|
||||||
|
return route
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
|
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
|
||||||
import {
|
import { FileLoaderData, HomeLoaderData, IndexLoaderData } from './types'
|
||||||
FileEntry,
|
|
||||||
FileLoaderData,
|
|
||||||
HomeLoaderData,
|
|
||||||
IndexLoaderData,
|
|
||||||
} from './types'
|
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
import { getProjectMetaByRouteId, paths } from './paths'
|
import { getProjectMetaByRouteId, paths } from './paths'
|
||||||
import { BROWSER_PATH } from 'lib/paths'
|
import { BROWSER_PATH } from 'lib/paths'
|
||||||
@ -14,33 +9,38 @@ import {
|
|||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
} from 'lib/constants'
|
} from 'lib/constants'
|
||||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||||
import {
|
|
||||||
getInitialDefaultDir,
|
|
||||||
getProjectsInDir,
|
|
||||||
initializeProjectDirectory,
|
|
||||||
} from './tauriFS'
|
|
||||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||||
import { join, sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
import { readTextFile, stat } from '@tauri-apps/plugin-fs'
|
import { readTextFile } from '@tauri-apps/plugin-fs'
|
||||||
import { codeManager, kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import {
|
||||||
|
getProjectInfo,
|
||||||
|
initializeProjectDirectory,
|
||||||
|
listProjects,
|
||||||
|
} from './tauri'
|
||||||
|
import { createSettings } from './settings/initialSettings'
|
||||||
|
|
||||||
// The root loader simply resolves the settings and any errors that
|
// The root loader simply resolves the settings and any errors that
|
||||||
// occurred during the settings load
|
// occurred during the settings load
|
||||||
export const settingsLoader: LoaderFunction = async ({
|
export const settingsLoader: LoaderFunction = async ({
|
||||||
params,
|
params,
|
||||||
}): ReturnType<typeof loadAndValidateSettings> => {
|
}): Promise<
|
||||||
let settings = await loadAndValidateSettings()
|
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
|
||||||
|
> => {
|
||||||
|
let { settings, configuration } = await loadAndValidateSettings()
|
||||||
|
|
||||||
// I don't love that we have to read the settings again here,
|
// I don't love that we have to read the settings again here,
|
||||||
// but we need to get the project path to load the project settings
|
// but we need to get the project path to load the project settings
|
||||||
if (params.id) {
|
if (params.id) {
|
||||||
const defaultDir = settings.app.projectDirectory.current || ''
|
const projectPathData = await getProjectMetaByRouteId(
|
||||||
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
params.id,
|
||||||
|
configuration
|
||||||
|
)
|
||||||
if (projectPathData) {
|
if (projectPathData) {
|
||||||
const { projectPath } = projectPathData
|
const { project_name } = projectPathData
|
||||||
settings = await loadAndValidateSettings(projectPath)
|
const { settings: s } = await loadAndValidateSettings(project_name)
|
||||||
|
settings = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export const settingsLoader: LoaderFunction = async ({
|
|||||||
|
|
||||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||||
const settings = await loadAndValidateSettings()
|
const { settings } = await loadAndValidateSettings()
|
||||||
const onboardingStatus = settings.app.onboardingStatus.current || ''
|
const onboardingStatus = settings.app.onboardingStatus.current || ''
|
||||||
const notEnRouteToOnboarding = !args.request.url.includes(
|
const notEnRouteToOnboarding = !args.request.url.includes(
|
||||||
paths.ONBOARDING.INDEX
|
paths.ONBOARDING.INDEX
|
||||||
@ -73,17 +73,19 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
|||||||
export const fileLoader: LoaderFunction = async ({
|
export const fileLoader: LoaderFunction = async ({
|
||||||
params,
|
params,
|
||||||
}): Promise<FileLoaderData | Response> => {
|
}): Promise<FileLoaderData | Response> => {
|
||||||
let settings = await loadAndValidateSettings()
|
let { configuration } = await loadAndValidateSettings()
|
||||||
|
|
||||||
const defaultDir = settings.app.projectDirectory.current || '/'
|
const projectPathData = await getProjectMetaByRouteId(
|
||||||
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
params.id,
|
||||||
|
configuration
|
||||||
|
)
|
||||||
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
|
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
|
||||||
|
|
||||||
if (!isBrowserProject && projectPathData) {
|
if (!isBrowserProject && projectPathData) {
|
||||||
const { projectName, projectPath, currentFileName, currentFilePath } =
|
const { project_name, project_path, current_file_name, current_file_path } =
|
||||||
projectPathData
|
projectPathData
|
||||||
|
|
||||||
if (!currentFileName || !currentFilePath) {
|
if (!current_file_name || !current_file_path || !project_name) {
|
||||||
return redirect(
|
return redirect(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
|
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
|
||||||
@ -93,35 +95,34 @@ export const fileLoader: LoaderFunction = async ({
|
|||||||
|
|
||||||
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
||||||
// until we support setting a project's entrypoint file
|
// until we support setting a project's entrypoint file
|
||||||
const code = await readTextFile(currentFilePath)
|
const code = await readTextFile(current_file_path)
|
||||||
const entrypointMetadata = await stat(
|
|
||||||
await join(projectPath, PROJECT_ENTRYPOINT)
|
|
||||||
)
|
|
||||||
const children = await invoke<FileEntry[]>('read_dir_recursive', {
|
|
||||||
path: projectPath,
|
|
||||||
})
|
|
||||||
// Update both the state and the editor's code.
|
// Update both the state and the editor's code.
|
||||||
// We explicitly do not write to the file here since we are loading from
|
// We explicitly do not write to the file here since we are loading from
|
||||||
// the file system and not the editor.
|
// the file system and not the editor.
|
||||||
codeManager.updateCurrentFilePath(currentFilePath)
|
codeManager.updateCurrentFilePath(current_file_path)
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
|
|
||||||
// Set the file system manager to the project path
|
// Set the file system manager to the project path
|
||||||
// So that WASM gets an updated path for operations
|
// So that WASM gets an updated path for operations
|
||||||
fileSystemManager.dir = projectPath
|
fileSystemManager.dir = project_path
|
||||||
|
|
||||||
const projectData: IndexLoaderData = {
|
const projectData: IndexLoaderData = {
|
||||||
code,
|
code,
|
||||||
project: {
|
project: isTauri()
|
||||||
name: projectName,
|
? await getProjectInfo(project_path, configuration)
|
||||||
path: projectPath,
|
: {
|
||||||
children,
|
name: project_name,
|
||||||
entrypointMetadata,
|
path: project_path,
|
||||||
},
|
children: [],
|
||||||
|
kcl_file_count: 0,
|
||||||
|
directory_count: 0,
|
||||||
|
},
|
||||||
file: {
|
file: {
|
||||||
name: currentFileName,
|
name: current_file_name,
|
||||||
path: currentFilePath,
|
path: current_file_path,
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +141,7 @@ export const fileLoader: LoaderFunction = async ({
|
|||||||
file: {
|
file: {
|
||||||
name: BROWSER_FILE_NAME,
|
name: BROWSER_FILE_NAME,
|
||||||
path: decodeURIComponent(BROWSER_PATH),
|
path: decodeURIComponent(BROWSER_PATH),
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,14 +154,12 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
|||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||||
}
|
}
|
||||||
const settings = await loadAndValidateSettings()
|
const { configuration } = await loadAndValidateSettings()
|
||||||
|
|
||||||
const projectDir = await initializeProjectDirectory(
|
const projectDir = await initializeProjectDirectory(configuration)
|
||||||
settings.app.projectDirectory.current || (await getInitialDefaultDir())
|
|
||||||
)
|
|
||||||
|
|
||||||
if (projectDir.path) {
|
if (projectDir) {
|
||||||
const projects = await getProjectsInDir(projectDir.path)
|
const projects = await listProjects(configuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
|
@ -1,100 +1,228 @@
|
|||||||
import {
|
|
||||||
getInitialDefaultDir,
|
|
||||||
getSettingsFilePaths,
|
|
||||||
readSettingsFile,
|
|
||||||
} from '../tauriFS'
|
|
||||||
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
||||||
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs'
|
import {
|
||||||
import { initPromise, tomlParse, tomlStringify } from 'lang/wasm'
|
defaultAppSettings,
|
||||||
|
defaultProjectSettings,
|
||||||
|
initPromise,
|
||||||
|
parseAppSettings,
|
||||||
|
parseProjectSettings,
|
||||||
|
tomlStringify,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
|
||||||
|
import { appThemeToTheme } from 'lib/theme'
|
||||||
|
import {
|
||||||
|
readAppSettingsFile,
|
||||||
|
readProjectSettingsFile,
|
||||||
|
writeAppSettingsFile,
|
||||||
|
writeProjectSettingsFile,
|
||||||
|
} from 'lib/tauri'
|
||||||
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We expect the settings to be stored in a TOML file
|
* Convert from a rust settings struct into the JS settings struct.
|
||||||
* or TOML-formatted string in localStorage
|
* We do this because the JS settings type has all the fancy shit
|
||||||
* under a top-level [settings] key.
|
* for hiding and showing settings.
|
||||||
* @param path
|
**/
|
||||||
* @returns
|
function configurationToSettingsPayload(
|
||||||
*/
|
configuration: Configuration
|
||||||
function getSettingsFromStorage(path: string) {
|
): Partial<SaveSettingsPayload> {
|
||||||
return isTauri()
|
return {
|
||||||
? readSettingsFile(path)
|
app: {
|
||||||
: (tomlParse(localStorage.getItem(path) ?? '')
|
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||||
.settings as Partial<SaveSettingsPayload>)
|
themeColor: configuration?.settings?.app?.appearance?.color
|
||||||
|
? configuration?.settings?.app?.appearance?.color.toString()
|
||||||
|
: undefined,
|
||||||
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
|
projectDirectory: configuration?.settings?.project?.directory,
|
||||||
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
|
},
|
||||||
|
modeling: {
|
||||||
|
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
||||||
|
mouseControls: mouseControlsToCameraSystem(
|
||||||
|
configuration?.settings?.modeling?.mouse_controls
|
||||||
|
),
|
||||||
|
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
||||||
|
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
||||||
|
},
|
||||||
|
textEditor: {
|
||||||
|
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
||||||
|
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
defaultProjectName:
|
||||||
|
configuration?.settings?.project?.default_project_name,
|
||||||
|
},
|
||||||
|
commandBar: {
|
||||||
|
includeSettings: configuration?.settings?.command_bar?.include_settings,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAndValidateSettings(projectPath?: string) {
|
function projectConfigurationToSettingsPayload(
|
||||||
const settings = createSettings()
|
configuration: ProjectConfiguration
|
||||||
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
): Partial<SaveSettingsPayload> {
|
||||||
// First, get the settings data at the user and project level
|
return {
|
||||||
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
app: {
|
||||||
|
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||||
|
themeColor: configuration?.settings?.app?.appearance?.color
|
||||||
|
? configuration?.settings?.app?.appearance?.color.toString()
|
||||||
|
: undefined,
|
||||||
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
|
},
|
||||||
|
modeling: {
|
||||||
|
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
||||||
|
mouseControls: mouseControlsToCameraSystem(
|
||||||
|
configuration?.settings?.modeling?.mouse_controls
|
||||||
|
),
|
||||||
|
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
||||||
|
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
||||||
|
},
|
||||||
|
textEditor: {
|
||||||
|
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
||||||
|
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
|
||||||
|
},
|
||||||
|
commandBar: {
|
||||||
|
includeSettings: configuration?.settings?.command_bar?.include_settings,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load the settings from the files
|
function localStorageAppSettingsPath() {
|
||||||
if (settingsFilePaths.user) {
|
return '/settings.toml'
|
||||||
await initPromise
|
}
|
||||||
const userSettings = await getSettingsFromStorage(settingsFilePaths.user)
|
|
||||||
if (userSettings) {
|
function localStorageProjectSettingsPath() {
|
||||||
setSettingsAtLevel(settings, 'user', userSettings)
|
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readLocalStorageAppSettingsFile(): Configuration {
|
||||||
|
// TODO: Remove backwards compatibility after a few releases.
|
||||||
|
let stored =
|
||||||
|
localStorage.getItem(localStorageAppSettingsPath()) ??
|
||||||
|
localStorage.getItem('/user.toml') ??
|
||||||
|
''
|
||||||
|
|
||||||
|
if (stored === '') {
|
||||||
|
return defaultAppSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the project settings if they exist
|
try {
|
||||||
if (settingsFilePaths.project) {
|
return parseAppSettings(stored)
|
||||||
const projectSettings = await getSettingsFromStorage(
|
} catch (e) {
|
||||||
settingsFilePaths.project
|
const settings = defaultAppSettings()
|
||||||
|
localStorage.setItem(localStorageAppSettingsPath(), tomlStringify(settings))
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLocalStorageProjectSettingsFile(): ProjectConfiguration {
|
||||||
|
// TODO: Remove backwards compatibility after a few releases.
|
||||||
|
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
|
||||||
|
|
||||||
|
if (stored === '') {
|
||||||
|
return defaultProjectSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parseProjectSettings(stored)
|
||||||
|
} catch (e) {
|
||||||
|
const settings = defaultProjectSettings()
|
||||||
|
localStorage.setItem(
|
||||||
|
localStorageProjectSettingsPath(),
|
||||||
|
tomlStringify(settings)
|
||||||
)
|
)
|
||||||
if (projectSettings) {
|
return settings
|
||||||
setSettingsAtLevel(settings, 'project', projectSettings)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSettings {
|
||||||
|
settings: ReturnType<typeof createSettings>
|
||||||
|
configuration: Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAndValidateSettings(
|
||||||
|
projectName?: string
|
||||||
|
): Promise<AppSettings> {
|
||||||
|
const settings = createSettings()
|
||||||
|
const inTauri = isTauri()
|
||||||
|
|
||||||
|
if (!inTauri) {
|
||||||
|
// Make sure we have wasm initialized.
|
||||||
|
await initPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the app settings from the file system or localStorage.
|
||||||
|
const appSettings = inTauri
|
||||||
|
? await readAppSettingsFile()
|
||||||
|
: readLocalStorageAppSettingsFile()
|
||||||
|
// Convert the app settings to the JS settings format.
|
||||||
|
const appSettingsPayload = configurationToSettingsPayload(appSettings)
|
||||||
|
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||||
|
|
||||||
|
// Load the project settings if they exist
|
||||||
|
if (projectName) {
|
||||||
|
const projectSettings = inTauri
|
||||||
|
? await readProjectSettingsFile(appSettings, projectName)
|
||||||
|
: readLocalStorageProjectSettingsFile()
|
||||||
|
|
||||||
|
const projectSettingsPayload =
|
||||||
|
projectConfigurationToSettingsPayload(projectSettings)
|
||||||
|
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the settings object
|
// Return the settings object
|
||||||
return settings
|
return { settings, configuration: appSettings }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSettings(
|
export async function saveSettings(
|
||||||
allSettings: typeof settings,
|
allSettings: typeof settings,
|
||||||
projectPath?: string
|
projectName?: string
|
||||||
) {
|
|
||||||
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
|
||||||
|
|
||||||
if (settingsFilePaths.user) {
|
|
||||||
const changedSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
|
||||||
|
|
||||||
await writeOrClearPersistedSettings(settingsFilePaths.user, changedSettings)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingsFilePaths.project) {
|
|
||||||
const changedSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
|
||||||
|
|
||||||
await writeOrClearPersistedSettings(
|
|
||||||
settingsFilePaths.project,
|
|
||||||
changedSettings
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeOrClearPersistedSettings(
|
|
||||||
settingsFilePath: string,
|
|
||||||
changedSettings: Partial<SaveSettingsPayload>
|
|
||||||
) {
|
) {
|
||||||
|
// Make sure we have wasm initialized.
|
||||||
await initPromise
|
await initPromise
|
||||||
if (changedSettings && Object.keys(changedSettings).length) {
|
const inTauri = isTauri()
|
||||||
if (isTauri()) {
|
|
||||||
await writeTextFile(
|
// Get the user settings.
|
||||||
settingsFilePath,
|
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
||||||
tomlStringify({ settings: changedSettings })
|
const tomlString = tomlStringify({ settings: jsAppSettings })
|
||||||
)
|
// Parse this as a Configuration.
|
||||||
}
|
const appSettings = parseAppSettings(tomlString)
|
||||||
localStorage.setItem(
|
|
||||||
settingsFilePath,
|
// Write the app settings.
|
||||||
tomlStringify({ settings: changedSettings })
|
if (inTauri) {
|
||||||
)
|
await writeAppSettingsFile(appSettings)
|
||||||
} else {
|
} else {
|
||||||
if (isTauri() && (await exists(settingsFilePath))) {
|
localStorage.setItem(
|
||||||
await remove(settingsFilePath)
|
localStorageAppSettingsPath(),
|
||||||
}
|
tomlStringify(appSettings)
|
||||||
localStorage.removeItem(settingsFilePath)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectName) {
|
||||||
|
// If we're not saving project settings, we're done.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the project settings.
|
||||||
|
const jsProjectSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
||||||
|
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
|
||||||
|
// Parse this as a Configuration.
|
||||||
|
const projectSettings = parseProjectSettings(projectTomlString)
|
||||||
|
|
||||||
|
// Write the project settings.
|
||||||
|
if (inTauri) {
|
||||||
|
await writeProjectSettingsFile(appSettings, projectName, projectSettings)
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(
|
||||||
|
localStorageProjectSettingsPath(),
|
||||||
|
tomlStringify(projectSettings)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
faArrowUp,
|
faArrowUp,
|
||||||
faCircle,
|
faCircle,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
const DESC = ':desc'
|
const DESC = ':desc'
|
||||||
|
|
||||||
@ -27,10 +27,7 @@ export function getNextSearchParams(currentSort: string, newSort: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSortFunction(sortBy: string) {
|
export function getSortFunction(sortBy: string) {
|
||||||
const sortByName = (
|
const sortByName = (a: Project, b: Project) => {
|
||||||
a: ProjectWithEntryPointMetadata,
|
|
||||||
b: ProjectWithEntryPointMetadata
|
|
||||||
) => {
|
|
||||||
if (a.name && b.name) {
|
if (a.name && b.name) {
|
||||||
return sortBy.includes('desc')
|
return sortBy.includes('desc')
|
||||||
? a.name.localeCompare(b.name)
|
? a.name.localeCompare(b.name)
|
||||||
@ -39,16 +36,13 @@ export function getSortFunction(sortBy: string) {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortByModified = (
|
const sortByModified = (a: Project, b: Project) => {
|
||||||
a: ProjectWithEntryPointMetadata,
|
if (a.metadata?.modified && b.metadata?.modified) {
|
||||||
b: ProjectWithEntryPointMetadata
|
const aDate = new Date(a.metadata.modified)
|
||||||
) => {
|
const bDate = new Date(b.metadata.modified)
|
||||||
if (a.entrypointMetadata?.mtime && b.entrypointMetadata?.mtime) {
|
|
||||||
return !sortBy || sortBy.includes('desc')
|
return !sortBy || sortBy.includes('desc')
|
||||||
? b.entrypointMetadata.mtime.getTime() -
|
? bDate.getTime() - aDate.getTime()
|
||||||
a.entrypointMetadata.mtime.getTime()
|
: aDate.getTime() - bDate.getTime()
|
||||||
: a.entrypointMetadata.mtime.getTime() -
|
|
||||||
b.entrypointMetadata.mtime.getTime()
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
150
src/lib/tauri.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// This file contains wrappers around the tauri commands we define in rust code.
|
||||||
|
|
||||||
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||||
|
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
|
||||||
|
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||||
|
|
||||||
|
// Get the app state from tauri.
|
||||||
|
export async function getState(): Promise<ProjectState | undefined> {
|
||||||
|
return await invoke<ProjectState | undefined>('get_state')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the app state in tauri.
|
||||||
|
export async function setState(state: ProjectState | undefined): Promise<void> {
|
||||||
|
return await invoke('set_state', { state })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the initial default dir for holding all projects.
|
||||||
|
export async function getInitialDefaultDir(): Promise<string> {
|
||||||
|
return invoke<string>('get_initial_default_dir')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showInFolder(path: string | undefined): Promise<void> {
|
||||||
|
if (!path) {
|
||||||
|
console.error('path is undefined cannot call tauri showInFolder')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return await invoke('show_in_folder', { path })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeProjectDirectory(
|
||||||
|
settings: Configuration
|
||||||
|
): Promise<string> {
|
||||||
|
return await invoke<string>('initialize_project_directory', {
|
||||||
|
configuration: settings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNewProjectDirectory(
|
||||||
|
projectName: string,
|
||||||
|
initialCode?: string,
|
||||||
|
configuration?: Configuration
|
||||||
|
): Promise<Project> {
|
||||||
|
if (!configuration) {
|
||||||
|
configuration = await readAppSettingsFile()
|
||||||
|
}
|
||||||
|
return await invoke<Project>('create_new_project_directory', {
|
||||||
|
configuration,
|
||||||
|
projectName,
|
||||||
|
initialCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listProjects(
|
||||||
|
configuration?: Configuration
|
||||||
|
): Promise<Project[]> {
|
||||||
|
if (!configuration) {
|
||||||
|
configuration = await readAppSettingsFile()
|
||||||
|
}
|
||||||
|
return await invoke<Project[]>('list_projects', { configuration })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectInfo(
|
||||||
|
projectPath: string,
|
||||||
|
configuration?: Configuration
|
||||||
|
): Promise<Project> {
|
||||||
|
if (!configuration) {
|
||||||
|
configuration = await readAppSettingsFile()
|
||||||
|
}
|
||||||
|
return await invoke<Project>('get_project_info', {
|
||||||
|
configuration,
|
||||||
|
projectPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(host: string): Promise<string> {
|
||||||
|
return await invoke('login', { host })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseProjectRoute(
|
||||||
|
configuration: Configuration,
|
||||||
|
route: string
|
||||||
|
): Promise<ProjectRoute> {
|
||||||
|
return await invoke<ProjectRoute>('parse_project_route', {
|
||||||
|
configuration,
|
||||||
|
route,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUser(
|
||||||
|
token: string | undefined,
|
||||||
|
host: string
|
||||||
|
): Promise<Models['User_type'] | Record<'error_code', unknown> | void> {
|
||||||
|
if (!token) {
|
||||||
|
console.error('token is undefined cannot call tauri getUser')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return await invoke<Models['User_type'] | Record<'error_code', unknown>>(
|
||||||
|
'get_user',
|
||||||
|
{
|
||||||
|
token: token,
|
||||||
|
hostname: host,
|
||||||
|
}
|
||||||
|
).catch((err) => console.error('error from Tauri getUser', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readDirRecursive(path: string): Promise<FileEntry[]> {
|
||||||
|
return await invoke<FileEntry[]>('read_dir_recursive', { path })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the contents of the app settings.
|
||||||
|
export async function readAppSettingsFile(): Promise<Configuration> {
|
||||||
|
return await invoke<Configuration>('read_app_settings_file')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the contents of the app settings.
|
||||||
|
export async function writeAppSettingsFile(
|
||||||
|
settings: Configuration
|
||||||
|
): Promise<void> {
|
||||||
|
return await invoke('write_app_settings_file', { configuration: settings })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read project settings file.
|
||||||
|
export async function readProjectSettingsFile(
|
||||||
|
appSettings: Configuration,
|
||||||
|
projectName: string
|
||||||
|
): Promise<ProjectConfiguration> {
|
||||||
|
return await invoke<ProjectConfiguration>('read_project_settings_file', {
|
||||||
|
appSettings,
|
||||||
|
projectName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write project settings file.
|
||||||
|
export async function writeProjectSettingsFile(
|
||||||
|
appSettings: Configuration,
|
||||||
|
projectName: string,
|
||||||
|
settings: ProjectConfiguration
|
||||||
|
): Promise<void> {
|
||||||
|
return await invoke('write_project_settings_file', {
|
||||||
|
appSettings,
|
||||||
|
projectName,
|
||||||
|
configuration: settings,
|
||||||
|
})
|
||||||
|
}
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS'
|
||||||
deepFileFilter,
|
|
||||||
getNextProjectIndex,
|
|
||||||
getPartsCount,
|
|
||||||
interpolateProjectNameWithIndex,
|
|
||||||
isRelevantFileOrDir,
|
|
||||||
} from './tauriFS'
|
|
||||||
import type { FileEntry } from './types'
|
|
||||||
import { MAX_PADDING } from './constants'
|
import { MAX_PADDING } from './constants'
|
||||||
|
|
||||||
describe('Test project name utility functions', () => {
|
describe('Test project name utility functions', () => {
|
||||||
@ -31,18 +24,22 @@ describe('Test project name utility functions', () => {
|
|||||||
{
|
{
|
||||||
name: 'new-project-04.kcl',
|
name: 'new-project-04.kcl',
|
||||||
path: '/projects/new-project-04.kcl',
|
path: '/projects/new-project-04.kcl',
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new-project-007.kcl',
|
name: 'new-project-007.kcl',
|
||||||
path: '/projects/new-project-007.kcl',
|
path: '/projects/new-project-007.kcl',
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new-project-05.kcl',
|
name: 'new-project-05.kcl',
|
||||||
path: '/projects/new-project-05.kcl',
|
path: '/projects/new-project-05.kcl',
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new-project-0.kcl',
|
name: 'new-project-0.kcl',
|
||||||
path: '/projects/new-project-0.kcl',
|
path: '/projects/new-project-0.kcl',
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -50,101 +47,3 @@ describe('Test project name utility functions', () => {
|
|||||||
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
|
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Test file tree utility functions', () => {
|
|
||||||
const baseFiles: FileEntry[] = [
|
|
||||||
{
|
|
||||||
name: 'show-me.kcl',
|
|
||||||
path: '/projects/show-me.kcl',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hide-me.jpg',
|
|
||||||
path: '/projects/hide-me.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '.gitignore',
|
|
||||||
path: '/projects/.gitignore',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const filteredBaseFiles: FileEntry[] = [
|
|
||||||
{
|
|
||||||
name: 'show-me.kcl',
|
|
||||||
path: '/projects/show-me.kcl',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
it('Only includes files relevant to the project in a flat directory', () => {
|
|
||||||
expect(deepFileFilter(baseFiles, isRelevantFileOrDir)).toEqual(
|
|
||||||
filteredBaseFiles
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const nestedFiles: FileEntry[] = [
|
|
||||||
...baseFiles,
|
|
||||||
{
|
|
||||||
name: 'show-me',
|
|
||||||
path: '/projects/show-me',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'show-me-nested',
|
|
||||||
path: '/projects/show-me/show-me-nested',
|
|
||||||
children: baseFiles,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hide-me',
|
|
||||||
path: '/projects/show-me/hide-me',
|
|
||||||
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hide-me',
|
|
||||||
path: '/projects/hide-me',
|
|
||||||
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const filteredNestedFiles: FileEntry[] = [
|
|
||||||
...filteredBaseFiles,
|
|
||||||
{
|
|
||||||
name: 'show-me',
|
|
||||||
path: '/projects/show-me',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'show-me-nested',
|
|
||||||
path: '/projects/show-me/show-me-nested',
|
|
||||||
children: filteredBaseFiles,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
it('Only includes directories that include files relevant to the project in a nested directory', () => {
|
|
||||||
expect(deepFileFilter(nestedFiles, isRelevantFileOrDir)).toEqual(
|
|
||||||
filteredNestedFiles
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const withHiddenDir: FileEntry[] = [
|
|
||||||
...baseFiles,
|
|
||||||
{
|
|
||||||
name: '.hide-me',
|
|
||||||
path: '/projects/.hide-me',
|
|
||||||
children: baseFiles,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
it(`Hides folders that begin with a ".", even if they contain relevant files`, () => {
|
|
||||||
expect(deepFileFilter(withHiddenDir, isRelevantFileOrDir)).toEqual(
|
|
||||||
filteredBaseFiles
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`Properly counts the number of relevant files and directories in a project`, () => {
|
|
||||||
expect(getPartsCount(nestedFiles)).toEqual({
|
|
||||||
kclFileCount: 2,
|
|
||||||
kclDirCount: 2,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
@ -1,154 +1,19 @@
|
|||||||
import {
|
import { appConfigDir } from '@tauri-apps/api/path'
|
||||||
mkdir,
|
|
||||||
exists,
|
|
||||||
readTextFile,
|
|
||||||
writeTextFile,
|
|
||||||
stat,
|
|
||||||
} from '@tauri-apps/plugin-fs'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import {
|
|
||||||
appConfigDir,
|
|
||||||
documentDir,
|
|
||||||
homeDir,
|
|
||||||
join,
|
|
||||||
sep,
|
|
||||||
} from '@tauri-apps/api/path'
|
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
|
import type { FileEntry } from 'lib/types'
|
||||||
import {
|
import {
|
||||||
FILE_EXT,
|
|
||||||
INDEX_IDENTIFIER,
|
INDEX_IDENTIFIER,
|
||||||
MAX_PADDING,
|
MAX_PADDING,
|
||||||
ONBOARDING_PROJECT_NAME,
|
ONBOARDING_PROJECT_NAME,
|
||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
PROJECT_FOLDER,
|
|
||||||
RELEVANT_FILE_TYPES,
|
|
||||||
SETTINGS_FILE_EXT,
|
|
||||||
} from 'lib/constants'
|
} from 'lib/constants'
|
||||||
import { SaveSettingsPayload, SettingsLevel } from './settings/settingsTypes'
|
|
||||||
import { initPromise, tomlParse } from 'lang/wasm'
|
|
||||||
import { bracket } from './exampleKcl'
|
import { bracket } from './exampleKcl'
|
||||||
import { paths } from './paths'
|
import { paths } from './paths'
|
||||||
|
import {
|
||||||
type PathWithPossibleError = {
|
createNewProjectDirectory,
|
||||||
path: string | null
|
listProjects,
|
||||||
error: Error | null
|
readAppSettingsFile,
|
||||||
}
|
} from './tauri'
|
||||||
|
|
||||||
export async function getInitialDefaultDir() {
|
|
||||||
if (!isTauri()) return ''
|
|
||||||
let dir
|
|
||||||
try {
|
|
||||||
dir = await documentDir()
|
|
||||||
} catch (e) {
|
|
||||||
dir = await join(await homeDir(), 'Documents') // for headless Linux (eg. Github Actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await join(dir, PROJECT_FOLDER)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes the project directory and returns the path
|
|
||||||
// with any Errors that occurred
|
|
||||||
export async function initializeProjectDirectory(
|
|
||||||
directory: string
|
|
||||||
): Promise<PathWithPossibleError> {
|
|
||||||
let returnValue: PathWithPossibleError = {
|
|
||||||
path: null,
|
|
||||||
error: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTauri()) return returnValue
|
|
||||||
|
|
||||||
if (directory) {
|
|
||||||
returnValue = await testAndCreateDir(directory, returnValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the directory from settings does not exist or could not be created,
|
|
||||||
// use the default directory
|
|
||||||
if (returnValue.path === null) {
|
|
||||||
const INITIAL_DEFAULT_DIR = await getInitialDefaultDir()
|
|
||||||
const defaultReturnValue = await testAndCreateDir(
|
|
||||||
INITIAL_DEFAULT_DIR,
|
|
||||||
returnValue,
|
|
||||||
{
|
|
||||||
exists: 'Error checking default directory.',
|
|
||||||
create: 'Error creating default directory.',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
returnValue.path = defaultReturnValue.path
|
|
||||||
returnValue.error =
|
|
||||||
returnValue.error === null ? defaultReturnValue.error : returnValue.error
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnValue
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testAndCreateDir(
|
|
||||||
directory: string,
|
|
||||||
returnValue = {
|
|
||||||
path: null,
|
|
||||||
error: null,
|
|
||||||
} as PathWithPossibleError,
|
|
||||||
errorMessages = {
|
|
||||||
exists:
|
|
||||||
'Error checking directory at path from saved settings. Using default.',
|
|
||||||
create:
|
|
||||||
'Error creating directory at path from saved settings. Using default.',
|
|
||||||
}
|
|
||||||
): Promise<PathWithPossibleError> {
|
|
||||||
const dirExists = await exists(directory).catch((e) => {
|
|
||||||
console.error(`Error checking directory ${directory}. Original error:`, e)
|
|
||||||
return new Error(errorMessages.exists)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (dirExists instanceof Error) {
|
|
||||||
returnValue.error = dirExists
|
|
||||||
} else if (dirExists === false) {
|
|
||||||
const newDirCreated = await mkdir(directory, { recursive: true }).catch(
|
|
||||||
(e) => {
|
|
||||||
console.error(
|
|
||||||
`Error creating directory ${directory}. Original error:`,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
return new Error(errorMessages.create)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (newDirCreated instanceof Error) {
|
|
||||||
returnValue.error = newDirCreated
|
|
||||||
} else {
|
|
||||||
returnValue.path = directory
|
|
||||||
}
|
|
||||||
} else if (dirExists === true) {
|
|
||||||
returnValue.path = directory
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnValue
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
|
|
||||||
return (
|
|
||||||
fileOrDir.children?.length &&
|
|
||||||
fileOrDir.children.some((child) => child.name === PROJECT_ENTRYPOINT)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the contents of a directory
|
|
||||||
// and return the valid projects
|
|
||||||
export async function getProjectsInDir(projectDir: string) {
|
|
||||||
const readProjects = (
|
|
||||||
await invoke<FileEntry[]>('read_dir_recursive', { path: projectDir })
|
|
||||||
).filter(isProjectDirectory)
|
|
||||||
|
|
||||||
const projectsWithMetadata = await Promise.all(
|
|
||||||
readProjects.map(async (p) => ({
|
|
||||||
entrypointMetadata: await stat(await join(p.path, PROJECT_ENTRYPOINT)),
|
|
||||||
...p,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
return projectsWithMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isHidden = (fileOrDir: FileEntry) =>
|
export const isHidden = (fileOrDir: FileEntry) =>
|
||||||
!!fileOrDir.name?.startsWith('.')
|
!!fileOrDir.name?.startsWith('.')
|
||||||
@ -156,97 +21,6 @@ export const isHidden = (fileOrDir: FileEntry) =>
|
|||||||
export const isDir = (fileOrDir: FileEntry) =>
|
export const isDir = (fileOrDir: FileEntry) =>
|
||||||
'children' in fileOrDir && fileOrDir.children !== undefined
|
'children' in fileOrDir && fileOrDir.children !== undefined
|
||||||
|
|
||||||
export function deepFileFilter(
|
|
||||||
entries: FileEntry[],
|
|
||||||
filterFn: (f: FileEntry) => boolean
|
|
||||||
): FileEntry[] {
|
|
||||||
const filteredEntries: FileEntry[] = []
|
|
||||||
for (const fileOrDir of entries) {
|
|
||||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
|
||||||
const filteredChildren = deepFileFilter(fileOrDir.children, filterFn)
|
|
||||||
if (filterFn(fileOrDir)) {
|
|
||||||
filteredEntries.push({
|
|
||||||
...fileOrDir,
|
|
||||||
children: filteredChildren,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (filterFn(fileOrDir)) {
|
|
||||||
filteredEntries.push(fileOrDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deepFileFilterFlat(
|
|
||||||
entries: FileEntry[],
|
|
||||||
filterFn: (f: FileEntry) => boolean
|
|
||||||
): FileEntry[] {
|
|
||||||
const filteredEntries: FileEntry[] = []
|
|
||||||
for (const fileOrDir of entries) {
|
|
||||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
|
||||||
const filteredChildren = deepFileFilterFlat(fileOrDir.children, filterFn)
|
|
||||||
if (filterFn(fileOrDir)) {
|
|
||||||
filteredEntries.push({
|
|
||||||
...fileOrDir,
|
|
||||||
children: filteredChildren,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
filteredEntries.push(...filteredChildren)
|
|
||||||
} else if (filterFn(fileOrDir)) {
|
|
||||||
filteredEntries.push(fileOrDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the contents of a project directory
|
|
||||||
// and return all relevant files and sub-directories recursively
|
|
||||||
export async function readProject(projectDir: string) {
|
|
||||||
const readFiles = await invoke<FileEntry[]>('read_dir_recursive', {
|
|
||||||
path: projectDir,
|
|
||||||
})
|
|
||||||
|
|
||||||
return deepFileFilter(readFiles, isRelevantFileOrDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a read project, return the number of .kcl files,
|
|
||||||
// both in the root directory and in sub-directories,
|
|
||||||
// and folders that contain at least one .kcl file
|
|
||||||
export function getPartsCount(project: FileEntry[]) {
|
|
||||||
const flatProject = deepFileFilterFlat(project, isRelevantFileOrDir)
|
|
||||||
|
|
||||||
const kclFileCount = flatProject.filter((f) =>
|
|
||||||
f.name?.endsWith(FILE_EXT)
|
|
||||||
).length
|
|
||||||
const kclDirCount = flatProject.filter((f) => f.children !== undefined).length
|
|
||||||
|
|
||||||
return {
|
|
||||||
kclFileCount,
|
|
||||||
kclDirCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if a file or directory is relevant to the project
|
|
||||||
// i.e. not a hidden file or directory, and is a relevant file type
|
|
||||||
// or contains at least one relevant file (even if it's nested)
|
|
||||||
// or is a completely empty directory
|
|
||||||
export function isRelevantFileOrDir(fileOrDir: FileEntry) {
|
|
||||||
let isRelevantDir = false
|
|
||||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
|
||||||
isRelevantDir =
|
|
||||||
!isHidden(fileOrDir) &&
|
|
||||||
(fileOrDir.children.some(isRelevantFileOrDir) ||
|
|
||||||
fileOrDir.children.length === 0)
|
|
||||||
}
|
|
||||||
const isRelevantFile =
|
|
||||||
!isHidden(fileOrDir) &&
|
|
||||||
RELEVANT_FILE_TYPES.some((ext) => fileOrDir.name?.endsWith(ext))
|
|
||||||
|
|
||||||
return (
|
|
||||||
(isDir(fileOrDir) && isRelevantDir) || (!isDir(fileOrDir) && isRelevantFile)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deeply sort the files and directories in a project like VS Code does:
|
// Deeply sort the files and directories in a project like VS Code does:
|
||||||
// The main.kcl file is always first, then files, then directories
|
// The main.kcl file is always first, then files, then directories
|
||||||
// Files and directories are sorted alphabetically
|
// Files and directories are sorted alphabetically
|
||||||
@ -279,47 +53,6 @@ export function sortProject(project: FileEntry[]): FileEntry[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new file in the default directory with the default project name
|
|
||||||
// Returns the path to the new file
|
|
||||||
export async function createNewProject(
|
|
||||||
path: string,
|
|
||||||
initCode = ''
|
|
||||||
): Promise<ProjectWithEntryPointMetadata> {
|
|
||||||
if (!isTauri) {
|
|
||||||
throw new Error('createNewProject() can only be called from a Tauri app')
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirExists = await exists(path)
|
|
||||||
if (!dirExists) {
|
|
||||||
await mkdir(path, { recursive: true }).catch((err) => {
|
|
||||||
console.error('Error creating new directory:', err)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeTextFile(await join(path, PROJECT_ENTRYPOINT), initCode).catch(
|
|
||||||
(err) => {
|
|
||||||
console.error('Error creating new file:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const m = await stat(path)
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: path.slice(path.lastIndexOf(sep()) + 1),
|
|
||||||
path: path,
|
|
||||||
entrypointMetadata: m,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: PROJECT_ENTRYPOINT,
|
|
||||||
path: await join(path, PROJECT_ENTRYPOINT),
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a regex to match the project name
|
// create a regex to match the project name
|
||||||
// replacing any instances of "$n" with a regex to match any number
|
// replacing any instances of "$n" with a regex to match any number
|
||||||
function interpolateProjectName(projectName: string) {
|
function interpolateProjectName(projectName: string) {
|
||||||
@ -373,55 +106,6 @@ function getPaddedIdentifierRegExp() {
|
|||||||
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserSettingsFilePath(
|
|
||||||
filename: string = SETTINGS_FILE_EXT
|
|
||||||
) {
|
|
||||||
const dir = await appConfigDir()
|
|
||||||
return await join(dir, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readSettingsFile(
|
|
||||||
path: string
|
|
||||||
): Promise<Partial<SaveSettingsPayload>> {
|
|
||||||
const dir = path.slice(0, path.lastIndexOf(sep()))
|
|
||||||
|
|
||||||
const dirExists = await exists(dir)
|
|
||||||
if (!dirExists) {
|
|
||||||
await mkdir(dir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingsExist = dirExists ? await exists(path) : false
|
|
||||||
|
|
||||||
if (!settingsExist) {
|
|
||||||
console.log(`Settings file does not exist at ${path}`)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await initPromise
|
|
||||||
const settings = await readTextFile(path)
|
|
||||||
// We expect the settings to be under a top-level [settings] key
|
|
||||||
return tomlParse(settings).settings as Partial<SaveSettingsPayload>
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error reading settings file:', e)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSettingsFilePaths(
|
|
||||||
projectPath?: string
|
|
||||||
): Promise<Partial<Record<SettingsLevel, string>>> {
|
|
||||||
const { user, project } = await getSettingsFolderPaths(projectPath)
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: user + 'user' + SETTINGS_FILE_EXT,
|
|
||||||
project:
|
|
||||||
project !== undefined
|
|
||||||
? project + (isTauri() ? sep() : '/') + 'project' + SETTINGS_FILE_EXT
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSettingsFolderPaths(projectPath?: string) {
|
export async function getSettingsFolderPaths(projectPath?: string) {
|
||||||
const user = isTauri() ? await appConfigDir() : '/'
|
const user = isTauri() ? await appConfigDir() : '/'
|
||||||
const project = projectPath !== undefined ? projectPath : undefined
|
const project = projectPath !== undefined ? projectPath : undefined
|
||||||
@ -433,18 +117,15 @@ export async function getSettingsFolderPaths(projectPath?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createAndOpenNewProject(
|
export async function createAndOpenNewProject(
|
||||||
projectDirectory: string,
|
|
||||||
navigate: (path: string) => void
|
navigate: (path: string) => void
|
||||||
) {
|
) {
|
||||||
const projects = await getProjectsInDir(projectDirectory)
|
const configuration = await readAppSettingsFile()
|
||||||
const nextIndex = await getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
const projects = await listProjects(configuration)
|
||||||
|
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
||||||
const name = interpolateProjectNameWithIndex(
|
const name = interpolateProjectNameWithIndex(
|
||||||
ONBOARDING_PROJECT_NAME,
|
ONBOARDING_PROJECT_NAME,
|
||||||
nextIndex
|
nextIndex
|
||||||
)
|
)
|
||||||
const newFile = await createNewProject(
|
const newFile = await createNewProjectDirectory(name, bracket, configuration)
|
||||||
await join(projectDirectory, name),
|
|
||||||
bracket
|
|
||||||
)
|
|
||||||
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
|
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
|
import { AppTheme } from 'wasm-lib/kcl/bindings/AppTheme'
|
||||||
|
|
||||||
export enum Themes {
|
export enum Themes {
|
||||||
Light = 'light',
|
Light = 'light',
|
||||||
Dark = 'dark',
|
Dark = 'dark',
|
||||||
System = 'system',
|
System = 'system',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function appThemeToTheme(
|
||||||
|
theme: AppTheme | undefined
|
||||||
|
): Themes | undefined {
|
||||||
|
switch (theme) {
|
||||||
|
case 'light':
|
||||||
|
return Themes.Light
|
||||||
|
case 'dark':
|
||||||
|
return Themes.Dark
|
||||||
|
case 'system':
|
||||||
|
return Themes.System
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the theme from the system settings manually
|
// Get the theme from the system settings manually
|
||||||
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
||||||
return typeof globalThis.window !== 'undefined' &&
|
return typeof globalThis.window !== 'undefined' &&
|
||||||
|
@ -1,35 +1,22 @@
|
|||||||
import { type FileInfo } from '@tauri-apps/plugin-fs'
|
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
|
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||||
|
|
||||||
export type IndexLoaderData = {
|
export type IndexLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
project?: ProjectWithEntryPointMetadata
|
project?: Project
|
||||||
file?: FileEntry
|
file?: FileEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileLoaderData = {
|
export type FileLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
project?: FileEntry | ProjectWithEntryPointMetadata
|
project?: FileEntry | Project
|
||||||
file?: FileEntry
|
file?: FileEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectWithEntryPointMetadata = FileEntry & {
|
|
||||||
entrypointMetadata: FileInfo
|
|
||||||
}
|
|
||||||
export type HomeLoaderData = {
|
export type HomeLoaderData = {
|
||||||
projects: ProjectWithEntryPointMetadata[]
|
projects: Project[]
|
||||||
}
|
|
||||||
|
|
||||||
// From https://github.com/tauri-apps/tauri/blob/1.x/tooling/api/src/fs.ts#L159
|
|
||||||
// Removed from tauri v2
|
|
||||||
export interface FileEntry {
|
|
||||||
path: string
|
|
||||||
/**
|
|
||||||
* Name of the directory/file
|
|
||||||
* can be null if the path terminates with `..`
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/** Children of this entry if it's a directory; null otherwise */
|
|
||||||
children?: FileEntry[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272
|
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272
|
||||||
|
@ -2,8 +2,8 @@ import { createMachine, assign } from 'xstate'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { VITE_KC_API_BASE_URL, VITE_KC_DEV_TOKEN } from 'env'
|
||||||
import { VITE_KC_API_BASE_URL } from 'env'
|
import { getUser as getUserTauri } from 'lib/tauri'
|
||||||
|
|
||||||
const SKIP_AUTH =
|
const SKIP_AUTH =
|
||||||
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
||||||
@ -112,14 +112,25 @@ export const authMachine = createMachine<UserContext, Events>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
async function getUser(context: UserContext) {
|
async function getUser(context: UserContext) {
|
||||||
|
const token =
|
||||||
|
context.token && context.token !== ''
|
||||||
|
? context.token
|
||||||
|
: getCookie(COOKIE_NAME) ||
|
||||||
|
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
||||||
|
VITE_KC_DEV_TOKEN
|
||||||
const url = withBaseURL('/user')
|
const url = withBaseURL('/user')
|
||||||
const headers: { [key: string]: string } = {
|
const headers: { [key: string]: string } = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.token && isTauri()) throw new Error('No token found')
|
if (!token && isTauri()) throw new Error('No token found')
|
||||||
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
|
if (token) headers['Authorization'] = `Bearer ${context.token}`
|
||||||
if (SKIP_AUTH) return LOCAL_USER
|
|
||||||
|
if (SKIP_AUTH)
|
||||||
|
return {
|
||||||
|
user: LOCAL_USER,
|
||||||
|
token,
|
||||||
|
}
|
||||||
|
|
||||||
const userPromise = !isTauri()
|
const userPromise = !isTauri()
|
||||||
? fetch(url, {
|
? fetch(url, {
|
||||||
@ -129,23 +140,15 @@ async function getUser(context: UserContext) {
|
|||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((err) => console.error('error from Browser getUser', err))
|
.catch((err) => console.error('error from Browser getUser', err))
|
||||||
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
|
: getUserTauri(context.token, VITE_KC_API_BASE_URL)
|
||||||
token: context.token,
|
|
||||||
hostname: VITE_KC_API_BASE_URL,
|
|
||||||
}).catch((err) => console.error('error from Tauri getUser', err))
|
|
||||||
|
|
||||||
const user = await userPromise
|
const user = await userPromise
|
||||||
|
|
||||||
if ('error_code' in user) throw new Error(user.message)
|
if ('error_code' in user) throw new Error(user.message)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user: user as Models['User_type'],
|
||||||
token:
|
token,
|
||||||
context.token && context.token !== ''
|
|
||||||
? context.token
|
|
||||||
: getCookie(COOKIE_NAME) ||
|
|
||||||
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
|
||||||
'',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|