diff --git a/.cursor/rules/autonomous-work.mdc b/.cursor/rules/autonomous-work.mdc new file mode 100644 index 00000000..73e8e5c1 --- /dev/null +++ b/.cursor/rules/autonomous-work.mdc @@ -0,0 +1,66 @@ +--- +description: Autonomous work policy - keep iterating through the TODO until done +alwaysApply: true +--- +# Autonomous Work Policy + +## Keep Working Until Done + +When the user says "proceed", "keep going", "continue", or similar: + +1. Check [TODO.md](mdc:apps/sim-core/TODO.md) for the next incomplete item NOT in the "Follow-up Required" section +2. Plan the work, execute it, test it, commit it +3. **Then immediately pick up the next item** — do NOT stop and ask for permission +4. Repeat until all actionable TODO items are complete or you hit a genuine blocker + +## Handling Blockers: Follow-up Required Section + +When you encounter an item that genuinely requires user intervention: +1. Move it to the **"Follow-up Required TODOs"** section at the bottom of TODO.md +2. Add a note explaining what decision or input is needed +3. **Continue working on the next actionable item** — do NOT stop + +## Console Errors and E2E Errors + +- **Fix all console errors** that appear during normal operation or E2E tests (unless deliberately caused as part of a test) +- After each batch of fixes, check `apps/sim-core/console.log` (written by the consoleToDisk Vite plugin) for new errors +- React warnings (unknown props, deprecated APIs) should be fixed at the source, not suppressed +- Failed fetch calls to removed cloud APIs should be removed from code entirely + +## What Counts as a Blocker (move to follow-up) + +- A design decision with multiple valid approaches and significant trade-offs +- A dependency upgrade that requires user input (e.g. paid API keys, license decisions) +- A hosting/deployment platform choice +- A change that touches code outside `apps/sim-core/` (out of scope) + +## What Does NOT Count as a Blocker (keep going) + +- The next TODO item is large — break it into sub-tasks and start +- You finished a commit — check for the next item and continue +- Tests pass — move on to the next phase +- A file needs reading before editing — read it and edit it +- A test failure you cannot diagnose after 2 attempts — move to follow-up, continue with next item + +## Work Loop + +``` +while (TODO has incomplete items outside "Follow-up Required") { + item = next incomplete item from TODO.md + plan sub-tasks + execute each sub-task (edit, test, fix) + check console.log for new errors, fix them + run E2E smoke tests + commit with descriptive message + update TODO.md (mark complete, move blockers to follow-up) +} +``` + +## Scope Boundary + +Everything outside `apps/sim-core/` is out of scope. Do not modify files there. +The `.cursor/rules/` directory is an exception (IDE config). + +## Commit Cadence + +Commit after each logical unit of work (one migration phase, one package upgrade, one feature removal). Do not accumulate uncommitted changes across phases. diff --git a/.cursor/rules/commit-messages.md b/.cursor/rules/commit-messages.md new file mode 100644 index 00000000..702d9603 --- /dev/null +++ b/.cursor/rules/commit-messages.md @@ -0,0 +1,24 @@ +# Commit Message Guidelines + +Write commit messages as concise action statements. + +- **Primary line:** Start with a clear verb (Add, Fix, Remove, Update, Refactor) describing what was done +- **Focus:** Capture the purpose (WHY) and the thing accomplished +- **Details:** If needed, add a blank line followed by a bulleted list of specific actions taken + +## Examples + +``` +Fix WASM serialization producing invalid JS object shapes +``` + +``` +Add centralized JSON helpers for Rust-to-JS conversion + +- Add to_js_json and from_js_json in util.rs +- Replace serde_wasm_bindgen with JSON path where JS expects plain objects +``` + +``` +Update agent state wrapper to persist custom fields correctly +``` diff --git a/.cursor/rules/docs-as-source-of-truth.mdc b/.cursor/rules/docs-as-source-of-truth.mdc new file mode 100644 index 00000000..fd1339d1 --- /dev/null +++ b/.cursor/rules/docs-as-source-of-truth.mdc @@ -0,0 +1,42 @@ +--- +description: Treat strategy and architecture docs as source of truth - read before changes, update after changes +alwaysApply: true +--- + +# Documentation as Source of Truth + +The following documents define how the project is structured, what decisions have been made, and what work is planned. **Treat them as authoritative** — read them before making changes, follow them during implementation, and update them when reality diverges. + +## Key Documents + +| Document | Purpose | Update When | +|----------|---------|-------------| +| [ARCHITECTURE.md](mdc:apps/sim-core/ARCHITECTURE.md) | System structure, component relationships, data flow | Adding/removing/moving modules, changing state management, altering data flow | +| [TODO.md](mdc:apps/sim-core/TODO.md) | Technical debt, migration roadmap, dependency status | Completing a TODO item, discovering new debt, upgrading a dependency | +| [TESTING_STRATEGY.md](mdc:apps/sim-core/TESTING_STRATEGY.md) | Test pyramid, TDD workflow, test conventions | Adding new test patterns, changing test tooling, updating commands | +| [README.md](mdc:apps/sim-core/README.md) | Entry point, build commands, project overview | Changing build commands, adding new apps/docs, altering project structure | +| [.cursor/rules/hash-labs.mdc](mdc:.cursor/rules/hash-labs.mdc) | AI agent conventions, error policies, build verification | Changing conventions, updating known-error lists, adding new patterns | + +## Before Making Changes + +**Study the relevant docs before starting any task.** Don't skim — actually read and internalize the sections that relate to the area you're about to touch. This applies to features, bug fixes, refactors, and migrations alike. + +1. **Read ARCHITECTURE.md** to understand how the affected area is structured, what depends on it, and what patterns are in use. +2. **Read TODO.md** to check for known issues, planned work, or migration phases that overlap with your task. +3. **Read TESTING_STRATEGY.md** to know how the change should be tested (unit vs E2E, TDD for bugs). +4. **Read hash-labs.mdc** for conventions that apply (state management, deletion hygiene, error policies). +5. **Follow established patterns.** If the docs describe a pattern (e.g. state management via Context, component directory structure), follow it unless you have a strong reason not to. +6. **If you plan to deviate**, state why in the commit message or PR description. + +## After Making Changes + +1. **Update any doc that is now inaccurate.** If your change makes a statement in ARCHITECTURE.md, TODO.md, or TESTING_STRATEGY.md wrong, fix it in the same commit. +2. **Mark completed TODO items.** If you finish something listed in TODO.md, update its status. +3. **Add new patterns.** If you introduce a new convention or pattern, document it in the appropriate file. +4. **Don't leave stale docs.** Outdated documentation is worse than no documentation — it actively misleads. + +## When to Propose vs. Apply + +- **Factual updates** (completed items, corrected commands, fixed file paths): apply directly. +- **Convention changes** (new patterns, changed policies, altered workflows): propose to the user before applying. +- **Cursor rules changes**: always propose and get approval before modifying `.cursor/rules/*.mdc`. diff --git a/.cursor/rules/hash-labs.mdc b/.cursor/rules/hash-labs.mdc new file mode 100644 index 00000000..a218e8b6 --- /dev/null +++ b/.cursor/rules/hash-labs.mdc @@ -0,0 +1,267 @@ +--- +alwaysApply: true +--- +# HASH Labs - AI Agent Guidelines + +## Project Overview + +This is HASH's labs monorepo containing experimental simulation tools and proof-of-concepts. The primary application is sim-core (hCore), an in-browser IDE for building and running agent-based simulations. + +## IMPORTANT: Scope Boundary + +**Everything outside `apps/sim-core/` is OUT OF SCOPE.** + +- Do NOT modify files outside `apps/sim-core/` (no changes to root configs, `sim-engine/`, `sim-core-plugins/`, `pocs/`, `.github/`, root `.gitignore`, etc.) +- All plan documents (`TODO.md`, `ARCHITECTURE.md`, `TESTING_STRATEGY.md`) live inside `apps/sim-core/` +- All build, test, and dev commands run from within `apps/sim-core/` +- The `.cursor/rules/` directory is an exception (IDE config, not repo content) +- **Goal:** `apps/sim-core/` will be extracted into its own repository at the end of this project. Keep it self-contained. + +## Important: Technical Debt Awareness + +Before making changes, review [TODO.md](mdc:apps/sim-core/TODO.md) for: +- Outdated dependencies that should NOT be upgraded in isolation +- Deprecated packages that need replacement +- Planned modernization phases +- Known compatibility issues + +Key warnings: +- React 18.2 (upgraded from 16.14). react-three-fiber/drei still on old versions (need @react-three/* migration) +- Build tooling: Vite 7 (migrated from Webpack 4; engine-web stdlib still uses webpack) + +## IMPORTANT: Application is Becoming Local-First + +This app is being transformed into a **free, fully-featured, local-only simulation IDE**. + +### Core Principles +- **NO USER ACCOUNTS** - Don't add authentication or login flows +- **NO CLOUD FEATURES** - Don't add cloud storage, sharing, or server-side features +- **NO ANALYTICS** - Don't add Sentry, FullStory, or tracking code +- **LOCAL STORAGE ONLY** - Use localStorage/IndexedDB for persistence +- **GitHub sync is FUTURE** - Don't implement yet (post-migration) + +### Features Already REMOVED (do not re-introduce) +- User authentication (ModalSignin, ModalSignup) — DELETED +- Cloud credits / CloudUsage — DELETED +- Project sharing (ModalShare*) — DELETED +- Access codes — DELETED +- Server project storage — DELETED +- Fork project — DELETED +- Project releases/versioning (ModalRelease*) — DELETED +- hCloud experiment runners — DELETED +- Sentry / FullStory analytics — DELETED +- Discord widget — DELETED +- hookrouter — REPLACED with custom `util/navigation.ts` + `util/usePathRouter.ts` + +### Features to KEEP +- All simulation execution (step, play, pause, reset) +- All viewer tabs (3D, geospatial, analysis, process chart, raw output) +- Code editing (Monaco, file tree, behaviors) +- Local experiments (parameter sweeps, optimization) +- hIndex shared behaviors library +- Import/Export .zip +- Onboarding tour + +## IMPORTANT: Redux is Being Removed + +Redux is being **completely removed** (not migrated to another library). DO NOT add any Redux code. + +Current state: Two Redux stores (App Store + Simulator Store) +Target state: React built-in state management (Context + hooks) + +If you need to add new state management: +- DON'T create new Redux slices +- DON'T add new useSelector/useDispatch usage +- DON'T add new state management libraries +- DO use React's built-in useState, useReducer, useContext +- DO keep state as local as possible (lift only when necessary) +- DO use localStorage for persistence +- See [TODO.md](mdc:apps/sim-core/TODO.md) for the full removal plan + +## Architecture Alignment + +Before implementing any feature or making significant changes: + +1. Review [ARCHITECTURE.md](mdc:apps/sim-core/ARCHITECTURE.md) to understand: + - How the component/feature fits into the overall system + - Existing patterns for similar functionality + +2. Check implementation decisions against established patterns: + - State management: Use React built-ins (useState, useContext, useReducer) + - Components: Should this follow the src/components/[Name]/ structure? + - Async operations: Use regular async functions (not Redux thunks) + +3. If deviating from architecture, document why in the PR/commit message. + +## Deletion Hygiene + +When deleting a component, module, or feature: + +1. **Find ALL sibling files** in the same directory — not just the main `.tsx`. Check for: + - `.scss` / `.css` stylesheets + - `.spec.tsx` / `.test.tsx` test files + - `util.ts`, `types.ts`, `index.ts` helper files + - Subdirectories (e.g. `VersionPicker/` inside `Modal/Release/`) +2. **Search for imports** of the deleted module across the codebase (`grep` / `rg`) to find orphaned references. +3. **Check shared stylesheets** for dead selectors that referenced the deleted component's CSS class names. +4. **Check e2e / integration tests** for references to deleted UI elements (e.g. CSS selectors like `.ModalSignin__Frame`). + +## Dependency Awareness + +When removing or moving a package dependency: + +1. **Search the entire monorepo** for actual usage of the package (not just the package.json that declares it). Yarn/npm hoisting means a package declared in one workspace can be silently used by another. +2. **After removing a dependency**, run the production build to verify nothing breaks from missing modules. +3. **When moving a dependency** between workspace packages, run `yarn install` (with `--ignore-scripts` if needed) and verify the module resolves from the consuming package's directory. + +## Type Safety After Refactoring + +When replacing a library or changing function signatures: + +1. **Match the original API's type flexibility**. If the old library accepted `boolean | string`, the replacement must too. Read call sites, not just the old library's TypeScript types. +2. **After modifying a type, interface, or function signature**, search for all call sites and verify they still type-check. Pay special attention to: + - Functions that lost a parameter (callers may still pass it) + - Functions that gained a required parameter (callers may pass too few) + - Functions whose parameter types narrowed (callers may pass wider types) +3. **Run the TypeScript compiler / Vite build** after every batch of related changes — do NOT defer this to the end of a phase. + +## Build & Test Verification + +**Bug fixes:** When fixing bugs, use a TDD approach — write a failing test first, then fix. Prefer unit/component tests; use E2E only when necessary. See [testing-bugs.mdc](mdc:.cursor/rules/testing-bugs.mdc) and [TESTING_STRATEGY.md](mdc:apps/sim-core/TESTING_STRATEGY.md). + +After every meaningful batch of changes (not just at the end of a phase): + +1. **Run unit tests**: `npx jest --forceExit --testPathIgnorePatterns "e2e"` in `apps/sim-core/packages/core` +2. **Run production build**: `vite build` in `apps/sim-core/packages/core` +3. **Check for errors explicitly**: pipe build output through `Select-String "ERROR"` — warnings are expected, errors are not. +4. **Fix immediately** — do not accumulate errors across multiple changes. Each commit should leave the build green. + +### TypeScript & Lint Error Policy + +After editing any source file, **always check for and fix TypeScript and lint errors** in the files you changed: + +1. **Use `ReadLints`** on every file you edited before considering the task done. +2. **Run the TypeScript compiler** (`npx tsc --noEmit` from `apps/sim-core`) or check the dev server's `fork-ts-checker` output to catch type errors in changed files. +3. **Fix all errors you introduced.** If a change causes new type errors in other files (e.g. changing a function signature breaks call sites), fix those too. +4. **Deliberately skipping errors is OK** only when ALL of these are true: + - The error is in code that is **explicitly scheduled for removal** (e.g. Redux slices, `react-redux` Provider types) + - The error is **pre-existing** (not introduced by your change) + - The error is **documented** in [TODO.md](mdc:apps/sim-core/TODO.md) or this file +5. **When skipping errors, state explicitly** which errors are being skipped and why (e.g. "Skipping 24 TS2769 dispatch errors in Redux code scheduled for removal per TODO.md"). + +**Known pre-existing errors that are OK to skip:** +- ~24 Redux dispatch TS2769 errors (RTK 1.5 / TS 5.3 incompatibility — resolves with Redux removal) +- ~23 `children` prop errors from `react-redux` `Provider` and other third-party library types (resolves with Redux removal and library upgrades) +- ~42 `children` errors in `.spec.tsx` test files using old render patterns (will update with Redux removal) + +### E2E Tests (MANDATORY after UI or routing changes) + +The E2E tests are Playwright-based regression tests that verify the full application renders and functions correctly. They **must** be run after: +- Any change to routing (`usePathRouter.ts`, `navigation.ts`, route definitions) +- Any change to the App bootstrap flow (`boot.ts`, `bootstrapQuery.ts`, `index.tsx`) +- Any change to major UI containers (`HashCore`, `HashRouter`, `App`) +- Any change to the file tree, simulation controls, or viewer tabs +- React version upgrades or build tooling changes + +**Commands:** +- Quick smoke check: `yarn test:e2e -- --grep "Smoke"` in `apps/sim-core/packages/core` +- Full E2E suite: `yarn test:e2e` in `apps/sim-core/packages/core` +- Tests run sequentially (1 worker) — the simulation engine is CPU-intensive + +**Important:** The E2E tests require either a running dev server (`yarn serve`) or will auto-start one via the Playwright config. If the dev server is already running, Playwright will reuse it. + +## Major Phase Review (MANDATORY) + +After completing each major phase (e.g. "Phase 1: Cleanup & Simplification", "Phase 3: Build System Modernization"): + +1. **Full build verification**: Run production build and confirm zero errors. +2. **Full unit test suite**: Run all unit tests and confirm zero failures. +3. **Full E2E test suite**: Run `yarn test:e2e` and confirm all tests pass. Do NOT skip this. +4. **Orphan scan**: Search for files referencing deleted modules (spec files, scss files, e2e tests, stale comments). +4. **Dependency audit**: Verify no packages were left dangling in the wrong package.json. +5. **Documentation review**: Update [TODO.md](mdc:apps/sim-core/TODO.md), [ARCHITECTURE.md](mdc:apps/sim-core/ARCHITECTURE.md), this rules file, and [README.md](mdc:apps/sim-core/README.md) to reflect completed work. +6. **Plan reassessment**: Based on what was learned during the phase, identify: + - Tasks that turned out easier or harder than expected + - New issues discovered + - Revised priority ordering for remaining phases +7. **Propose cursor rules updates**: After each major review, draft specific additions or changes to THIS file and present them to the user for approval before applying. + +## Commit Discipline + +After completing each logical section of work (e.g. a migration phase, a feature, a bug fix): + +1. **Run the review panel** on the diff — assemble expert personas, address critical issues, follow recommendations. See [reviewers.mdc](mdc:.cursor/rules/reviewers.mdc) Commit Workflow. +2. **Run `git status` at the repository root** to check for uncommitted changes +3. **Stage and commit** all related changes before moving on to the next task +4. Group changes into logical commits (one per phase/feature, not one giant commit) +5. Never leave uncommitted work behind when finishing a task or phase +6. If a task spans multiple phases, commit each phase separately with a clear message + +This prevents work from being lost and keeps the history clean and reviewable. + +## Documentation Maintenance + +After completing significant changes, update: + +1. [ARCHITECTURE.md](mdc:apps/sim-core/ARCHITECTURE.md) - For structural changes +2. [README.md](mdc:apps/sim-core/README.md) - For entry point/build changes +3. [TODO.md](mdc:apps/sim-core/TODO.md) - For dependency updates or new tech debt +4. This rules file - For convention changes (propose changes, get approval) + +## Repository Structure (In-Scope) + +apps/sim-core/ - **ALL WORK HAPPENS HERE** + ARCHITECTURE.md - System structure docs + TODO.md - Migration roadmap / tech debt tracker + TESTING_STRATEGY.md - Test pyramid and conventions + packages/core/ - React/TypeScript simulation IDE (hCore) + packages/engine-web/ - WASM simulation engine bridge + packages/engine/ - Rust ↔ JS engine bindings + packages/utils/ - Shared utilities + packages/sim-engine-types/ - Type definitions for the engine + +Out of scope (do not modify): + apps/sim-engine/ - Standalone Rust engine + apps/sim-core-plugins/ - hCore plugins + pocs/ - Proof of concepts + .github/ - CI/CD workflows + +## Tech Stack (Note: Many packages outdated - see [TODO.md](mdc:apps/sim-core/TODO.md)) + +sim-core Frontend: +- React 18.2.0 (upgraded from 16.14) +- Redux Toolkit 1.5 (outdated - BEING REMOVED ENTIRELY) +- TypeScript 5.3.3 (updated from 4.1; ~80 RTK type errors suppressed until Redux removal) +- Vite 7 (migrated from Webpack 4) +- SCSS modules for styling + +## Two Redux Stores Pattern (LEGACY - Being Deleted) + +App Store (features/store.ts) - General UI state +Simulator Store (features/simulator/store.ts) - High-frequency simulation updates + +**DO NOT USE** - These are scheduled for complete removal. See [TODO.md](mdc:apps/sim-core/TODO.md). + +## Build Commands + +sim-core: + yarn - Install and build + yarn serve:core - Vite dev server (not webpack-dev-server) + yarn build:core - Production build + yarn fmt:core - Format code + yarn test:core - Run tests + +## Common Gotchas + +1. Redux is being removed - don't add any Redux code +2. Don't upgrade React/Redux partially - ecosystem must stay in sync +3. Routing uses custom `util/navigation.ts` + `util/usePathRouter.ts` (not hookrouter or react-router) +4. Legacy code from 2019-2020 - be cautious with refactoring +5. NODE_OPTIONS=--openssl-legacy-provider no longer needed (resolved by Vite migration) + +## References + +- [sim-core README](mdc:apps/sim-core/README.md) +- [ARCHITECTURE.md](mdc:apps/sim-core/ARCHITECTURE.md) +- [TODO.md](mdc:apps/sim-core/TODO.md) +- [TESTING_STRATEGY.md](mdc:apps/sim-core/TESTING_STRATEGY.md) \ No newline at end of file diff --git a/.cursor/rules/reviewers.mdc b/.cursor/rules/reviewers.mdc new file mode 100644 index 00000000..273c9549 --- /dev/null +++ b/.cursor/rules/reviewers.mdc @@ -0,0 +1,66 @@ +--- +description: Code review panel - assemble expert personas when reviewing PRs or code +globs: "**/*" +alwaysApply: true +--- + +# Code Review Panel + +When reviewing code, PRs, or implementation work, **assemble a panel of expert personas** and provide a structured review from each perspective. + +## Core Panel + +| Persona | Focus | Key Questions | +|---------|-------|---------------| +| **QA Engineer** | Testability, edge cases, regression risk | Are tests adequate? What could break? Happy path + error paths covered? | +| **Frontend Architect** | React patterns, state, bundle, performance | Does it fit ARCHITECTURE.md? State management correct? Any Redux creep? | +| **Rust Architect** | sim-engine design, idioms, safety | Ownership, lifetimes, error handling. Does it fit the engine crate layout? | +| **WASM Expert** | Rust↔JS boundary, FFI, memory | Serialization, JS object shapes, worker isolation. No accidental sync blocking? | +| **UX Expert** | User flows, feedback, affordances | Loading states, error recovery, discoverability. Local-first assumptions preserved? | +| **UI Designer** | Visual consistency, hierarchy, responsiveness | Layout, spacing, contrast. Matches existing patterns? | + +## Additional Personas (include when relevant) + +| Persona | When to Include | Focus | +|---------|-----------------|-------| +| **Security Reviewer** | Auth, FFI, user input, file handling | No XSS, safe WASM sandbox, no unintended data exposure | +| **Performance Engineer** | Simulation, 3D, heavy compute | Frame rate, memory, worker blocking. Bundle size impact? | +| **Accessibility (a11y) Expert** | UI changes, forms, controls | Keyboard nav, ARIA, screen readers, focus management | +| **DevOps/CI Specialist** | Build, test, deploy changes | Scripts, env vars, CI workflows. E2E stability? | +| **Documentation Advocate** | New APIs, complex logic | Comments, README, TODO.md. Will future maintainers understand? | + +## Review Format + +Structure the review as: + +``` +## QA Engineer +[Findings and recommendations] + +## Frontend Architect +[Findings and recommendations] + +## Rust Architect +[Include only if Rust/engine-web touched] + +... (other relevant personas) + +## Summary +[Critical issues | Nice-to-haves | Overall] +``` + +## Project-Specific Checks + +- **Sim-core**: No Redux, no cloud/auth, no analytics. React Context + hooks. Vite build. +- **Engine-web**: `err instanceof Error` before accessing `.message`. Worker isolation. +- **Local-first**: No server calls for core flows. localStorage/IndexedDB for persistence. + +## Commit Workflow (MANDATORY) + +**Before each commit:** + +1. **Run the review panel** on the staged changes (or full diff if nothing staged). Assemble the relevant personas based on which files changed. +2. **Address critical issues** — fix or explicitly defer with a TODO. Do not commit with unresolved critical findings. +3. **Follow recommendations** — implement or document why not. For nice-to-haves, either fix now or add a follow-up task. +4. **Re-run the panel** if changes were made in response to feedback. +5. **Then commit** using [commit message guidelines](mdc:.cursor/rules/commit-messages.md). diff --git a/.cursor/rules/testing-bugs.mdc b/.cursor/rules/testing-bugs.mdc new file mode 100644 index 00000000..bcb19f6b --- /dev/null +++ b/.cursor/rules/testing-bugs.mdc @@ -0,0 +1,35 @@ +--- +description: When user reports a bug - use TDD approach, verify with test first (unit preferred, E2E if needed) +alwaysApply: true +--- + +# Bug Fixes: Test-First (TDD) Workflow + +When the user reports a bug, **always take a TDD approach**. Do not modify production code until you have a failing test that reproduces the issue. + +## Workflow + +1. **Reproduce with a test** — Write a test that fails and demonstrates the bug. +2. **Verify the test fails** — Run it; the failure must match the reported behavior. +3. **Fix the code** — Implement the fix so the test passes. +4. **Run the full suite** — Ensure no regressions. + +## Test Type Selection + +| Prefer | When | +|--------|------| +| **Unit test** | Pure logic, utilities, selectors, parsers, pure functions | +| **Component test** | React component behavior, hooks, context in isolation | +| **E2E test** | Full user flows, UI interactions, routing, simulation, import/export | + +**Rule:** Use the lightest test type that can reliably reproduce the bug. Prefer unit over component over E2E. + +## Commands + +- **Unit:** `npx jest --forceExit --testPathIgnorePatterns "e2e"` in `apps/sim-core/packages/core` +- **E2E:** `yarn ws:core test:e2e` (or `test:e2e:smoke` for quick check) in `apps/sim-core` +- **Single E2E file:** `yarn ws:core test:e2e path/to/spec.ts` + +## Full Strategy + +See [TESTING_STRATEGY.md](mdc:apps/sim-core/TESTING_STRATEGY.md) for the complete testing strategy, test pyramid, and conventions. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..bc3669ae --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,64 @@ +name: E2E + +on: + pull_request: + paths: + - "apps/sim-core/**" + - ".github/workflows/e2e.yml" + push: + branches: + - main + paths: + - "apps/sim-core/**" + - ".github/workflows/e2e.yml" + +defaults: + run: + shell: bash + working-directory: apps/sim-core/packages/core + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + e2e: + name: Playwright E2E + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: yarn + cache-dependency-path: apps/sim-core/yarn.lock + + - name: Install dependencies + working-directory: apps/sim-core + run: yarn install --frozen-lockfile --ignore-scripts + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Build + run: yarn build + + - name: Run E2E tests + run: yarn test:e2e + env: + E2E_USE_BUILD: "1" + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: apps/sim-core/packages/core/test-results/ + retention-days: 7 diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..356c7072 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,56 @@ +name: Frontend + +on: + pull_request: + paths: + - "apps/sim-core/**" + - ".github/workflows/frontend.yml" + push: + branches: + - main + paths: + - "apps/sim-core/**" + - ".github/workflows/frontend.yml" + +defaults: + run: + shell: bash + working-directory: apps/sim-core/packages/core + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + name: Lint, Test & Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: yarn + cache-dependency-path: apps/sim-core/yarn.lock + + - name: Install dependencies + working-directory: apps/sim-core + run: yarn install --frozen-lockfile --ignore-scripts + + - name: ESLint + run: npx eslint --quiet "src/**/*.{ts,tsx}" + + - name: TypeScript + run: npx tsc --noEmit + + - name: Unit tests + run: npx jest --forceExit --testPathIgnorePatterns "/node_modules/|/tests/e2e/" + + - name: Production build + run: yarn build diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/README.md b/README.md index 9cb23b13..fb1de6ca 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,80 @@ The `pocs` folder contains **proof of concepts** and other one-off experiments. A number of older POCs can be found in our `hasharchives` organization, including: - [`wasm-ts-esm-in-node-jest-and-nextjs`](https://github.com/hasharchives/wasm-ts-esm-in-node-jest-and-nextjs) - A **Wasm + TypeScript + ESM in Node.js, Jest and Next.js 13** example project + +--- + +## Agent Context + +This section provides context for AI agents working with this codebase. + +### Quick Start for Agents + +| Task | Entry Point | Key Files | +|------|-------------|-----------| +| Frontend feature work | `apps/sim-core/packages/core/src/` | `features/`, `components/` | +| State management | `apps/sim-core/packages/core/src/features/` | `store.ts`, `rootReducer.ts` | +| Simulation state | `apps/sim-core/packages/core/src/features/simulator/` | `store.ts`, `simulate/slice.ts` | +| Engine work (Rust) | `apps/sim-engine/` | `bin/cli/`, `lib/execution/` | +| LLM agents (Python) | `pocs/hash-agents/app/` | `agents/`, `routes/` | + +### Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ sim-core (hCore) │ +├─────────────────────────────────────────────────────────────────┤ +│ React/Vite Frontend │ WASM Engine (via Web Workers) │ +│ ─────────────────── │ ────────────────────────────── │ +│ • HashCore (IDE shell) │ • engine-web bindings │ +│ • SimulationRunner │ • Rust → WASM compilation │ +│ • AgentScene (3D) │ • promise-worker communication │ +│ • Monaco Editor │ │ +├─────────────────────────────────────────────────────────────────┤ +│ Two Redux Stores: │ +│ • App Store: UI state, files, project │ +│ • Simulator Store: High-frequency simulation updates │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key Patterns + +1. **Two Redux Stores**: Use `useDispatch()`/`useSelector()` for app state; use `useSimulatorDispatch()`/`useSimulatorSelector()` for simulation state. + +2. **Feature Slices**: Redux state organized in `src/features/[name]/` with `slice.ts`, `selectors.ts`, `types.ts`. + +3. **Component Structure**: Functional components with hooks, SCSS modules for styling, PascalCase naming. + +4. **Async Actions**: Use `createAppAsyncThunk` for async operations. + +### Build Commands + +```bash +# sim-core frontend +cd apps/sim-core +yarn # Install and build +yarn serve:core # Development server (localhost:8080) +yarn test:core # Run unit tests +yarn fmt:core # Format code + +# E2E Tests (Playwright) - CRITICAL for migration validation +cd apps/sim-core/packages/core +npx playwright install # First time only - install browsers +yarn test:e2e # Run E2E tests (starts dev server automatically) +yarn test:e2e:ui # Interactive UI mode for debugging +yarn test:e2e:headed # Run with visible browser + +# sim-engine (Rust) +cd apps/sim-engine +cargo build # Build +cargo test # Test +cargo fmt && cargo clippy # Lint +``` + +### Documentation + +- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Detailed technical architecture with code references +- **[docs/TESTING_STRATEGY.md](docs/TESTING_STRATEGY.md)** - Testing strategy and TDD workflow for bug fixes +- **[TODO.md](TODO.md)** - Technical debt, outdated dependencies, and modernization roadmap +- **[.cursor/rules/hash-labs.mdc](.cursor/rules/hash-labs.mdc)** - AI agent guidelines and conventions +- **[.github/CONTRIBUTING.md](.github/CONTRIBUTING.md)** - Contribution process \ No newline at end of file diff --git a/apps/sim-core/.gitignore b/apps/sim-core/.gitignore index c7e05681..fba3785e 100644 --- a/apps/sim-core/.gitignore +++ b/apps/sim-core/.gitignore @@ -26,6 +26,16 @@ yarn-error.log .eslintcache packages/hashai/.eslintcache -#ts cache +# TypeScript incremental build info tsconfig.tsbuildinfo + +# Vercel CLI local project link .vercel + +# Dev console log (written by consoleToDisk Vite plugin) +console.log + +# Playwright E2E test artifacts +**/test-results/ +**/playwright-report/ +**/.playwright/ \ No newline at end of file diff --git a/apps/sim-core/.npmrc b/apps/sim-core/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/apps/sim-core/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/sim-core/.nvmrc b/apps/sim-core/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/apps/sim-core/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/apps/sim-core/ARCHITECTURE.md b/apps/sim-core/ARCHITECTURE.md new file mode 100644 index 00000000..86076339 --- /dev/null +++ b/apps/sim-core/ARCHITECTURE.md @@ -0,0 +1,1064 @@ +# sim-core Architecture + +This document provides technical architecture documentation for **sim-core** (hCore), a free, fully-featured, local-first simulation IDE. + +> **Scope**: This document covers `apps/sim-core/` only. sim-engine, hash-agents, and other monorepo contents are out of scope. sim-core will be extracted into its own repository at the end of this project. + +## Table of Contents + +- [Repository Overview](#repository-overview) +- [sim-core Architecture](#sim-core-architecture) + - [Application Structure](#application-structure) + - [Build System](#build-system) + - [State Management](#state-management) + - [Component Architecture](#component-architecture) + - [Engine Integration](#engine-integration) + - [Data Flow](#data-flow) +- [Deployment](#deployment) + - [Build Pipeline](#build-pipeline) + - [Build Output](#build-output) + - [Hosting Requirements](#hosting-requirements) + - [Environment Configuration](#environment-configuration) + - [Embed Mode](#embed-mode) + - [External Dependencies](#external-dependencies) +- [sim-engine Architecture](#sim-engine-architecture) +- [Key Files Reference](#key-files-reference) +- [Feature Development Guide](#feature-development-guide) +- [Common Patterns](#common-patterns) + +--- + +## Repository Overview + +``` +hashintel-labs/ +├── apps/ +│ ├── sim-core/ # Primary: React/TypeScript simulation IDE +│ │ ├── packages/ +│ │ │ ├── core/ # Main frontend application +│ │ │ ├── engine/ # Legacy Rust simulation engine (WASM) +│ │ │ ├── engine-web/ # WASM bindings and TypeScript API +│ │ │ ├── sim-engine-types/ # Shared Rust types +│ │ │ │ └── public/ +│ │ │ │ └── example_projects/ # Sample simulation .zip files + manifest.json +│ │ │ └── utils/ # Shared utilities +│ ├── sim-engine/ # Standalone Rust simulation engine +│ │ ├── bin/ # CLI and engine binaries +│ │ ├── lib/ # Core library crates +│ │ ├── stdlib/ # Standard library (JS/Python) +│ │ └── tests/ # Integration tests +│ └── sim-core-plugins/ # hCore plugins +│ └── process-modeler/ # Business process modeling plugin +└── pocs/ # Proof of concepts + ├── hash-agents/ # Python LLM agents (LangChain/FastAPI) + ├── distributed_collab/ # Elixir distributed system + └── hash_helm_chart/ # Kubernetes Helm charts +``` + +--- + +## sim-core Architecture + +### Application Structure + +```mermaid +flowchart TB + subgraph Entry[Application Entry] + Index[index.tsx] + Boot[boot.ts] + end + + subgraph Providers[Context Providers] + VP[ViewerProvider] + UP[UserProvider] + PP[ProjectProvider] + FP[FilesProvider] + TP[ToastProvider] + EP[ExamplesProvider] + SP[SimulatorProvider] + end + + subgraph Core[Core Application] + App[App Component] + HashRouter[HashRouter] + HashCore[HashCore] + end + + subgraph Views[Main Views] + Editor[TabbedEditor
Monaco] + Viewer[SimulationViewer] + Analysis[Analysis] + Scene[AgentScene
Three.js / R3F] + end + + subgraph Controls[Simulation Controls] + Runner[SimulationRunner] + PlayPause[PlayPause] + Timeline[Timeline] + end + + Index --> Boot + Boot --> Providers + Providers --> App + App --> HashRouter + HashRouter --> HashCore + HashCore --> Editor + HashCore --> Viewer + HashCore --> Runner + Viewer --> Scene + Runner --> PlayPause + Runner --> Timeline +``` + +#### Entry Points + +**`src/index.tsx`** — Application bootstrap: +```typescript +// 1. Handle version caching for staging +// 2. Call boot() to initialize services +// 3. Render React app wrapped in Context Providers +``` + +**`src/boot.ts`** — Service initialization: +```typescript +export const boot = async (forExperiments: boolean) => { + configureTheme(); // CSS variables + enableMapSet(); // Immer support + configureMonaco(); // Code editor + buildSimulationProvider(forExperiments); // WASM workers +}; +``` + +### Build System + +sim-core uses **Vite 7** for both development and production builds: + +| Tool | Version | Purpose | +|------|---------|---------| +| Vite | 7.3 | Dev server + production bundler (Rollup) | +| TypeScript | 5.3 | Type checking | +| Babel | 7 | Jest test transformation | +| Jest | 29.7 | Unit tests (118 suites, 310 tests) | +| Playwright | — | E2E tests (66 tests across 8 spec files) | +| Node.js | 24 LTS | Runtime | + +Key config files: +- `vite.config.ts` — Vite config with WASM, Monaco, and React plugins +- `tsconfig.json` — TypeScript config (strict mode, `useUnknownInCatchVariables: true`) +- `babel.config.js` — Babel presets for Jest (React, Env, TypeScript) +- `playwright.config.ts` — E2E test configuration + +### State Management + +sim-core uses **React built-in state management** (Context + hooks) for all application state, with a lightweight custom store for the performance-critical simulator. + +```mermaid +flowchart LR + subgraph AppContexts[React Context Providers] + direction TB + Files[FilesContext
useReducer + Immer] + Project[ProjectContext
useReducer] + User[UserContext
useReducer] + Viewer[ViewerContext
useReducer] + Search[SearchContext
useState] + Toast[ToastContext
useState] + Examples[ExamplesContext
useState] + end + + subgraph SimStore[Simulator Store] + direction TB + Simulator[SimpleStore
reduxCompat.ts] + Middleware[middleware chain] + Subscribers[store subscribers] + end + + subgraph Sync[Context → Store Sync] + StoreSync[StoreSync component
useEffect hooks] + end + + AppContexts --> StoreSync + StoreSync --> SimStore +``` + +#### App State (React Context) + +All general UI state is managed through React Context providers. Each provider is pure React with no external dependencies: + +| Context | Hook | State Mechanism | Purpose | +|---------|------|-----------------|---------| +| `FilesContext` | `useFiles()` | `useReducer` + Immer | File tree, open files, editor state | +| `ProjectContext` | `useProject()` | `useReducer` | Current project, access gates | +| `UserContext` | `useUser()` | `useReducer` | Tour progress, preferences | +| `ViewerContext` | `useViewer()` | `useReducer` | Tabs, editor/activity visibility | +| `SearchContext` | `useSearch()` | `useState` | Search query state | +| `ToastContext` | `useToast()` | `useState` | Toast notifications | +| `ExamplesContext` | `useExamples()` | `useState` | Example project list | + +#### Simulator Store (`src/features/simulator/store.ts`) + +The simulator uses a lightweight custom store (`reduxCompat.ts`) for high-frequency simulation updates. This store provides Redux-compatible APIs (`dispatch`, `getState`, `subscribe`) without the Redux dependency: + +```typescript +import { createStore } from "../reduxCompat"; + +export const simulatorStore = createStore(rootReducer, [ + simulatorMiddleware, // Provider message handling + observeMiddleware(...), // Action observability + simulatorAnalysisMiddleware // Plot data computation +]); +``` + +Components access simulation state via `useSyncExternalStore`: + +```typescript +import { useSimulatorSelector, useSimulatorDispatch } from "../features/simulator/context"; + +const running = useSimulatorSelector(selectRunning); +const dispatch = useSimulatorDispatch(); +``` + +**Simulator State:** + +| Property | Type | Purpose | +|----------|------|---------| +| `simulationData` | `Record` | All simulation runs | +| `currentSimulation` | `string \| null` | Active simulation ID | +| `analysisMode` | `AnalysisMode` | Current analysis view | +| `history` | Entity state | Project history items | +| `stepsPerSecond` | `number` | Playback speed | + +#### Context ↔ Store Synchronization + +The `StoreSync` component (`src/features/simulator/simulate/StoreSync.tsx`) bridges React contexts to the simulator store using `useEffect` hooks: + +- Project changes → reset simulation +- Globals file changes → update runner (when running) +- Tab changes → toggle analysis visibility +- Analysis source changes → clear cached plot data + +#### Compatibility Layer (`src/features/reduxCompat.ts`) + +A thin utility providing Redux-compatible APIs using only Immer and Reselect: + +| API | Purpose | +|-----|---------| +| `createSlice` | Generates reducer + action creators (Immer-wrapped) | +| `createAction` | Typed action creator with `.type` and `.match()` | +| `createEntityAdapter` | Sorted entity CRUD with selectors | +| `createStore` | Minimal store with middleware chain + thunk support | +| `createSelector` | Re-exported from Reselect | + +### Component Architecture + +#### Directory Structure + +``` +src/components/ +├── HashCore/ # Main IDE shell +│ ├── HashCore.tsx # Root component +│ ├── Header/ # Top navigation bar +│ ├── Main/ # Main content area +│ ├── Files/ # File tree and management +│ ├── AccessGate/ # Permission checks +│ └── Tour/ # Onboarding tour +├── SimulationRunner/ # Playback controls +│ ├── SimulationRunner.tsx +│ └── Controls/ # PlayPause, Reset, Timeline, etc. +├── AgentScene/ # 3D visualization (@react-three/fiber) +│ ├── AgentScene.tsx # Three.js scene +│ └── README.md # Visualization docs +├── TabbedEditor/ # Monaco integration +├── Modal/ # Dialog system +├── Analysis/ # Analysis views +└── PlotViewer/ # Plotly charts +``` + +#### Key Components + +**HashCore** (`src/components/HashCore/HashCore.tsx`): +```typescript +export const HashCore: FC = memo(function HashCore() { + const { currentProject } = useProject(); + const { accessGate } = useProject(); + + useParameterisedUi(); // URL parameter handling + useKeyboardShortcuts(); // Global shortcuts + useSaveOrFork(); // Cmd+S handling + useShouldUnload(); // Unsaved changes warning + + return ( + <> + + + + + + + + ); +}); +``` + +**SimulationRunner** (`src/components/SimulationRunner/SimulationRunner.tsx`): +```typescript +export const SimulationRunner: FC = () => { + const dispatch = useSimulatorDispatch(); + + useKeyboardShortcuts({ + meta: { Enter: () => dispatch(toggleCurrentSimulator()) }, + alt: { Enter: () => dispatch(pauseAndNew()) }, + metaShift: { Enter: () => dispatch(stepSimulator()) }, + }); + + return ( +
+ + + + + +
+ ); +}; +``` + +### Engine Integration + +```mermaid +sequenceDiagram + participant UI as React UI + participant Store as Simulator Store + participant Provider as SimulationProvider + participant Worker as Web Worker + participant WASM as WASM Engine + + UI->>Store: dispatch(runSimulation) + Store->>Provider: handleRequest + Provider->>Worker: postMessage + Worker->>WASM: wasm.step() + WASM-->>Worker: SimulationStates + Worker-->>Provider: RunnerStatus + Provider-->>Store: alertSubscribers + Store-->>UI: state update +``` + +#### SimulationProvider (`src/features/simulator/simulate/provider.ts`) + +Manages connections to simulation runners: + +```typescript +export class SimulationProvider implements ExperimentRunner { + targets: Record | null = null; + + build(workerFileName: string, numWorkers = 4, devMode = false) { + const dedicatedRunner = new WebWorkerRunner( + "worker-web-dedicated", workerFileName, devMode + ); + this.targets = { + web: { + target: "web", + dedicatedRunner, + experimentRunners: new Map([ + ["experimenter-web-0", new WebExperimentRunner(numWorkers, devMode, workerFileName)], + ]), + }, + }; + } +} +``` + +#### Web Worker (`src/workers/simulation-worker/index.ts`) + +Runs WASM engine in background thread: + +```typescript +import { RunnerState, WasmRequestHandler, wasm } from "@hashintel/engine-web"; + +const runner: Promise = (async () => ({ + wasmlib: await wasm(), // Load WASM module + datasetCache: new Map(), + pyodide: null, // Python runtime (lazy loaded) + parsedSimulation: null, + running: false, + stepsLeft: 0, +}))(); + +RegisterPromiseWorker(async (message) => { + return typeof message === "object" + ? await WasmRequestHandler(message, await runner) + : null; +}); +``` + +### Data Flow + +#### File System Abstraction + +Simulations use a virtual file system stored in React Context: + +```mermaid +flowchart LR + subgraph Files[File Types] + Behavior[HcBehaviorFile] + Dataset[HcDatasetFile] + Analysis[HcAnalysisFile] + Shared[HcSharedBehaviorFile] + end + + subgraph Storage[Storage Layer] + Context[FilesContext
useReducer] + LocalStorage[localStorage] + end + + subgraph Operations[Operations] + Import[Import .zip] + Export[Export .zip] + Edit[Edit in Monaco] + end + + Import --> Context + Context --> LocalStorage + Edit --> Context + Context --> Export +``` + +**File Types** (`src/features/files/types.ts`): + +| Type | Kind | Purpose | +|------|------|---------| +| `HcBehaviorFile` | `Behavior` | Agent behaviors (JS/Python) | +| `HcSharedBehaviorFile` | `SharedBehavior` | Dependencies from hIndex | +| `HcDatasetFile` | `Dataset` | JSON/CSV data files | +| `HcAnalysisFile` | `Analysis` | Analysis definitions | + +--- + +## Deployment + +sim-core is a **fully static single-page application**. It compiles to HTML, JavaScript, CSS, and WASM files that can be served from any static file host. There is no application server — the simulation engine runs entirely in the browser via WebAssembly. + +### Build Pipeline + +```mermaid +flowchart LR + subgraph Prebuild[Pre-build] + Rust[Rust Engine
Cargo.toml] + WasmPack[wasm-pack build
→ .wasm + JS bindings] + Codegen[graphql-codegen
→ auto-types.ts] + end + + subgraph Build[Vite Build] + Vite[vite build
Rollup bundler] + TS[TypeScript
→ JS] + SCSS[SCSS
→ CSS] + Workers[Web Workers
→ worker bundles] + WASM[WASM bindings
→ asset chunks] + end + + subgraph Output[dist/] + HTML[index.html
embed.html] + Assets[assets/*.js
assets/*.css
assets/*.wasm] + WorkerOut[worker bundles] + end + + Rust --> WasmPack + WasmPack --> Vite + Codegen --> Vite + Vite --> TS + Vite --> SCSS + Vite --> Workers + Vite --> WASM + TS --> Output + SCSS --> Output + Workers --> Output + WASM --> Output +``` + +The full build sequence: + +```bash +# 1. Build WASM (runs automatically as engine-web prebuild hook) +cd apps/sim-core/packages/engine-web +wasm-pack build --target bundler --out-dir wasm/bundler --out-name hash + +# 2. Build the frontend +cd apps/sim-core/packages/core +yarn build # runs: yarn codegen && vite build +``` + +`yarn codegen` generates TypeScript types from the GraphQL schema (`codegen.yml`). `vite build` compiles the entire app — TypeScript, SCSS, WASM bindings, and workers — into the `dist/` directory. + +### Build Output + +``` +dist/ +├── index.html # Main app (SPA entry) +├── embed.html # Embed mode entry +└── assets/ + ├── index-[hash].js # Main app bundle (ES module) + ├── index-[hash].css # Combined styles + ├── hash_bg-[hash].wasm # Simulation engine (compiled from Rust) + ├── vendor-[hash].js # Third-party libraries + ├── monaco-[hash].js # Code editor (large; ~3MB) + ├── worker-[hash].js # Simulation worker + ├── analyzer-[hash].js # Analysis worker + ├── editor.worker-[hash].js # Monaco editor worker + ├── ts.worker-[hash].js # Monaco TypeScript worker + ├── json.worker-[hash].js # Monaco JSON worker + └── ... # Additional code-split chunks +``` + +The total build size is large (~15–25 MB uncompressed) due to Monaco Editor, Plotly, Three.js, deck.gl, and the WASM engine. With gzip/brotli compression, the initial download is ~5–8 MB. + +### Hosting Requirements + +Any static file server works. The key requirements: + +#### SPA Fallback Routing (Required) + +The app uses client-side routing. All paths must serve `index.html` (except actual static files). + +```nginx +# nginx +location / { + try_files $uri $uri/ /index.html; +} +``` + +```apache +# Apache (.htaccess) +FallbackResource /index.html +``` + +``` +# Netlify (_redirects) +/* /index.html 200 +``` + +```json +// Vercel (vercel.json) +{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } +``` + +For embed mode, requests to `/embed*` should serve `embed.html`: + +```nginx +location /embed { + try_files $uri /embed.html; +} +``` + +#### WASM MIME Type (Required) + +`.wasm` files must be served with `Content-Type: application/wasm`. Most modern servers handle this automatically. For older servers: + +```nginx +# nginx +types { + application/wasm wasm; +} +``` + +#### Recommended Headers + +``` +Cache-Control: public, max-age=31536000, immutable # For hashed assets (assets/*.js, assets/*.wasm) +Cache-Control: no-cache # For index.html, embed.html +``` + +Optionally, for maximum WASM performance: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +These headers enable `SharedArrayBuffer`, which allows WASM to use threads. Not currently required but would improve simulation performance if threading is added. + +### Environment Configuration + +sim-core is configured entirely at **build time**. No runtime environment variables are needed. + +| Variable | Build-Time | Default | Purpose | +|----------|-----------|---------|---------| +| `MAPBOX_API_TOKEN` | Optional | `null` | Enables geospatial map viewer. Without it, the map tab shows a placeholder. | +| `NODE_ENV` | Set by Vite | `production` | Controls dev/prod mode | + +To set the Mapbox token: + +```bash +MAPBOX_API_TOKEN=pk.your_token_here yarn build +``` + +All other configuration is hardcoded or derived from the build mode: + +| Config | Value | Source | +|--------|-------|--------| +| `LOCAL_API` | `true` | `vite.config.ts` — hardcoded for local-first | +| `WEBPACK_BUILD_STAMP` | `hash-{mode}-{timestamp}` | Generated at build time | +| `WEBPACK_PUBLIC_PATH` | `"/"` | `vite.config.ts` | + +> **Note**: The `WEBPACK_*` variable names are legacy from the Webpack era. They will be renamed to `BUILD_STAMP` and `PUBLIC_PATH` in a future cleanup (see TODO.md Phase 4b). + +### Embed Mode + +The app supports an embed mode for displaying simulations in iframes on other sites: + +```html + +``` + +Embed mode: +- Uses `embed.html` as the entry point (separate from `index.html`) +- Loads `EmbedApp` instead of the full IDE +- Fetches the project by path from URL parameters +- Applies the `embed` CSS class to `` for a compact layout +- Hides IDE chrome (file tree, activity panel, some controls) + +### External Dependencies + +The app is designed to work offline, but currently references some external resources: + +| Resource | URL | Required? | Notes | +|----------|-----|-----------|-------| +| Favicons | `cdn-us1.hash.ai/assets/img/brand/` | No | Cosmetic; page works without them | +| Web manifest | `cdn-us1.hash.ai/assets/other/site.webmanifest` | No | PWA manifest | +| Twitter card | `cdn-us1.hash.ai/assets/hash-card.png` | No | Social media preview | +| hIndex API | `api.hash.ai/graphql` | Partial | Required only for searching/adding shared behaviors from the hIndex library. Simulations run without it. | +| Mapbox tiles | `api.mapbox.com` | No | Only for geospatial viewer; requires `MAPBOX_API_TOKEN` | + +For fully offline deployments: +1. Copy favicon/icon assets to the `public/` directory and update `index.html` +2. The hIndex search will show errors but the rest of the app functions normally +3. The geospatial viewer will be unavailable without Mapbox + +### Deployment Examples + +#### Local Preview + +```bash +cd apps/sim-core/packages/core +yarn build +yarn start # vite preview on http://localhost:4173 +``` + +#### Docker (Nginx) + +```dockerfile +FROM node:24 AS build +WORKDIR /app +COPY . . +RUN cd apps/sim-core && yarn install +RUN cd apps/sim-core/packages/core && yarn build + +FROM nginx:alpine +COPY --from=build /app/apps/sim-core/packages/core/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +``` + +```nginx +# nginx.conf +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location / { + try_files $uri $uri/ /index.html; + } + + types { + application/wasm wasm; + } +} +``` + +#### GitHub Pages + +```yaml +# .github/workflows/deploy.yml +name: Deploy to GitHub Pages +on: + push: + branches: [main] +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - run: cd apps/sim-core && yarn install + - run: cd apps/sim-core/packages/core && yarn build + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: apps/sim-core/packages/core/dist +``` + +> **Note**: GitHub Pages doesn't natively support SPA routing. You'll need a `404.html` workaround or use a service like Netlify/Vercel instead. + +### CI/CD Status + +#### What Exists + +**Rust CI** (`.github/workflows/rust.yml`) — the only active workflow: +- Triggers on PRs, pushes to `main`/`dev/**`, and merge group +- Smart crate detection: only lints/tests crates with changed files +- Lint: `cargo fmt --check`, `cargo clippy` (with SARIF upload to GitHub code scanning), doc checks +- Test: `cargo nextest run`, plus miri for nightly toolchains +- Uses `Swatinem/rust-cache` and Turbo remote caching +- Concurrency: cancels in-progress runs for the same PR +- Gate: `merging-enabled` job blocks merge if lint or test fails + +**Renovate** (`.github/renovate.json`) — dependency update bot: +- Configured with `dependencyDashboardApproval: true` (manual approval required) +- Groups packages by ecosystem (Jest, ESLint, GraphQL, Cargo crates, etc.) +- Stale: references teams/packages from a parent monorepo that don't apply here (Block Protocol, ProseMirror, Sentry, OpenTelemetry) + +#### What's Missing + +There is **no CI for the frontend** (sim-core). The following are not automated: + +| Check | Status | Impact | +|-------|--------|--------| +| TypeScript type checking | Not in CI | Type errors can reach `main` | +| Jest unit tests (310 tests) | Not in CI | Regressions go undetected | +| Vite production build | Not in CI | Build failures go undetected | +| Playwright E2E tests (66 tests) | Not in CI | Functional regressions go undetected | +| ESLint / Prettier | Not in CI | Style drift | +| WASM build (engine-web) | Not in CI | WASM compilation failures go undetected | +| Deployment | Not in CI | Manual process only | + +#### Outdated Action Versions + +| Action | Current | Latest | +|--------|---------|--------| +| `actions/checkout` | v3.6.0 | v4 | +| `actions/setup-python` | v4.7.0 | v5 | +| `Swatinem/rust-cache` | v2.6.2 | v2.7+ | +| `taiki-e/install-action` | v2.17.7 | v2.26+ | +| `github/codeql-action` | v2.21.5 | v3 | + +#### Recommended CI Architecture + +``` +.github/workflows/ +├── rust.yml # ✅ Exists — Rust lint + test +├── frontend.yml # 🆕 TypeScript + Jest + build +├── e2e.yml # 🆕 Playwright E2E tests +└── deploy.yml # 🆕 Static site deployment +``` + +**`frontend.yml`** (recommended): +```yaml +name: Frontend +on: + pull_request: + paths: ['apps/sim-core/**', '!apps/sim-engine/**'] + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: '24' } + - run: cd apps/sim-core && yarn install --frozen-lockfile + - run: cd apps/sim-core/packages/core && npx tsc --noEmit # Type check + - run: cd apps/sim-core/packages/core && yarn test # Jest (310 tests) + - run: cd apps/sim-core/packages/core && yarn build # Vite build +``` + +**`e2e.yml`** (recommended): +```yaml +name: E2E +on: + pull_request: + paths: ['apps/sim-core/**'] + +jobs: + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: '24' } + - run: cd apps/sim-core && yarn install --frozen-lockfile + - run: cd apps/sim-core/packages/core && yarn build + - run: cd apps/sim-core/packages/core && npx playwright install --with-deps + - run: cd apps/sim-core/packages/core && yarn test:e2e +``` + +--- + +## sim-engine Architecture + +The standalone Rust simulation engine: + +```mermaid +flowchart TB + subgraph CLI[CLI Binary] + Args[Argument Parsing] + Manifest[Project Manifest] + Orchestrator[Orchestrator] + end + + subgraph Engine[Engine Binary] + ExpControl[experiment-control] + SimControl[simulation-control] + Execution[execution] + end + + subgraph Core[Core Libraries] + Stateful[stateful
Agent State] + Memory[memory
Apache Arrow] + Runners[runners
JS/Python] + end + + CLI --> Engine + Engine --> Core + Args --> Manifest + Manifest --> Orchestrator + Orchestrator --> ExpControl + ExpControl --> SimControl + SimControl --> Execution + Execution --> Stateful + Execution --> Memory + Execution --> Runners +``` + +### Library Crates + +| Crate | Purpose | +|-------|---------| +| `execution` | Simulation execution logic, runner management | +| `experiment-control` | Experiment lifecycle management | +| `experiment-structure` | Configuration and manifest parsing | +| `simulation-control` | Individual simulation management | +| `stateful` | Agent state and field management | +| `memory` | Apache Arrow-based memory management | +| `flatbuffers_gen` | Generated FlatBuffers types | +| `nano` | IPC communication | +| `orchestrator` | Process orchestration | + +--- + +## Key Files Reference + +### Entry Points + +| File | Purpose | +|------|---------| +| `apps/sim-core/packages/core/src/index.tsx` | React app entry | +| `apps/sim-core/packages/core/src/boot.ts` | Service initialization | +| `apps/sim-engine/bin/cli/src/main.rs` | CLI entry | +| `apps/sim-engine/bin/hash_engine/src/main.rs` | Engine entry | + +### State Management + +| File | Purpose | +|------|---------| +| `src/features/files/FilesContext.tsx` | File state (useReducer + Immer reducer) | +| `src/features/files/slice.ts` | Files reducer and action creators | +| `src/features/files/adapter.ts` | Pure entity adapter for file CRUD | +| `src/features/project/ProjectContext.tsx` | Project state (useReducer) | +| `src/features/viewer/ViewerContext.tsx` | Viewer/UI state (useReducer) | +| `src/features/user/UserContext.tsx` | User preferences (useReducer) | +| `src/features/toast/ToastContext.tsx` | Toast notifications (useState) | +| `src/features/search/SearchContext.tsx` | Search state (useState) | +| `src/features/examples/ExamplesContext.tsx` | Examples list (useState) | +| `src/features/simulator/store.ts` | Simulator store (SimpleStore) | +| `src/features/simulator/simulate/slice.ts` | Simulator reducer (1800+ lines) | +| `src/features/simulator/simulate/StoreSync.tsx` | Context → store sync | +| `src/features/reduxCompat.ts` | Redux-compatible utilities | + +### Components + +| Directory | Purpose | +|-----------|---------| +| `src/components/HashCore/` | Main IDE shell | +| `src/components/SimulationRunner/` | Playback controls | +| `src/components/AgentScene/` | 3D visualization (@react-three/fiber) | +| `src/components/Modal/` | Dialog system | +| `src/components/TabbedEditor/` | Code editor (Monaco) | +| `src/components/Analysis/` | Analysis views | +| `src/components/PlotViewer/` | Plotly charts | + +### Engine Integration + +| File | Purpose | +|------|---------| +| `src/features/simulator/simulate/provider.ts` | Simulation runner management | +| `src/features/simulator/simulate/buildprovider.ts` | Worker initialization | +| `src/workers/simulation-worker/index.ts` | WASM worker | +| `packages/engine-web/src/` | WASM bindings | + +--- + +## Feature Development Guide + +### Adding New State + +Use React's built-in state management. Choose the simplest approach that works: + +```typescript +// 1. Local state (preferred — use when state is component-local) +const [value, setValue] = useState(initialValue); + +// 2. Shared state via Context (use when state crosses component boundaries) +const { currentProject } = useProject(); +const { allFiles, updateFile } = useFiles(); + +// 3. localStorage for persistence +localStorage.setItem('preferences', JSON.stringify(prefs)); +``` + +**Do NOT** add new state management libraries. The project uses only React built-ins plus a thin compatibility layer (`reduxCompat.ts`) for the simulator store. + +### Adding New Components + +Follow the existing `src/components/[Name]/` structure: + +```typescript +// src/components/MyFeature/MyFeature.tsx +import React, { FC } from "react"; +import { useFiles } from "../../features/files/FilesContext"; +import { useProject } from "../../features/project/ProjectContext"; +import "./MyFeature.scss"; + +export const MyFeature: FC = () => { + const { currentFile } = useFiles(); + const { currentProject } = useProject(); + + return ( +
+ {/* ... */} +
+ ); +}; +``` + +```scss +// src/components/MyFeature/MyFeature.scss +.MyFeature { + color: var(--theme-dark); + background: var(--theme-light); +} +``` + +--- + +## Common Patterns + +### Navigation / Routing + +The app uses custom lightweight routing utilities (replacing the abandoned `hookrouter` package): + +```typescript +// Programmatic navigation +import { navigate, setQueryParams } from "../util/navigation"; + +navigate("/path/to/page"); // Push navigation +navigate("/path", true); // Replace current entry +navigate("/path", false, { key: "value" }); // With query params +setQueryParams({ view: "3d" }); // Update query params only + +// Route matching in components +import { usePathRouter, RouteMap } from "../util/usePathRouter"; + +const routes: RouteMap = { + "/": () => , + "/project/:id": ({ id }) => , + "/@*": () => , +}; + +const element = usePathRouter(routes); +``` + +### Accessing State + +```typescript +// App state — use context hooks +import { useFiles } from "../../features/files/FilesContext"; +import { useProject } from "../../features/project/ProjectContext"; +import { useViewer } from "../../features/viewer/ViewerContext"; + +const { allFiles, currentFile, updateFile } = useFiles(); +const { currentProject } = useProject(); +const { currentTab, toggleEditor } = useViewer(); + +// Simulation state — use simulator hooks +import { useSimulatorSelector, useSimulatorDispatch } from "../../features/simulator/context"; + +const running = useSimulatorSelector(selectRunning); +const dispatch = useSimulatorDispatch(); +``` + +**`useSimulatorSelector` and shallow equality**: The simulator store uses `useSyncExternalStore` +under the hood. The `useSimulatorSelector` hook includes a `shallowEqualArrays` check to avoid +infinite re-render loops when selectors return new array references with identical contents +(e.g. `historySelectors.selectAll`). For object-returning selectors, reference equality is +sufficient because `createSelector` memoizes output references. If you add a new selector that +creates a new object on every call, you must either memoize it with `createSelector` or extend +`useSimulatorSelector` to support shallow object comparison. + +### Monaco Editor Model Lifecycle + +Monaco text models are created asynchronously via `syncModels()` in `MonacoModelSync`. Because +this runs in a `useEffect`, there is a brief render window where a file's text model does not yet +exist. The `HashCoreEditorFile` component handles this by calling `getTextModel(file, projectUrl)` +(the optional variant, not `getTextModelRequired`) and rendering a "Loading editor..." placeholder +when the model is `null`. Once `syncModels` runs and creates the model, the component re-renders +with the full editor. + +The `appBridge` singleton bridges React context state to the external simulator. `StoreSync` +pushes `FilesContext` and `ProjectContext` state into `appBridge` on every change, and the +`appBridge` snapshot invalidation pattern (returning a new object reference from `getState()`) +ensures that `reselect` selectors recompute when file contents change. + +### Scopes (Permissions) + +The scopes system determines what actions are available: + +```typescript +import { Scope, useScope, useScopes } from "../../features/scopes"; + +const canEdit = useScope(Scope.edit); +const { canSave, canEdit } = useScopes(Scope.save, Scope.edit); +``` + +### Modal System + +```typescript +import { useModal } from "react-modal-hook"; + +const [showModal, hideModal] = useModal(() => ( + + Title + Content + + + + +)); +``` + +--- + +## Additional Resources + +- [sim-core README](README.md) — Setup and running instructions +- [TODO.md](TODO.md) — Technical debt and modernization roadmap +- [TESTING_STRATEGY.md](TESTING_STRATEGY.md) — Test pyramid and conventions +- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — Contribution guidelines +- [.cursor/rules/hash-labs.mdc](.cursor/rules/hash-labs.mdc) — AI agent guidelines diff --git a/apps/sim-core/Cargo.lock b/apps/sim-core/Cargo.lock index 31075f62..0e64b982 100644 --- a/apps/sim-core/Cargo.lock +++ b/apps/sim-core/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -11,6 +11,24 @@ dependencies = [ "memchr 0.1.11", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr 2.8.0", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "assert_approx_eq" version = "1.1.0" @@ -19,13 +37,13 @@ checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" [[package]] name = "async-trait" -version = "0.1.50" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -34,16 +52,16 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi 0.3.9", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bencher" @@ -53,33 +71,15 @@ checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" [[package]] name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "bstr" -version = "0.2.16" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" -dependencies = [ - "lazy_static", - "memchr 2.4.0", - "regex-automata", - "serde", -] +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" - -[[package]] -name = "byteorder" -version = "1.4.3" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" @@ -89,43 +89,44 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "cast" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57cdfa5d50aad6cb4d44dcab6101a7f79925bd59d82ca42f38a9856a28865374" -dependencies = [ - "rustc_version", -] +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] -name = "cfg-if" -version = "0.1.10" +name = "cc" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "libc", - "num-integer", + "iana-time-zone", + "js-sys", "num-traits", - "time", - "winapi 0.3.9", + "wasm-bindgen", + "windows-link", ] [[package]] name = "clap" -version = "2.33.3" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", "textwrap", @@ -134,32 +135,38 @@ dependencies = [ [[package]] name = "console_error_panic_hook" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "criterion" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" dependencies = [ "atty", "cast", "clap", "criterion-plot", "csv", - "itertools 0.10.0", + "itertools 0.10.5", "lazy_static", "num-traits", "oorandom", "plotters", "rayon", - "regex 1.5.4", + "regex 1.12.3", "serde", "serde_cbor", "serde_derive", @@ -170,86 +177,65 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" dependencies = [ "cast", - "itertools 0.9.0", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", + "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" -version = "0.8.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if 1.0.0", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.4" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "cfg-if 1.0.0", "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.4" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" -dependencies = [ - "autocfg", - "cfg-if 1.0.0", - "lazy_static", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "csv" -version = "1.1.6" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ - "bstr", "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.10" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ - "memchr 2.4.0", + "memchr 2.8.0", ] [[package]] name = "either" -version = "1.6.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "env_logger" @@ -261,6 +247,12 @@ dependencies = [ "regex 0.1.80", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" @@ -275,9 +267,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -290,9 +282,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -300,15 +292,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -317,53 +309,47 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] name = "futures-sink" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.15" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", - "memchr 2.4.0", - "pin-project-lite 0.2.6", - "pin-utils", - "proc-macro-hack", - "proc-macro-nested", + "memchr 2.8.0", + "pin-project-lite 0.2.16", "slab", ] @@ -382,7 +368,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", @@ -391,22 +377,22 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "half" -version = "1.7.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "hash_types" @@ -448,6 +434,7 @@ dependencies = [ "hashintel-core", "js-sys", "serde", + "serde-wasm-bindgen", "serde_json", "uuid", "wasm-bindgen", @@ -456,52 +443,74 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] -name = "itertools" -version = "0.8.2" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "either", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log 0.4.29", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] name = "itertools" -version = "0.9.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.10.0" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "0.4.7" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -517,15 +526,15 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.94" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "log" @@ -533,17 +542,14 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" dependencies = [ - "log 0.4.14", + "log 0.4.29", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -556,62 +562,40 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" - -[[package]] -name = "memoffset" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num-integer" -version = "0.1.44" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] [[package]] -name = "oorandom" -version = "11.1.3" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "pest" -version = "2.1.3" +name = "oorandom" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "pin-project-lite" @@ -621,21 +605,15 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "plotters" -version = "0.3.1" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -646,44 +624,35 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.0" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.0" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -699,9 +668,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -787,12 +756,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] @@ -810,15 +777,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", - "num_cpus", ] [[package]] @@ -836,7 +800,7 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" dependencies = [ - "aho-corasick", + "aho-corasick 0.5.3", "memchr 0.1.11", "regex-syntax 0.3.9", "thread_local", @@ -845,20 +809,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "regex-syntax 0.6.25", + "aho-corasick 1.1.4", + "memchr 2.8.0", + "regex-automata", + "regex-syntax 0.8.9", ] [[package]] name = "regex-automata" -version = "0.1.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "byteorder", + "aho-corasick 1.1.4", + "memchr 2.8.0", + "regex-syntax 0.8.9", ] [[package]] @@ -869,24 +838,21 @@ checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] -name = "rustc_version" -version = "0.3.3" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -897,36 +863,13 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "serde" -version = "1.0.126" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -942,53 +885,92 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_cbor" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ "half", "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", - "ryu", + "memchr 2.8.0", "serde", + "serde_core", + "zmij", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" -version = "0.4.3" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "syn" -version = "1.0.72" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1019,16 +1001,6 @@ dependencies = [ "thread-id", ] -[[package]] -name = "time" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -1061,26 +1033,20 @@ checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] -name = "ucd-trie" -version = "0.1.3" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" - -[[package]] -name = "unicode-xid" -version = "0.2.2" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8-ranges" @@ -1094,18 +1060,17 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.17", "serde", ] [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi 0.3.9", "winapi-util", ] @@ -1117,54 +1082,44 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", + "once_cell", + "rustversion", "serde", "serde_json", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" -dependencies = [ - "bumpalo", - "lazy_static", - "log 0.4.14", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.24" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1172,28 +1127,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.116", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -1229,11 +1187,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi 0.3.9", + "windows-sys", ] [[package]] @@ -1241,3 +1199,97 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/apps/sim-core/Cargo.toml b/apps/sim-core/Cargo.toml index 000849ab..d5159d88 100644 --- a/apps/sim-core/Cargo.toml +++ b/apps/sim-core/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["packages/engine", "packages/engine-web", "packages/engine-types"] +members = ["packages/engine", "packages/engine-web", "packages/sim-engine-types"] [profile.dev] opt-level = 0 diff --git a/apps/sim-core/README.md b/apps/sim-core/README.md index d6da8399..06d970fb 100644 --- a/apps/sim-core/README.md +++ b/apps/sim-core/README.md @@ -1,152 +1,107 @@ -[github_star]: https://github.com/hashintel/labs# -[hash]: https://hash.ai/platform/hash?utm_medium=organic&utm_source=github_readme_labs-repo_apps-sim-core -[hash core]: https://hash.ai/platform/core?utm_medium=organic&utm_source=github_readme_labs-repo_apps-sim-core -[hash engine]: https://hash.ai/platform/engine?utm_medium=organic&utm_source=github_readme_labs-repo_apps-sim-core +# sim-core (hCore) -[![github_star](https://img.shields.io/github/stars/hashintel/labs?label=Star%20on%20GitHub&style=social)][github_star] +A free, fully-featured, local-first simulation IDE for building and running agent-based simulations in the browser. -# HASH Core +## Features -[HASH Core] (**hCore**) is a self-contained, in-browser environment for building and interfacing with agent-based simulations compatible with [HASH]. +- **Monaco Code Editor** — Full-featured editor with syntax highlighting, multi-file tabs, search, and diff view +- **3D Agent Visualization** — Real-time 3D rendering of simulation agents using Three.js +- **Simulation Controls** — Step, play/pause, reset, timeline scrubbing, speed control +- **Multiple Viewer Tabs** — 3D viewer, geospatial (deck.gl), analysis/plots (Plotly), process chart, raw output, step explorer +- **Local Experiments** — Parameter sweeps, value ranges, linspace, optimization — all running locally via WASM +- **hIndex Dependencies** — Import shared behaviors from the hIndex library +- **Import/Export** — Load and save simulations as `.zip` files +- **Fully Local** — No accounts, no cloud, no analytics. All data stays in your browser (localStorage) -It uses a legacy version of hEngine which is no longer maintained, separate from the primary [HASH Engine] (**hEngine**) found in this repository. +## Prerequisites +- [Node.js](https://nodejs.org/) 24+ (LTS recommended) +- [Rust](https://www.rust-lang.org/learn/get-started) (nightly-2024-12-01) +- [Yarn](https://yarnpkg.com/) 1.x +- [wasm-pack](https://rustwasm.github.io/wasm-pack/) -## Project Status - -**hCore** is currently in the process of transitioning from being closed-source and hosted on our internal infrastructure towards being a free, open-source IDE available to self-host. Much of this code dates from 2019-2020. While we're making it available at this time so that users can continue to work with and run existing simulations, additional migration work is ongoing, and we'll be changing the way simulations are created in the future. Upcoming tasks in 'phase one' of the migration include: - -- [X] Temporary removal of legacy Git-based UI elements -- [x] Allow for "project export" functionality in the development environment -- [ ] Re-enable "new simulation" creation flow -- [ ] Introduce local file storage for working offline (outside of local storage) -- [ ] Introduce GitHub integration for simulation management and storage -- [ ] Introduce prompt to ask users to insert Mapbox keys (securely stored) where not provided as an environment variable -- [ ] Re-introduce "Example projects" accessible via the menus -- [ ] Re-enable Git-based UI elements (such as the resources and activity panes, as well as ability to fork projects) -- [ ] Re-enable executing simulations in hCloud from hCore itself (allowing access to cloud-only features such as optimization experiments) - -While we work toward completing phase one, please be mindful of the software's current limitations. - -Phase two of our migration process involves enabling users to create, work with and run [HASH Core] and [HASH Engine] simulations in the [HASH] application directly. This will involved re-enabling simulation/behavior/dataset publishing directly on HASH, and a whole new approach to using typed entities in simulations. - - -## Limitations - -In its present form, the version of hCore published here is for the most part limited to providing a run-only environment for simulations. Current recommended use is as follows: - -1. Run hCore (this `apps/sim-core` project) on `localhost` and view it in your browser -1. To open a simulation, use the 'import' functionality and target a `.zip` file containing a previously exported simulation. _This can be downloaded from a project's hIndex listing page._ -1. You can now run and edit this simulation, however file storage is simply maintained within your browser (using `localstorage`), and changes you make will only be preserved within this web browser. -1. You can use the 'recent projects' menu to switch between other projects that you have imported. -1. To experiment with an example project, import an example project .zip file from the `example_projects` folder. - -Please exercise caution if authoring work inside the self-hosted environment because any simulations you author are **not being preserved** outside of the browser environment. These limitations will lift as the project status goals above are accomplished. - -## Using hCore - -You can either [self-host](#self-hosting) hCore on-prem or in your own cloud, or simply run it [locally](#run-or-develop-locally) on your machine. - -A [hosted preview](#hosted) of hCore also exists for demonstration purposes. - -## Self-hosting - -To host hCore yourself, you need: -1. To build hCore with output suitable for serving directly from a webserver -2. A webserver - -There are countless options for this, but we use [Vercel](https://vercel.com/), for which [instructions](#deploying-to-vercel) are below. - -### Building the files - -First, the environment in which you are building the files must have the correct dependencies available. - -If doing so locally, you can follow the [installation instructions](#run-or-develop-locally) below. - -If doing so remotely: -1. Ensure Node and Yarn are available in your environment, e.g. by - - using a Docker image that already has them - - use a runtime that already has them (e.g. the Vercel Node runtime) - - installing them as part of your build script -1. Run `sh scripts/install-dependencies.sh` -1. Run `yarn ws:core build --copy-index-to-root` - -The output files will be at `packages/core/dist`. Serve the contents of this folder from your webserver. - -### Deploying to Vercel - -If you want to host hCore on Vercel, you should: -1. Create a fork of this repository. -1. Create a new project in Vercel, and select your fork. -1. Select 'Other' from framework. -1. In 'Settings' -> 'General', set the 'Root Directory' to `apps/sim-core - -Deploy (or re-deploy) the project, then visit the preview URL. Future pushes to your fork will result in a new deployment. - -## Hosted - -A demonstration deployment of hCore can be [found in our sandbox](https://core.labs.hashsandbox.com/). - -## Run or develop locally - -### Installation - -Before running this software, your environment will need to have installed modern versions of: - -[Node](https://nodejs.org/en/), [Rust](https://www.rust-lang.org/learn/get-started), and [Yarn](https://yarnpkg.com/lang/en/). - -With these in place, you must use yarn to install wasm-pack: ```sh yarn global add wasm-pack ``` -To verify your installation, from the `sim-core` directory run: -```sh -node -v -yarn -v -rustup default -``` -If these commands output version numbers, you're all set. -For the first build, simply run: +## Quick Start + +From this directory (`apps/sim-core`): + ```sh +# Install dependencies and build engine WASM yarn + +# Start the development server +yarn serve:core ``` -#### Supported Environments +The app will be available at [http://localhost:8080](http://localhost:8080). -The required dependencies above are available (and consistent) across platforms. hCore can be built and run in modern Windows, macOS, and Ubuntu Linux environments, as well as within common VMs and containers. +## Build Commands -### Running `sim-core` +| Command | Description | +|---------|-------------| +| `yarn` | Install dependencies and build all packages | +| `yarn serve:core` | Start Vite dev server with HMR | +| `yarn build:core` | Production build (outputs to `packages/core/dist/`) | +| `yarn test:core` | Run Jest unit tests | +| `yarn test:e2e` | Run Playwright E2E tests (requires dev server or auto-starts) | +| `yarn test:e2e:smoke` | Quick E2E smoke check (5 tests, ~25s) | +| `yarn fmt:core` | Format code with Prettier | -To run hCore, after following the [installation](#installation) instructions , run: +## Project Structure -```sh -yarn start:core +``` +apps/sim-core/ + ARCHITECTURE.md — System architecture documentation + TODO.md — Technical debt and migration roadmap + TESTING_STRATEGY.md — Test pyramid and conventions + packages/ + core/ — React/TypeScript frontend (hCore IDE) + src/ + components/ — React components (HashCore, AgentScene, etc.) + features/ — State management (contexts, simulator, files) + util/ — Utilities (navigation, API stubs, types) + tests/ + e2e/ — Playwright E2E tests + vite.config.ts — Vite build configuration + engine-web/ — WASM simulation engine bridge + engine/ — Rust simulation engine (compiled to WASM) + utils/ — Shared utilities + sim-engine-types/ — TypeScript type definitions for the engine ``` -This will compile the application and host it for you at a default location of [`localhost:8080`](http://localhost:8080). +## Tech Stack -### Development and Troubleshooting +- **React** 18.2 with TypeScript 5.3 +- **Vite** 7 (dev server + production builds) +- **Three.js** 0.170 via `@react-three/fiber` 8 + `@react-three/drei` 9 +- **Monaco Editor** 0.25 (code editing) +- **Plotly.js** 3.3 (analysis/plots) +- **deck.gl** 8.3 (geospatial visualization) +- **Rust** nightly-2024-12-01 (simulation engine, compiled to WASM) +- **Playwright** (E2E testing) + **Jest** 29 (unit testing) -If you want to run the application in development mode, which will enable hot-reloading when you make changes, run: +## Usage -```sh -yarn serve:core -``` +1. **Import a simulation**: Use the import button to load a `.zip` file containing a simulation project +2. **Browse examples**: Use **File > Example projects** to load any of the built-in examples (served from `packages/core/public/example_projects/`) +3. **Edit code**: Modify behavior files, init files, and globals in the Monaco editor +4. **Run simulations**: Use the step/play/pause/reset controls to execute your simulation +5. **Analyze results**: Switch between viewer tabs to see 3D visualization, plots, raw data, etc. +6. **Export**: Save your work as a `.zip` file for sharing or backup + +## Development -See the README in [`packages/core`](https://github.com/hashintel/labs/tree/main/apps/sim-core/packages/core) for more details. +See [ARCHITECTURE.md](ARCHITECTURE.md) for system architecture and code patterns. +See [TESTING_STRATEGY.md](TESTING_STRATEGY.md) for testing conventions. +See [TODO.md](TODO.md) for the migration roadmap and technical debt tracker. -### Repository Structure +## Supported Platforms -Several different packages in this repository are orchestrated as yarn workspaces. Important packages include: - - `core`, which is the React/Redux/TypeScript frontend of hCore - - `engine` contains the hCore simulation engine, written in Rust. This is a legacy engine that is less powerful than the newer [HASH Engine], which [can be found separately](https://github.com/hashintel/labs/tree/main/apps/sim-engine) in this repo. - - `engine-web` bundles the `engine` package into a WebAssembly-backed JavaScript interface using `wasm-bindgen`. +hCore can be built and run on modern Windows, macOS, and Linux environments. The simulation engine compiles to WebAssembly, so the resulting application runs in any modern browser. - Additional utility packages also exist to facilitate minor conveniences. +## License - While each package can be built and run separately using the `yarn` commands within its package (see the given package's package.json file for guidance), the most common commands you will run are: - - `yarn start:core`, to rebuild everything and then host the hCore application - - `yarn serve:core`, to rebuild everything and then host the hCore application, in development mode - - `yarn`, to rebuild everything. - - `yarn fmt`, to apply formatting to source code when doing development work +See [LICENSE.md](LICENSE.md). diff --git a/apps/sim-core/TESTING_STRATEGY.md b/apps/sim-core/TESTING_STRATEGY.md new file mode 100644 index 00000000..9c13b0b4 --- /dev/null +++ b/apps/sim-core/TESTING_STRATEGY.md @@ -0,0 +1,108 @@ +# Testing Strategy + +This document describes how to approach testing in sim-core, with emphasis on **test-driven bug fixes**. + +> **Scope**: Covers `apps/sim-core/` only. + +## Test Pyramid + +| Layer | Tool | When to Use | Speed | +|-------|------|-------------|-------| +| **Unit** | Jest | Pure logic, utilities, selectors, hooks | Fast | +| **Component** | Jest + React Testing Library | React components in isolation | Fast | +| **Integration** | Jest | Multi-module flows, thunks, middleware | Medium | +| **E2E** | Playwright | Full user flows, UI, routing, simulation | Slow | + +**Prefer lower layers** when they can adequately verify the behavior. Use E2E only when the bug or feature spans the full stack. + +--- + +## Bug Fixes: TDD Approach (MANDATORY) + +When fixing a bug, **always start by writing a failing test** that reproduces the issue. Then fix the code until the test passes. + +### 1. Reproduce First + +Before changing any production code: + +1. **Write a test** that fails and demonstrates the bug. +2. **Run the test** — it must fail in a way that matches the reported behavior. +3. **Fix the code** until the test passes. +4. **Run the full suite** to ensure no regressions. + +### 2. Test Type Selection + +| Bug Type | Preferred Test | Fallback | +|----------|----------------|----------| +| Pure logic / util / selector | Unit test | — | +| React component behavior | Component test (`.spec.tsx`) | E2E | +| Hook or context behavior | Unit/component test | E2E | +| Multi-step user flow | E2E | — | +| UI layout / visibility | E2E | — | +| Import/export, file handling | Unit if logic-only, else E2E | — | +| Simulation execution | E2E | — | + +**Rule:** If a unit or component test can reliably reproduce the bug, use it. Otherwise use E2E. + +### 3. Where to Put Tests + +- **Unit/component:** Co-located with source (`*.spec.ts`, `*.spec.tsx`) or in `__tests__/` +- **E2E:** `apps/sim-core/packages/core/tests/e2e/*.spec.ts` +- **Shared E2E helpers:** `tests/e2e/fixtures/test-helpers.ts` + +### 4. Example Workflow + +**Bug:** "Importing a project shows 'Error importing project files: undefined'" + +1. Add `tests/e2e/import-project.spec.ts` that: + - Loads the app, opens File menu, triggers import with a zip + - Asserts no "undefined" in console errors +2. Run the test — it fails (reproduces the bug). +3. Fix error handling in `HashCoreHeaderMenuFiles.tsx` and `hooks.ts`. +4. Run the test — it passes. +5. Run full E2E suite to confirm no regressions. + +--- + +## Running Tests + +### sim-core (packages/core) + +```bash +# Unit + component tests +cd apps/sim-core/packages/core +npx jest --forceExit --testPathIgnorePatterns "e2e" + +# E2E smoke (quick) +yarn ws:core test:e2e:smoke + +# Full E2E +yarn ws:core test:e2e + +# Single E2E file +yarn ws:core test:e2e tests/e2e/import-project.spec.ts +``` + +### sim-engine + +```bash +cd apps/sim-engine +cargo test +``` + +--- + +## Test Conventions + +- **Descriptive names:** `"should import wildfires project zip without error"` not `"import works"` +- **Isolated:** Each test should be runnable in isolation; avoid shared mutable state. +- **Deterministic:** No flaky timeouts or race conditions; use `waitFor` over fixed `setTimeout` where possible. +- **Focused:** One logical assertion per test when practical. + +--- + +## References + +- [hash-labs.mdc](.cursor/rules/hash-labs.mdc) — Build & Test Verification, E2E requirements +- [tests/README.md](apps/sim-core/packages/core/tests/README.md) — Legacy E2E notes +- [test-helpers.ts](apps/sim-core/packages/core/tests/e2e/fixtures/test-helpers.ts) — Shared E2E utilities diff --git a/apps/sim-core/TODO.md b/apps/sim-core/TODO.md new file mode 100644 index 00000000..51b36ecb --- /dev/null +++ b/apps/sim-core/TODO.md @@ -0,0 +1,1267 @@ +# Technical Debt & Modernization TODO + +This document tracks outdated dependencies, deprecated patterns, and proposed upgrades for the sim-core application. + +**Last Updated**: May 2026 +**Scope**: `apps/sim-core/` only — everything else in the monorepo is out of scope +**Extraction Goal**: sim-core will be extracted into its own repository at the end of this project +**Review Panel**: Expert personas (QA, Frontend, WASM, UX, DevOps, Documentation) reviewed TODO plan and PR work; recommendations captured in "Review Panel" sections below. + +--- + +## Executive Summary + +| Area | Severity | Effort | Notes | +|------|----------|--------|-------| +| ~~React & React Ecosystem~~ | ✅ Done | — | React 18.2, @react-three/fiber 8, drei 9, Recoil removed | +| ~~Redux Removal~~ | ✅ Done | — | Redux entirely removed; replaced with React Context + reduxCompat.ts | +| ~~Feature Removal~~ | ✅ Done | — | Cloud/auth/sharing features removed | +| ~~Sentry Removal~~ | ✅ Done | — | Sentry + FullStory analytics removed | +| ~~Dev Tooling Cleanup~~ | ✅ Done | — | why-did-you-render removed | +| ~~Build Tooling~~ | ✅ Done | — | Migrated to Vite 7.3 | +| ~~Deprecated Packages~~ | ✅ Done | — | All deprecated packages replaced or removed | +| ~~Example Projects~~ | ✅ Done | — | Zip-based loading from public/example_projects/; inline blob deleted | + +--- + +## Application Architecture Change + +**New Model**: Free, fully-featured, local-first simulation IDE + +### Storage Strategy +- **Current State**: Cloud-based with user accounts, hCloud, server-side storage +- **Target State**: Local-first with browser storage (localStorage/IndexedDB) +- **Future Enhancement**: GitHub sync for project persistence (post-migration) + +### Features to KEEP + +| Category | Features | +|----------|----------| +| **Simulation Execution** | Step, Play/Pause, Reset, Timeline, Speed Control | +| **Viewer Tabs** | 3D Viewer, Geospatial, Analysis/Plots, Process Chart, Raw Output, Step Explorer | +| **Code Editing** | Monaco Editor, Multi-file tabs, Syntax highlighting, Diff view, Search | +| **File Management** | File tree, Create behaviors, Create datasets, Import/Export .zip, Behavior keys | +| **Experiments** | Local experiment runner (parameter sweeps, values, linspace, optimization) | +| **Dependencies** | hIndex shared behaviors library | +| **UI/UX** | Onboarding tour, Toasts, Loading states, Error boundaries, Keyboard shortcuts | +| **Data/Analysis** | Output metrics, Plots configuration | + +### Features to REMOVE + +#### ❌ User & Cloud Features (Item 6) - REMOVE ENTIRELY +| Feature | Files to Delete | +|---------|-----------------| +| User Authentication | `ModalSignin`, `ModalSignup`, auth flows | +| User Accounts | `features/user/` slice (simplify to local prefs only) | +| Cloud Credits | `CloudUsage` modal and tracking | +| Project Sharing | `ModalShare*` components | +| Access Codes | Access code system | +| hCloud Integration | Cloud experiment runners | + +**Rationale**: App becomes free and fully-featured, no accounts needed. + +#### ❌ Project Management (Item 7) - REMOVE ENTIRELY +| Feature | Files to Delete | +|---------|-----------------| +| New Project (server) | `ModalNewProject` (replace with local template) | +| Fork Project | Fork functionality | +| Server Save | Save to HASH servers | +| Releases/Versioning | `ModalRelease*` components | +| Project Metadata Sync | Server-side metadata | + +**Rationale**: Projects stored locally; GitHub sync added post-migration. + +#### ❌ Integrations (Item 10) - REMOVE MOST +| Feature | Action | +|---------|--------| +| Sentry | REMOVE (already planned) | +| FullStory | REMOVE (already planned) | +| Discord Widget | REMOVE | +| hIndex Dependencies | **KEEP** - shared behaviors library | + +### Simplified Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ sim-core (Local-First) │ +├─────────────────────────────────────────────────────────────────┤ +│ React Frontend │ WASM Engine (Web Workers) │ +│ ───────────────── │ ───────────────────────── │ +│ • HashCore (IDE shell) │ • engine-web bindings │ +│ • SimulationRunner │ • Rust → WASM compilation │ +│ • AgentScene (3D) │ • Local execution only │ +│ • Monaco Editor │ │ +├─────────────────────────────────────────────────────────────────┤ +│ Local State Only: │ +│ • React Context + useState for UI state │ +│ • localStorage for project persistence │ +│ • IndexedDB for larger datasets (future) │ +├─────────────────────────────────────────────────────────────────┤ +│ Future: GitHub Integration │ +│ • Save/load projects from user's GitHub repos │ +│ • No HASH account required │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Migration Phases for Feature Removal + +#### Phase 1: Remove Analytics & Widgets +- [x] Remove Sentry (`@sentry/*` packages) +- [x] Remove FullStory (`@fullstory/browser`) +- [x] Remove Discord widget +- [x] Remove why-did-you-render + +#### Phase 2: Remove Auth & User System +- [x] Remove `ModalSignin`, `ModalSignup` +- [x] Remove user authentication flows +- [x] Simplify `features/user/` to local preferences only +- [x] Remove cloud credits tracking + +#### Phase 3: Remove Cloud Features +- [x] Remove hCloud experiment runners (keep local runner) +- [x] Remove server-side project save +- [x] Remove sharing features (`ModalShare*`) +- [x] Remove access code system + +#### Phase 4: Remove Project Management +- [x] Remove `ModalRelease*` components +- [x] Remove fork functionality (replaced with local fork) +- [x] Simplify new project to local templates +- [x] Remove server metadata sync + +#### Phase 5: Simplify to Local Storage +- [x] Implement localStorage-based project persistence +- [x] Implement local project templates +- [x] Ensure import/export .zip works standalone +- [x] Test fully offline operation (verified: all E2E tests run locally without cloud APIs) + + +#### Phase 5b: Example Projects Architecture (Mar 2026) +- [x] Move example .zip files from `example_projects/` to `packages/core/public/example_projects/` +- [x] Create `manifest.json` with metadata for all 13 examples +- [x] Delete `builtinSimulations.ts` (177-line inline data blob) +- [x] Rewrite bootstrap to fetch manifest instead of reading inline data +- [x] Auto-import default example zip on first visit (no hardcoded localStorage seed) +- [x] Example projects menu fetches + imports zips on click +- [x] Extract reusable `parseZipToProject()`, `fetchAndParseProject()`, `useImportProjectFromUrl()` +- [x] Add E2E test coverage: `example-projects.spec.ts` (13 tests, one per example zip) +### Packages to Remove (Feature-Related) + +``` +# Auth/Cloud related +- (Done) Removed dead API query files: canUserEditProject, createNewSimulationProject, + forkProjectQuery, forkAndReleaseBehaviorsQuery, projectReleaseTags, + requestPrivateProjectAccessCode, userForks, commitActions, + createReleaseWithUpdate, registerEvents +- (Done) Analytics no-opped (trackEvent/trackEvents are stubs) +- (Done) getReleaseMeta returns empty data locally + +# Sharing +- (Done) ModalShare components deleted + +# Analytics (already in plan) +- @sentry/browser (removed) +- @sentry/integrations (removed) +- @sentry/tracing (removed) +- @sentry/fullstory (removed) +- @sentry/webpack-plugin (removed) +- @fullstory/browser (removed) +``` + +--- + +## sim-core Frontend + +### 🔴 Critical: React Ecosystem (Major Version Behind) + +| Package | Current | Latest | Action | +|---------|---------|--------|--------| +| `react` | ~~16.14.0~~ **18.2.0** | ✅ Done | Upgraded via 16→17→18 | +| `react-dom` | ~~16.14.0~~ **18.2.0** | ✅ Done | createRoot migrated | +| `react-redux` | ~~7.2.4~~ | ✅ Removed | Replaced with `useSyncExternalStore` | +| `@reduxjs/toolkit` | ~~1.5.0~~ | ✅ Removed | Replaced with `reduxCompat.ts` | +| `redux` | ~~*~~ | ✅ Removed | Transitive dep of RTK, no longer needed | +| `recoil` | ~~0.4.1~~ | ✅ Removed | Replaced with React Context | + +**React Migration**: ✅ COMPLETE (16 → 17 → 18.2) +- No legacy lifecycle methods found (0 instances) +- Only 2 class components (ErrorBoundary, StepExplorer) — both compatible +- Entry points migrated to createRoot API +- Test files still use legacy ReactDOM.render (deprecated warnings; clean up with Jest 29) + +--- + +### 🔴 Critical: Redux Removal + +**Decision**: Remove Redux entirely. Use React's built-in state management (no replacement library). + +**Rationale**: +- Redux adds massive boilerplate for what this application needs +- Modern React 18 has excellent built-in state management +- No additional libraries needed +- Dramatically simpler codebase +- Easier onboarding for contributors + +**Current Redux Architecture (TO BE DELETED)**: +``` +┌─────────────────────────────────────────────────────────┐ +│ App Store (features/store.ts) │ +│ ├── files slice (1200+ lines) │ +│ ├── project slice │ +│ ├── user slice │ +│ ├── viewer slice │ +│ ├── search slice │ +│ ├── toast slice │ +│ └── examples slice │ +├─────────────────────────────────────────────────────────┤ +│ Simulator Store (features/simulator/store.ts) │ +│ └── simulator slice (1800+ lines) │ +├─────────────────────────────────────────────────────────┤ +│ Middleware: localStorage, analytics, RxJS sync │ +├─────────────────────────────────────────────────────────┤ +│ Async: createAppAsyncThunk │ +└─────────────────────────────────────────────────────────┘ +``` + +**Target Architecture (React Built-ins Only)**: +``` +┌─────────────────────────────────────────────────────────┐ +│ React Context + Hooks (where truly needed) │ +│ ├── ProjectContext - project state │ +│ ├── SimulatorContext - simulation state │ +│ └── Component-local state for everything else │ +├─────────────────────────────────────────────────────────┤ +│ Simple hooks for persistence │ +│ └── useLocalStorage() for persistence │ +├─────────────────────────────────────────────────────────┤ +│ Async: Regular async functions │ +└─────────────────────────────────────────────────────────┘ +``` + +**Packages Removed**: +- ~~`@reduxjs/toolkit`~~ ✅ Removed — replaced with `reduxCompat.ts` +- ~~`react-redux`~~ ✅ Removed — replaced with `useSyncExternalStore` +- ~~`redux`~~ ✅ Removed — transitive dep of RTK +- ~~`recoil`~~ ✅ Removed — replaced with React Context +- `rxjs` — still used (analysis middleware, experiment queueing, search) + +**Redux Slice Migration Progress**: +- [x] `search` (4 consumers) → `SearchContext` — fully removed from Redux +- [x] `viewer` (22 consumers) → `ViewerContext` — pure React (useReducer), no Redux dependency +- [x] `user` (15 consumers) → `UserContext` — pure React (useReducer), no Redux dependency +- [x] `toast` (4 consumers) → `ToastContext` — pure React (useState), no Redux dependency +- [x] `examples` (2 consumers) → `ExamplesContext` — pure React (useState), no Redux dependency +- [x] `project` (88 consumers) → `ProjectContext` — pure React (useReducer), no Redux dependency +- [x] `files` (80 consumers) → `FilesContext` — pure Immer reducer with useReducer, no RTK dependency +- [x] `simulator` store (35 consumers) → still uses Redux (useSimulatorSelector/useSimulatorDispatch) +- [x] Swap context facade internals from Redux to pure React state (useReducer) + - App Store completely removed; all 7 app contexts are pure React + - Files slice converted from RTK `createSlice` to pure Immer reducer + - Entity adapter replaced with pure TypeScript implementation + - Actions converted from `createAction` to plain action creators + - Selectors use `reselect` directly (no RTK re-export) + - `immer` and `reselect` added as direct dependencies +- [x] Rebuild scopes system as context-aware hooks +- [x] Rebuild localStorage persistence and auto-save as useEffect hooks +- [x] Convert simulator store from configureStore to pure SimpleStore + - Created `reduxCompat.ts` compatibility layer (createSlice, createAction, createEntityAdapter, createStore) + - Replaced configureStore with createStore (minimal Redux-compatible store with middleware/thunk support) + - Replaced react-redux Provider with useSyncExternalStore + - Zero source files import from @reduxjs/toolkit or redux + - Only .spec.tsx test files still reference react-redux (pre-existing) +- [x] Remove `@reduxjs/toolkit`, `react-redux` packages from package.json +- [x] Re-enable `useUnknownInCatchVariables` in tsconfig (fixed 3 catch clauses) +- [x] Update .spec.tsx test files to remove react-redux usage (done with Jest 29 upgrade) + +**Packages to Add**: +- None (React 18 built-ins are sufficient) + +--- + +### ✅ Done: Sentry Removal + +Sentry and related analytics integrations have been removed: +- `@sentry/browser`, `@sentry/integrations`, `@sentry/tracing`, `@sentry/fullstory`, `@sentry/webpack-plugin` removed +- `@fullstory/browser` removed +- `initSentry()` removed from `boot.ts`; `src/util/initSentry.ts` deleted +- Sentry webpack plugin removed from `webpack.config.js` +- `trackEvent`/`trackEvents` converted to no-op stubs + +--- + +### ✅ Done: Dev Tooling Cleanup + +- `@welldone-software/why-did-you-render` removed +- Initialization code removed from `src/index.tsx` +- Discord widget removed from HashCore + +--- + +### Redux Migration Strategy + +#### Step 1: Audit state usage +Analyze each Redux slice and categorize: +- **Truly global**: Needs Context (project, user auth) +- **Component tree local**: Lift to common ancestor with useState +- **Single component**: Keep as local state + +#### Step 2: Create minimal Context for truly global state +```typescript +// contexts/ProjectContext.tsx +import { createContext, useContext, useState, ReactNode } from 'react'; + +interface ProjectContextValue { + project: Project | null; + setProject: (project: Project | null) => void; +} + +const ProjectContext = createContext(null); + +export function ProjectProvider({ children }: { children: ReactNode }) { + const [project, setProject] = useState(null); + return ( + + {children} + + ); +} + +export const useProject = () => { + const ctx = useContext(ProjectContext); + if (!ctx) throw new Error('useProject must be within ProjectProvider'); + return ctx; +}; +``` + +#### Step 3: Simple localStorage hook +```typescript +// hooks/useLocalStorage.ts +import { useState, useEffect } from 'react'; + +export function useLocalStorage(key: string, initial: T) { + const [value, setValue] = useState(() => { + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : initial; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue] as const; +} +``` + +#### Step 4: Migrate components +```typescript +// Before (Redux) +import { useSelector, useDispatch } from 'react-redux'; +import { selectCurrentProject } from '../features/project/selectors'; + +const MyComponent = () => { + const dispatch = useDispatch(); + const project = useSelector(selectCurrentProject); + const handleClick = () => dispatch(setProject(newProject)); +}; + +// After (React Context) +import { useProject } from '../contexts/ProjectContext'; + +const MyComponent = () => { + const { project, setProject } = useProject(); + const handleClick = () => setProject(newProject); +}; +``` + +#### Step 5: Delete Redux infrastructure + +**Files to Delete** (entire `features/` directory structure): +- `src/features/store.ts` +- `src/features/rootReducer.ts` +- `src/features/simulator/store.ts` +- `src/features/simulator/context.tsx` +- `src/features/*/slice.ts` +- `src/features/*/selectors.ts` +- `src/features/middleware/*` +- `src/features/actionObservable.ts` +- `src/features/createAppAsyncThunk.ts` +- `src/features/types.ts` + +**Files to Update**: +- `src/components/App/App.tsx` - Remove Provider, add Context providers +- `src/boot.ts` - Remove store setup +- All components using `useSelector`/`useDispatch` + +**Estimated Effort**: 4-6 weeks for complete removal + +**Benefits**: +- ~15KB+ bundle reduction (RTK + react-redux + redux) +- No Redux DevTools, actions, reducers, selectors, thunks, middleware +- Standard React patterns - no learning curve +- Dramatically simpler mental model + +### 🔴 Critical: Abandoned/Deprecated Packages + +| Package | Status | Replacement | +|---------|--------|-------------| +| `hookrouter` 1.2.3 | ✅ Removed | Custom `usePathRouter` + `navigate` utilities | +| `request` 2.88.2 | ✅ Removed | `fetch` API | +| `request-promise-native` | ✅ Removed | `fetch` API | +| `@material-ui/core` 4.11.4 | ⚠️ Renamed/Deprecated | `@mui/material` 5.x | +| `@material-ui/lab` | ⚠️ Renamed/Deprecated | `@mui/lab` 5.x | +| `react-three-fiber` 5.0.6 | ✅ Migrated | `@react-three/fiber` 8.18.0 | +| `drei` 1.5.7 | ✅ Migrated | `@react-three/drei` 9.122.0 | +| `recoil` 0.4.1 | ✅ Removed | Replaced with React Context (`SceneContext.tsx`) | + +**Action Items**: +- [x] Replace `hookrouter` with custom `usePathRouter` + `navigate` utilities +- [x] Replace `request`/`request-promise-native` with fetch +- [x] Remove `@material-ui/*` (only used by deleted staging deploy tool) +- [x] Migrate `react-three-fiber` 5.0.6 to `@react-three/fiber` 8.18.0 +- [x] Migrate `drei` 1.5.7 to `@react-three/drei` 9.122.0 +- [x] Upgrade `three` 0.119.1 to 0.170.0 +- [x] Remove `recoil` 0.4.1 (replaced with React Context in SceneContext.tsx) + +### ✅ Done: Build Tooling — Migrate Webpack 4 → Vite + +| Package | Current | Action | Notes | +|---------|---------|--------|-------| +| `webpack` | ~~4.44.2~~ **removed** | ✅ Done | Replaced by Vite 7 | +| `webpack-cli` | ~~3.3.12~~ **removed** | ✅ Done | | +| `webpack-dev-server` | ~~3.11.0~~ **removed** | ✅ Done | Vite dev server replaces this | +| `typescript` | ~~4.1.3~~ **5.3.3** | ✅ Done | `satisfies`, const type params, decorators | +| `jest` | 26.6.3 | 29.7+ | Or migrate to Vitest | +| `ts-jest` | ~~26.4.4~~ **removed** | ✅ Done | Switched to `babel-jest` (ts-jest 26 incompatible with TS 5) | +| `babel-loader` | ~~8.2.1~~ **removed** | ✅ Done | Vite uses esbuild for dev, Rollup for prod | +| `Node.js` | ~~20.8.0~~ **24.13.1** | ✅ Done | Upgraded to 24 LTS | + +**Decision**: ✅ COMPLETE - Migrated to Vite 7. Benefits achieved: +- Sub-second dev server startup (vs 30s+ with Webpack 4) +- Native ESM, HMR via esbuild +- Eliminates `--openssl-legacy-provider` workaround +- Modern toolchain, better DX +- Simpler config (~30 lines vs ~300 lines) + +**Migration completed**: All steps completed successfully: +1. ✅ Upgraded Node.js 20 → 24 LTS +2. ✅ Installed Vite + plugins (`@vitejs/plugin-react`, `vite-plugin-wasm`, `vite-plugin-top-level-await`, `vite-plugin-monaco-editor`) +3. ✅ Created `vite.config.ts` with resolve aliases, define globals, SCSS support +4. ✅ Moved HTML entry to project root (Vite convention), added module script tags +5. ✅ Replaced `!!raw-loader!` imports with Vite `?raw` suffix +6. ✅ Refactored worker loading for Vite +7. ✅ Verified WASM loading works via `vite-plugin-wasm` +8. ✅ Removed webpack magic comments from dynamic imports +9. ✅ Simplified build stamp system +10. ✅ Updated npm scripts, verified build + tests, removed all webpack infrastructure + +**Packages to add**: `vite`, `@vitejs/plugin-react`, `vite-plugin-wasm`, `vite-plugin-top-level-await`, `vite-plugin-monaco-editor` + +**Packages removed**: `webpack`, `webpack-cli`, `webpack-dev-server`, `html-webpack-plugin`, `webpack-manifest-plugin`, `webpack-messages`, `webpack-retry-chunk-load-plugin`, `unused-modules-webpack-plugin`, `url-loader`, `file-loader`, `raw-loader`, `css-loader`, `style-loader`, `babel-loader`, `source-map-loader`, `null-loader`, `monaco-editor-webpack-plugin`, `postcss-loader`, `sass-loader`, and many `@babel/*` packages (Vite uses esbuild). + +**Note**: `engine-web/webpack.config.js` (stdlib build) is a separate concern — can stay on Webpack or be converted to esbuild later. + +### 🟠 High: Significant Version Gaps + +| Package | Current | Latest | Gap | +|---------|---------|--------|-----| +| `monaco-editor` | 0.25.2 | 0.52.2 | 27 versions (see Monaco Assessment below) | +| `three` | ~~0.119.1~~ **0.170.0** | ✅ Done | Upgraded with @react-three/fiber v8 | +| `rxjs` | 6.6.6 | 7.8+ | Major version | +| `plotly.js` | ~~1.57.1~~ **3.3.1** | ✅ Done | Upgraded to `plotly.js-dist-min` (ESM-ready) | +| `@sentry/browser` | 6.2.0 | - | **REMOVING** | +| `@deck.gl/core` | 8.3.7 | 8.9+ | | +| `graphql` | 15.5.0 | 16.8+ | Major version | +| `date-fns` | 2.17.0 | 3.3+ | Major version | + +### Monaco Editor Assessment (Feb 2026) + +**Current**: 0.52.2 (upgraded from 0.25.2) | **Latest stable**: 0.55.1 | **Gap**: 3 minor versions + +#### Already Implemented (Easy Wins) + +- [x] **Better editor options** — smooth cursor/scrolling, minimap, mouse wheel zoom, bracket matching, parameter hints, format-on-paste +- [x] **Python completion provider** — HASH simulation API autocomplete for `state.`, `context.`, `hstd.` in Python behaviors +- [x] **JS diagnostic validation** — enabled `checkJs` and semantic validation for JavaScript behaviors, with `ES2020` target + +#### Features Gained by Upgrading to 0.52 + +| Feature | Available Since | Impact | +|---------|----------------|--------| +| **Bracket pair colorization** | 0.30 | Matching brackets get distinct colors — major readability win | +| **Bracket pair guides** | 0.30 | Vertical lines connecting bracket pairs | +| **Inline completions API** | 0.32 | Required for AI-powered code suggestions (Copilot-style) | +| **Sticky scroll** | 0.36 | Shows current scope/function context at editor top | +| **Inlay hints** | 0.35 | Inline type annotations (e.g. parameter names, inferred types) | +| **Unicode highlighting** | 0.31 | Security: highlights confusable Unicode characters | +| **New diff editor** | 0.42 | Dramatically improved diff viewing experience | +| **Drop into editor** | 0.33 | Drag & drop files/text into the editor | +| **Column selection mode** | 0.31 | Better multi-cursor column editing | +| **Improved find/replace** | 0.35+ | Better regex support, match highlighting | +| **Accessibility improvements** | various | Screen reader, keyboard navigation enhancements | + +#### Breaking Changes in Upgrade Path (0.25 -> 0.52) + +| Version | Breaking Change | Migration | +|---------|----------------|-----------| +| 0.45 | `wordBasedSuggestions: boolean` -> string enum | Change to `'currentDocument'` or `'off'` | +| 0.45 | `occurrencesHighlight: boolean` -> string enum | Change to `'singleFile'` or `'off'` | +| 0.42 | New diff editor is default | Review diff editor usage in `TabbedEditorDiffPanel` | +| 0.44 | Old diff editor removed | Ensure `renderSideBySide`/`enableSplitViewResizing` still work | +| 0.51 | `editor.main.nls.js` removed | Verify Vite plugin compatibility | +| 0.52 | Dispose with listeners bug | Test `onDidChangeContent` usage in `monaco.ts` | + +#### Upgrade Plan + +1. [ ] **Phase 1: Compatibility check** — Verify `vite-plugin-monaco-editor-esm` supports 0.52. If not, check alternatives (`@aspect-build/vite-plugin-monaco-editor`, or manual worker config). +2. [ ] **Phase 2: Upgrade Monaco** — `yarn add monaco-editor@0.52.2`, fix breaking changes in editor options, diff editor, and type imports. +3. [ ] **Phase 3: Enable new features** — Bracket pair colorization, sticky scroll, bracket guides, inlay hints. +4. [ ] **Phase 4: Worker configuration** — Verify JSON/TypeScript workers still load. Consider adding CSS worker if needed. +5. [ ] **Phase 5: Test thoroughly** — Run full E2E suite, verify all file types render correctly, check Git conflict decorator compatibility. + +**Estimated effort**: 1-2 days for upgrade + testing. Medium risk due to diff editor and worker changes. + +#### AI Code Assistance (Requires Monaco 0.32+) + +**Goal**: Add Copilot-style inline code completions using user-provided LLM API keys. + +**Architecture** (local-first, no backend required): + +``` +User provides API key (stored in localStorage) + | + v +Monaco inline completions provider (browser-side) + | + v +Direct HTTPS call to LLM API (OpenAI, Anthropic, Mistral, etc.) + | + v +Ghost text shown in editor, user accepts with Tab +``` + +**Implementation plan**: + +1. [ ] **Settings UI** — Add a settings panel where users can: + - Choose an LLM provider (OpenAI, Anthropic, Mistral, or custom endpoint) + - Enter their API key (stored encrypted in localStorage) + - Toggle AI completions on/off + - Choose trigger mode (on-idle, on-demand via keyboard shortcut) + +2. [ ] **Inline completions provider** — Register a `languages.registerInlineCompletionsProvider` that: + - Collects context (current file, cursor position, language, neighboring files) + - Sends a completion request directly to the chosen LLM API via `fetch()` + - Returns ghost text for the user to accept/reject + - Debounces requests to avoid excessive API calls + +3. [ ] **Provider adapters** — Implement adapters for: + - OpenAI Chat Completions API (`gpt-4o-mini`, `gpt-4o`) + - Anthropic Messages API (`claude-sonnet`, `claude-haiku`) + - Mistral Codestral API (purpose-built for code completion) + - Custom OpenAI-compatible endpoint (for local models like Ollama) + +4. [ ] **Context enrichment** — Include in the prompt: + - Current file contents with cursor position marked + - HASH simulation API documentation (state/context/hstd methods) + - Active globals.json and init.json for context about the simulation + - Language-specific instructions (JS behavior vs Python behavior vs JSON config) + +**Dependencies**: Requires Monaco 0.32+ (inline completions API). Do upgrade first. + +**Alternative**: The `monacopilot` npm package provides this out of the box but requires a backend API handler. For our local-first app, a custom browser-side implementation is more appropriate. However, `monacopilot`'s `registerCompletion` could be studied for API design patterns. + +**Security note**: API keys stored in localStorage are accessible to any JS running on the page. Since this is a local-first app with no third-party scripts, this is acceptable. Add a clear warning in the settings UI. + +### 🟡 Medium: Build/Dev Optimizations + +- [x] Silence SCSS deprecation warnings (silenceDeprecations in vite.config.ts) +- [x] Fix `?? false` esbuild warning in Analysis/modals.ts +- [x] Suppress Playwright webServer stderr for cleaner E2E output +- [x] Add `serve:quiet` script for minimal-output dev server + +### 🟡 Medium: Testing Infrastructure + +| Issue | Current State | Recommended | +|-------|---------------|-------------| +| Test runner | Jest 26 | Jest 29 or Vitest | +| React testing | @testing-library/react 11 | @testing-library/react 14+ | +| E2E tests | None | **Playwright** (see Migration Regression Tests below) | +| Coverage | Unknown | Add coverage requirements | + +--- + +## Migration Regression Tests (Playwright E2E) + +**Purpose**: Ensure application integrity during Redux removal and other migrations. + +### Test Suite Overview + +These E2E tests should pass before AND after each migration phase: + +``` +tests/e2e/ +├── playwright.config.ts # Playwright configuration +├── smoke.spec.ts # Quick health check +├── simulation-run.spec.ts # Core simulation functionality +└── fixtures/ + └── test-helpers.ts # Shared test utilities +``` + +### Critical Test Scenarios + +#### 1. Smoke Test (`smoke.spec.ts`) — all passing +- [x] Application loads without errors +- [x] No console errors on startup +- [x] Main UI elements render (editor, viewer, controls) + +#### 2. Simulation Run Test (`simulation-run.spec.ts`) — all passing +- [x] Load built-in "Wildfires - Regrowth" simulation +- [x] Verify simulation initializes (step 0) +- [x] Click "Step" button 5 times +- [x] Verify step count increases to 5 +- [x] Verify 3D viewer or Raw Output shows agent data +- [x] Click "Play" button +- [x] Verify simulation runs (step count increases automatically) +- [x] Click "Pause" button +- [x] Verify simulation stops +- [x] Click "Reset" button +- [x] Verify simulation resets to step 0 + +#### 3. State Persistence Test (`persistence.spec.ts`) — all passing +- [x] Make changes (run simulation) +- [x] Refresh page +- [x] Verify appropriate state is restored + +### Test Implementation + +**Package to Add**: +```bash +yarn add -D @playwright/test +``` + +**Key Selectors** (from codebase analysis): +```typescript +// Simulation controls +const SELECTORS = { + stepButton: '.step.simulation-control button', + playPauseButton: '.playpause.simulation-control button', + resetButton: '.reset.simulation-control button', + stepCounter: '.simulation-control-container .step-display', // or timeline + rawOutputTab: '[data-tab="raw-output"]', + rawOutputContent: '.monaco-editor', // Raw output uses Monaco + agentViewer: '.agent-scene', // 3D viewer + simulationViewer: '.SimulationViewer', +}; +``` + +**Example Test**: +```typescript +// tests/e2e/simulation-run.spec.ts +import { test, expect } from '@playwright/test'; + +test.describe('Simulation Execution', () => { + test.beforeEach(async ({ page }) => { + // Load the app with built-in wildfires simulation + await page.goto('/@hash/wildfires-regrowth/main'); + // Wait for app to fully load + await page.waitForSelector('.simulation-control-container'); + }); + + test('should run simulation for multiple steps', async ({ page }) => { + // Get initial step (should be 0 or undefined) + const timeline = page.locator('.timeline'); + + // Click step button 5 times + const stepButton = page.locator('.step.simulation-control button'); + for (let i = 0; i < 5; i++) { + await stepButton.click(); + // Wait for step to complete + await page.waitForTimeout(500); + } + + // Verify we're at step 5 + await expect(timeline).toContainText('5'); + }); + + test('should display agent data in viewer', async ({ page }) => { + // Step once to generate data + await page.locator('.step.simulation-control button').click(); + await page.waitForTimeout(1000); + + // Check that either 3D viewer or raw output has content + const viewer = page.locator('.SimulationViewer'); + await expect(viewer).toBeVisible(); + + // Switch to raw output tab if available + const rawTab = page.locator('[data-testid="raw-output-tab"]'); + if (await rawTab.isVisible()) { + await rawTab.click(); + const output = page.locator('.monaco-editor'); + await expect(output).toContainText('['); // JSON array of agents + } + }); + + test('should reset simulation', async ({ page }) => { + // Run a few steps + const stepButton = page.locator('.step.simulation-control button'); + await stepButton.click(); + await stepButton.click(); + await page.waitForTimeout(500); + + // Reset + await page.locator('.reset.simulation-control button').click(); + await page.waitForTimeout(500); + + // Verify reset (step should be 0) + // Implementation depends on how step is displayed + }); +}); +``` + +### Running Tests + +```bash +# Install Playwright browsers (first time) +npx playwright install + +# Run all E2E tests +yarn test:e2e + +# Run with UI mode (debugging) +yarn test:e2e:ui + +# Run specific test file +yarn test:e2e simulation-run.spec.ts +``` + +### CI Integration + +Add to `.github/workflows/`: +```yaml +e2e-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '18' + - run: yarn install + - run: yarn build:core + - run: npx playwright install --with-deps + - run: yarn test:e2e +``` + +### Migration Checkpoints + +Run E2E tests at each migration phase: + +| Phase | Before | After | Notes | +|-------|--------|-------|-------| +| Analytics removal | ✓ | ✓ | Should have no impact | +| Auth/Cloud removal | ✓ | ✓ | May simplify app | +| Dev tooling removal | ✓ | ✓ | Should have no impact | +| Redux removal Step 1 | ✓ | ✓ | Create contexts | +| Redux removal Step 2 | ✓ | ✓ | Migrate components | +| Redux removal Step 3 | ✓ | ✓ | Delete Redux files | +| React 16 → 17 | ✓ | ✓ | Compatibility | +| React 17 → 18 | ✓ | ✓ | createRoot migration | + +### Feature Coverage for E2E Tests + +The following features are being KEPT and are now covered by E2E tests: + +#### Core Simulation (HIGH priority) ✅ IMPLEMENTED +- [x] **Simulation Controls**: Step, Play/Pause, Reset (`simulation-run.spec.ts`) +- [x] **Timeline/Scrubber**: Timeline presence verified (`simulation-run.spec.ts`) +- [x] **Simulation Initialization**: Load builtin simulation (`smoke.spec.ts`) + +#### Viewer Tabs (MEDIUM priority) ✅ IMPLEMENTED +- [x] **3D Viewer (AgentScene)**: Viewer displays content (`viewer-tabs.spec.ts`) +- [x] **Geospatial (MapViewer)**: Tab loads when available (`viewer-tabs.spec.ts`) +- [x] **Analysis/Plots**: Tab renders with data (`viewer-tabs.spec.ts`) +- [x] **Process Chart**: Tab presence tested (`viewer-tabs.spec.ts`) +- [x] **Raw Output**: JSON agent state displays (`viewer-tabs.spec.ts`, `simulation-run.spec.ts`) +- [x] **Step Explorer**: Opens via View menu, tab renders (`viewer-tabs.spec.ts`) + +#### Code Editing (HIGH priority) ✅ IMPLEMENTED +- [x] **Monaco Editor**: Opens, content visible (`file-management.spec.ts`) +- [x] **Multi-file tabs**: Can switch files (`file-management.spec.ts`) +- [x] **File Tree**: Navigate project files (`file-management.spec.ts`) +- [x] **Create Behavior**: Add behavior action available (`dependencies.spec.ts`) +- [x] **Behavior Keys**: Context tested (`dependencies.spec.ts`) + +#### File Management (MEDIUM priority) ✅ IMPLEMENTED +- [x] **File Tree Operations**: Display and click (`file-management.spec.ts`) +- [x] **Import .zip**: File input accessible (`file-management.spec.ts`) +- [x] **Export .zip**: Export option accessible (`file-management.spec.ts`) + +#### Experiments (LOW priority) ✅ IMPLEMENTED +- [x] **Local Experiment Runner**: Button, menu, modal tested (`experiments.spec.ts`) +- [x] **Experiment Configuration**: Parameter sweep options (`experiments.spec.ts`) + +#### Dependencies (MEDIUM priority) ✅ IMPLEMENTED +- [x] **hIndex Search**: Search functionality present (`dependencies.spec.ts`) +- [x] **Add Dependency**: Add behavior action accessible (`dependencies.spec.ts`) +- [x] **Shared Behavior Indicator**: Indicator checked (`dependencies.spec.ts`) + +#### UI/UX (LOW priority) ✅ IMPLEMENTED +- [x] **Onboarding Tour**: Tour dismissible, elements render (`ui-features.spec.ts`) +- [x] **Keyboard Shortcuts**: Step, search, save tested (`ui-features.spec.ts`) +- [x] **Error Boundaries**: No render errors assertion (`all spec files`) +- [x] **Window Resize**: Graceful handling (`ui-features.spec.ts`) + +#### Local Storage (HIGH priority) ✅ IMPLEMENTED +- [x] **localStorage Usage**: Values stored (`persistence.spec.ts`) +- [x] **App Reload**: State survives refresh (`persistence.spec.ts`) +- [x] **Preferences**: localStorage errors handled (`persistence.spec.ts`) +- [x] **Local Simulation**: WASM runs locally (`persistence.spec.ts`) +- [x] **Project Persistence**: Full project save via auto-persist (`persistence.spec.ts`) + +### E2E Test Files Summary + +| File | Tests | Status | +|------|-------|--------| +| `smoke.spec.ts` | 4 | ✅ All passing | +| `simulation-run.spec.ts` | 10 | ✅ Core passing | +| `viewer-tabs.spec.ts` | 7 | ✅ Implemented | +| `file-management.spec.ts` | 8 | ✅ Implemented | +| `experiments.spec.ts` | 5 | ✅ All passing | +| `ui-features.spec.ts` | 9 | ✅ Implemented | +| `persistence.spec.ts` | 9 | ✅ Implemented | +| `dependencies.spec.ts` | 7 | ✅ Implemented | +| `example-projects.spec.ts` | 13 | ✅ Implemented | + +**Total: 72 tests covering all identified features** (59 in the eight original spec files + 13 in `example-projects.spec.ts`) + +### 🟢 Low: Minor Updates Needed + +| Package | Current | Latest | +|---------|---------|--------| +| `classnames` | 2.3.1 | 2.5+ | +| `uuid` | 8.3.1 | 9.0+ | +| `jszip` | 3.7.0 | 3.10+ | +| `lodash-es` | 4.17.21 | 4.17.21 ✓ | +| `immer` | (via RTK) | Included in RTK 2.x | + +--- + +## Repository Extraction + +**Goal**: Extract `apps/sim-core/` into its own standalone repository at the end of this project. + +**Preparation checklist**: +- [x] All plan docs live inside `apps/sim-core/` (TODO.md, ARCHITECTURE.md, TESTING_STRATEGY.md) +- [x] No build/test commands depend on files outside `apps/sim-core/` +- [x] `.gitignore` entries are in `apps/sim-core/.gitignore` (not the root) +- [x] Console log Vite plugin writes to `apps/sim-core/console.log` (not repo root) +- [x] All E2E test fixtures and configs are self-contained within `apps/sim-core/` +- [x] README.md in `apps/sim-core/` is complete for standalone use + +> **Note**: sim-engine (Rust) and hash-agents (Python) are outside the scope of this project. + + +## Recommended Prioritization + +### Phase 1: Cleanup & Simplification +1. [x] **Remove Analytics & Widgets** + - [x] Remove Sentry (`@sentry/*` packages) + - [x] Remove FullStory (`@fullstory/browser`) + - [x] Remove Discord widget + - [x] Remove why-did-you-render +2. [x] Run security audits (npm audit: 113 vulns, all in transitive deps - need major upgrades of vega, cypress) +3. [x] Replace abandoned `hookrouter` (custom usePathRouter + navigate utilities) +4. [x] Replace deprecated `request` package (replaced with native fetch) +5. [x] Update Rust nightly toolchain (nightly-2024-12-01, needs build verification) + +### Phase 1 Review Findings (Feb 2026) + +During Phase 1 verification, we discovered and fixed: +- **Orphaned files**: `Modal/Release/` had leftover `.scss`, `.spec.tsx`, `util.ts`, and `VersionPicker/` after component deletion. All cleaned up. +- **`bowser` dependency**: Was declared in `core/package.json` but only used by `engine-web`. Moved to `engine-web/package.json` where it belongs. This was a pre-existing build error. +- **TypeScript errors from hookrouter migration**: `navigate()` type signature didn't accept boolean query params (hookrouter did). `RouteHandler` type was too strict. `useScopes` called with 1 arg after `Scope.login` removal (requires 2; switched to `useScope`). All fixed. +- **Build verification**: Vite production build passes (exit 0, warnings only). Jest 124/124 suites pass, 369 tests pass. +- **Pre-existing warnings**: wasm critical dependency warning in engine-web, asset size limit warnings — resolved with Vite migration. + +### Phase 2: Remove Auth & Cloud Features — ✅ COMPLETE +All items completed in Migration Phases above (Phases 2–5). +- [x] Remove user authentication, signin/signup, cloud credits +- [x] Remove hCloud runners, server-side save, sharing, access codes +- [x] Remove ModalRelease, fork, server metadata sync +- [x] Implement localStorage persistence, local templates, zip import/export + +### Phase 3: Build System Modernization +1. [x] Upgrade TypeScript 4.1 → 5.3.3 + - fork-ts-checker-webpack-plugin upgraded to 6.5.3 + - Jest switched from ts-jest to babel-jest (ts-jest 26 incompatible with TS 5) + - ~80 RTK 1.5 dispatch type errors suppressed (resolve when Redux removed) + - `useUnknownInCatchVariables: false` set in tsconfig (re-enable after Redux removal) +2. [x] Upgrade Node.js 20 → 24 LTS +3. [x] Migrate Webpack 4 → Vite (see Build Tooling section for detailed plan) + - [x] Install Vite + plugins, create vite.config.ts + - [x] Create HTML entry files at project root + - [x] Replace raw-loader imports with ?raw suffix + - [x] Refactor worker loading for Vite + - [x] Verify WASM loading in main thread and workers + - [x] Remove webpack magic comments + - [x] Simplify build stamp system + - [x] Update scripts, verify build + tests + - [x] Remove webpack infrastructure and dependencies +4. [x] Remove `--openssl-legacy-provider` workaround (resolved by Vite migration) +5. [x] Update Jest 26 → 29 + - Upgraded jest, @types/jest, babel-jest to 29.7.0 + - Added jest-environment-jsdom, removed ts-jest + - Fixed ESM compatibility (nanoid, TextEncoder polyfill) + - Cleaned up 14 test files (removed react-redux, added context mocks) + - 118/119 suites pass, 310 tests pass + +### Phase 4: React & State Management +1. [x] React 16 → 17 → 18.2 migration (no legacy lifecycle blockers; createRoot migrated) +2. [x] **Remove Redux entirely** — ✅ COMPLETE + - [x] All 7 app context facades migrated to pure React (useState/useReducer) + - [x] Files slice: pure Immer reducer, pure entity adapter + - [x] Simulator store: replaced with SimpleStore (reduxCompat.ts) + - [x] All source imports migrated from RTK to reduxCompat.ts + - [x] `@reduxjs/toolkit` and `react-redux` removed from package.json + - [x] `useUnknownInCatchVariables` re-enabled + - [x] Clean up .spec.tsx test files (removed react-redux from 14 files) +3. [x] ~~Remove Recoil~~ Already removed (replaced with SceneContext.tsx) +4. [x] ~~@material-ui → @mui migration~~ Removed (only used by deleted staging tool) +5. [x] ~~react-three-fiber → @react-three/fiber~~ Migrated to v8.18.0 +6. [x] ~~drei → @react-three/drei~~ Migrated to v9.122.0 + +### Phase 4b: Post-Migration Cleanup (Codebase Assessment Feb 2026) + +Found during a comprehensive codebase audit after completing Phases 1–4. + +#### 🔴 High: Dead Code Removal + +- [x] **Delete dead GraphQL query files** (5 files): + - `src/util/api/queries/trackTourProgress.ts` — tour progress stored locally + - `src/util/api/queries/myProjects.ts` — server call never invoked (move `prepareUserProjects` to bootstrapQuery) + - `src/util/api/queries/exampleSimulations.ts` — server call never invoked (move `prepareExamples` to bootstrapQuery) + - `src/util/api/queries/createDatasetQuery.ts` — imported but never called + - `src/util/api/queries/addDatasetToProject.ts` — not imported anywhere + - Remove dead exports from `util/api/index.ts` and `util/api/queries/index.ts` +- [x] **Remove stale hCloud UI references** (5 files): + - `components/Modal/Experiments/ExperimentModal.tsx` — "Save and run in hCloud" button text, "in hCloud" vs "locally" text + - `components/HashCore/Header/Menu/CloudStatus/HashCoreHeaderMenuCloudStatus.tsx` — "Contact us to use hCloud" text + - `components/FileBanner/PythonSafari/FileBannerPythonSafari.tsx` — hCloud text reference + - `features/simulator/simulate/provider.ts` — stale comment +- [x] **Remove stale login/signin component** (FileBannerSignIn deleted): + - `components/FileBanner/SignIn/FileBannerSignIn.tsx` — component for deleted feature + - `components/FileBanner/Wrapper/FileBannerWrapper.tsx` — imports and uses FileBannerSignIn + - `features/user/utils.ts` — `navigate("/signin")` call + - `features/scopes.ts` — `Scope.login`, `Scope.forkIfSignedIn`, `Scope.saveIfSignedIn` are dead + - `components/HashRouter/Effect/routes.tsx` — `/signup`, `/signin` route stubs +- [x] **Remove dead release toast components**: + - `components/Toast/ReleaseSuccess/ToastReleaseSuccess.tsx` + - `components/Toast/ReadOnlyRelease/ToastReadOnlyRelease.tsx` + - `components/Toast/ReleaseBehaviorSuccess/ToastReleaseBehaviorSuccess.tsx` +- [x] **Remove Discord remnants**: + - `components/Icon/Discord/` — icon component directory + - `styles.css` — CSS variables `--discord-button-y-offset`, `--discord-button-size` + - `components/ActivityHistory/Inspector/Inspector.css` — uses Discord CSS variables +- [x] **Remove dead utility files**: + - `util/postFormData.ts` — file upload utility (server uploads removed) + - `util/prepareFormDataWithFile.ts` — file upload utility (server uploads removed) + - Remove imports from `features/files/slice.ts` + +#### 🔴 High: Module Modernization + +- [x] **Replace `lodash` imports with `lodash-es`** (27 files): + - `features/simulator/simulate/util.ts`, `features/monaco/monaco.ts`, `features/files/selectors.ts`, + `features/files/FilesContext.tsx`, `components/Analysis/AnalysisViewer.tsx`, and 15+ more + - Currently importing CommonJS `lodash` instead of ESM `lodash-es` — prevents tree-shaking + - Global find-replace: `from "lodash` → `from "lodash-es` (and `from "lodash/` → `from "lodash-es/`) +- [x] ~~**Replace `process.env.NODE_ENV`**~~ — kept as-is; Vite natively replaces `process.env.NODE_ENV` +- [x] **Rename `WEBPACK_BUILD_STAMP` to `BUILD_STAMP`** — all webpack naming removed from source + +#### 🟡 Medium: Commented/Stubbed Code Cleanup + +- [x] **Clean up stubbed GraphQL queries** — removed commented-out server calls from 3 files +- [x] **Clean up stale SCSS comments** — ModalShare reference removed +- [x] **Remove `trackEvent` stubs** — deleted `features/analytics.ts` and all 20+ call sites +- [x] **Remove `require()` in ProjectContext.tsx** — replaced with static import + +#### 🟡 Medium: Stale Dependencies + +- [x] **Remove unused packages from workspace root** — `yarn-audit-fix`, `yarn-run-all` removed +- [x] **Move webpack-only deps to engine-web** — webpack, webpack-cli, babel-loader moved +- [x] **Upgrade Babel packages** — all @babel/* to 7.28+, removed obsolete proposal plugins, removed core-js +- [x] **Upgrade linting/formatting tools**: + - ESLint 7.29 → 9.39, flat config format, typescript-eslint 8.56 + - Prettier 2.2 → 3.8 + - Removed eslint-plugin-react, eslint-plugin-import (simplified) + - Fixed 87 lint errors (unused imports + short identifiers) + - Added @types/lodash-es +- [x] **Remove GraphQL codegen infrastructure**: + - Deleted `auto-types.ts` (21k lines, 127KB) and `graphql-schema.json` (679KB) + - Created `apiTypes.ts` (~160 lines) with manually-defined types + - Removed `codegen.yml` and `yarn codegen` from all build scripts + - Removed `graphql`, `@graphql-codegen/*` packages (5 packages) + - Deleted dead `partialProjectByPath.ts` query + - Note: 6 queries still send GraphQL strings via fetch; the server API hasn't changed +- [x] **Upgrade @testing-library/react** 11.2 → 14.2 (+ @testing-library/dom 9) + +#### 🟢 Low: Type Safety Improvements + +- [x] **Audit and fix `@ts-ignore` / `@ts-expect-error` comments**: + - Fixed 17 of 30 instances (removed 4, replaced 8 with casts, converted 5 to @ts-expect-error) + - 10 remaining are in dead `whyDidYouRender` blocks (commented-out code) + - 3 remaining are genuine library type gaps (deck.gl, luma.gl, gradient-path — no types) +- [x] **Fix TypeScript errors** — reduced from 45 to 40 (all remaining are library type issues) +- [x] **Migrate test files from `ReactDOM.render` to `@testing-library/react`** — 107 spec files migrated + +### Phase 5: Future - GitHub Integration (Post-Migration) +- [ ] Design GitHub OAuth flow (no HASH account) +- [ ] Implement save/load from user's GitHub repos +- [ ] Add project sync functionality + + +### CI/CD (assessed Feb 2026) + +**Current state**: Only Rust CI exists (`.github/workflows/rust.yml`). There is NO frontend CI — TypeScript errors, test failures, and build breakages are not caught automatically. + +#### 🔴 High: Add Frontend CI + +- [x] **Create `frontend.yml` workflow**: ESLint + TypeScript + Jest + Vite build +- [x] **Create `e2e.yml` workflow**: Playwright E2E tests with artifact upload + +#### 🟡 Medium: Deployment Pipeline + +- [ ] **Create `deploy.yml` workflow**: Deploy to static hosting + - Options: GitHub Pages, Netlify, Vercel, Cloudflare Pages + - Trigger: push to `main` (or manual dispatch) + - Remove legacy `scripts/deploy.ts` (AWS S3 deployment) + +#### 🟡 Medium: Clean Up Existing CI + +- [x] **Update action versions** in `rust.yml` — checkout v4, setup-python v5, rust-cache v2, codeql-action v3 +- [x] **Clean up Renovate config** — removed stale groups, added relevant package groups + +#### 🟢 Low: Additional CI + +- [ ] **Add WASM build verification** to frontend CI (deferred: .github/ out of scope) + - Ensures `wasm-pack build` still succeeds after Rust changes +- [ ] **Add ESLint/Prettier check** to frontend CI (deferred: .github/ out of scope) +- [ ] **Add dependency audit** (`yarn audit`) as non-blocking check + +### Ongoing Maintenance +- [x] ~~Establish automated dependency update process~~ Renovate is configured (needs cleanup) +- [ ] Add CI checks for outdated dependencies +- [ ] Document upgrade procedures for major dependencies + +--- + +## Potential Removals (Updated Feb 2026) + +### Package Status + +| Package | Status | Notes | +|---------|--------|-------| +| `recoil` | ✅ Removed | Replaced with SceneContext | +| `@fullstory/browser` | ✅ Removed | Analytics removed | +| `@sentry/*` | ✅ Removed | Analytics removed | +| `@reduxjs/toolkit` | ✅ Removed | Replaced with reduxCompat.ts | +| `react-redux` | ✅ Removed | Replaced with useSyncExternalStore | +| `react-shepherd` | **KEEP** | Onboarding tour is staying | +| `@msrvida/sanddance-explorer` | **KEEP** | Data visualization staying | +| `gradient-path` | **KEEP** | Used in SVG visualization | +| `fp-ts` | **KEEP** | Used in PlotViewer, palette, GeospatialMap | +| `rxjs` | **KEEP** | Used in search, simulator, analysis middleware | +| `lodash-es` | **KEEP** | But fix 20+ files importing `lodash` instead | +| `yarn-audit-fix` | **REMOVE** | Not used in any script | +| `yarn-run-all` | **REMOVE** | Not used (`npm-run-all` used instead) | + +### Consolidation Opportunities + +| Current | Consolidate To | +|---------|----------------| +| `lodash` + `lodash-es` imports | `lodash-es` only (20+ files to fix) | +| `WEBPACK_BUILD_STAMP` variable | Rename to `BUILD_STAMP` | +| `process.env.NODE_ENV` | `import.meta.env.PROD` (Vite) | +| `yarn.lock` (sim-core) + `package-lock.json` (subdirs) | Yarn everywhere | + +- [x] **Standardize package manager to Yarn** within sim-core + - [x] Remove `package-lock.json` from `packages/core/tests/` + - [x] All JS/TS deps now use `yarn.lock` only + +--- + +## Notes for AI Agents + +When working on this codebase: + +### Architecture Principles + +1. **LOCAL-FIRST**: This app is becoming a free, fully-featured, local-only simulation IDE +2. **NO USER ACCOUNTS**: Don't add authentication, login flows, or user systems +3. **NO CLOUD FEATURES**: Don't add cloud storage, sharing, or server-side features +4. **NO ANALYTICS**: Don't add Sentry, FullStory, or tracking code +5. **GitHub sync is FUTURE**: Don't implement GitHub integration yet (post-migration) + +### Known Build Issues + +**Rollup tree-shaking crash on Windows**: Rollup 4's tree-shaking algorithm crashes silently during +production builds on Windows. The process is killed by the OS (no JavaScript error, no exit handler) +during the recursive module-graph walk with 7100+ modules. The crash persists across Node 22/24, +64MB native stacks, WASM Rollup backend, and JIT-disabled modes. Root cause is likely in Rollup's +native NAPI module corrupting the call stack during tree-shaking of deeply circular dependency chains +(`@msrvida/sanddance-explorer`, `office-ui-fabric-react`, `@fluentui/react`). + +**Workaround**: `treeshake: false` in `vite.config.ts` rollupOptions. esbuild minification still +performs expression-level dead-code elimination. Manual chunks split vendor dependencies for caching. +Build completes in ~53s. Re-enabling tree-shaking is blocked on either a Rollup fix or replacing +the circular-dependency-heavy packages (`@fluentui/react` v7 → v9, `@msrvida/sanddance-explorer`). + +### Monaco Model Creation (E2E Blocker) + +After Redux removal, the Monaco editor's `subscribe()` was never wired to the appBridge. The monaco creates text models in response to store changes, but no code calls `subscribe(appBridge)`. As a result, `getTextModelRequired` throws "text model does not exist" when opening files, causing the app to hit the error boundary. This blocks E2E tests. + +**Fix required**: Wire `subscribe` from `features/monaco` to `appBridge` in StoreSync, and add `appBridge.dispatch` that forwards `updateFile` actions to `filesDispatch`. Care must be taken to avoid infinite loops (model.setValue → onDidChangeContent → dispatch → setState → monaco listener → model.setValue). + +### Code Guidance + +1. **Redux is gone** — `@reduxjs/toolkit` and `react-redux` have been removed. Use React Context + hooks. +2. **Use `reduxCompat.ts` only for the simulator store** — all other state uses React Context directly. +3. **Test thoroughly after any dependency update** — many packages are interconnected. +4. **Check for breaking changes** before upgrading any major version. +5. **Import from `lodash-es`** not `lodash` — prevents tree-shaking issues. +6. **Use `import.meta.env`** not `process.env` — we use Vite, not Webpack. +7. **The Python POC may be stale** — verify if hash-agents is actively used before investing in updates. + +### Features Already REMOVED (do not re-introduce) + +- User authentication (ModalSignin, ModalSignup) — DELETED +- Cloud credits / CloudUsage — DELETED +- Project sharing (ModalShare*) — DELETED +- Access codes — DELETED +- Server project storage — DELETED +- Project releases/versioning (ModalRelease*) — DELETED +- hCloud experiment runners — DELETED +- Sentry / FullStory analytics — DELETED +- Discord widget — DELETED +- Redux / @reduxjs/toolkit / react-redux — DELETED + +### State Management (Current) + +```typescript +// App state — use context hooks +const { allFiles, updateFile } = useFiles(); +const { currentProject } = useProject(); +const { currentTab } = useViewer(); + +// Simulation state — use simulator hooks +const running = useSimulatorSelector(selectRunning); +const dispatch = useSimulatorDispatch(); + +// Local state +const [value, setValue] = useState(initial); + +// Persistence +localStorage.setItem('project:' + projectId, JSON.stringify(projectData)); +``` + +--- + +## Review Panel: TODO Plan Additions (Feb 2026) + +Expert panel review of the TODO plan — suggested additions: + +### QA Engineer +- [x] **Investigate "object is not extensible" console error** — occurs when adding simulation run to store; may indicate frozen object mutation. Root cause in simulator/history store. +- [x] **Fix flaky E2E tests** — `experiments.spec.ts` (open menu, create experiment) and `wasm-worker-smoke.spec.ts` timeout at 2m. Add retries or fix selectors. +- [x] **Add Step Explorer E2E coverage** — agent state inspection not yet tested. +- [x] **Add E2E regression test for ActivityHistory** — ensure useSyncExternalStore fix doesn't regress. + +### Frontend Architect +- [x] **Audit other useSyncExternalStore usages** — useSimulatorSelector fix (shallow array equality) may be needed elsewhere. Check for similar getSnapshot patterns. +- [x] **Consider extracting shallowEqualArrays** — if used in multiple selectors, move to shared util. +- [x] **Document Monaco workaround** — HashCoreEditorFile uses getTextModel + null return as interim fix; full fix requires wiring subscribe to appBridge. + +### WASM Expert +- [x] **Audit engine-web catch blocks** — ensure all `err.message` access uses `err instanceof Error ? err.message : String(err)`. actions.ts, wasm-runner.ts, JsCustomBehavior.ts done. +- [x] **Investigate "object is not extensible"** — may relate to Rust→JS serialization producing frozen objects. Check simulation data flow. + +### UX Expert +- [x] **Empty editor UX** — when Monaco model missing, HashCoreEditorFile returns null; user sees blank. Consider loading skeleton or "Opening file…" placeholder. +- [x] **Verify DefaultProject fallback** (now auto-imports default example zip) — ensure @hash/wildfires-regrowth loads correctly on first visit with empty localStorage. + +### Performance Engineer +- [x] **Track treeshake: false impact** — bundle ~53s build; re-enable when Rollup fixes or @fluentui/sanddance replaced. Add bundle size baseline. +- [x] **Verify manualChunks sizes** — vendor-monaco, vendor-plotly, etc. Ensure no single chunk exceeds reasonable limits. + +### DevOps/CI Specialist +- [ ] **Add smoke-only CI gate** — run `yarn test:e2e:smoke` (4 tests, ~11s) as required check; full E2E can be optional/scheduled. +- [x] **Document flaky E2E tests** — experiments.spec.ts, wasm-worker-smoke.spec.ts. Add to CI retry or skip until fixed. +- [ ] **Add WASM build to CI** — verify wasm-pack build succeeds on Rust changes. + +### Documentation Advocate +- [x] **Document useSimulatorSelector pattern** — shallow array equality for useSyncExternalStore. Add to Code Guidance or ARCHITECTURE.md. +- [x] **Update Monaco E2E blocker** — note interim workaround (getTextModel + null) in Known Build Issues. + +### Security Reviewer +- [x] **Review "object is not extensible"** — frozen object mutation could indicate unsafe Object.freeze usage or prototype pollution. Low priority if internal only. + +--- + +## Review Panel: PR Work Recommendations (Feb 2026) + +Expert panel review of commits 39ab6a5–2178f59 (WIP fixes, ActivityHistory, reviewer rules): + +### Summary +| Persona | Critical | Nice-to-have | +|---------|----------|--------------| +| QA Engineer | Fix flaky E2E; investigate object extensible | Step Explorer E2E | +| Frontend Architect | Audit useSyncExternalStore; document Monaco workaround | Extract shallowEqual | +| WASM Expert | Audit remaining catch blocks | — | +| UX Expert | Consider editor placeholder when model missing | — | +| DevOps/CI | Smoke gate in CI; document flaky tests | WASM build in CI | +| Documentation | Document useSimulatorSelector pattern | Update Monaco blocker text | + +### Captured in TODO +All recommendations above have been added to the "Review Panel: TODO Plan Additions" section. + +### Deferred +- **HashCoreEditorFile placeholder** — UX improvement; not blocking. Can add when Monaco subscribe is fixed. +- **Shallow-equal for objects** — useSimulatorSelector only needs arrays for historySelectors.selectAll. Objects use reference equality. + +--- + +## Console Error Policy + +All console errors and warnings during normal operation and E2E tests should be fixed, not suppressed. + +**Fixed (Feb 2026)**: +- [x] R3F deprecated props (`colorManagement`, `invalidateFrameloop`) replaced with v8 equivalents (`flat`, `frameloop`) +- [x] Cloud API fetch errors (`projectHistory`, `searchResourceProjects`) replaced with local no-op stubs +- [x] "object is not extensible" entity adapter mutations fixed (spread instead of Object.assign) +- [x] engine-web catch blocks audited and fixed for `err instanceof Error` pattern + +**Monitoring**: The `consoleToDisk` Vite plugin writes all browser console output to `apps/sim-core/console.log`. Check this file after E2E runs for new errors. + +--- + +## Follow-up Required TODOs + +Items that need user input or decisions before proceeding: + +- [ ] **Deploy.yml workflow** — needs hosting platform decision (GitHub Pages, Netlify, Vercel, Cloudflare Pages?) +- [ ] **GitHub Integration (Phase 5)** — explicitly future/post-migration, needs OAuth flow design decisions +- [ ] **WASM build in CI** — needs decision on whether to run `wasm-pack build` in GitHub Actions (adds Rust toolchain requirement) +- [ ] **Build/test commands outside sim-core** — `yarn serve:core` currently triggers `yarn build:engine-web` which lives in the monorepo root scripts. Needs refactoring for standalone repo extraction. +- [ ] **ESLint/Prettier CI check** — .github/ is out of scope for this project; implement after repo extraction +- [ ] **Dependency audit CI** — `yarn audit` as non-blocking check; implement after repo extraction +- [ ] **CI checks for outdated dependencies** — implement after repo extraction +- [ ] **Document upgrade procedures** — needs decisions on format and what to cover +- [ ] **Add smoke-only CI gate** — .github/ out of scope; add after repo extraction +- [x] **Monaco Editor 0.25 -> 0.52 upgrade** — Completed. Bracket pair colorization, sticky scroll, inlay hints, and improved worker configuration all enabled. See "Monaco Editor Assessment" section. +- [ ] **AI code assistance** — Copilot-style inline completions via user-provided API keys. Monaco 0.52 has full inline completions API support. See "AI Code Assistance" section. Larger effort (3-5 days). +- [ ] **Fix ReactDOM.render warning** — third-party library (likely sanddance-explorer or react-shepherd) calling deprecated API. Requires library upgrade or replacement. + +--- + +## References + +- [React 18 Upgrade Guide](https://react.dev/blog/2022/03/08/react-18-upgrade-guide) +- [React Context Documentation](https://react.dev/reference/react/useContext) +- [Vite Migration from Webpack](https://vitejs.dev/guide/migration-from-v4) diff --git a/apps/sim-core/package.json b/apps/sim-core/package.json index 42623df9..c36ad101 100644 --- a/apps/sim-core/package.json +++ b/apps/sim-core/package.json @@ -7,13 +7,12 @@ "// 01": "Global scripts: these belong to the workspace itself", "// 02": "", "preinstall": "node scripts/preinstall.js", - "postinstall": "yarn build:utils && yarn build:engine-web", - "prepare": "cd ../.. && husky install .config/husky", + "postinstall": "yarn build:engine-web && yarn build:utils", "all": "npx npm-run-all", - "fmt:scripts": "prettier --write --cache \"scripts/**/*.{ts,tsx,js,json}\" ", - "lint:scripts": "prettier --check --cache \"scripts/**/*.{ts,tsx,js,json}\" && tsc --noEmit", + "fmt:scripts": "prettier \"scripts/**/*.{ts,tsx,js,json}\" --write", + "fmt-check:scripts": "prettier \"scripts/**/*.{ts,tsx,js,json}\" --check", "fmt": "yarn all fmt:*", - "lint": "yarn all lint:*", + "fmt-check": "yarn all fmt-check:*", "test": "yarn all test:*", "clippy": "touch packages/engine/src/lib.rs && cargo clippy --all", "// 03": "", @@ -24,7 +23,7 @@ "clean:engine-web": "yarn ws:engine-web clean", "fmt:rustfmt": "cargo fmt -v --all", "fmt:engine-web": "yarn ws:engine-web fmt", - "lint:engine-web": "yarn ws:engine-web lint", + "fmt-check:engine-web": "yarn ws:engine-web fmt-check", "test:engine-web": "yarn ws:engine-web test", "test:rust-engine": "cargo test --verbose", "// 06": "", @@ -38,11 +37,11 @@ "predeploy:core": "yarn build:engine-web", "deploy:core": "yarn ws:core deploy", "fmt:core": "yarn ws:core fmt", - "lint:core": "yarn ws:core lint", + "fmt-check:core": "yarn ws:core fmt-check", "preserve:core": "yarn build:engine-web", "serve:core": "yarn ws:core serve", + "preview:core": "yarn ws:core start", "start:core": "yarn ws:core start", - "preview:core": "yarn ws:core preview", "test:core": "yarn ws:core test", "g": "yarn ws:core g", "// 09": "", @@ -57,7 +56,7 @@ "ws:utils": "yarn workspace @hashintel/utils", "build:utils": "yarn ws:utils build", "fmt:utils": "yarn ws:utils fmt", - "lint:utils": "yarn ws:utils lint" + "fmt-check:utils": "yarn ws:utils fmt-check" }, "workspaces": [ "packages/core", @@ -66,26 +65,22 @@ "packages/utils" ], "devDependencies": { - "@babel/core": "7.12.3", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-string-parser": "^7.23.4", - "@babel/preset-env": "7.12.11", - "@babel/preset-react": "7.14.5", - "@babel/preset-typescript": "7.12.1", - "@jridgewell/gen-mapping": "^0.3.3", + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.0", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@types/classnames": "2.2.11", "@types/dom-mediacapture-record": "1.0.7", "@types/file-saver": "2.0.2", - "@types/hookrouter": "2.2.3", - "@types/jest": "26.0.20", + "@types/jest": "^29.5.0", "@types/json-schema": "7.0.7", "@types/jszip": "3.4.1", "@types/lodash": "4.14.165", "@types/mapbox-gl": "1.13.0", "@types/node": "14.14.7", "@types/prettier": "2.2.2", - "@types/react": "16.9.56", - "@types/react-dom": "16.9.9", + "@types/react": "18.2.48", + "@types/react-dom": "18.2.18", "@types/react-plotly.js": "2.2.4", "@types/react-redux": "7.1.16", "@types/react-select": "3.0.26", @@ -94,48 +89,41 @@ "@types/react-timeago": "4.1.1", "@types/react-transition-group": "4.4.0", "@types/react-window": "1.8.2", - "@types/request-promise-native": "1.0.17", "@types/shelljs": "0.8.8", "@types/stats": "0.16.30", "@types/url-join": "4.0.0", "@types/uuid": "8.3.0", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@typescript-eslint/parser": "^6.10.0", - "babel-jest": "26.6.3", - "babel-loader": "8.2.1", - "babel-plugin-dynamic-import-node": "^2.3.3", - "caniuse-lite": "^1.0.30001563", - "cross-env": "7.0.3", - "eslint": "^8.53.0", - "eslint-config-prettier": "9.0.0", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "fork-ts-checker-webpack-plugin": "6.0.3", - "husky": "^8.0.0", + "@typescript-eslint/eslint-plugin": "4.8.1", + "@typescript-eslint/parser": "4.15.2", + "babel-jest": "^29.7.0", + "cross-env": "7.0.2", + "dotenv": "8.2.0", + "eslint": "9", + "eslint-config-prettier": "10", + "eslint-plugin-react-hooks": "^5", + "globals": "16", "identity-obj-proxy": "3.0.0", - "jest": "26.6.3", + "jest": "^29.7.0", "jest-canvas-mock": "2.3.0", + "jest-environment-jsdom": "^29.7.0", "npm-run-all": "4.1.5", - "prettier": "3.1.0", + "prettier": "^3", "prop-types": "15.7.2", "random-emoji": "1.0.2", - "request": "2.88.2", - "request-promise-native": "1.0.9", "rimraf": "3.0.2", "shelljs": "0.8.5", "time-stamp": "2.2.0", "tmp": "0.2.1", - "ts-jest": "26.4.4", "ts-node": "9.1.1", - "typescript": "^4.3.5", - "webpack": "4.44.2", - "webpack-cli": "3.3.12", - "yarn-audit-fix": "^10.0.1", - "yarn-run-all": "3.1.1" + "typescript": "5.3.3", + "typescript-eslint": "8" + }, + "engines": { + "node": ">=24.0.0" }, "resolutions": { - "@types/react": "16.9.56" + "@types/react": "18.2.48", + "@babel/parser": "^7.29.0" }, "dependencies": {} } diff --git a/apps/sim-core/packages/core/.gitignore b/apps/sim-core/packages/core/.gitignore index e86ef849..5232c913 100644 --- a/apps/sim-core/packages/core/.gitignore +++ b/apps/sim-core/packages/core/.gitignore @@ -1,2 +1,11 @@ coverage -dist \ No newline at end of file + +# auto-generated types for API queries +src/util/api/auto-types.ts + +# Playwright E2E test artifacts +tests/e2e/test-results/ +tests/e2e/playwright-report/ +tests/e2e/.playwright/ +playwright-report/ +test-results/ \ No newline at end of file diff --git a/apps/sim-core/packages/core/.prettierignore b/apps/sim-core/packages/core/.prettierignore new file mode 100644 index 00000000..1eae0cf6 --- /dev/null +++ b/apps/sim-core/packages/core/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/apps/sim-core/packages/core/babel.jest.config.js b/apps/sim-core/packages/core/babel.jest.config.js new file mode 100644 index 00000000..94e46bfa --- /dev/null +++ b/apps/sim-core/packages/core/babel.jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + "@babel/preset-react", + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], +}; diff --git a/apps/sim-core/packages/core/embed.html b/apps/sim-core/packages/core/embed.html new file mode 100644 index 00000000..403eaa4e --- /dev/null +++ b/apps/sim-core/packages/core/embed.html @@ -0,0 +1,34 @@ + + + + HASH Core + + + + + + + + +
+ + + + diff --git a/apps/sim-core/packages/core/eslint.config.js b/apps/sim-core/packages/core/eslint.config.js new file mode 100644 index 00000000..32a3911a --- /dev/null +++ b/apps/sim-core/packages/core/eslint.config.js @@ -0,0 +1,47 @@ +const tseslint = require("typescript-eslint"); +const reactHooks = require("eslint-plugin-react-hooks"); +const eslintConfigPrettier = require("eslint-config-prettier"); + +module.exports = tseslint.config( + { + ignores: ["dist/**", "node_modules/**", "**/*.d.ts"], + }, + { + files: ["**/*.{ts,tsx,js,jsx}"], + extends: [tseslint.configs.base, eslintConfigPrettier], + plugins: { + "react-hooks": reactHooks, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + jsx: true, + }, + }, + rules: { + "id-length": [ + "error", + { + min: 2, + exceptions: ["_", "x", "y", "z", "a", "b"], + properties: "never", + }, + ], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": [ + "warn", + { + additionalHooks: "(^useModal$)|(^useUserGatedEffect$)", + }, + ], + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_+", varsIgnorePattern: "^_+" }, + ], + "no-unused-expressions": "error", + "prefer-const": "error", + eqeqeq: ["error", "always", { null: "ignore" }], + }, + }, +); diff --git a/apps/sim-core/packages/core/index.html b/apps/sim-core/packages/core/index.html new file mode 100644 index 00000000..5ed5f5eb --- /dev/null +++ b/apps/sim-core/packages/core/index.html @@ -0,0 +1,51 @@ + + + + HASH Core + + + + + + + + + + + + + + + + +
+ + + + diff --git a/apps/sim-core/packages/core/package.json b/apps/sim-core/packages/core/package.json index 1d574307..a2a574a6 100644 --- a/apps/sim-core/packages/core/package.json +++ b/apps/sim-core/packages/core/package.json @@ -1,6 +1,5 @@ { "name": "@hashintel/core", - "type": "module", "version": "0.1.0", "description": "HASH Core (hCore) frontend", "repository": "https://github.com/hashintel/labs", @@ -8,24 +7,38 @@ "private": true, "scripts": { "clean": "rimraf dist", - "serve": "yarn dev-env vite serve", - "prod-env": "cross-env-shell NODE_OPTIONS=\"--max_old_space_size=8192 --openssl-legacy-provider\" NODE_ENV=production", - "dev-env": "cross-env-shell NODE_OPTIONS=\"--max_old_space_size=4096 --openssl-legacy-provider\" NODE_ENV=development", - "build": "yarn prod-env vite build", - "start": "yarn prod-env vite build && vite preview", - "preview": "yarn prod-env vite preview", - "fmt": "prettier --write --cache \"*.{ts,tsx,js,jsx,json,css,scss,cjs}\" \"{scripts,src}/**/*.{ts,tsx,js,jsx,json,css,scss,cjs}\" && eslint --quiet --fix \"*.{ts,tsx,js,cjs}\" \"{scripts,src}/**/*.{ts,tsx,js,cjs}\"", - "lint": "prettier --check --cache \"*.{ts,tsx,js,jsx,json,css,scss,cjs}\" \"{scripts,src}/**/*.{ts,tsx,js,jsx,json,css,scss,cjs}\" && eslint --quiet \"*.{ts,tsx,js,cjs}\" \"{scripts,src}/**/*.{ts,tsx,js,cjs}\" && tsc --noEmit", - "deploy": "ts-node --project scripts/tsconfig.json scripts/deploy.ts", + "serve": "vite", + "serve:quiet": "vite --logLevel error", + "prod-env": "cross-env NODE_ENV=production", + "dev-env": "cross-env NODE_ENV=development", + "build": "vite build", + "build-dev": "vite build --mode development", + "start": "vite preview", + "fmt": "prettier \"*.{ts,tsx,js,jsx,json,css,scss}\" \"{scripts,src}/**/*.{ts,tsx,js,jsx,json,css,scss}\" --write; eslint --quiet --fix \"*.{ts,tsx,js}\" \"{scripts,src}/**/*.{ts,tsx,js}\"", + "fmt-check": "prettier \"*.{ts,tsx,js,jsx,json,css,scss}\" \"{scripts,src}/**/*.{ts,tsx,js,jsx,json,css,scss}\" --check || exit 1; eslint --quiet \"*.{ts,tsx,js}\" \"{scripts,src}/**/*.{ts,tsx,js}\"", "g": "ts-node --project scripts/tsconfig.json scripts/cli", - "test": "jest --forceExit --testPathIgnorePatterns '/node_modules/|/tests/e2e/'" + "test": "jest --forceExit", + "test:e2e": "playwright test --config=tests/e2e/playwright.config.ts", + "test:e2e:smoke": "playwright test --config=tests/e2e/playwright.config.ts --grep Smoke", + "test:e2e:ui": "playwright test --config=tests/e2e/playwright.config.ts --ui", + "test:e2e:headed": "playwright test --config=tests/e2e/playwright.config.ts --headed", + "test:e2e:debug": "playwright test --config=tests/e2e/playwright.config.ts --debug", + "test:e2e:build": "cross-env E2E_USE_BUILD=1 playwright test --config=tests/e2e/playwright.config.ts", + "test:all": "jest --forceExit && playwright test --config=tests/e2e/playwright.config.ts" }, "jest": { - "preset": "ts-jest/presets/js-with-babel", - "globals": { - "ts-jest": { - "babelConfig": true - } + "testPathIgnorePatterns": [ + "/node_modules/", + "/tests/e2e/" + ], + "testEnvironment": "jest-environment-jsdom", + "transform": { + "^.+\\.[jt]sx?$": [ + "babel-jest", + { + "configFile": "./babel.jest.config.js" + } + ] }, "moduleNameMapper": { "^.+\\.(css|less|scss|sass)$": "identity-obj-proxy", @@ -36,14 +49,22 @@ "/src/setupTests.ts" ], "transformIgnorePatterns": [ - "/node_modules/(?!monaco-editor).+\\.js$" + "/node_modules/(?!monaco-editor|nanoid|lodash-es).+\\.js$" ] }, + "// browserslist": [ + "this list is an `intersection` of browsers (organized as they are in `https://caniuse.com/`", + "for easier reference) that support the features we need/use, namely (really wish this was a", + "`featureslist` instead):", + "", + "We have compiled a browserlist query based on the below at https://docs.google.com/spreadsheets/d/1pCjtbvxIgnbzIbgfrJuYIk6TLHhLhqIxNAA2Iord1gE/edit#gid=0", + "" + ], "browserslist": [ - "edge >= 88", - "firefox >= 78", - "chrome >= 87", - "safari >= 14" + "edge >= 79", + "firefox >= 67", + "chrome >= 66", + "safari >= 12.1" ], "// dependencies": [ "We compile to a single-page app, so technically everything should be in '/devDependencies'", @@ -53,83 +74,89 @@ "@deck.gl/core": "8.3.7", "@deck.gl/layers": "8.3.7", "@fluentui/react": "7.150.0", - "@fullstory/browser": "1.4.5", "@hashintel/engine-web": "0.1.1", "@hashintel/utils": "0.0.1", - "@juggle/resize-observer": "^3.4.0", + "@juggle/resize-observer": "3.2.0", "@loaders.gl/core": "*", "@luma.gl/core": "8.3.1", - "@material-ui/core": "4.11.4", - "@material-ui/lab": "4.0.0-alpha.56", + "@material-ui/core": "4.12.4", + "@material-ui/lab": "4.0.0-alpha.61", "@msrvida/sanddance-explorer": "3.1.0", - "@react-three/drei": "1.5.9", + "@react-three/drei": "9.122.0", + "@react-three/fiber": "8.18.0", "@reduxjs/toolkit": "1.5.0", "@svgr/core": "5.5.0", - "@types/node": "14.14.7", - "bowser": "2.11.0", + "@svgr/plugin-jsx": "5.5.0", + "@svgr/plugin-svgo": "5.5.0", + "@types/three": "0.170.0", "classnames": "2.3.1", "clipboard-polyfill": "3.0.2", + "css-color-names": "1.0.1", "date-fns": "2.17.0", "empty-module": "0.0.2", "escape-string-regexp": "4.0.0", "file-saver": "2.0.2", "fp-ts": "2.9.5", "gradient-path": "2.1.0", - "hookrouter": "1.2.3", "idb-keyval": "5.0.2", + "immer": "^9.0.0", "js-levenshtein": "1.1.6", "json-stringify-pretty-compact": "2.0.0", "jszip": "3.7.0", "line-column": "1.0.2", "lodash-es": "4.17.21", "mapbox-gl": "1.13.1", - "monaco-editor": "0.25.2", + "monaco-editor": "0.52.2", "monaco-themes": "0.3.3", "monocle-ts": "2.3.5", "neverthrow": "4.2.1", - "plotly.js": "2.27.1", - "react": "16.14.0", - "react-dom": "16.14.0", + "plotly.js-dist-min": "^3.3.1", + "react": "18.2.0", + "react-dom": "18.2.0", "react-dropzone": "11.2.4", "react-hook-form": "6.11.3", "react-intersection-observer": "8.31.0", "react-mapbox-gl": "5.1.1", "react-markdown": "5.0.3", "react-modal-hook": "3.0.0", - "react-plotly.js": "2.5.0", + "react-plotly.js": "^2.6.0", "react-promise-suspense": "0.3.3", - "react-redux": "7.2.4", + "react-redux": "8.1.3", "react-select": "3.1.0", "react-shepherd": "3.3.3", "react-splitter-layout": "4.0.0", "react-svg": "11.1.1", - "react-tabs": "4.3.0", - "react-three-fiber": "5.0.6", + "react-tabs": "3.1.2", "react-timeago": "5.2.0", "react-tiny-popover": "5.1.0", "react-transition-group": "4.4.2", "react-window": "1.8.6", "recoil": "0.4.1", - "redux": "*", + "reselect": "^4.0.0", "rxjs": "6.6.6", "simplebar-react": "3.0.0-beta.6", "slugify": "1.4.6", - "three": "0.119.1", + "three": "0.170.0", "url-join": "4.0.1", "uuid": "8.3.1", "vega": "5.17.3" }, "devDependencies": { - "@testing-library/react": "11.2.2", + "@playwright/test": "^1.41.0", + "@testing-library/dom": "^9", + "@testing-library/react": "^14", + "@testing-library/user-event": "^14.5.1", "@types/js-levenshtein": "^1.1.1", "@types/line-column": "^1.0.0", - "@vitejs/plugin-react": "^4.2.0", + "@types/lodash-es": "^4.17.12", + "@types/plotly.js-dist-min": "^2.3.4", + "@vitejs/plugin-react": "^5.1.4", "autoprefixer": "10.0.2", + "dotenv": "^16.3.1", "postcss": "8.2.13", - "sass": "1.29.0", - "vite": "^5.0.0", - "vite-plugin-monaco-editor": "^1.1.0", - "vite-plugin-top-level-await": "^1.3.1", - "vite-plugin-wasm": "^3.2.2" + "sass": "^1.97.3", + "vite": "^7.3.1", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0" } } diff --git a/apps/sim-core/packages/core/public/example_projects/ant-foraging.zip b/apps/sim-core/packages/core/public/example_projects/ant-foraging.zip new file mode 100644 index 00000000..af562c30 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/ant-foraging.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/boids-3d.zip b/apps/sim-core/packages/core/public/example_projects/boids-3d.zip new file mode 100644 index 00000000..bbe0bc3a Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/boids-3d.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/city-infection-model.zip b/apps/sim-core/packages/core/public/example_projects/city-infection-model.zip new file mode 100644 index 00000000..81ed667b Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/city-infection-model.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/connection-example.zip b/apps/sim-core/packages/core/public/example_projects/connection-example.zip new file mode 100644 index 00000000..4a9d7709 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/connection-example.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/empty-project.zip b/apps/sim-core/packages/core/public/example_projects/empty-project.zip new file mode 100644 index 00000000..36c104f5 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/empty-project.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/empty-template-project.zip b/apps/sim-core/packages/core/public/example_projects/empty-template-project.zip new file mode 100644 index 00000000..3d14c970 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/empty-template-project.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/manifest.json b/apps/sim-core/packages/core/public/example_projects/manifest.json new file mode 100644 index 00000000..6a3d344f --- /dev/null +++ b/apps/sim-core/packages/core/public/example_projects/manifest.json @@ -0,0 +1,101 @@ +[ + { + "slug": "ant-foraging", + "name": "Ant Foraging", + "file": "ant-foraging.zip", + "type": "Simulation", + "description": "A model of ants foraging for food. This is a classic agent-based modeling example that shows intelligent system behavior emerging from simple individual behavior." + }, + { + "slug": "boids-3d", + "name": "Boids 3D", + "file": "boids-3d.zip", + "type": "Simulation", + "description": "Demonstrates the \"Boids\" flocking algorithm in both 2D and 3D space." + }, + { + "slug": "city-infection-model", + "name": "City Infection Model", + "file": "city-infection-model.zip", + "type": "Simulation", + "description": "This model simulates how a virus spreads through a human population." + }, + { + "slug": "connection-example", + "name": "Connection Example", + "file": "connection-example.zip", + "type": "Simulation", + "description": "The simulation demonstrates how you might handle agents linking, or connecting, while avoiding any race conditions." + }, + { + "slug": "empty-project", + "name": "Empty Project", + "file": "empty-project.zip", + "type": "Simulation", + "description": "This is a new simulation — an empty scaffold to build from." + }, + { + "slug": "empty-template-project", + "name": "Empty Template Project", + "file": "empty-template-project.zip", + "type": "Simulation", + "description": "This starter template shows how to use basic features of HASH." + }, + { + "slug": "model-market", + "name": "Model Market", + "file": "model-market.zip", + "type": "Simulation", + "description": "This model contains two types of agents: shops and buyers." + }, + { + "slug": "published-display-behaviors", + "name": "Published Display Behaviors", + "file": "published-display-behaviors.zip", + "type": "Simulation", + "description": "Separate all display elements from agents with published composable behaviors." + }, + { + "slug": "rainfall", + "name": "Rainfall", + "file": "rainfall.zip", + "type": "Simulation", + "description": "Demonstrates the use of library behaviors to simulate rainfall and pooling behavior." + }, + { + "slug": "rumor-mill-public-health-practices", + "name": "Rumor Mill: Public Health Practices", + "file": "rumor-mill-public-health-practices.zip", + "type": "Simulation", + "description": "This simulation models the spread of hygiene practices in a community, and their trust in a federal authority." + }, + { + "slug": "sugarscape", + "name": "Sugarscape", + "file": "sugarscape.zip", + "type": "Simulation", + "description": "This simulation implements Epstein and Axtell's Sugarscape model." + }, + { + "slug": "virus-mutation-and-drug-resistance", + "name": "Virus Mutation and Drug Resistance", + "file": "virus-mutation-and-drug-resistance.zip", + "type": "Simulation", + "description": "This model simulates mutation in viruses and bacteria, with vaccine introduction at a specific timestep." + }, + { + "slug": "warehouse-logistics", + "name": "Warehouse Logistics", + "file": "warehouse-logistics.zip", + "type": "Simulation", + "description": "A basic warehouse model with shelf agents and worker agents handling item storage and retrieval." + }, + { + "slug": "wildfires-regrowth", + "name": "Wildfires - Regrowth", + "file": "wildfires-regrowth.zip", + "type": "Simulation", + "description": "This model simulates the spread of wildfires in a regrowing forest. Trees grow over time and have a small chance of being struck by lightning.", + "default": true + } +] diff --git a/apps/sim-core/packages/core/public/example_projects/model-market.zip b/apps/sim-core/packages/core/public/example_projects/model-market.zip new file mode 100644 index 00000000..3090116d Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/model-market.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/published-display-behaviors.zip b/apps/sim-core/packages/core/public/example_projects/published-display-behaviors.zip new file mode 100644 index 00000000..1baf06e3 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/published-display-behaviors.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/rainfall.zip b/apps/sim-core/packages/core/public/example_projects/rainfall.zip new file mode 100644 index 00000000..ba000fc1 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/rainfall.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/rumor-mill-public-health-practices.zip b/apps/sim-core/packages/core/public/example_projects/rumor-mill-public-health-practices.zip new file mode 100644 index 00000000..b8f7e2fe Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/rumor-mill-public-health-practices.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/sugarscape.zip b/apps/sim-core/packages/core/public/example_projects/sugarscape.zip new file mode 100644 index 00000000..54697f78 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/sugarscape.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/virus-mutation-and-drug-resistance.zip b/apps/sim-core/packages/core/public/example_projects/virus-mutation-and-drug-resistance.zip new file mode 100644 index 00000000..d64ec151 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/virus-mutation-and-drug-resistance.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/warehouse-logistics.zip b/apps/sim-core/packages/core/public/example_projects/warehouse-logistics.zip new file mode 100644 index 00000000..03271cd0 Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/warehouse-logistics.zip differ diff --git a/apps/sim-core/packages/core/public/example_projects/wildfires-regrowth.zip b/apps/sim-core/packages/core/public/example_projects/wildfires-regrowth.zip new file mode 100644 index 00000000..2c2518bd Binary files /dev/null and b/apps/sim-core/packages/core/public/example_projects/wildfires-regrowth.zip differ diff --git a/apps/sim-core/packages/core/scripts/cli/index.ts b/apps/sim-core/packages/core/scripts/cli/index.ts index a48e0ce4..ffecc2b8 100755 --- a/apps/sim-core/packages/core/scripts/cli/index.ts +++ b/apps/sim-core/packages/core/scripts/cli/index.ts @@ -11,7 +11,7 @@ function isString(value: string | undefined): value is string { return value !== undefined; } -export function cli() { +export async function cli() { const { dryRun, verbose, fromIcon, _ } = parseArgs(); // TODO: @mysterycommand - is there a way to make `yargs` do the validation? @@ -29,9 +29,12 @@ export function cli() { // n.b. `fromIcon!` is fine(?) below, because `isValidIcon` checks for string // value, `.svg` extension, and file existence const names = - isValidIcon && _.length === 0 ? [basename(fromIcon, ".svg")] : _; + isValidIcon && _.length === 0 ? [basename(fromIcon!, ".svg")] : _; - names.forEach(generateFiles({ dryRun, verbose, isValidIcon, fromIcon })); + const gen = generateFiles({ dryRun, verbose, isValidIcon, fromIcon }); + for (const name of names) { + await gen(String(name)); + } } -cli(); +void cli(); diff --git a/apps/sim-core/packages/core/scripts/cli/utils/generateFile.ts b/apps/sim-core/packages/core/scripts/cli/utils/generateFile.ts index 0c3f4f62..37e5e100 100644 --- a/apps/sim-core/packages/core/scripts/cli/utils/generateFile.ts +++ b/apps/sim-core/packages/core/scripts/cli/utils/generateFile.ts @@ -3,12 +3,12 @@ import { extname, join, relative } from "path"; import { format } from "prettier"; type NameContentTuple = [string, string]; -type FileGenerator = (pair: NameContentTuple) => void; -interface FileGeneratorContext { +type FileGenerator = (pair: NameContentTuple) => Promise; +type FileGeneratorContext = { dryRun: boolean; verbose: boolean; componentDir: string; -} +}; type FileGeneratorFactory = (ctx: FileGeneratorContext) => FileGenerator; /** diff --git a/apps/sim-core/packages/core/scripts/cli/utils/generateFiles.ts b/apps/sim-core/packages/core/scripts/cli/utils/generateFiles.ts index 872253e5..ae636ac3 100644 --- a/apps/sim-core/packages/core/scripts/cli/utils/generateFiles.ts +++ b/apps/sim-core/packages/core/scripts/cli/utils/generateFiles.ts @@ -10,13 +10,13 @@ import { } from "./templates"; import { generateFile, parseIcon } from "."; -type FilesGenerator = (name: string) => void; -interface FilesGeneratorContext { +type FilesGenerator = (name: string) => Promise; +type FilesGeneratorContext = { dryRun: boolean; verbose: boolean; isValidIcon: boolean; fromIcon?: string; -} +}; type FilesGeneratorFactory = (ctx: FilesGeneratorContext) => FilesGenerator; const pascalCase = (words: string) => upperFirst(camelCase(words)); @@ -34,7 +34,7 @@ const componentsDir = join(__dirname, "../../../src/components"); */ export const generateFiles: FilesGeneratorFactory = ({ dryRun = false, verbose = false, isValidIcon, fromIcon }) => - (name) => { + async (name) => { const folderName = pascalCase(name); const componentName = `${isValidIcon ? "Icon" : ""}${folderName}`; const componentDir = join( @@ -57,7 +57,8 @@ export const generateFiles: FilesGeneratorFactory = const indexFileName = "index.ts"; const indexFileContent = indexTemplate(componentName); - Object.entries({ + const write = generateFile({ dryRun, verbose, componentDir }); + for (const entry of Object.entries({ ...(isValidIcon ? {} : { @@ -66,5 +67,7 @@ export const generateFiles: FilesGeneratorFactory = [testFileName]: testFileContent, [componentFileName]: componentFileContent, [indexFileName]: indexFileContent, - }).forEach(generateFile({ dryRun, verbose, componentDir })); + })) { + await write(entry as [string, string]); + } }; diff --git a/apps/sim-core/packages/core/scripts/cli/utils/parseArgs.ts b/apps/sim-core/packages/core/scripts/cli/utils/parseArgs.ts index 49259e5c..4d3730e9 100644 --- a/apps/sim-core/packages/core/scripts/cli/utils/parseArgs.ts +++ b/apps/sim-core/packages/core/scripts/cli/utils/parseArgs.ts @@ -1,7 +1,8 @@ import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; export const parseArgs = () => - yargs + yargs(hideBin(process.argv)) .options({ dryRun: { alias: ["dry-run", "n"], @@ -21,4 +22,5 @@ export const parseArgs = () => }) .help("help") .alias("help", "h") - .version(false).argv; + .version(false) + .parseSync(); diff --git a/apps/sim-core/packages/core/scripts/cli/utils/templates.ts b/apps/sim-core/packages/core/scripts/cli/utils/templates.ts index 00971bbb..f9d2c247 100644 --- a/apps/sim-core/packages/core/scripts/cli/utils/templates.ts +++ b/apps/sim-core/packages/core/scripts/cli/utils/templates.ts @@ -4,6 +4,7 @@ import { theme } from "../../../src/util/theme"; /** * give the new component one of our brighter theme colors to make it stand out + * more … also because empty classes are a lint error … also whimsy! */ const themeColors = Object.keys(theme) .filter((key) => key.match(/white|grey|dark|black/) === null) diff --git a/apps/sim-core/packages/core/scripts/types.d.ts b/apps/sim-core/packages/core/scripts/types.d.ts index 7ed38e3b..cceaa29d 100644 --- a/apps/sim-core/packages/core/scripts/types.d.ts +++ b/apps/sim-core/packages/core/scripts/types.d.ts @@ -10,14 +10,14 @@ declare module "@svgr/core" { dimensions: boolean; expandProps: "start" | "end" | false; prettier: boolean; - prettierConfig: Record; + prettierConfig: { [key: string]: any }; svgo: boolean; svgoConfig: { - plugins: Record[]; + plugins: { [key: string]: any }[]; }; ref: boolean; - replaceAttrValues: Record; - svgProps: Record; + replaceAttrValues: { [key: string]: string }; + svgProps: { [key: string]: string }; title: boolean; template: ({ template }: any, _: any, { jsx }: any) => string; // only partially documented, but necessary diff --git a/apps/sim-core/packages/core/site.d.ts b/apps/sim-core/packages/core/site.d.ts index 520c932b..fcfcfbf5 100644 --- a/apps/sim-core/packages/core/site.d.ts +++ b/apps/sim-core/packages/core/site.d.ts @@ -1,8 +1,19 @@ /** - * provided by vite - * @see: ./vite.config.ts + * Build-time globals provided by Vite's `define` config. + * @see ./vite.config.ts */ -declare let BUILD_STAMP: string; +declare var PUBLIC_PATH: string; +declare var BUILD_STAMP: string; +declare var LOCAL_API: boolean; +declare var MAPBOX_API_TOKEN: string; + +/** + * Vite raw import suffix — importing with ?raw returns file contents as string. + */ +declare module "*.d.ts?raw" { + const content: string; + export default content; +} /** * Like `Omit` but distributes over unions diff --git a/apps/sim-core/packages/core/src/boot.ts b/apps/sim-core/packages/core/src/boot.ts index 2178781d..54ac00a8 100644 --- a/apps/sim-core/packages/core/src/boot.ts +++ b/apps/sim-core/packages/core/src/boot.ts @@ -1,12 +1,10 @@ -import { enableMapSet } from "immer"; +import { enableMapSet, setAutoFreeze } from "immer"; import * as api from "./util/api"; import { buildSimulationProvider } from "./features/simulator/simulate/buildprovider"; import { configureMonaco } from "./util/monaco-config"; import { resizeObserverPromise } from "./util/resizeObserverPromise"; import { simulatorStore } from "./features/simulator/store"; -import { store } from "./features/store"; -import { syncStores } from "./features/simulator/simulate/sync"; import { theme } from "./util/theme"; const configureTheme = () => { @@ -17,18 +15,16 @@ const configureTheme = () => { }; export const boot = async (forExperiments: boolean) => { - // Expose for console access: Object.assign(window as any, { api, - store, simulatorStore, }); configureTheme(); enableMapSet(); + setAutoFreeze(false); configureMonaco(); buildSimulationProvider(forExperiments); - syncStores(store, simulatorStore); await resizeObserverPromise; }; diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityEmpty.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityEmpty.tsx index 01350bbe..1195ff5c 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityEmpty.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityEmpty.tsx @@ -1,7 +1,7 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import "./ActivityEmpty.scss"; -export const ActivityEmpty: FC = ({ children }) => ( +export const ActivityEmpty: FC = ({ children }) => (
{children}
); diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistory.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistory.tsx index a8c87e1a..64db9078 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistory.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistory.tsx @@ -1,5 +1,4 @@ import React, { FC, useCallback, useRef, useState } from "react"; -import { useSelector } from "react-redux"; import SplitterLayout from "react-splitter-layout"; import classNames from "classnames"; @@ -19,8 +18,8 @@ import { historySelectors, selectHistoryComplete, } from "../../features/simulator/simulate/selectors"; -import { selectProjectRef } from "../../features/project/selectors"; import { theme } from "../../util/theme"; +import { useProject } from "../../features/project/ProjectContext"; import { useInfiniteScrollingHistory } from "./hooks"; import { useScrollState } from "../../hooks/useScrollState"; import { useSimulatorSelector } from "../../features/simulator/context"; @@ -43,7 +42,7 @@ export const ActivityHistory: FC<{ visible: boolean }> = ({ visible }) => { const [spinnerRef, shouldShowHistory, historyInitialized] = useInfiniteScrollingHistory(containerRef, visible); const canEdit = useScope(Scope.edit); - const projectRef = useSelector(selectProjectRef); + const { projectRef } = useProject(); const historyItemsFromStore = useSimulatorSelector( historySelectors.selectAll, diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryGroup/ActivityHistoryGroup.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryGroup/ActivityHistoryGroup.tsx index 915ae95c..2a53cc35 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryGroup/ActivityHistoryGroup.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryGroup/ActivityHistoryGroup.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import classnames from "classnames"; import { @@ -8,12 +8,9 @@ import { import "./ActivityHistoryGroup.scss"; -export const ActivityHistoryGroup: FC = ({ - className, - children, - open, - ...props -}) => ( +export const ActivityHistoryGroup: FC< + PropsWithChildren +> = ({ className, children, open, ...props }) => ( void; - } & ({ title: ReactNode; loading?: false } | { loading: true; title?: null }) + PropsWithChildren< + { + open?: boolean; + onOpenChange?: (open: boolean) => void; + } & ( + | { title: ReactNode; loading?: false } + | { loading: true; title?: null } + ) + > > = ({ open = false, onOpenChange, title, loading, children }) => (

= ({ - canOpen, - children, -}) => ( +export const ActivityHistoryGroupTitle: FC< + PropsWithChildren<{ canOpen: boolean }> +> = ({ canOpen, children }) => ( <>
{children}
{canOpen ? ( diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItem.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItem.tsx index b4544e86..1d76cdb0 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItem.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItem.tsx @@ -5,12 +5,12 @@ import { Link, LinkProps } from "../Link/Link"; import "./ActivityHistoryItem.scss"; -interface ActivityHistoryItemPropsShared { +type ActivityHistoryItemPropsShared = { open?: boolean; tooltip?: ReactNode | null; viewable?: boolean; after?: ReactNode | null; -} +}; type ActivityHistoryItemPropsDiv = ActivityHistoryItemPropsShared & Omit, "ref" | "as">; diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommit.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommit.tsx index 53be8a0b..09f6511c 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommit.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommit.tsx @@ -3,7 +3,7 @@ import { format } from "date-fns"; import { ActivityHistoryGroupSectionItem } from "./ActivityHistoryGroup/ActivityHistoryGroupSectionItem"; import { ActivityHistoryItemTooltip } from "./ActivityHistoryItemTooltip"; -import { CommitWithoutStats } from "../../util/api/queries/commitActions"; +import { CommitWithoutStats } from "../../features/actions"; import { SimulationRunId } from "../SimulationRunId/SimulationRunId"; import { urlFromProject } from "../../routes"; import { useCurrentRefItem } from "./hooks"; diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommitGroup.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommitGroup.tsx index 5e6f1cd7..7cdf2e73 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommitGroup.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemCommitGroup.tsx @@ -1,6 +1,5 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; -import { EntityId } from "@reduxjs/toolkit"; +import type { EntityId } from "../../features/reduxCompat"; import { ActivityHistoryGroup } from "./ActivityHistoryGroup/ActivityHistoryGroup"; import { ActivityHistoryGroupSection } from "./ActivityHistoryGroup/ActivityHistoryGroupSection"; @@ -9,8 +8,8 @@ import { ActivityHistoryItemCommit } from "./ActivityHistoryItemCommit"; import { ActivityTime } from "./ActivityTime"; import { SimulatorHistoryItemCommitGroup } from "../../features/simulator/simulate/types"; import { selectHistoryCurrentCommitGroup } from "../../features/simulator/simulate/selectors"; -import { selectProjectPathWithNamespaceRequired } from "../../features/project/selectors"; import { toggleCommitGroup } from "../../features/simulator/simulate/slice"; +import { useProject } from "../../features/project/ProjectContext"; import { useSimulatorDispatch, useSimulatorSelector, @@ -25,7 +24,11 @@ export const ActivityHistoryItemCommitGroup: FC<{ const open = useSimulatorSelector(selectHistoryCurrentCommitGroup) === historyId; - const pathWithNamespace = useSelector(selectProjectPathWithNamespaceRequired); + const { currentProject } = useProject(); + const pathWithNamespace = currentProject?.pathWithNamespace; + if (!pathWithNamespace) { + throw new Error("Project does not exist when it is required"); + } const simDispatch = useSimulatorDispatch(); return ( diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemTooltip.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemTooltip.tsx index 6bcc32ca..e4f66b7c 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemTooltip.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ActivityHistoryItemTooltip.tsx @@ -1,14 +1,13 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import classNames from "classnames"; import { SimpleTooltip } from "../SimpleTooltip"; import "./ActivityHistoryItemTooltip.scss"; -export const ActivityHistoryItemTooltip: FC<{ className?: string }> = ({ - children, - className, -}) => ( +export const ActivityHistoryItemTooltip: FC< + PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( = ({ tag, createdAt }) => { - const pathWithNamespace = useSelector(selectProjectPathWithNamespace); + const { currentProject } = useProject(); + const pathWithNamespace = currentProject?.pathWithNamespace; const ref = useRef(null); const { current, currentlySwitchingTo } = useCurrentRefItem(tag, ref); @@ -46,7 +46,7 @@ export const ActivityHistoryRelease: FC<{ {currentlySwitchingTo ? (
- +
) : null} {createdAt === null ? null : } diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroup.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroup.tsx index 2b9ae855..16c6d73f 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroup.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroup.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { createSelector } from "@reduxjs/toolkit"; +import { createSelector } from "reselect"; import { ActivityHistoryGroup } from "../ActivityHistoryGroup/ActivityHistoryGroup"; import { ActivityHistoryGroupTitle } from "../ActivityHistoryGroup/ActivityHistoryGroupTitle"; @@ -88,15 +88,8 @@ const emptySimIds: string[] = []; const makeSelectExperimentById = (id: string) => createSelector( [selectExperimentRuns, selectPendingExperimentRuns], - (experiments, pendingExperiments) => { - const experimentRun = experiments[id] ?? pendingExperiments[id]; - - if (!experimentRun) { - throw new Error("data missing for experiment"); - } - - return experimentRun; - }, + (experiments, pendingExperiments) => + experiments[id] ?? pendingExperiments[id] ?? null, ); export const ExperimentGroup: FC<{ @@ -107,17 +100,20 @@ export const ExperimentGroup: FC<{ const simDispatch = useSimulatorDispatch(); const open = - useSimulatorSelector(selectCurrentExperimentId) === data.experimentId; - const experimentFinished = hasExperimentFinished(data.status); - - const pending = !experimentRunInitialized(data); - const readyToShow = useReadyToShowExperiment(open, pending, data.startedTime); + useSimulatorSelector(selectCurrentExperimentId) === + data?.experimentId; + + const pending = data ? !experimentRunInitialized(data) : true; + const readyToShow = useReadyToShowExperiment( + open, + pending, + data?.startedTime ?? 0, + ); const [hovered, setHovered] = useState(false); - const simIds = experimentRunInitialized(data) - ? data.simulationIds - : emptySimIds; + const simIds = + data && experimentRunInitialized(data) ? data.simulationIds : emptySimIds; const anySimsViewableSelector = useCallback( (state: SimulatorRootState) => { @@ -130,10 +126,11 @@ export const ExperimentGroup: FC<{ const anySimsViewable = useSimulatorSelector(anySimsViewableSelector); - if (!readyToShow) { + if (!data || !readyToShow) { return null; } + const experimentFinished = hasExperimentFinished(data.status); const experimentFailed = data.status === "errored"; const experimentPendingAndFailed = pending && experimentFailed; @@ -181,10 +178,10 @@ export const ExperimentGroup: FC<{ open ? hovered ? theme["dark-hover-hover"] - : theme.black + : theme["black"] : hovered ? theme["dark-hover"] - : theme.dark + : theme["dark"] } /> diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroupSectionItem.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroupSectionItem.tsx index 769f847c..01d4ccc5 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroupSectionItem.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/ExperimentGroup/ExperimentGroupSectionItem.tsx @@ -56,7 +56,7 @@ export const ExperimentGroupSectionItem = forwardRef< * by the time they render. We hide it with CSS instead */} )} diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.css b/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.css index c2a646ff..d3960638 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.css +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.css @@ -43,9 +43,7 @@ .AgentInspector__List { overflow: auto; scrollbar-width: none; - padding-bottom: calc( - var(--discord-button-y-offset) + var(--discord-button-size) - ); + padding-bottom: 58px; } .AgentInfo { diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.tsx index 5d3462dc..a4b3e90b 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/Inspector/Inspector.tsx @@ -1,19 +1,16 @@ -import React, { FC, useState } from "react"; +import React, { FC, PropsWithChildren, useState } from "react"; import classNames from "classnames"; import { SerializableAgentState } from "@hashintel/engine-web"; -import { useRecoilState, useRecoilValue } from "recoil"; -import * as sceneState from "../../AgentScene/state/SceneState"; +import { useSceneContext } from "../../AgentScene/state/SceneContext"; import { ActivityEmpty } from "../ActivityEmpty"; import { IconClose } from "../../Icon"; import "./Inspector.css"; -import { WrappedSplitterLayout } from "../../WrappedSplitterLayout/WrappedSplitterLayout"; export const AgentInspector: FC = () => { - const [selectedAgentIds, setSelectedAgents] = useRecoilState( - sceneState.SelectedAgentIds, - ); + const { selectedAgentIds, setSelectedAgentIds: setSelectedAgents } = + useSceneContext(); const agentIds = Object.keys(selectedAgentIds).reverse(); if (agentIds.length === 0) { @@ -47,17 +44,18 @@ export const AgentInspector: FC = () => { }; const AgentInfo: FC<{ id: string }> = ({ id }) => { + const { + getSelectedAgentData, + hoveredAgent, + selectedAgentIds: selectedAgents, + setSelectedAgentIds: setSelectedAgents, + } = useSceneContext(); + // Toggled means "open" so the contents are visible // Agents are closed by default const [toggled, setToggled] = useState(true); - const agent = useRecoilValue(sceneState.SelectedAgentData(id)); - const hoveredAgent = useRecoilValue(sceneState.HoveredAgent); + const agent = getSelectedAgentData(id); const isAgentHovered = id === hoveredAgent; - - // Provide a way to deselect the agent - const [selectedAgents, setSelectedAgents] = useRecoilState( - sceneState.SelectedAgentIds, - ); const unselectAgent = () => { const tempIds = { ...selectedAgents }; delete tempIds[id]; @@ -206,7 +204,10 @@ const AgentProperty: FC<{ return null; }; -const InfoHeader: FC<{ name: string }> = ({ name, children }) => { +const InfoHeader: FC> = ({ + name, + children, +}) => { const [toggled, setToggled] = useState(false); const toggle = () => setToggled(!toggled); @@ -238,19 +239,3 @@ const prettifyField = (val: any) => (val ?? "null").toLocaleString(undefined, { maximumSignificantDigits: 21, }); - -export const AgentInspectorSplitterLayout = () => ( -
- - -
- -
-); diff --git a/apps/sim-core/packages/core/src/components/ActivityHistory/SingleRun/ActivityHistorySingleRun.tsx b/apps/sim-core/packages/core/src/components/ActivityHistory/SingleRun/ActivityHistorySingleRun.tsx index 9d20c811..3a866b3a 100644 --- a/apps/sim-core/packages/core/src/components/ActivityHistory/SingleRun/ActivityHistorySingleRun.tsx +++ b/apps/sim-core/packages/core/src/components/ActivityHistory/SingleRun/ActivityHistorySingleRun.tsx @@ -1,5 +1,5 @@ import React, { FC, useMemo, useRef } from "react"; -import { createSelector } from "@reduxjs/toolkit"; +import { createSelector } from "reselect"; import { ActivityHistoryItem } from "../ActivityHistoryItem"; import { ActivityTime } from "../ActivityTime"; @@ -17,15 +17,7 @@ import { useSimulatorSelector } from "../../../features/simulator/context"; import "./ActivityHistorySingleRun.scss"; const makeSelectSingleRun = (runId: string) => - createSelector(selectAllSimulationData, (data) => { - const run = data[runId]; - - if (!run) { - throw new Error("Data missing for run"); - } - - return run; - }); + createSelector(selectAllSimulationData, (data) => data[runId] ?? null); export const ActivityHistorySingleRun: FC<{ id: string; @@ -39,6 +31,8 @@ export const ActivityHistorySingleRun: FC<{ const [onContextMenu, exportingTooltip, exporting] = useSimulationRunContextMenu(itemRef, id); + if (!run) return null; + return ( , ) => { - const projectRef = useSelector(selectProjectRef); - const switchingTo = useSelector(selectVersionSwitchingTo); + const { projectRef, versionSwitchingTo: switchingTo } = useProject(); const current = projectRef === tag; const currentlySwitchingTo = switchingTo === tag; diff --git a/apps/sim-core/packages/core/src/components/AgentScene/AgentScene.tsx b/apps/sim-core/packages/core/src/components/AgentScene/AgentScene.tsx index 222b5b26..5f1a3be5 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/AgentScene.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/AgentScene.tsx @@ -1,17 +1,10 @@ import React, { useEffect, useRef } from "react"; -import { useSelector } from "react-redux"; -import { Canvas } from "react-three-fiber"; +import { Canvas } from "@react-three/fiber"; import * as THREE from "three"; import { Json, SerializableAgentState } from "@hashintel/engine-web"; -// import { Stats } from "@react-three/drei"; -import { - useRecoilBridgeAcrossReactRoots_UNSTABLE, - useRecoilCallback, - useRecoilState, - useRecoilValue, -} from "recoil"; - -import * as SceneState from "./state/SceneState"; +import { Stats } from "@react-three/drei"; + +import { useSceneContext } from "./state/SceneContext"; import { AgentRenderer } from "./components/AgentRenderer"; import { HoveredAgent } from "./components/HoveredAgent"; import { NetworkEdges } from "./components/NetworkEdges"; @@ -19,9 +12,7 @@ import { SceneSettings } from "./components/SceneSettings"; import { SimulationViewerLazyTab } from "../SimulationViewer/LazyTab/SimulationViewerLazyTab"; import { ViewerControls, orthoCamera } from "./components/Controls"; import { ViewerStage } from "./components/Stage"; -import { resetViewer } from "./state/resetViewer"; -import { selectEmbedded } from "../../features/viewer/selectors"; -import { updateTransitionMap } from "./state/updateTransitionMap"; +import { useViewer } from "../../features/viewer/ViewerContext"; import "./AgentScene.css"; @@ -29,7 +20,7 @@ import "./AgentScene.css"; // - https://threejs.org/examples/#webgl_trails // - https://github.com/mrdoob/three.js/blob/master/examples/webgl_buffergeometry_drawrange.html -export interface SimulationStepProps { +export type SimulationStepProps = { simulationRunId: string | undefined; properties: Json; simulationStep: SerializableAgentState[] | null; @@ -37,40 +28,30 @@ export interface SimulationStepProps { visible: boolean; resetting: boolean; errored: boolean; -} - -THREE.Object3D.DefaultUp.set(0, 0, 1); - -/** - * Provide some reducers/callbacks to modify groups of agent state - */ -const use3DViewer = () => { - // No deps forces it to permanently memoize and therefore be free - return { - updateTransitionMap: useRecoilCallback(updateTransitionMap, []), - resetViewer: useRecoilCallback(resetViewer, []), - }; }; +THREE.Object3D.DEFAULT_UP.set(0, 0, 1); + export const AgentScene = ({ simulationStep, resetting, errored, simulationRunId, }: SimulationStepProps) => { - const [mappedTransitions, setMappedTransitions] = useRecoilState( - SceneState.MappedTransitions, - ); - - // Stats element - // const showStats = useRecoilValue(SceneState.StatsEnabled); - // const statsContainerRef = useRef(null); + const { + mappedTransitions, + setMappedTransitions, + statsEnabled: showStats, + updatesEnabled, + edgesEnabled, + sampleLevel, + updateTransitionMap, + resetViewer, + } = useSceneContext(); - const updatesEnabled = useRecoilValue(SceneState.UpdatesEnabled); - const edgesEnabled = useRecoilValue(SceneState.EdgesEnabled); - const sampleLevel = useRecoilValue(SceneState.SampleLevel); + const statsContainerRef = useRef(null); - const embedded = useSelector(selectEmbedded); + const { embedded } = useViewer(); /** * Updating the stage is an async process, but it can only be done on at a @@ -78,9 +59,11 @@ export const AgentScene = ({ * whenever you want to schedule an update to the stage, and it'll wait until * the last update was done. */ - const stageUpdateChainRef = useRef>(Promise.resolve()); + const stageUpdateChainRef = useRef>(null as any); + if (!stageUpdateChainRef.current) { + stageUpdateChainRef.current = Promise.resolve(); + } - const { resetViewer, updateTransitionMap } = use3DViewer(); useEffect(() => { if (resetting) { stageUpdateChainRef.current = stageUpdateChainRef.current @@ -117,28 +100,19 @@ export const AgentScene = ({ } }, [resetting, simulationStep, updateTransitionMap]); - /* - # Hold up - - Recoil is *designed* for react-three-fiber, but the context will need to - be bridged if it tries to exist in an isolated reconciler (the Canvas object). - https://github.com/facebookexperimental/Recoil/commit/2b1cd3a8576b96e15f985ddb729b66b0ea3bace9 - */ - const RecoilBridge = useRecoilBridgeAcrossReactRoots_UNSTABLE(); - if (simulationRunId && !simulationStep && !errored) { return ; } return (
- {/* */} +
gl.setClearColor("#0e0d15")} - invalidateFrameloop={!updatesEnabled} + frameloop={updatesEnabled ? "always" : "demand"} > - {/* eslint-disable react/no-unknown-property */} - - - - - {edgesEnabled && ( - - )} - - - - - - - {/* eslint-disable react/no-unknown-property */} + + + {edgesEnabled && } + + + + + + {!embedded && }
diff --git a/apps/sim-core/packages/core/src/components/AgentScene/components/AgentMesh.tsx b/apps/sim-core/packages/core/src/components/AgentScene/components/AgentMesh.tsx index 548616b6..6b9fd8a7 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/components/AgentMesh.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/components/AgentMesh.tsx @@ -1,21 +1,20 @@ import React, { FC, useRef } from "react"; -import { useFrame } from "react-three-fiber"; +import { useFrame } from "@react-three/fiber"; import usePromise from "react-promise-suspense"; import * as THREE from "three"; import { BufferGeometry, InstancedBufferAttribute } from "three"; -import { useRecoilState, useRecoilValue } from "recoil"; -import * as sceneState from "../state/SceneState"; +import { useSceneContext } from "../state/SceneContext"; import { RawGeometry, loadGeometryMesh } from "../util/geometry-loader"; import { lerpAnimValue } from "../util/anim"; -interface PolyMeshProps { +type PolyMeshProps = { meshId: string; clock: { lastTime: number; animLength: number; }; -} +}; const tempObject = new THREE.Object3D(); tempObject.up = new THREE.Vector3(0, 0, 1); @@ -27,16 +26,15 @@ tempObject.up = new THREE.Vector3(0, 0, 1); export const AgentMesh: FC = ({ meshId, clock }) => { const ref = useRef(); - const [hoveredAgentId, setHoveredAgentIds] = useRecoilState( - sceneState.HoveredAgent, - ); - const [selectedAgentIds, setSelectedAgentIds] = useRecoilState( - sceneState.SelectedAgentIds, - ); + const { + hoveredAgent: hoveredAgentId, + setHoveredAgent: setHoveredAgentIds, + selectedAgentIds, + setSelectedAgentIds, + getShapedMeshesEntries, + } = useSceneContext(); - // Only update the render agents when agents changes - const renderAgents = - useRecoilValue(sceneState.ShapedMeshesEntries(meshId)) ?? {}; + const renderAgents = getShapedMeshesEntries(meshId) ?? {}; const numMeshes = renderAgents.length; const bufferedMeshCount = getMeshCount(numMeshes, meshId); @@ -119,8 +117,6 @@ export const AgentMesh: FC = ({ meshId, clock }) => { }); return ( - /* eslint-disable react/no-unknown-property */ - = ({ meshId, clock }) => { } } }} - /* eslint-enable react/no-unknown-property */ - // Agent is being clicked onPointerDown={(evt) => { const id = evt.instanceId; @@ -145,7 +139,7 @@ export const AgentMesh: FC = ({ meshId, clock }) => { const [agentId] = renderAgents[id]; const temp = { ...selectedAgentIds }; - if (Object.prototype.hasOwnProperty.call(selectedAgentIds, agentId)) { + if (selectedAgentIds.hasOwnProperty(agentId)) { delete temp[agentId]; setSelectedAgentIds(temp); } else { diff --git a/apps/sim-core/packages/core/src/components/AgentScene/components/AgentRenderer.tsx b/apps/sim-core/packages/core/src/components/AgentScene/components/AgentRenderer.tsx index 950129fe..9331cd54 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/components/AgentRenderer.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/components/AgentRenderer.tsx @@ -1,14 +1,13 @@ import React, { FC, Suspense, useMemo, useRef } from "react"; import * as THREE from "three"; -import { useRecoilValue } from "recoil"; import { AgentMesh } from "./AgentMesh"; -import { PositionedMeshes } from "../state/SceneState"; +import { useSceneContext } from "../state/SceneContext"; import { RenderSummary } from "../util/anim"; -interface AgentRendererProps { +type AgentRendererProps = { mappedTransitions: RenderSummary; -} +}; export const AgentRenderer: FC = ({ mappedTransitions, @@ -28,8 +27,7 @@ export const AgentRenderer: FC = ({ 3. On each frame, follow the agenst as they move */ - // Group the transition map by mesh - const positionedMeshes = useRecoilValue(PositionedMeshes); + const { positionedMeshes } = useSceneContext(); const clock = useRef(); if (!clock.current) { diff --git a/apps/sim-core/packages/core/src/components/AgentScene/components/Controls.tsx b/apps/sim-core/packages/core/src/components/AgentScene/components/Controls.tsx index 3ad9ec7d..37dbe060 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/components/Controls.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/components/Controls.tsx @@ -1,10 +1,9 @@ import React, { FC, useEffect, useRef, useState } from "react"; -import { CanvasProps, useThree } from "react-three-fiber"; +import { CanvasProps, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { MapControls, OrbitControls } from "@react-three/drei"; -import { useRecoilValue } from "recoil"; -import * as sceneState from "../state/SceneState"; +import { useSceneContext } from "../state/SceneContext"; import { RenderSummary } from "../util/anim"; const cameraRot = new THREE.Object3D(); @@ -31,10 +30,12 @@ export const ViewerControls: FC<{ resetting: boolean; mappedTransitions: RenderSummary; }> = ({ resetting, mappedTransitions }) => { - const cameraFov = useRecoilValue(sceneState.CameraFov); - const dimensions = useRecoilValue(sceneState.StageDimensions); + const { + cameraFov, + stageDimensions: dimensions, + sceneView: view, + } = useSceneContext(); const controlsRef = useRef(); - const view = useRecoilValue(sceneState.SceneView); const { camera } = useThree(); /* @@ -72,9 +73,7 @@ export const ViewerControls: FC<{ camera.position.set(0, 0, 1000); camera.lookAt(0, 0, 0); - // Flatten the camera - // @ts-expect-error 'fov' does in fact exist. - camera.fov = 1; + (camera as THREE.PerspectiveCamera).fov = 1; camera.updateProjectionMatrix(); controlsRef.current?.update!(); diff --git a/apps/sim-core/packages/core/src/components/AgentScene/components/HoveredAgent.tsx b/apps/sim-core/packages/core/src/components/AgentScene/components/HoveredAgent.tsx index 05e1d461..410717bb 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/components/HoveredAgent.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/components/HoveredAgent.tsx @@ -1,22 +1,21 @@ import React, { FC } from "react"; import * as THREE from "three"; import { Vec3 } from "@hashintel/engine-web"; -import { useRecoilState } from "recoil"; -import * as sceneState from "../state/SceneState"; +import { useSceneContext } from "../state/SceneContext"; import { RenderSummary } from "../util/anim"; const tempObject = new THREE.Object3D(); tempObject.up = new THREE.Vector3(0, 0, 1); -interface HoveredAgentProps { +type HoveredAgentProps = { transitions: RenderSummary; -} +}; /* * Creates the appropriate ThreeJS representation of a "hovered" agent */ export const HoveredAgent: FC = ({ transitions }) => { - const [hoveredAgentId] = useRecoilState(sceneState.HoveredAgent); + const { hoveredAgent: hoveredAgentId } = useSceneContext(); if (hoveredAgentId) { // HoveredAgentID might be stale (cursor improperly focused and ID gets outdated) @@ -38,7 +37,6 @@ export const HoveredAgent: FC = ({ transitions }) => { const [posx, posy, posz] = agent.position.to; const pos: Vec3 = [posx, posy, posz + offsetZ]; - /* eslint-disable react/no-unknown-property */ return ( = ({ transitions }) => { rotation={tempObject.rotation} up={[0, 0, 1]} > - + = ({ transitions }) => { /> ); - /* eslint-enable react/no-unknown-property */ } } return null; diff --git a/apps/sim-core/packages/core/src/components/AgentScene/components/NetworkEdges.tsx b/apps/sim-core/packages/core/src/components/AgentScene/components/NetworkEdges.tsx index 5ecc013b..49f7d4da 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/components/NetworkEdges.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/components/NetworkEdges.tsx @@ -1,13 +1,12 @@ import React, { FC, useMemo } from "react"; import { ArrowHelper, Vector3 } from "three"; -import { useRecoilValue } from "recoil"; -import * as sceneState from "../state/SceneState"; +import { useSceneContext } from "../state/SceneContext"; import { RenderSummary } from "../util/anim"; -interface NetworkEdgesProps { +type NetworkEdgesProps = { mappedTransitions: RenderSummary; -} +}; // Arguments for constructing THREE arrowHelper type ArrowConstructorArgs = typeof ArrowHelper extends new ( @@ -16,10 +15,7 @@ type ArrowConstructorArgs = typeof ArrowHelper extends new ( ? U : never; -interface ArrowData { - key: string; - args: ArrowConstructorArgs; -} +type ArrowData = { key: string; args: ArrowConstructorArgs }; /** * Convert the array vector in agent state to a THREE Vector3 @@ -40,9 +36,8 @@ const isValidNetworkArray = (value: unknown): value is string[] => * @todo Animate the transition between an arrow's previous position and its new one. Can use AgentMesh's useFrame (specifically, the position section) as a reference. */ export const NetworkEdges: FC = ({ mappedTransitions }) => { - // Which agents are hovered or selected? We'll highlight connected lines - const hoveredAgentId = useRecoilValue(sceneState.HoveredAgent); - const selectedAgents = useRecoilValue(sceneState.SelectedAgentIds); + const { hoveredAgent: hoveredAgentId, selectedAgentIds: selectedAgents } = + useSceneContext(); const selectedAgentIds = Object.keys(selectedAgents); const highlightedAgents = [hoveredAgentId, ...selectedAgentIds].filter( Boolean, @@ -138,7 +133,6 @@ export const NetworkEdges: FC = ({ mappedTransitions }) => { return ( <> {arrowData.map(({ key, args }) => ( - // eslint-disable-next-line react/no-unknown-property ))} diff --git a/apps/sim-core/packages/core/src/components/AgentScene/components/SceneSettings.tsx b/apps/sim-core/packages/core/src/components/AgentScene/components/SceneSettings.tsx index fca7986a..ad963a5b 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/components/SceneSettings.tsx +++ b/apps/sim-core/packages/core/src/components/AgentScene/components/SceneSettings.tsx @@ -1,48 +1,41 @@ import React, { FC } from "react"; -import { useRecoilState } from "recoil"; -import { - AxesEnabled, - EdgesEnabled, - FloorEnabled, - GridColor, - GridEnabled, - SampleLevel, - SceneView, - StageColor, - StatsEnabled, - UpdatesEnabled, -} from "../state/SceneState"; +import { useSceneContext } from "../state/SceneContext"; import { CheckboxInput } from "../../Inputs/Checkbox/CheckboxInput"; import { IconSettings } from "../../Icon/Settings"; import { SimpleTooltip } from "../../SimpleTooltip"; import { TextOrNumberInput } from "../../Inputs"; export const SceneSettings: FC = () => { - const [floorEnabled, setFloorEnabled] = useRecoilState(FloorEnabled); - const toggleStage = () => setFloorEnabled(!floorEnabled); + const { + floorEnabled, + setFloorEnabled, + gridEnabled, + setGridEnabled, + axesEnabled, + setAxesEnabled, + statsEnabled, + setStatsEnabled, + edgesEnabled, + setEdgesEnabled, + updatesEnabled, + setUpdatesEnabled, + sceneView: view, + setSceneView: setView, + stageColor, + setStageColor, + gridColor, + setGridColor, + } = useSceneContext(); - const [gridEnabled, setGridEnabled] = useRecoilState(GridEnabled); + const toggleStage = () => setFloorEnabled(!floorEnabled); const toggleGrid = () => setGridEnabled(!gridEnabled); - - const [axesEnabled, setAxesEnabled] = useRecoilState(AxesEnabled); const toggleAxes = () => setAxesEnabled(!axesEnabled); - - const [statsEnabled, setStatsEnabled] = useRecoilState(StatsEnabled); const toggleStats = () => setStatsEnabled(!statsEnabled); - - const [edgesEnabled, setEdgesEnabled] = useRecoilState(EdgesEnabled); const toggleEdges = () => setEdgesEnabled(!edgesEnabled); - - const [updatesEnabled, setUpdatesEnabled] = useRecoilState(UpdatesEnabled); const toggleUpdates = () => setUpdatesEnabled(!updatesEnabled); - - const [view, setView] = useRecoilState(SceneView); const toggleView = () => setView(view === "2d" ? "3d" : "2d"); - const [stageColor, setStageColor] = useRecoilState(StageColor); - const [gridColor, setGridColor] = useRecoilState(GridColor); - return (
@@ -135,7 +128,7 @@ const Toggler: FC<{ }; const SampleLevelSlider: FC = () => { - const [sampleLevel, setSampleLevel] = useRecoilState(SampleLevel); + const { sampleLevel, setSampleLevel } = useSceneContext(); return (
{ - // A Square grid centered around the middle of the agents - const dims = useRecoilValue(sceneState.StageDimensions); - const showGrid = useRecoilValue(sceneState.GridEnabled); - const showFloor = useRecoilValue(sceneState.FloorEnabled); - const showAxes = useRecoilValue(sceneState.AxesEnabled); - const stageColor = useRecoilValue(sceneState.StageColor); - const gridColor = useRecoilValue(sceneState.GridColor); + const { + stageDimensions: dims, + gridEnabled: showGrid, + floorEnabled: showFloor, + axesEnabled: showAxes, + stageColor, + gridColor, + } = useSceneContext(); // Position of the grid to the center of the min/max const { pxMax, pxMin, pyMax, pyMin } = dims; @@ -25,7 +25,6 @@ export const ViewerStage: FC = () => { pyMin, ); - /* eslint-disable react/no-unknown-property */ return ( <> { position={[centerX, centerY, -0.1]} visible={showFloor} > - + { ); - /* eslint-enable react/no-unknown-property */ }; function getStagePlacement( diff --git a/apps/sim-core/packages/core/src/components/AgentScene/state/SceneContext.tsx b/apps/sim-core/packages/core/src/components/AgentScene/state/SceneContext.tsx new file mode 100644 index 00000000..06df1225 --- /dev/null +++ b/apps/sim-core/packages/core/src/components/AgentScene/state/SceneContext.tsx @@ -0,0 +1,473 @@ +import React, { + createContext, + FC, + PropsWithChildren, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import { AgentState, Vec3 } from "@hashintel/engine-web"; +import * as THREE from "three"; + +import { AgentTransition, RenderSummary } from "../util/anim"; +import { getItem, setItem } from "../../../hooks/useLocalStorage/utils"; +import { useFiles } from "../../../features/files/FilesContext"; +import { useProject } from "../../../features/project/ProjectContext"; + +const tempColor = new THREE.Color(); + +// ---------------------- Settings localStorage helpers ---------------------- + +type ViewerSettingValue = number | string | boolean; +type ViewerSettingsStorageObject = { + lastSet: ViewerSettingValue; + [projectPath: string]: ViewerSettingValue; +}; + +function loadSetting( + key: string, + defaultValue: T, + projectPath: string | undefined, +): T { + const storageKey = `sceneSettings.${key}`; + const saved = getItem(storageKey); + if (projectPath && saved?.[projectPath] != null) + return saved[projectPath] as T; + if (saved?.lastSet != null) return saved.lastSet as T; + return defaultValue; +} + +function saveSetting( + key: string, + value: ViewerSettingValue, + projectPath: string | undefined, +) { + const storageKey = `sceneSettings.${key}`; + const saved: ViewerSettingsStorageObject = { + ...(getItem(storageKey) ?? {}), + lastSet: value, + }; + if (projectPath) saved[projectPath] = value; + setItem(storageKey, saved); +} + +function usePersistedSetting( + key: string, + defaultValue: T, + projectPath: string | undefined, +): [T, (val: T) => void] { + const [value, setRaw] = useState(() => + loadSetting(key, defaultValue, projectPath), + ); + const set = useCallback( + (val: T) => { + setRaw(val); + saveSetting(key, val, projectPath); + }, + [key, projectPath], + ); + return [value, set]; +} + +// ---------------------- Dimension defaults ---------------------- + +export const dimensionDefaults = { + pxMax: 10, + pxMin: -10, + pyMax: 10, + pyMin: -10, +}; + +export type StageDimensionsType = typeof dimensionDefaults; + +// ---------------------- Context value type ---------------------- + +export interface SceneContextValue { + // Core data + mappedTransitions: RenderSummary; + setMappedTransitions: React.Dispatch>; + stageDimensions: StageDimensionsType; + setStageDimensions: React.Dispatch>; + selectedAgentIds: Record; + setSelectedAgentIds: React.Dispatch< + React.SetStateAction> + >; + hoveredAgent: string | null; + setHoveredAgent: React.Dispatch>; + + // Derived data + positionedMeshes: Record; + getShapedMeshesEntries: (shape: string) => [string, AgentTransition][]; + getSelectedAgentData: (id: string) => AgentTransition | undefined; + + // Settings + sceneView: "3d" | "2d"; + setSceneView: (v: "3d" | "2d") => void; + cameraFov: number; + setCameraFov: (v: number) => void; + stageColor: string; + setStageColor: (v: string) => void; + gridColor: string; + setGridColor: (v: string) => void; + gridEnabled: boolean; + setGridEnabled: (v: boolean) => void; + floorEnabled: boolean; + setFloorEnabled: (v: boolean) => void; + axesEnabled: boolean; + setAxesEnabled: (v: boolean) => void; + edgesEnabled: boolean; + setEdgesEnabled: (v: boolean) => void; + updatesEnabled: boolean; + setUpdatesEnabled: (v: boolean) => void; + lightEnabled: boolean; + setLightEnabled: (v: boolean) => void; + statsEnabled: boolean; + setStatsEnabled: (v: boolean) => void; + sampleLevel: number; + setSampleLevel: (v: number) => void; + + // Actions + updateTransitionMap: ( + oldSummary: RenderSummary, + states: AgentState[], + ) => void; + resetViewer: () => void; +} + +const SceneContext = createContext(null); + +export const useSceneContext = () => { + const ctx = useContext(SceneContext); + if (!ctx) throw new Error("useSceneContext must be inside SceneProvider"); + return ctx; +}; + +// ---------------------- Provider ---------------------- + +export const SceneProvider: FC = ({ children }) => { + const { currentProject } = useProject(); + const { globalsSrc } = useFiles(); + const projectPath = currentProject?.pathWithNamespace; + + // Core state + const [mappedTransitions, setMappedTransitions] = useState({}); + const [stageDimensions, setStageDimensions] = + useState(dimensionDefaults); + const [selectedAgentIds, setSelectedAgentIds] = useState< + Record + >({}); + const [hoveredAgent, setHoveredAgent] = useState(null); + + // Settings (persisted to localStorage) + const [sceneView, setSceneView] = usePersistedSetting<"3d" | "2d">( + "view", + "3d", + projectPath, + ); + const [cameraFov, setCameraFov] = usePersistedSetting( + "fov", + 30, + projectPath, + ); + const [stageColor, setStageColor] = usePersistedSetting( + "stageColor", + "#111216", + projectPath, + ); + const [gridColor, setGridColor] = usePersistedSetting( + "gridColor", + "#444444", + projectPath, + ); + const [gridEnabled, setGridEnabled] = usePersistedSetting( + "gridEnabled", + true, + projectPath, + ); + const [floorEnabled, setFloorEnabled] = usePersistedSetting( + "floorEnabled", + true, + projectPath, + ); + const [axesEnabled, setAxesEnabled] = usePersistedSetting( + "axesEnabled", + true, + projectPath, + ); + const [edgesEnabled, setEdgesEnabled] = usePersistedSetting( + "edgesEnabled", + true, + projectPath, + ); + const [updatesEnabled, setUpdatesEnabled] = usePersistedSetting( + "updatesEnabled", + true, + projectPath, + ); + const [lightEnabled, setLightEnabled] = usePersistedSetting( + "lightEnabled", + true, + projectPath, + ); + const [statsEnabled, setStatsEnabled] = usePersistedSetting( + "statsEnabled", + false, + projectPath, + ); + const [sampleLevel, setSampleLevel] = usePersistedSetting( + "sampleLevel", + 3, + projectPath, + ); + + // Derived: group transitions by mesh type + const positionedMeshes = useMemo(() => { + const meshes: Record = {}; + for (const [id, agent] of Object.entries(mappedTransitions)) { + if (!meshes[agent.shape]) meshes[agent.shape] = {}; + meshes[agent.shape][id] = agent; + } + return meshes; + }, [mappedTransitions]); + + const getShapedMeshesEntries = useCallback( + (shape: string): [string, AgentTransition][] => { + if (shape === "pickedAgent") { + const output: RenderSummary = {}; + for (const id of Object.keys(selectedAgentIds)) { + const trans = mappedTransitions[id]; + if (trans) output[id] = trans; + } + return Object.entries(output); + } + return Object.entries(positionedMeshes[shape] ?? {}); + }, + [positionedMeshes, selectedAgentIds, mappedTransitions], + ); + + const getSelectedAgentData = useCallback( + (id: string): AgentTransition | undefined => mappedTransitions[id], + [mappedTransitions], + ); + + // Action: update transition map from new simulation step + const updateTransitionMap = useCallback( + (oldSummary: RenderSummary, states: AgentState[]) => { + const removals = new Set(Object.keys(oldSummary)); + const newSummary = { ...oldSummary }; + + for (const agent of states) { + const agentId = agent.agent_id ?? "AGENT_ID_NOT_FOUND"; + if (!agent.position) continue; + + const oldAgent = newSummary[agentId] as AgentTransition | undefined; + const [posX, posY, posZ] = [...(agent.position ?? [1, 1, 1])]; + const newPosition: Vec3 = [posX, posY ?? 0, posZ ?? 0]; + + const scalex = agent.scale ? agent.scale[0] : 1; + const scaley = agent.scale ? agent.scale[1] : 1; + const scalez = agent.height ?? (agent.scale ? agent.scale[2] : 1); + const newScale: Vec3 = [scalex, scaley, scalez]; + const useHeight = + agent.scale === undefined || agent.height !== undefined; + + const [newDirX, newDirY, newDirZ] = [ + ...((Array.isArray(agent.direction) ? agent.direction : null) ?? + (Array.isArray(agent.velocity) ? agent.velocity : null) ?? [ + 0, 0, 0, + ]), + ]; + const newDirection: Vec3 = [newDirX ?? 0, newDirY ?? 0, newDirZ ?? 0]; + if ( + newDirection[0] === 0 && + newDirection[1] === 0 && + newDirection[2] === 0 + ) { + const oldDir = oldAgent?.direction.to ?? oldAgent?.direction.current; + if (oldDir) { + newDirection[0] = oldDir[0]; + newDirection[1] = oldDir[1]; + newDirection[2] = oldDir[2]; + } + } + + tempColor.set(agent.color ?? "green"); + const newColor: Vec3 = [tempColor.r, tempColor.g, tempColor.b]; + if (agent.rgb && !agent.color) { + newColor[0] = agent.rgb[0] / 255; + newColor[1] = agent.rgb[1] / 255; + newColor[2] = agent.rgb[2] / 255; + } + + let shape = agent.shape ?? oldAgent?.shape; + if (!shape) { + shape = agent.direction || agent.velocity ? "cone" : "box"; + } + + if (oldAgent) { + newSummary[agentId] = { + ...oldAgent, + shape, + original: agent, + hidden: agent.hidden ?? false, + color: { current: [...oldAgent.color.to], to: newColor }, + direction: { current: oldAgent.direction.to, to: newDirection }, + scale: { current: oldAgent.scale.to, to: newScale }, + position: { current: oldAgent.position.to, to: newPosition }, + network_neighbor_ids: agent.network_neighbor_ids, + network_neighbor_in_ids: agent.network_neighbor_in_ids, + network_neighbor_out_ids: agent.network_neighbor_out_ids, + }; + } else { + newSummary[agentId] = { + color: { current: newColor, to: newColor }, + direction: { current: newDirection, to: newDirection }, + position: { current: newPosition, to: newPosition }, + scale: { current: [0, 0, 0], to: newScale }, + network_neighbor_ids: agent.network_neighbor_ids, + network_neighbor_in_ids: agent.network_neighbor_in_ids, + network_neighbor_out_ids: agent.network_neighbor_out_ids, + useHeight, + remove: false, + shape, + original: agent, + hidden: agent.hidden ?? false, + }; + } + removals.delete(agentId); + } + + for (const removal of removals.values()) { + const oldAgent = newSummary[removal]; + if (oldAgent) { + if (oldAgent.remove) { + delete newSummary[removal]; + } else { + newSummary[removal] = { + ...oldAgent, + remove: true, + scale: { ...oldAgent.scale, to: [0, 0, 0] }, + }; + } + } + } + + setMappedTransitions(newSummary); + + setStageDimensions((dims) => { + let { pxMax, pxMin, pyMax, pyMin } = dims; + for (const agent of Object.values(newSummary)) { + pxMax = Math.max(agent.position.to[0], pxMax); + pxMin = Math.min(agent.position.to[0], pxMin); + pyMax = Math.max(agent.position.to[1], pyMax); + pyMin = Math.min(agent.position.to[1], pyMin); + } + return { pxMax, pxMin, pyMax, pyMin }; + }); + }, + [], + ); + + // Action: reset viewer to initial state + const resetViewer = useCallback(() => { + let { pxMin, pxMax, pyMin, pyMax } = dimensionDefaults; + if (globalsSrc) { + try { + const { topology } = JSON.parse(globalsSrc); + if (topology) { + pxMin = topology.x_bounds?.[0] ?? pxMin; + pxMax = topology.x_bounds?.[1] ?? pxMax; + pyMin = topology.y_bounds?.[0] ?? pyMin; + pyMax = topology.y_bounds?.[1] ?? pyMax; + } + } catch { + // globals.json is not valid JSON + } + } + setStageDimensions({ pxMin, pxMax, pyMin, pyMax }); + setSelectedAgentIds({}); + setHoveredAgent(null); + }, [globalsSrc]); + + const value = useMemo( + () => ({ + mappedTransitions, + setMappedTransitions, + stageDimensions, + setStageDimensions, + selectedAgentIds, + setSelectedAgentIds, + hoveredAgent, + setHoveredAgent, + positionedMeshes, + getShapedMeshesEntries, + getSelectedAgentData, + sceneView, + setSceneView, + cameraFov, + setCameraFov, + stageColor, + setStageColor, + gridColor, + setGridColor, + gridEnabled, + setGridEnabled, + floorEnabled, + setFloorEnabled, + axesEnabled, + setAxesEnabled, + edgesEnabled, + setEdgesEnabled, + updatesEnabled, + setUpdatesEnabled, + lightEnabled, + setLightEnabled, + statsEnabled, + setStatsEnabled, + sampleLevel, + setSampleLevel, + updateTransitionMap, + resetViewer, + }), + [ + mappedTransitions, + stageDimensions, + selectedAgentIds, + hoveredAgent, + positionedMeshes, + getShapedMeshesEntries, + getSelectedAgentData, + sceneView, + cameraFov, + stageColor, + gridColor, + gridEnabled, + floorEnabled, + axesEnabled, + edgesEnabled, + updatesEnabled, + lightEnabled, + statsEnabled, + sampleLevel, + updateTransitionMap, + resetViewer, + setSceneView, + setCameraFov, + setStageColor, + setGridColor, + setGridEnabled, + setFloorEnabled, + setAxesEnabled, + setEdgesEnabled, + setUpdatesEnabled, + setLightEnabled, + setStatsEnabled, + setSampleLevel, + ], + ); + + return ( + {children} + ); +}; diff --git a/apps/sim-core/packages/core/src/components/AgentScene/util/anim.ts b/apps/sim-core/packages/core/src/components/AgentScene/util/anim.ts index 27a84c73..597053d6 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/util/anim.ts +++ b/apps/sim-core/packages/core/src/components/AgentScene/util/anim.ts @@ -1,10 +1,7 @@ import { AgentState, Vec3 } from "@hashintel/engine-web"; -export interface AnimValue { - current: A; - to: A; -} -export interface AgentTransition { +export type AnimValue = { current: A; to: A }; +export type AgentTransition = { position: AnimValue; direction: AnimValue; color: AnimValue; @@ -18,9 +15,11 @@ export interface AgentTransition { network_neighbor_ids?: unknown; network_neighbor_in_ids: unknown; network_neighbor_out_ids: unknown; -} +}; -export type RenderSummary = Record; +export type RenderSummary = { + [agent_id: string]: AgentTransition; +}; // Mutably advances "cur" to "to" based on the lerpval export function lerpAnimValue( diff --git a/apps/sim-core/packages/core/src/components/AgentScene/util/builtinmodels.ts b/apps/sim-core/packages/core/src/components/AgentScene/util/builtinmodels.ts index 2ee168a1..c37838e6 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/util/builtinmodels.ts +++ b/apps/sim-core/packages/core/src/components/AgentScene/util/builtinmodels.ts @@ -1,13 +1,13 @@ -interface PolyModel { +type PolyModel = { folderPath: string; resourceUrls: string[]; slug: string; -} +}; -export const BUILTIN_MODELS: Record< - string, - { rotX?: number; rotY?: number; rotZ?: number } -> = { +export const BUILTIN_MODELS: { + // rotation as degrees + [id: string]: { rotX?: number; rotY?: number; rotZ?: number }; +} = { "elm-tree": {}, "palm-tree": {}, "spruce-tree": {}, diff --git a/apps/sim-core/packages/core/src/components/AgentScene/util/geometry-loader.ts b/apps/sim-core/packages/core/src/components/AgentScene/util/geometry-loader.ts index 4b97cc72..447ff00a 100644 --- a/apps/sim-core/packages/core/src/components/AgentScene/util/geometry-loader.ts +++ b/apps/sim-core/packages/core/src/components/AgentScene/util/geometry-loader.ts @@ -1,7 +1,7 @@ import * as THREE from "three"; -import { BufferGeometryUtils } from "three/examples/jsm/utils/BufferGeometryUtils"; -import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader"; -import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"; +import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js"; +import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js"; import { BUILTIN_MODELS, BUILTIN_MODELS_DB } from "./builtinmodels"; @@ -13,66 +13,60 @@ export const loadGeometryMesh = async ( ): Promise => { switch (userMeshName) { case "box": - return geoHelper("BoxBufferGeometry", num, [1, 1, 1]); - case "cone": { - const [geo, mat] = geoHelper("ConeBufferGeometry", num, [0.5, 1, 30]); + return geoHelper("BoxGeometry", num, [1, 1, 1]); + case "cone": + const [geo, mat] = geoHelper("ConeGeometry", num, [0.5, 1, 30]); // Our cones point in the forward direction // Cones normally point up // We need to rotate them so they point the correct direction geo.rotateX(Math.PI); geo.rotateY(Math.PI / 2); return [geo, mat]; - } - case "flatplane": { - const [geoPlane, matPlane] = geoHelper( - "PlaneBufferGeometry", - num, - [1, 1], - ); + case "flatplane": + const [geoPlane, matPlane] = geoHelper("PlaneGeometry", num, [1, 1]); geoPlane.translate(0, 0, -0.5); return [geoPlane, matPlane]; - } case "cylinder": - return geoHelper("CylinderBufferGeometry", num, [0.5, 0.5]); + return geoHelper("CylinderGeometry", num, [0.5, 0.5]); case "dodecahedron": - return geoHelper("DodecahedronBufferGeometry", num, [0.5]); + return geoHelper("DodecahedronGeometry", num, [0.5]); case "icosahedron": - return geoHelper("IcosahedronBufferGeometry", num, [0.5]); + return geoHelper("IcosahedronGeometry", num, [0.5]); case "octahedron": - return geoHelper("OctahedronBufferGeometry", num, [0.5]); + return geoHelper("OctahedronGeometry", num, [0.5]); case "sphere": - return geoHelper("SphereBufferGeometry", num, [0.5]); + return geoHelper("SphereGeometry", num, [0.5]); case "tetrahedron": - return geoHelper("TetrahedronBufferGeometry", num, [0.5]); + return geoHelper("TetrahedronGeometry", num, [0.5]); case "torus": - return geoHelper("TorusBufferGeometry", num, [0.3, 0.2, 10, 10]); + return geoHelper("TorusGeometry", num, [0.3, 0.2, 10, 10]); case "torusknot": - return geoHelper("TorusKnotBufferGeometry", num, [0.3, 0.2, 10, 10]); + return geoHelper("TorusKnotGeometry", num, [0.3, 0.2, 10, 10]); case "pickedAgent": return pickedMesh(); default: try { - const model = await polyLoader(userMeshName); + const model = await polyLoader(userMeshName, num); return model; - } catch (err) { + } catch { // Fail through and produce a box - return geoHelper("BoxBufferGeometry", num, [1, 1, 1]); + return geoHelper("BoxGeometry", num, [1, 1, 1]); } } }; type SupportedShapes = - | "BoxBufferGeometry" - | "ConeBufferGeometry" - | "PlaneBufferGeometry" - | "CylinderBufferGeometry" - | "DodecahedronBufferGeometry" - | "IcosahedronBufferGeometry" - | "OctahedronBufferGeometry" - | "SphereBufferGeometry" - | "TetrahedronBufferGeometry" - | "TorusBufferGeometry" - | "TorusKnotBufferGeometry"; + | "BoxGeometry" + | "ConeGeometry" + | "PlaneGeometry" + | "CylinderGeometry" + | "DodecahedronGeometry" + | "IcosahedronGeometry" + | "OctahedronGeometry" + | "SphereGeometry" + | "TetrahedronGeometry" + | "TorusGeometry" + | "TorusKnotGeometry"; /** * Create a new InstanceMesh from a geometry name and constructor parameters @@ -89,46 +83,40 @@ const geoHelper = ( geometry.setAttribute("color", new THREE.InstancedBufferAttribute(colors, 3)); const material = new THREE.MeshPhongMaterial({ vertexColors: true, - shininess: 0.1, - reflectivity: 0.1, + shininess: 30, }); return [geometry, material]; }; const pickedMesh = (): RawGeometry => { - const modelGeometry = new THREE.Geometry(); - - // The hover diamond const coneGeometry = new THREE.ConeGeometry(); - coneGeometry.translate(0, 0, 0); coneGeometry.scale(0.2, 0.2, 0.2); coneGeometry.rotateX(Math.PI); coneGeometry.translate(0, 0.8, 0); - // The bounding box const boxGeometry = new THREE.BoxGeometry(); boxGeometry.scale(1.05, 1.05, 1.05); - // Merge them - modelGeometry.merge(coneGeometry); - modelGeometry.merge(boxGeometry); - const bufGeometry = new THREE.BufferGeometry().fromGeometry(modelGeometry); + const merged = mergeGeometries([coneGeometry, boxGeometry], false); + if (!merged) throw new Error("Failed to merge picked-agent geometry"); + + merged.rotateX(Math.PI / 2); - // A simple bounding box const material = new THREE.MeshStandardMaterial({ color: "white", wireframe: true, }); - // We need to align the coordinate systems of the normal geometry and the models - bufGeometry.rotateX(Math.PI / 2); - return [bufGeometry, material]; + return [merged, material]; }; /** * Fetch a model from the API and return its geoemtry and materials */ -export const polyLoader = async (meshName: string): Promise => { +export const polyLoader = async ( + meshName: string, + numMeshes: number, +): Promise => { // Check for a built-in and any specific rotation information const builtin = BUILTIN_MODELS[meshName]; if (!builtin) { @@ -170,14 +158,9 @@ export const polyLoader = async (meshName: string): Promise => { }); mergedMaterials.concat(Object.values(materials.materials)); - const geometry = BufferGeometryUtils.mergeBufferGeometries( - mergedGeometries, - true, - ); + const geometry = mergeGeometries(mergedGeometries, true); - // Yes, it's deprecated, but it's the only way to get material merging to work - const material = new THREE.MultiMaterial(Object.values(mergedMaterials)); - material.vertexColors = true; + const material = mergedMaterials[0] ?? new THREE.MeshStandardMaterial(); // Resize the mesh to fit within a single cube const boundingBox = new THREE.Box3(); @@ -200,13 +183,25 @@ export const polyLoader = async (meshName: string): Promise => { geometry.rotateY((rotY ?? 0) * (Math.PI / 180)); geometry.rotateZ((rotZ ?? 0) * (Math.PI / 180)); - return [geometry, material]; + // Per-instance color buffer so each agent can have its own color from the + // simulation state (e.g. green for healthy trees, red for fire). + const colors = new Float32Array(numMeshes * 3).map(() => 0); + geometry.setAttribute( + "color", + new THREE.InstancedBufferAttribute(colors, 3), + ); + const coloredMaterial = new THREE.MeshPhongMaterial({ + vertexColors: true, + shininess: 30, + }); + + return [geometry, coloredMaterial]; }; -const fetchPolyFromBuiltinDb = (slug: string) => { +const fetchPolyFromBuiltinDb = async (slug: string) => { const { folderPath, resourceUrls } = BUILTIN_MODELS_DB.find((model) => { return model.slug === slug; - }) ?? { folderPath: null, resourceUrls: [] }; + }) || { folderPath: null, resourceUrls: [] }; if (!folderPath) { throw new Error("No folderPath found for built-in model " + slug); diff --git a/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewer.tsx b/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewer.tsx index 4f417a65..c516d868 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewer.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewer.tsx @@ -1,10 +1,9 @@ import React, { FC, useCallback, useEffect, useState } from "react"; import { unstable_batchedUpdates } from "react-dom"; -import { useDispatch, useSelector } from "react-redux"; import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; import { useModal } from "react-modal-hook"; import classNames from "classnames"; -import { sum } from "lodash"; +import { sum } from "lodash-es"; import { AnalysisProps, Plot } from "./types"; import { AnalysisViewerActionButtons } from "./AnalysisViewerActionButtons"; @@ -23,18 +22,19 @@ import { onPlotsModalSave, } from "./modals"; import { selectAnalysisMode } from "../../features/simulator/simulate/selectors"; -import { selectEmbedded } from "../../features/viewer/selectors"; import { useAnalysisSrcForCurrentActivityItem } from "../../hooks/useAnalysisSrcForCurrentActivityItem"; +import { useFiles } from "../../features/files/FilesContext"; import { useParseAnalysis } from "../../hooks/useParseAnalysis"; import { useResizeObserver } from "../../hooks/useResizeObserver/useResizeObserver"; import { useSimulatorSelector } from "../../features/simulator/context"; +import { useViewer } from "../../features/viewer/ViewerContext"; import "./AnalysisViewer.scss"; export const AnalysisViewer: FC = ({ currentStep }) => { - const dispatch = useDispatch(); + const { filesDispatch } = useFiles(); const analysisMode = useSimulatorSelector(selectAnalysisMode); - const embedded = useSelector(selectEmbedded); + const { embedded } = useViewer(); const canEdit = useScope(Scope.edit); const { analysis: analysisString, readonly: analysisReadOnly } = @@ -46,7 +46,7 @@ export const AnalysisViewer: FC = ({ currentStep }) => { // TODO: discuss if we also need the useCancellableDebounce trick here - const outputs = analysis?.outputs || {}; + const outputs = (analysis && analysis.outputs) || {}; const metricKeys = Object.keys(outputs); const analysisOutputMetricsDataAvailable = metricKeys.length > 0; const analysisPlotsDataAvailable = analysis?.plots?.length > 0; @@ -71,19 +71,19 @@ export const AnalysisViewer: FC = ({ currentStep }) => { const onOutputMetricsModalSaveHandler = useCallback( (data: any, previousKey?: string) => onOutputMetricsModalSave({ - dispatch, + dispatch: filesDispatch, setAnalysis, analysisString, analysis, data, previousKey, }), - [dispatch, setAnalysis, analysis, analysisString], + [filesDispatch, setAnalysis, analysis, analysisString], ); const onOutputMetricsModalDeleteHandler = (keyToDelete: string) => onOutputMetricsModalDelete({ - dispatch, + dispatch: filesDispatch, setAnalysis, analysisString, analysis, @@ -93,29 +93,29 @@ export const AnalysisViewer: FC = ({ currentStep }) => { const onDuplicateMetricHandler = (metricKey: string) => onDuplicateMetric({ analysis, - dispatch, + dispatch: filesDispatch, setAnalysis, analysisString, metricKey, }); const onPlotsModalSaveHandler = useCallback( - (data, plotIndex) => + (data: Parameters[0]["data"], plotIndex?: number) => onPlotsModalSave({ data, plotIndex, analysis, analysisString, - dispatch, + dispatch: filesDispatch, setAnalysis, }), - [analysis, analysisString, dispatch], + [analysis, analysisString, filesDispatch], ); const onPlotsModalDeleteHandler = (indexToDelete: number) => onPlotsModalDelete({ indexToDelete, - dispatch, + dispatch: filesDispatch, setAnalysis, analysisString, analysis, diff --git a/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewerActionButtons.tsx b/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewerActionButtons.tsx index 3a27cbae..d157def9 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewerActionButtons.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/AnalysisViewerActionButtons.tsx @@ -13,13 +13,13 @@ import { SimpleTooltip } from "../SimpleTooltip"; import "./TabListActionButtons.scss"; -interface ListItemProps { +type ListItemProps = { icon: ReactElement; tooltipContent: ReactFragment; onClick?: MouseEventHandler; listIndex: number; disabled?: boolean; -} +}; const ListItem: FC = ({ icon, diff --git a/apps/sim-core/packages/core/src/components/Analysis/ButtonCallToAction.spec.tsx b/apps/sim-core/packages/core/src/components/Analysis/ButtonCallToAction.spec.tsx index 81cbcb5e..77abfeae 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/ButtonCallToAction.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/ButtonCallToAction.spec.tsx @@ -1,31 +1,18 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { ModalProvider } from "react-modal-hook"; import { ButtonCallToAction } from "./ButtonCallToAction"; import { ErrorBoundary } from "../ErrorBoundary"; -import { mockProject } from "../../features/project/mocks"; -import { setProjectWithMeta } from "../../features/actions"; -import { store } from "../../features/store"; it("renders without crashing", () => { - const div = document.createElement("div"); - - //@ts-expect-error Redux types need to be repaired. - store.dispatch(setProjectWithMeta(mockProject)); - - ReactDOM.render( - - - - -

Testing

-
-
-
-
, - div, + render( + + + +

Testing

+
+
+
, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Analysis/HelpParagraph.tsx b/apps/sim-core/packages/core/src/components/Analysis/HelpParagraph.tsx index 3c7e65f6..08377569 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/HelpParagraph.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/HelpParagraph.tsx @@ -2,9 +2,9 @@ import React, { FC } from "react"; import { IconHelpCircleOutline } from "../Icon/HelpCircleOutline"; -interface HelpParagraphProps { +type HelpParagraphProps = { text: string; -} +}; export const HelpParagraph: FC = ({ text }) => (
diff --git a/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.spec.tsx b/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.spec.tsx index d48339b1..665b5764 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.spec.tsx @@ -1,14 +1,10 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { ModalProvider } from "react-modal-hook"; import { ComparisonTypes, Operation, OperationTypes } from "./types"; import { ErrorBoundary } from "../ErrorBoundary"; import { OutputMetricsGrid } from "./OutputMetricsGrid"; -import { mockProject } from "../../features/project/mocks"; -import { setProjectWithMeta } from "../../features/actions"; -import { store } from "../../features/store"; const noop = () => {}; const operations: Operation[] = [ @@ -23,24 +19,15 @@ const operations: Operation[] = [ const metrics = { metricName: operations }; it("renders without crashing", () => { - const div = document.createElement("div"); - - //@ts-expect-error Redux types need to be repaired. - store.dispatch(setProjectWithMeta(mockProject)); - - ReactDOM.render( - - - - - - - , - div, + render( + + + + + , ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.tsx b/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.tsx index 77d706bd..f92b12f6 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/OutputMetricsGrid.tsx @@ -1,7 +1,7 @@ import React, { FC, useReducer } from "react"; import { useModal } from "react-modal-hook"; import classNames from "classnames"; -import { omit } from "lodash"; +import { omit } from "lodash-es"; import { IconAddDatapoint } from "../Icon/AddDatapoint"; import { IconContentDuplicate } from "../Icon/ContentDuplicate"; diff --git a/apps/sim-core/packages/core/src/components/Analysis/PlotsTab.tsx b/apps/sim-core/packages/core/src/components/Analysis/PlotsTab.tsx index dc950d90..a8bf1efa 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/PlotsTab.tsx +++ b/apps/sim-core/packages/core/src/components/Analysis/PlotsTab.tsx @@ -1,5 +1,4 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; import { AnalysisViewerPlotsTabProps } from "./types"; import { ButtonCallToAction } from "./ButtonCallToAction"; @@ -7,7 +6,7 @@ import { HelpParagraph } from "./HelpParagraph"; import { IconCreatePlot } from "../Icon/CreatePlot"; import { PlotViewer } from "../PlotViewer/PlotViewer"; import { Scope, useScopes } from "../../features/scopes"; -import { selectEmbedded } from "../../features/viewer/selectors"; +import { useViewer } from "../../features/viewer/ViewerContext"; export const PlotsTab: FC = ({ analysisPlotsDataAvailable, @@ -21,7 +20,7 @@ export const PlotsTab: FC = ({ readonly, }) => { const { canEdit, canLogin } = useScopes(Scope.edit, Scope.login); - const embedded = useSelector(selectEmbedded); + const { embedded } = useViewer(); if (!analysisMode) { return embedded ? null : ( diff --git a/apps/sim-core/packages/core/src/components/Analysis/TabListActionButtons.scss b/apps/sim-core/packages/core/src/components/Analysis/TabListActionButtons.scss index cc063943..679fd2d2 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/TabListActionButtons.scss +++ b/apps/sim-core/packages/core/src/components/Analysis/TabListActionButtons.scss @@ -2,7 +2,8 @@ max-width: calc( (var(--analysis-tab-container-width) - 10px) - var( --AnalysisViewer__ActionButtons__Tooltip--index - ) * 38px + ) * + 38px ); min-width: 0; --clip-y-below: -50px; diff --git a/apps/sim-core/packages/core/src/components/Analysis/modals.ts b/apps/sim-core/packages/core/src/components/Analysis/modals.ts index 2cd69757..c36537ba 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/modals.ts +++ b/apps/sim-core/packages/core/src/components/Analysis/modals.ts @@ -1,4 +1,5 @@ -import { omit } from "lodash"; +import { PlotDefinition } from "@hashintel/engine-web"; +import { omit } from "lodash-es"; import { ChartTypes, Operation, Plot, YAxisItemType } from "./types"; import { ParsedAnalysis } from "../../features/files/types"; @@ -7,17 +8,17 @@ import { updateFile } from "../../features/files/slice"; export const MAGIC_STEPS_KEY = "Use steps on the X Axis"; -interface ModalsBaseProps { +type ModalsBaseProps = { dispatch: Function; setAnalysis: Function; analysis: any; analysisString?: string; -} +}; -interface OutputMetricsModalSubmitType { +type OutputMetricsModalSubmitType = { title: string; operations: Operation[]; -} +}; type OnOutputMetricsModalSaveInputType = ModalsBaseProps & { data: OutputMetricsModalSubmitType; @@ -36,51 +37,51 @@ type OnPlotsModalDeleteType = ModalsBaseProps & { indexToDelete: number; }; -interface PlotsModalChartTypeOption { +type PlotsModalChartTypeOption = { value: string; label: string; -} +}; -interface PlotsModalYAxisItemType { +type PlotsModalYAxisItemType = { name: string; metric: string; -} +}; -interface PlotsModalXAxisItemType { +type PlotsModalXAxisItemType = { name: string; metric: string; -} +}; -interface PlotsModalLayoutType { +type PlotsModalLayoutType = { width: string; height: string; -} +}; -interface PlotsModalPositionType { +type PlotsModalPositionType = { x: string; y: string; -} +}; -interface PlotsModalSubmitType { +type PlotsModalSubmitType = { title: string; chartType: PlotsModalChartTypeOption; yitems?: PlotsModalYAxisItemType[]; xitems?: PlotsModalXAxisItemType[]; layout: PlotsModalLayoutType; position: PlotsModalPositionType; -} +}; type OnPlotsModalSaveType = ModalsBaseProps & { data: PlotsModalSubmitType; plotIndex?: number; }; -interface saveToAnalysisFile { +type saveToAnalysisFile = { dispatch: Function; setAnalysis: Function; analysisString?: string; newValues: any; -} +}; const saveToAnalysisFile = ({ dispatch, @@ -159,8 +160,8 @@ export const onDuplicateMetric = ({ // Reads the data definition and transforms it to a format understood // by the Plots modal export const getYAxisItemsFromDataDefinition = ( - input: any, -): YAxisItemType[] => { + input: PlotDefinition & any, +): Array => { if (!input.type && input[ChartTypes.timeseries]) { return input.timeseries.map((metric: any) => ({ name: metric, @@ -195,8 +196,8 @@ export const getYAxisItemsFromDataDefinition = ( // Reads the data definition and transforms it to a format understood // by the Plots modal export const getXAxisItemsFromDataDefinition = ( - input: any, -): YAxisItemType[] => { + input: PlotDefinition & any, +): Array => { if (!input.type) { return input.data; } @@ -210,13 +211,16 @@ export const getXAxisItemsFromDataDefinition = ( ); }; -export const getPlotTypeFromDataDefinition = (input: any): string => - input.type ?? ChartTypes.timeseries; +export const getPlotTypeFromDataDefinition = ( + input: PlotDefinition & any, +): string => input.type ?? ChartTypes.timeseries; const chartItemLabel = (item: { name?: string; metric?: string }) => item.name ?? item.metric; -export const transformPlotDataBasedOnChartType = (input: any) => { +export const transformPlotDataBasedOnChartType = ( + input: PlotDefinition & any, +) => { const result = Object.assign({}, input); switch (input.type) { // http://localhost:8080/@hash/city-infection-model/6.1.1 @@ -261,12 +265,12 @@ export const transformPlotDataBasedOnChartType = (input: any) => { break; case ChartTypes.line: - case ChartTypes.scatter: { + case ChartTypes.scatter: // this assumes we have both X and Y OR we have only Y and X=steps const hasMagicStepsKey = input.data?.xitems.length === 1 && input.data.xitems[0].metric === MAGIC_STEPS_KEY; - const hasYItems = input.data?.yitems.length > 0 ?? false; + const hasYItems = (input.data?.yitems.length ?? 0) > 0; const hasXItems = hasMagicStepsKey ? false : input.data?.xitems.length > 0; @@ -290,7 +294,7 @@ export const transformPlotDataBasedOnChartType = (input: any) => { } break; - } + case ChartTypes.bar: default: result.data = input.data?.yitems?.map((item: any) => ({ diff --git a/apps/sim-core/packages/core/src/components/Analysis/types.ts b/apps/sim-core/packages/core/src/components/Analysis/types.ts index 984f020d..04482f88 100644 --- a/apps/sim-core/packages/core/src/components/Analysis/types.ts +++ b/apps/sim-core/packages/core/src/components/Analysis/types.ts @@ -1,27 +1,27 @@ import { MouseEvent } from "react"; -import { PlotParams } from "react-plotly.js"; +import type { PlotParams } from "react-plotly.js"; import { AnalysisMode } from "../../features/simulator/simulate/enum"; import { ReactSelectOption } from "../Dropdown/types"; -export interface AnalysisProps { +export type AnalysisProps = { currentStep: number; visible?: boolean; -} +}; -export interface AnalysisViewerPlotsTabProps { +export type AnalysisViewerPlotsTabProps = { analysisPlotsDataAvailable: boolean; analysisOutputMetricsDataAvailable: boolean; currentStep: number; - outputs: Record; + outputs: { [index: string]: any[] }; analysisMode?: AnalysisMode | null; onPlotsModalSaveHandler: Function; onPlotsModalDeleteHandler: Function; showPlotsModal: (event: MouseEvent) => void; readonly: boolean; -} +}; -export interface AnalysisViewerOutputMetricsTabProps { +export type AnalysisViewerOutputMetricsTabProps = { analysisOutputMetricsDataAvailable: boolean; showOutputMetricsModal: (event: MouseEvent) => void; analysis: any; @@ -29,7 +29,7 @@ export interface AnalysisViewerOutputMetricsTabProps { onOutputMetricsModalDeleteHandler: Function; onDuplicateMetricHandler: Function; readonly: boolean; -} +}; export type OutputPlotProps = PlotParams & { key: string; @@ -42,12 +42,12 @@ export enum ButtonCallToActionType { PLOTS = "PLOTS", } -export interface ButtonCallToActionProps { +export type ButtonCallToActionProps = { children: JSX.Element | JSX.Element[]; onClick?: (event: MouseEvent) => void; -} +}; -export interface AnalysisViewerActionButtonsProps { +export type AnalysisViewerActionButtonsProps = { canCreateNewPlot?: boolean; showOutputMetricsModal: (event: MouseEvent) => void; showPlotsModal: (event: MouseEvent) => void; @@ -57,7 +57,7 @@ export interface AnalysisViewerActionButtonsProps { * it */ canEdit: true; -} +}; export enum ComparisonTypes { eq = "eq", @@ -78,7 +78,7 @@ export enum OperationTypes { mean = "mean", } -export interface OperationItemProps { +export type OperationItemProps = { operation: Operation; index: number; onDelete: (event: MouseEvent) => void; @@ -86,33 +86,33 @@ export interface OperationItemProps { permittedOperations: ReactSelectOption[]; // the operations that preceed this one. hideDelete?: boolean; behaviorKeysOptions?: ReactSelectOption[]; // used for "field" -} +}; -export interface Operation { +export type Operation = { op: OperationTypes; field?: string; comparison?: ComparisonTypes; value?: any; -} +}; -export interface OutputMetricsGridProps { +export type OutputMetricsGridProps = { onOutputMetricsModalSave: Function; - metrics?: Record; + metrics?: { [index: string]: Operation[] }; onOutputMetricsModalDelete?: Function; onDuplicateMetric?: Function; sizeClassname?: string; readonly: boolean; -} +}; -interface PlotLayout { +type PlotLayout = { width: string; height: string; -} +}; -interface PlotPosition { +type PlotPosition = { x: string; y: string; -} +}; enum PlotType { timeseries, @@ -121,39 +121,39 @@ enum PlotType { line, } -interface PlotData { +type PlotData = { y: string; name: string; -} +}; -export interface Plot { +export type Plot = { title: string; layout: PlotLayout; position: PlotPosition; type?: PlotType; data?: PlotData[]; timeseries?: string[]; -} +}; -export interface AnalysisObject { - outputs: Record; +export type AnalysisObject = { + outputs: { [index: string]: Operation[] }; plots: Plot[]; -} +}; -export interface AnalysisState { +export type AnalysisState = { lastAnalysisString?: any; analysis?: AnalysisObject; error: any; -} +}; -export interface OnOutputMetricsModalSaveType { +export type OnOutputMetricsModalSaveType = { title: string; operations: Operation[]; -} +}; -export interface OnOutputMetricsModalSaveProps { +export type OnOutputMetricsModalSaveProps = { data: OnOutputMetricsModalSaveType; -} +}; export enum ChartTypes { area = "area", @@ -171,18 +171,18 @@ export enum ChartTypes { // scatter3d = "scatter3d", } -interface AxisItemType { +type AxisItemType = { name: string; metric: string; -} +}; export type YAxisItemType = AxisItemType; export type XAxisItemType = AxisItemType; -export interface YAxisItemProps { +export type YAxisItemProps = { item: YAxisItemType; index: number; metricKeysOptions: ReactSelectOption[]; onDelete: (event: MouseEvent) => void; onChange: Function; hideDelete: boolean; -} +}; diff --git a/apps/sim-core/packages/core/src/components/App/App.tsx b/apps/sim-core/packages/core/src/components/App/App.tsx index 1132412e..c6d98311 100644 --- a/apps/sim-core/packages/core/src/components/App/App.tsx +++ b/apps/sim-core/packages/core/src/components/App/App.tsx @@ -1,34 +1,56 @@ -import React, { FC } from "react"; -import { Provider } from "react-redux"; +import React, { FC, PropsWithChildren } from "react"; import { ModalProvider } from "react-modal-hook"; -import { Store } from "@reduxjs/toolkit"; -import { RecoilRoot } from "recoil"; import { ErrorBoundary } from "../ErrorBoundary"; +import { ExamplesProvider } from "../../features/examples/ExamplesContext"; +import { FilesProvider } from "../../features/files/FilesContext"; import { FontsPreloader } from "../FontsPreloader"; +import { ProjectProvider } from "../../features/project/ProjectContext"; import { MonacoContainerProvider } from "../TabbedEditor/hooks"; +import { SceneProvider } from "../AgentScene/state/SceneContext"; +import { SearchProvider } from "../../features/search/SearchContext"; import { SimulatorProvider } from "../../features/simulator/context"; +import { MonacoModelSync } from "../../features/monaco/MonacoModelSync"; +import { StoreSync } from "../../features/simulator/simulate/StoreSync"; +import { ToastProvider } from "../../features/toast/ToastContext"; +import { UserProvider } from "../../features/user/UserContext"; +import { ViewerProvider } from "../../features/viewer/ViewerContext"; import "./App.css"; -interface AppProps { - store: Store; -} - -export const App: FC = ({ store, children }) => ( - - - - - - - -
{children}
-
-
-
-
-
-
-
+/** + * Provider ordering: ProjectProvider is below Viewer, Toast, Files so it can + * coordinate setProjectWithMeta across those contexts. The simulator keeps + * its own Redux store for performance; StoreSync bridges app contexts to it. + */ +export const App: FC = ({ children }) => ( + + + + + + + + + + + + + + + +
{children}
+
+
+
+
+
+
+
+
+
+
+
+
+
); diff --git a/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysFieldForm.tsx b/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysFieldForm.tsx index 0031f946..4967c7ce 100644 --- a/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysFieldForm.tsx +++ b/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysFieldForm.tsx @@ -1,7 +1,6 @@ import React, { FC, useState } from "react"; -import { batch } from "react-redux"; import classNames from "classnames"; -import { debounce } from "lodash"; +import { debounce } from "lodash-es"; import { BehaviorKeysField, @@ -76,10 +75,8 @@ export const BehaviorKeysFieldForm: FC = ({ value={fieldName} onChange={(evt) => { const value = evt.target.value; - batch(() => { - setIsErrorOpen(false); - onNameChange(value); - }); + setIsErrorOpen(false); + onNameChange(value); }} onBlur={() => { onNameCommit(); diff --git a/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysForm.tsx b/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysForm.tsx index 1e7a4967..470aa92a 100644 --- a/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysForm.tsx +++ b/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysForm.tsx @@ -1,6 +1,5 @@ import React, { FC, useEffect, useRef, useState } from "react"; -import { batch, useDispatch, useSelector } from "react-redux"; -import produce, { Draft } from "immer"; +import { produce, Draft } from "immer"; import { BehaviorKeysDraftField, @@ -18,15 +17,8 @@ import { Projection } from "./types"; import { ScrollFadeShadow } from "../ScrollFade/ScrollFadeShadow"; import { SimpleTooltip } from "../SimpleTooltip"; import { addField } from "./utils"; -import { - parseAndShowBehaviorKeys, - updateBehaviorKeysDynamicAccess, -} from "../../features/files/slice"; -import { - selectBehaviorKeysDynamicAccess, - selectSharedBehaviorKeyFieldNames, -} from "../../features/files/selectors"; -import { useAbortingDispatch } from "../../hooks/useAbortingDispatch"; +import { selectSharedBehaviorKeyFieldNames } from "../../features/files/selectors"; +import { useFiles, useFilesSelector } from "../../features/files/FilesContext"; import { useScrollState } from "../../hooks/useScrollState"; import "./BehaviorKeysForm.scss"; @@ -67,10 +59,14 @@ export const BehaviorKeysForm: FC<{ const onDataChangeRef = useRef(onDataChange); const clashes = calculateRowClashes(data.rows); - const dispatch = useDispatch(); - const dynamicAccess = useSelector(selectBehaviorKeysDynamicAccess); + const { + updateBehaviorKeysDynamicAccess, + behaviorKeysDynamicAccess: dynamicAccess, + } = useFiles(); - const sharedBehaviorKeyNames = useSelector(selectSharedBehaviorKeyFieldNames); + const sharedBehaviorKeyNames = useFilesSelector( + selectSharedBehaviorKeyFieldNames, + ); const lockedNames = projection.length === 0 ? sharedBehaviorKeyNames : []; const formDisabled = projection.length === 0 @@ -96,8 +92,20 @@ export const BehaviorKeysForm: FC<{ const listRef = useRef(null); const [scrollStateRef, contentRemaining] = useScrollState(); - const [dispatchParseAndShowBehaviorKeys, isParsingDisabled] = - useAbortingDispatch(parseAndShowBehaviorKeys, [autosuggest]); + const { handleParseAndShowBehaviorKeys } = useFiles(); + const [isParsingDisabled, setIsParsingDisabled] = useState(false); + const dispatchParseAndShowBehaviorKeys = async ({ + fileId, + }: { + fileId: string; + }) => { + setIsParsingDisabled(true); + try { + await handleParseAndShowBehaviorKeys(fileId); + } finally { + setIsParsingDisabled(false); + } + }; const focusLast = () => { const fields = @@ -112,7 +120,7 @@ export const BehaviorKeysForm: FC<{ const onAddField = () => { setData((draft) => addField(draft, projection.length === 0)); - setTimeout(() => { + setImmediate(() => { if (listRef.current) { scrollToEnd(listRef.current); focusLast(); @@ -131,12 +139,7 @@ export const BehaviorKeysForm: FC<{ disabled={disabled} id="dynamicAccessCheckbox" onChange={(evt) => { - dispatch( - updateBehaviorKeysDynamicAccess({ - fileId, - dynamicAccess: evt.target.checked, - }), - ); + updateBehaviorKeysDynamicAccess(fileId, evt.target.checked); }} /> Access all fields defined in other behaviors @@ -242,10 +245,8 @@ export const BehaviorKeysForm: FC<{ }} onNameCommit={() => { if (draftData) { - batch(() => { - onDataChangeRef.current(draftData.draft.rows); - setDraftData(null); - }); + onDataChangeRef.current(draftData.draft.rows); + setDraftData(null); } }} disabled={disabled || formDisabled} diff --git a/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysRow.tsx b/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysRow.tsx index 583b2654..f6677345 100644 --- a/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysRow.tsx +++ b/apps/sim-core/packages/core/src/components/BehaviorKeys/BehaviorKeysRow.tsx @@ -1,6 +1,6 @@ import React, { FC, HTMLProps } from "react"; import classNames from "classnames"; -import omit from "lodash/omit"; +import omit from "lodash-es/omit"; import "./BehaviorKeysRow.scss"; diff --git a/apps/sim-core/packages/core/src/components/BehaviorKeys/project.ts b/apps/sim-core/packages/core/src/components/BehaviorKeys/project.ts index 2945ed7f..12f313c8 100644 --- a/apps/sim-core/packages/core/src/components/BehaviorKeys/project.ts +++ b/apps/sim-core/packages/core/src/components/BehaviorKeys/project.ts @@ -1,4 +1,4 @@ -import produce from "immer"; +import { produce } from "immer"; import { BehaviorKeysDraftFieldWithRows, diff --git a/apps/sim-core/packages/core/src/components/BehaviorKeys/types.ts b/apps/sim-core/packages/core/src/components/BehaviorKeys/types.ts index 7620c708..bc48eda7 100644 --- a/apps/sim-core/packages/core/src/components/BehaviorKeys/types.ts +++ b/apps/sim-core/packages/core/src/components/BehaviorKeys/types.ts @@ -2,14 +2,14 @@ import { Draft } from "immer"; import { BehaviorKeysDraftField } from "../../features/files/behaviorKeys"; -export interface ProjectionItem { +export type ProjectionItem = { label: string; idx: number; -} +}; export type Projection = ProjectionItem[]; -export interface BehaviorKeysFieldFormProps { +export type BehaviorKeysFieldFormProps = { fieldName: string; clash: boolean; projection: ProjectionItem[]; @@ -29,4 +29,4 @@ export interface BehaviorKeysFieldFormProps { disabled: boolean; typeDisabled: boolean; emptyName: boolean; -} +}; diff --git a/apps/sim-core/packages/core/src/components/DataLoader/DataLoader.tsx b/apps/sim-core/packages/core/src/components/DataLoader/DataLoader.tsx index c6383b14..37dcb031 100644 --- a/apps/sim-core/packages/core/src/components/DataLoader/DataLoader.tsx +++ b/apps/sim-core/packages/core/src/components/DataLoader/DataLoader.tsx @@ -25,14 +25,14 @@ const paginationPx = 26; const rowHeightRem = 1.2; const rowBorderPx = 1; -interface DataLoaderProps { +type DataLoaderProps = { url: string; editorInstance: EditorInstance | undefined; manifestId: string | null; file: HcDatasetFile; setDidFallback: Dispatch>; containerHeight?: number; -} +}; export const DataLoader: FC = ({ url, diff --git a/apps/sim-core/packages/core/src/components/DataLoader/hooks/useDataLoaderParser.ts b/apps/sim-core/packages/core/src/components/DataLoader/hooks/useDataLoaderParser.ts index 772d7354..27bc1aff 100644 --- a/apps/sim-core/packages/core/src/components/DataLoader/hooks/useDataLoaderParser.ts +++ b/apps/sim-core/packages/core/src/components/DataLoader/hooks/useDataLoaderParser.ts @@ -9,7 +9,6 @@ import { import type { DataLoaderParserReducer, DataLoaderParserState } from "../types"; import { HcDatasetFile } from "../../../features/files/types"; import { jsonToRows } from "../utils"; -import { getErrorMessage } from "../../../features/utils"; export const loadingMessage = "Loading..."; export const successMessage = "Success!"; @@ -83,10 +82,10 @@ export const useDataLoaderParser = ( } ({ pathname, format } = result); - } catch (error) { + } catch (error: any) { dispatch({ type: "invalidUrl", - payload: { url, errorMessage: getErrorMessage(error) }, + payload: { url, errorMessage: error.message }, }); return; } diff --git a/apps/sim-core/packages/core/src/components/DataLoader/types.ts b/apps/sim-core/packages/core/src/components/DataLoader/types.ts index dfaa9890..159a7b38 100644 --- a/apps/sim-core/packages/core/src/components/DataLoader/types.ts +++ b/apps/sim-core/packages/core/src/components/DataLoader/types.ts @@ -1,42 +1,42 @@ import { Reducer } from "react"; -interface DataLoaderParserMessage { +type DataLoaderParserMessage = { message: string; -} +}; -interface DataLoaderParserData { +type DataLoaderParserData = { headings?: string[]; records?: any[][]; contents?: string; -} +}; export type DataLoaderParserState = DataLoaderParserData & DataLoaderParserMessage; -interface DataLoaderParserActionSuccess { +type DataLoaderParserActionSuccess = { type: "success"; payload: DataLoaderParserData; -} +}; -interface DataLoaderParserActionInvalidUrl { +type DataLoaderParserActionInvalidUrl = { type: "invalidUrl"; payload: { url: string; errorMessage: string }; -} +}; -interface DataLoaderParserActionUnparseableValue { +type DataLoaderParserActionUnparseableValue = { type: "unparseableValue"; payload: { pathname: string; errorMessage: string }; -} +}; -interface DataLoaderParserActionUnsupportedExtension { +type DataLoaderParserActionUnsupportedExtension = { type: "unsupportedExtension"; payload: { pathname: string; ext: string }; -} +}; -interface DataLoaderParserActionLoadingError { +type DataLoaderParserActionLoadingError = { type: "loadingError"; payload: { pathname: string; errorMessage: string }; -} +}; type DataLoaderParserAction = | DataLoaderParserActionSuccess diff --git a/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.spec.tsx b/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.spec.tsx index 37b83993..5ed7cf6f 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.spec.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.spec.tsx @@ -1,10 +1,12 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DataTableBody } from "./DataTableBody"; it("renders without crashing", () => { - const table = document.createElement("table"); - ReactDOM.render(, table); - ReactDOM.unmountComponentAtNode(table); + render( + + +
, + ); }); diff --git a/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.tsx b/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.tsx index b21c0bcc..07bd2a35 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Body/DataTableBody.tsx @@ -4,10 +4,10 @@ import { DataTableRow } from ".."; import "./DataTableBody.css"; -interface DataTableBodyProps { +type DataTableBodyProps = { beginIndex: number; records: any[][]; -} +}; export const DataTableBody: FC = memo( ({ beginIndex, records }) => ( diff --git a/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.spec.tsx b/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.spec.tsx index 930915bf..a3858e3b 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.spec.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.spec.tsx @@ -1,10 +1,16 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DataTableCell } from "./DataTableCell"; it("renders without crashing", () => { - const tr = document.createElement("tr"); - ReactDOM.render(, tr); - ReactDOM.unmountComponentAtNode(tr); + render( + + + + + + +
, + ); }); diff --git a/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.tsx b/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.tsx index 7e9d0a0a..380bd358 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Cell/DataTableCell.tsx @@ -5,9 +5,9 @@ import { IconCheck, IconClose } from "../../Icon"; import "./DataTableCell.css"; -interface DataTableCellProps { +type DataTableCellProps = { cellValue: any; -} +}; enum TypeOf { Undefined = "undefined", diff --git a/apps/sim-core/packages/core/src/components/DataTable/DataTable.spec.tsx b/apps/sim-core/packages/core/src/components/DataTable/DataTable.spec.tsx index 7831bf13..09cdc5c7 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/DataTable.spec.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/DataTable.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DataTable } from "./DataTable"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/DataTable/DataTable.tsx b/apps/sim-core/packages/core/src/components/DataTable/DataTable.tsx index bbc209b5..d11fe63f 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/DataTable.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/DataTable.tsx @@ -4,11 +4,11 @@ import { DataTableBody, DataTableHead, DataTablePagination } from "."; import "./DataTable.css"; -interface DataTableProps { +type DataTableProps = { headings: string[]; records: any[][]; recordsPerPage?: number; -} +}; export const DataTable: FC = memo( ({ headings, records, recordsPerPage = 50 }) => { diff --git a/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.spec.tsx b/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.spec.tsx index a4697778..620734fb 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.spec.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.spec.tsx @@ -1,10 +1,12 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DataTableHead } from "./DataTableHead"; it("renders without crashing", () => { - const table = document.createElement("table"); - ReactDOM.render(, table); - ReactDOM.unmountComponentAtNode(table); + render( + + +
, + ); }); diff --git a/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.tsx b/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.tsx index c0730ed3..d71aba1b 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Head/DataTableHead.tsx @@ -1,11 +1,11 @@ import React, { FC, memo } from "react"; -import { kebabCase } from "lodash"; +import { kebabCase } from "lodash-es"; import "./DataTableHead.css"; -interface DataTableHeadProps { +type DataTableHeadProps = { headings: string[]; -} +}; export const DataTableHead: FC = memo(({ headings }) => ( <> diff --git a/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.spec.tsx b/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.spec.tsx index 3af3ef96..1a9c32d3 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.spec.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.spec.tsx @@ -1,17 +1,14 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DataTablePagination } from "./DataTablePagination"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render( {}} totalPages={1} />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.tsx b/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.tsx index ad7ffc97..e7b66b3b 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -4,11 +4,11 @@ import { FancyButton } from "../../Fancy"; import "./DataTablePagination.css"; -interface DataTablePaginationProps { +type DataTablePaginationProps = { currentPage: number; setCurrentPage: Dispatch>; totalPages: number; -} +}; export const DataTablePagination: FC = memo( ({ currentPage, setCurrentPage, totalPages }) => ( diff --git a/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.spec.tsx b/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.spec.tsx index 2de326b9..41b25cd7 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.spec.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.spec.tsx @@ -1,10 +1,14 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DataTableRow } from "./DataTableRow"; it("renders without crashing", () => { - const tbody = document.createElement("tbody"); - ReactDOM.render(, tbody); - ReactDOM.unmountComponentAtNode(tbody); + render( + + + + +
, + ); }); diff --git a/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.tsx b/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.tsx index 940cf147..f6b9d705 100644 --- a/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.tsx +++ b/apps/sim-core/packages/core/src/components/DataTable/Row/DataTableRow.tsx @@ -2,10 +2,10 @@ import React, { FC, memo } from "react"; import { DataTableCell } from "../Cell"; -interface DataTableRowProps { +type DataTableRowProps = { rowIndex: number; record: any[]; -} +}; export const DataTableRow: FC = memo( ({ rowIndex, record }) => ( diff --git a/apps/sim-core/packages/core/src/components/Dropdown/Dropdown.spec.tsx b/apps/sim-core/packages/core/src/components/Dropdown/Dropdown.spec.tsx index 68d9095c..a737eacc 100644 --- a/apps/sim-core/packages/core/src/components/Dropdown/Dropdown.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Dropdown/Dropdown.spec.tsx @@ -1,13 +1,10 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { Dropdown } from "./Dropdown"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render( {}} />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.spec.tsx b/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.spec.tsx index 744fe0d3..41763dc7 100644 --- a/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { DropdownMenuList } from "./DropdownMenuList"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render({[]}, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.tsx b/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.tsx index e8aa0c5d..94808af9 100644 --- a/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.tsx +++ b/apps/sim-core/packages/core/src/components/Dropdown/MenuList/DropdownMenuList.tsx @@ -1,11 +1,17 @@ -import React, { FC, useRef, useEffect, Children } from "react"; +import React, { + FC, + PropsWithChildren, + useRef, + useEffect, + Children, +} from "react"; import { VariableSizeList } from "react-window"; import type { ReactSelectOption } from "../types"; -interface DropdownMenuListProps { +type DropdownMenuListProps = { options: ReactSelectOption[]; -} +}; /** * these numbers are kinda magic numbers, it's known and tolerated for @@ -20,7 +26,7 @@ const SUB_LABEL_AVG_SIZE = 46; const SUB_LABEL_MAX_SIZE = 60; const LIST_HEIGHT = 200; -export const DropdownMenuList: FC = ({ +export const DropdownMenuList: FC> = ({ options, children, }) => { diff --git a/apps/sim-core/packages/core/src/components/Dropdown/types.ts b/apps/sim-core/packages/core/src/components/Dropdown/types.ts index a3a5093c..6365ba04 100644 --- a/apps/sim-core/packages/core/src/components/Dropdown/types.ts +++ b/apps/sim-core/packages/core/src/components/Dropdown/types.ts @@ -1,12 +1,12 @@ import { SelectComponents } from "react-select/src/components"; -export interface ReactSelectOption { +export type ReactSelectOption = { label: string; subLabel?: string; value: string; -} +}; -export interface DropdownProps { +export type DropdownProps = { options: ReactSelectOption[]; value: ReactSelectOption | ReactSelectOption[] | undefined; onChange: (option: any) => void; @@ -28,4 +28,4 @@ export interface DropdownProps { largeList?: boolean; className?: string; creatableIsCaseInsensitive?: boolean; -} +}; diff --git a/apps/sim-core/packages/core/src/components/EmbedApp/EmbedApp.tsx b/apps/sim-core/packages/core/src/components/EmbedApp/EmbedApp.tsx index 899cb6fa..5d92f719 100644 --- a/apps/sim-core/packages/core/src/components/EmbedApp/EmbedApp.tsx +++ b/apps/sim-core/packages/core/src/components/EmbedApp/EmbedApp.tsx @@ -1,18 +1,13 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; import { HashCoreAccessGate } from "../HashCore/AccessGate/HashCoreAccessGate"; import { HashCoreSection } from "../HashCore/Section/HashCoreSection"; -import { - selectAccessGate, - selectProjectLoaded, -} from "../../features/project/selectors"; +import { useProject } from "../../features/project/ProjectContext"; import "./EmbedApp.scss"; export const EmbedApp: FC = () => { - const projectLoaded = useSelector(selectProjectLoaded); - const accessGate = useSelector(selectAccessGate); + const { projectLoaded, accessGate } = useProject(); if (accessGate) { return ; diff --git a/apps/sim-core/packages/core/src/components/EmbedApp/bootEmbed.tsx b/apps/sim-core/packages/core/src/components/EmbedApp/bootEmbed.tsx index 757fcf54..9c9f6d16 100644 --- a/apps/sim-core/packages/core/src/components/EmbedApp/bootEmbed.tsx +++ b/apps/sim-core/packages/core/src/components/EmbedApp/bootEmbed.tsx @@ -10,54 +10,72 @@ import "../../util/api"; */ import "../OpenInCore/OpenInCore"; -import React from "react"; -import { render } from "react-dom"; +import React, { FC, useEffect, useRef } from "react"; +import { createRoot } from "react-dom/client"; import { App } from "../App"; import { BasicUser } from "../../util/api/types"; import { EmbedApp } from "./EmbedApp"; import { RemoteSimulationProject } from "../../features/project/types"; import { ValidatedEmbedParams } from "../../util/getEmbedParams"; -import { activateEmbedded } from "../../features/viewer/slice"; import { boot } from "../../boot"; -import { fetchProject } from "../../features/project/slice"; import { getUiQueryParams } from "../../hooks/useParameterisedUi"; -import { setBasicUser } from "../../features/user/slice"; -import { store } from "../../features/store"; +import { useProject } from "../../features/project/ProjectContext"; +import { useUser } from "../../features/user/UserContext"; +import { useViewer } from "../../features/viewer/ViewerContext"; -// @todo error handling -export const bootEmbed = async ( - params: ValidatedEmbedParams, - prefetchedProjectPromise: Promise, - basicUserPromise: Promise, -) => { - await boot(false); +interface EmbedBootstrapProps { + params: ValidatedEmbedParams; + basicUserPromise: Promise; +} + +/** + * Inner component that initializes embed state via context hooks on mount. + * This replaces the old pattern of dispatching Redux actions before render. + */ +const EmbedBootstrap: FC = ({ + params, + basicUserPromise, +}) => { + const { activateEmbedded } = useViewer(); + const { fetchProject } = useProject(); + const { setBasicUser } = useUser(); + const initialized = useRef(false); + + useEffect(() => { + if (initialized.current) return; + initialized.current = true; - const { tabs, view } = getUiQueryParams(); + const { tabs, view } = getUiQueryParams(); + activateEmbedded({ tabs, tab: view }); - store.dispatch(activateEmbedded({ tabs, tab: view })); + fetchProject({ + project: { pathWithNamespace: params.project, ref: params.ref }, + redirect: false, + }); - await Promise.all([ - store.dispatch( - //@ts-expect-error redux problems - fetchProject({ - project: { pathWithNamespace: params.project, ref: params.ref }, - prefetchedRemoteProject: prefetchedProjectPromise, - redirect: false, - access: params.access, - }), - ), basicUserPromise.then((basicUser) => { if (basicUser) { - return store.dispatch(setBasicUser(basicUser)); + setBasicUser(basicUser); } - }), - ]); + }); + }, [activateEmbedded, fetchProject, setBasicUser, params, basicUserPromise]); + + return ; +}; + +// @todo error handling +export const bootEmbed = async ( + params: ValidatedEmbedParams, + _prefetchedProjectPromise: Promise, + basicUserPromise: Promise, +) => { + await boot(false); - render( - - + const root = createRoot(document.getElementById("root")!); + root.render( + + , - document.getElementById("root"), ); }; diff --git a/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.spec.tsx b/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.spec.tsx index 27b08b02..f7f56e6a 100644 --- a/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.spec.tsx +++ b/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ErrorBoundary } from "./ErrorBoundary"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render({null}, div); - ReactDOM.unmountComponentAtNode(div); + render({null}); }); diff --git a/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.tsx b/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.tsx index d014e1e7..246b2ef2 100644 --- a/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/apps/sim-core/packages/core/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,14 +1,15 @@ import React, { Component, createContext, + ErrorInfo, FC, + PropsWithChildren, useContext, useMemo, useState, } from "react"; import { customAlphabet } from "nanoid"; -import { BasicDiscordWidget } from "../DiscordWidget/DiscordWidget"; import { BigModal } from "../Modal"; import { ErrorDetails } from "../ErrorDetails"; import { FancyButton } from "../Fancy"; @@ -41,17 +42,15 @@ const quotableId = (() => { .padStart(2, "0")}${generateHashEventId()}`; })(); -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface ErrorBoundaryProps {} -interface ErrorBoundaryState { +type ErrorBoundaryProps = {}; +type ErrorBoundaryState = { didError: boolean; errorName?: string; errorMessage?: string; errorStack?: string; - eventId: string | null; detailsHidden: boolean; hashEventId: string | null; -} +}; type TErrorBoundaryContext = { handlePromiseRejection: (promise: Promise) => void; @@ -78,7 +77,7 @@ export const useFatalError = () => useContext(ErrorBoundaryContext)!.fatalError; * * @see https://github.com/facebook/react/issues/14981#issuecomment-468460187 */ -const ErrorBoundaryContextProvider: FC = ({ children }) => { +const ErrorBoundaryContextProvider: FC = ({ children }) => { const [, catchError] = useState(); const contextValue = useMemo(() => { const fatalError = (err: any) => { @@ -125,11 +124,14 @@ export class ErrorBoundary extends Component< errorName: undefined, errorMessage: undefined, errorStack: undefined, - eventId: null, detailsHidden: true, hashEventId: null, }; + componentDidCatch(_error: Error, _errorInfo: ErrorInfo) { + // Error captured and displayed to user + } + render() { const { didError, @@ -161,7 +163,6 @@ export class ErrorBoundary extends Component<
troubleshooting guide @@ -194,7 +195,6 @@ export class ErrorBoundary extends Component< REFRESH PAGE -
) : ( diff --git a/apps/sim-core/packages/core/src/components/ErrorDetails/ErrorDetails.spec.tsx b/apps/sim-core/packages/core/src/components/ErrorDetails/ErrorDetails.spec.tsx index e3517de6..f7391168 100644 --- a/apps/sim-core/packages/core/src/components/ErrorDetails/ErrorDetails.spec.tsx +++ b/apps/sim-core/packages/core/src/components/ErrorDetails/ErrorDetails.spec.tsx @@ -1,18 +1,15 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ErrorDetails } from "./ErrorDetails"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render(
); diff --git a/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.spec.tsx b/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.spec.tsx index 22d36875..3292cb08 100644 --- a/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.spec.tsx +++ b/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.spec.tsx @@ -1,42 +1,35 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { ModalProvider } from "react-modal-hook"; import { Ext } from "../../../util/files/enums"; import { FileBannerShared } from "./FileBannerShared"; import type { HcSharedBehaviorFile } from "../../../features/files/types"; import { SimulationProject } from "../../../features/project/types"; -import { store } from "../../../features/store"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( - - - - - , - div, + render( + + + , ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.tsx b/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.tsx index 6628b0b1..427571b2 100644 --- a/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.tsx +++ b/apps/sim-core/packages/core/src/components/FileBanner/Shared/FileBannerShared.tsx @@ -1,5 +1,4 @@ import React, { FC, MouseEventHandler, useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { Ext } from "../../../util/files/enums"; import type { HcSharedBehaviorFile } from "../../../features/files/types"; @@ -8,17 +7,20 @@ import { LinkBehavior } from "../../Link/LinkBehavior"; import type { ParsedPath } from "../../../util/files/types"; import { SimulationProject } from "../../../features/project/types"; import { destinationPathInUse, parse } from "../../../util/files"; -import { forkOpenBehavior } from "../../../features/files/slice"; import { selectIdKindAndPathFromFiles } from "../../../features/files/selectors"; +import { + useFiles, + useFilesSelector, +} from "../../../features/files/FilesContext"; import { useModalNameBehavior } from "../../HashCore/Files/hooks/useModalNameBehavior"; import "../FileBanner.css"; import "./FileBannerShared.css"; -interface FileBannerSharedProps { +type FileBannerSharedProps = { file: HcSharedBehaviorFile; project: SimulationProject; -} +}; export const FileBannerShared: FC = ({ file, @@ -29,10 +31,10 @@ export const FileBannerShared: FC = ({ [file], ); - const dispatch = useDispatch(); + const { forkOpenBehavior } = useFiles(); const copy = (destination: ParsedPath) => { - dispatch(forkOpenBehavior({ source: file, destination, project })); + forkOpenBehavior({ source: file, destination, project }); }; const showModalNameBehavior = useModalNameBehavior( @@ -44,7 +46,7 @@ export const FileBannerShared: FC = ({ destination, ); - const files = useSelector(selectIdKindAndPathFromFiles); + const files = useFilesSelector(selectIdKindAndPathFromFiles); const onClick: MouseEventHandler = (evt) => { evt.preventDefault(); diff --git a/apps/sim-core/packages/core/src/components/FileBanner/Upgrade/FileBannerUpgrade.spec.tsx b/apps/sim-core/packages/core/src/components/FileBanner/Upgrade/FileBannerUpgrade.spec.tsx index 20081420..0eca7859 100644 --- a/apps/sim-core/packages/core/src/components/FileBanner/Upgrade/FileBannerUpgrade.spec.tsx +++ b/apps/sim-core/packages/core/src/components/FileBanner/Upgrade/FileBannerUpgrade.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { FileBannerUpgrade } from "./FileBannerUpgrade"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.spec.tsx b/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.spec.tsx index 44b8a58c..e9ffd538 100644 --- a/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.spec.tsx +++ b/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.spec.tsx @@ -1,10 +1,7 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; it("renders without crashing", () => { - const div = document.createElement("div"); - // TODO: figure out how to test things with @hashintel/engine-web in import path - ReactDOM.render(
, div); - ReactDOM.unmountComponentAtNode(div); + render(
); }); diff --git a/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.tsx b/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.tsx index 31c24d3b..0b55a42c 100644 --- a/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.tsx +++ b/apps/sim-core/packages/core/src/components/FileBanner/Wrapper/FileBannerWrapper.tsx @@ -1,7 +1,5 @@ import React, { FC } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { AppDispatch } from "../../../features/types"; import { Ext } from "../../../util/files/enums"; import { FileBannerBuiltin, @@ -10,40 +8,33 @@ import { FileBannerUpgrade, } from ".."; import { FileBannerPythonSafari } from "../PythonSafari"; -import { FileBannerSignIn } from "../SignIn/FileBannerSignIn"; import type { HcFile, HcSharedBehaviorFile, } from "../../../features/files/types"; import { HcFileKind } from "../../../features/files/enums"; -import { Scope, useScopes } from "../../../features/scopes"; -import { addDependencies } from "../../../features/files/slice"; +import { Scope, useScope } from "../../../features/scopes"; import { fetchDependencies } from "../../../util/api"; import { getTextModelRequired } from "../../../features/monaco"; -import { isReadOnly } from "../../../features/files/utils"; import { pyodideEnabled } from "../../../util/pyodideEnabled"; -import { selectAllFiles } from "../../../features/files/selectors"; -import { - selectCurrentProject, - selectCurrentProjectUrl, -} from "../../../features/project/selectors"; -import { store } from "../../../features/store"; +import { useFiles } from "../../../features/files/FilesContext"; +import { useProject } from "../../../features/project/ProjectContext"; -interface FileBannerWrapperProps { +type FileBannerWrapperProps = { file: HcFile; nextContents: string | null; setNextContents: (nextContents: string | null) => void; -} +}; export const FileBannerWrapper: FC = ({ file, nextContents, setNextContents, }) => { - const dispatch = useDispatch(); - const project = useSelector(selectCurrentProject); - const projectUrl = useSelector(selectCurrentProjectUrl); - const { canEdit, canLogin } = useScopes(Scope.edit, Scope.login); + const { handleAddDependencies, allFiles } = useFiles(); + const { currentProject: project, currentProjectUrl: projectUrl } = + useProject(); + const canEdit = useScope(Scope.edit); /** * show the Python/Safari banner for any `.py` file (even local) if Pyodide @@ -58,11 +49,7 @@ export const FileBannerWrapper: FC = ({ } if (!canEdit) { - if (canLogin && isReadOnly(file, false)) { - return ; - } else { - return null; - } + return null; } /** @@ -100,14 +87,11 @@ export const FileBannerWrapper: FC = ({ }} labelB={`Upgrade to (v${latestTag})`} onChooseB={async () => { - await dispatch( - //@ts-expect-error redux problems - addDependencies({ - [file.path.formatted]: latestTag, - }), - ); + await handleAddDependencies({ + [file.path.formatted]: latestTag, + }); - const nextFile = selectAllFiles(store.getState()).find( + const nextFile = allFiles.find( (potentialFile) => potentialFile.path.formatted === file.path.formatted && (potentialFile as HcSharedBehaviorFile).ref === file.latestTag, diff --git a/apps/sim-core/packages/core/src/components/FileName/FileNameWithIcon.tsx b/apps/sim-core/packages/core/src/components/FileName/FileNameWithIcon.tsx index fabee3f6..a2f16259 100644 --- a/apps/sim-core/packages/core/src/components/FileName/FileNameWithIcon.tsx +++ b/apps/sim-core/packages/core/src/components/FileName/FileNameWithIcon.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import { FileName } from "./FileName"; import { IconFileOutline } from "../Icon/FileOutline"; @@ -22,9 +22,11 @@ function getIcon(icon: IconType) { } } -export const FileNameWithIcon: FC<{ - icon: IconType; -}> = ({ icon, children }) => ( +export const FileNameWithIcon: FC< + PropsWithChildren<{ + icon: IconType; + }> +> = ({ icon, children }) => ( {getIcon(icon)}
{children}
diff --git a/apps/sim-core/packages/core/src/components/FileName/FileNameWithShortnameInner.tsx b/apps/sim-core/packages/core/src/components/FileName/FileNameWithShortnameInner.tsx index b8e6b185..410df723 100644 --- a/apps/sim-core/packages/core/src/components/FileName/FileNameWithShortnameInner.tsx +++ b/apps/sim-core/packages/core/src/components/FileName/FileNameWithShortnameInner.tsx @@ -5,11 +5,11 @@ import type { ParsedPath } from "../../util/files/types"; import "./FileNameWithShortnameInner.css"; -export interface FileNameWithShortnameProps { +export type FileNameWithShortnameProps = { current?: boolean; path: ParsedPath; hasTitle?: boolean; -} +}; export const FileNameWithShortnameInner: FC = ({ current = false, diff --git a/apps/sim-core/packages/core/src/components/FontsPreloader/FontsPreloader.tsx b/apps/sim-core/packages/core/src/components/FontsPreloader/FontsPreloader.tsx index a3a35333..297b4bc1 100644 --- a/apps/sim-core/packages/core/src/components/FontsPreloader/FontsPreloader.tsx +++ b/apps/sim-core/packages/core/src/components/FontsPreloader/FontsPreloader.tsx @@ -1,4 +1,11 @@ -import React, { FC, useEffect, useState, Fragment, CSSProperties } from "react"; +import React, { + FC, + PropsWithChildren, + useEffect, + useState, + Fragment, + CSSProperties, +} from "react"; import { createPortal } from "react-dom"; const fontsToPreload: [string, CSSProperties[]][] = Object.entries({ @@ -23,7 +30,7 @@ const fontsToPreload: [string, CSSProperties[]][] = Object.entries({ "Apercu Mono": [{ fontWeight: "normal" }, { fontWeight: "bold" }], }); -export const FontsPreloader: FC = ({ children }) => { +export const FontsPreloader: FC = ({ children }) => { const [target, setTarget] = useState(null); useEffect(() => { diff --git a/apps/sim-core/packages/core/src/components/GeospatialMap/GeospatialMap.tsx b/apps/sim-core/packages/core/src/components/GeospatialMap/GeospatialMap.tsx index 9ce360a7..391e43f9 100644 --- a/apps/sim-core/packages/core/src/components/GeospatialMap/GeospatialMap.tsx +++ b/apps/sim-core/packages/core/src/components/GeospatialMap/GeospatialMap.tsx @@ -1,10 +1,10 @@ import React, { FC, useCallback, useEffect, useRef, useState } from "react"; import ReactMapboxGl, { Layer, Popup, Source } from "react-mapbox-gl"; -import * as o from "fp-ts/es6/Option"; -import * as r from "fp-ts/es6/Record"; +import * as option from "fp-ts/es6/Option"; +import * as record from "fp-ts/es6/Record"; import { AgentState } from "@hashintel/engine-web"; import { MapLayerMouseEvent } from "mapbox-gl"; -import { debounce } from "lodash"; +import { debounce } from "lodash-es"; import { SimulationViewerLazyTab } from "../SimulationViewer/LazyTab/SimulationViewerLazyTab"; import { mapColor } from "../../util/palette"; @@ -13,22 +13,20 @@ import { useResizeObserver } from "../../hooks/useResizeObserver/useResizeObserv import "mapbox-gl/dist/mapbox-gl.css"; import "./GeospatialMap.css"; -export interface GeospatialMapProps { +export type GeospatialMapProps = { simulationStep: AgentState[] | null; simulationId: string | null | undefined; errored: boolean; -} +}; -interface PopupData { +type PopupData = { coordinates: [number, number]; description: string; -} +}; -// Injected by vite. -// To specify, add a '.env' file containing, e.g., -// MAPBOX_API_TOKEN=pk.eyJ1IjoianV[...]kZWFsbHtbinwPK4yA -// Then rebuild. -const accessToken = import.meta.env.MAPBOX_API_TOKEN; +// Injected at build time via vite.config.ts define. +// Set MAPBOX_API_TOKEN env var before building. +const accessToken = MAPBOX_API_TOKEN; const MapComponent = accessToken ? ReactMapboxGl({ accessToken, @@ -73,11 +71,7 @@ const GeospatialMapPlaceholder: FC = () => (

You can create the .env file if it doesn't exist.

Mapbox API access tokens can be found at{" "} - + https://account.mapbox.com/access-tokens/

@@ -97,13 +91,10 @@ export const GeospatialMap: FC = !MapComponent const agentAverageCenter: [number, number] | undefined = lngLatAgents.length > 0 ? (lngLatAgents - .reduce<[number, number]>( - (acc, agent) => [ - acc[0] + agent.lng_lat[0], - acc[1] + agent.lng_lat[1], - ], - [0, 0], - ) + .reduce< + [number, number] + >((acc, agent) => [acc[0] + agent.lng_lat[0], acc[1] + agent.lng_lat[1]], [0, + 0]) .map((val) => val / lngLatAgents.length) as [number, number]) : undefined; @@ -164,20 +155,20 @@ export const GeospatialMap: FC = !MapComponent }, properties: { description: JSON.stringify( - r.filterWithIndex((idx) => - ((agent.popup_fields as string[]) ?? []).includes(idx), + record.filterWithIndex((idx) => + ((agent.popup_fields as Array) ?? []).includes( + idx, + ), )(agent), null, 2, ), agent_idx: idx, - color: `#${o.getOrElse(() => "ffffff")( - o.map((color: number) => color.toString(16))( - o.fromNullable( - mapColor( - agent.geo_color ?? agent.color ?? "random", - agent.agent_id, - ), + color: `#${option.getOrElse(() => "ffffff")( + option.map((color: number) => color.toString(16))( + mapColor( + agent.geo_color ?? agent.color ?? "random", + agent.agent_id, ), ), )}`, diff --git a/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsEditor.tsx b/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsEditor.tsx index 437e0f27..2de315b3 100644 --- a/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsEditor.tsx +++ b/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsEditor.tsx @@ -6,7 +6,6 @@ import React, { useRef, useState, } from "react"; -import { batch, useDispatch, useSelector } from "react-redux"; import { JsonMap } from "@hashintel/engine-web"; import { FancyButton } from "../Fancy/Button"; @@ -16,9 +15,8 @@ import { ParsedGlobals } from "../../features/files/types"; import { globalConfigSchema } from "../../util/monaco-config"; import { globalsFileId, stringifyGlobals } from "../../features/files/utils"; import { parseGlobals } from "./utils"; -import { selectCanToggleVisualGlobals } from "../../features/scopes"; import { selectGlobals } from "../../features/files/selectors"; -import { toggleVisualGlobals, updateFile } from "../../features/files/slice"; +import { useFiles, useFilesSelector } from "../../features/files/FilesContext"; import { useCancellableDebounce } from "../../hooks/useCancellableDebounce"; import "./GlobalsEditor.scss"; @@ -32,9 +30,9 @@ const emptyMessage = ( const skipSchema = (field: string) => field !== "schema"; export const GlobalsEditor: FC = () => { - const dispatch = useDispatch(); - const globalsString = useSelector(selectGlobals); - const canToggleVisualGlobals = useSelector(selectCanToggleVisualGlobals); + const { updateFile, toggleVisualGlobals } = useFiles(); + const globalsString = useFilesSelector(selectGlobals); + const canToggleVisualGlobals = true; const [globalsState, setGlobals] = useState(parseGlobals(globalsString)); const globalsStateRef = useRef(globalsState); @@ -79,23 +77,15 @@ export const GlobalsEditor: FC = () => { scheduleUpdate(() => { const contents = stringifyGlobals(globals); - /** - * We need to ensure our local copy of globals is updated in the same - * render as the redux copy to ensure we don't overwrite our local copy - * with the redux copy (and that we don't re-parse the redux copy which - * breaks performance). - */ - batch(() => { - setGlobals({ - globals, - lastGlobalsString: contents, - error: null, - }); - dispatch(updateFile({ id: globalsFileId, contents })); + setGlobals({ + globals, + lastGlobalsString: contents, + error: null, }); + updateFile(globalsFileId, contents); }, 200); }, - [dispatch, scheduleUpdate], + [updateFile, scheduleUpdate], ); /** @@ -132,7 +122,7 @@ export const GlobalsEditor: FC = () => { theme="blue" onClick={(evt) => { evt.preventDefault(); - dispatch(toggleVisualGlobals()); + toggleVisualGlobals(); }} > Edit globals.json diff --git a/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsRowContainer.tsx b/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsRowContainer.tsx index bd4dc874..191699a6 100644 --- a/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsRowContainer.tsx +++ b/apps/sim-core/packages/core/src/components/GlobalsEditor/GlobalsRowContainer.tsx @@ -1,11 +1,13 @@ -import React, { FC, useState } from "react"; +import React, { FC, PropsWithChildren, useState } from "react"; import classNames from "classnames"; -export const GlobalsRowContainer: FC<{ - field: string; - nested?: boolean; - depth: number; -}> = ({ field, nested = false, children, depth }) => { +export const GlobalsRowContainer: FC< + PropsWithChildren<{ + field: string; + nested?: boolean; + depth: number; + }> +> = ({ field, nested = false, children, depth }) => { const [open, setOpen] = useState(true); return ( diff --git a/apps/sim-core/packages/core/src/components/GlobalsEditor/types.ts b/apps/sim-core/packages/core/src/components/GlobalsEditor/types.ts index eac6f1e6..7094bc6c 100644 --- a/apps/sim-core/packages/core/src/components/GlobalsEditor/types.ts +++ b/apps/sim-core/packages/core/src/components/GlobalsEditor/types.ts @@ -1,6 +1,6 @@ import { JSONSchema7 } from "json-schema"; -export interface SharedGlobalsProps { +export type SharedGlobalsProps = { schema?: JSONSchema7 | undefined; depth: number; -} +}; diff --git a/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.spec.tsx b/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.spec.tsx index 85a9ffd6..20a7bc88 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.spec.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.spec.tsx @@ -1,17 +1,10 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { HashCoreAccessGateNotFound } from "./HashCoreAccessGateNotFound"; -import { store } from "../../../../features/store"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( - - - , - div, + render( + , ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.tsx b/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.tsx index 0240d6cf..95ba53ae 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/AccessGate/NotFound/HashCoreAccessGateNotFound.tsx @@ -3,9 +3,9 @@ import React, { FC } from "react"; import { IconFileFind } from "../../../Icon"; import { LinkableProject } from "../../../../features/project/types"; -export interface HashCoreAccessGateNotFoundProps { +export type HashCoreAccessGateNotFoundProps = { requestedProject: LinkableProject | null; -} +}; export const HashCoreAccessGateNotFound: FC< HashCoreAccessGateNotFoundProps & { embedded: boolean } diff --git a/apps/sim-core/packages/core/src/components/HashCore/AccessGate/types.ts b/apps/sim-core/packages/core/src/components/HashCore/AccessGate/types.ts index 5d3f778a..692398f7 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/AccessGate/types.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/AccessGate/types.ts @@ -1,7 +1,7 @@ import { HashCoreAccessGateKind } from "./enums"; import { HashCoreAccessGateNotFoundProps } from "./NotFound"; -export interface HashCoreAccessGateKindWithProps { +export type HashCoreAccessGateKindWithProps = { kind: HashCoreAccessGateKind.NotFound; props: HashCoreAccessGateNotFoundProps; -} +}; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsole.tsx b/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsole.tsx index 47124f75..fea88ae9 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsole.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsole.tsx @@ -1,5 +1,4 @@ -import React, { FC, memo, ReactNode, useEffect, useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import React, { FC, memo, ReactNode, useMemo } from "react"; import classnames from "classnames"; import format from "date-fns/format"; @@ -7,10 +6,9 @@ import { HashCoreConsoleAlert } from "./HashCoreConsoleAlert"; import { IconAlert, IconCheck, IconClose, IconStop } from "../../Icon"; import { Scrollable } from "../../Scrollable"; import type { UserAlert } from "../../../features/viewer/types"; -import { clearUserAlerts } from "../../../features/viewer/slice"; import { selectIdKindAndPathFromFiles } from "../../../features/files/selectors"; -import { selectUserAlerts } from "../../../features/viewer/selectors"; -import { useModalCloudUsage } from "../../Modal/CloudUsage"; +import { useFilesSelector } from "../../../features/files/FilesContext"; +import { useViewer } from "../../../features/viewer/ViewerContext"; import "./HashCoreConsole.css"; @@ -27,28 +25,15 @@ const errorIconMap: Record = { * @todo nathggns: remove when the above is fixed */ export const HashCoreConsole: FC = memo(function HashCoreConsole() { - const userAlerts = useSelector(selectUserAlerts); - const dispatch = useDispatch(); + const { userAlerts, clearUserAlerts } = useViewer(); - const files = useSelector(selectIdKindAndPathFromFiles); + const files = useFilesSelector(selectIdKindAndPathFromFiles); const filesMap = useMemo( () => Object.fromEntries(files.map((file) => [file.path.formatted, file])), [files], ); - // Show a modal on more important alert messages - const [showModal, hideModal] = useModalCloudUsage(); - useEffect(() => { - const errorContexts = userAlerts.map((err) => err.message); - if (errorContexts.includes("Out of cloud compute credits")) { - showModal(); - return () => { - hideModal(); - }; - } - }, [userAlerts, showModal, hideModal]); - return (
-
dispatch(clearUserAlerts())} - > +
clearUserAlerts()}>
diff --git a/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsoleAlert.tsx b/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsoleAlert.tsx index b2752249..6ac5ddd1 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsoleAlert.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Console/HashCoreConsoleAlert.tsx @@ -1,18 +1,16 @@ -import React, { FC, Fragment, useEffect, useMemo } from "react"; -import { useDispatch } from "react-redux"; +import React, { FC, Fragment, useMemo } from "react"; import type { HcFile } from "../../../features/files/types"; import { SIM_DOCS_URL } from "../../../util/api/paths"; import type { UserAlertInState } from "../../../features/viewer/types"; -import { setCurrentFileId } from "../../../features/files/slice"; -import { trackEvent } from "../../../features/analytics"; +import { useFiles } from "../../../features/files/FilesContext"; -interface HashCoreConsoleAlertProps { +type HashCoreConsoleAlertProps = { alert: UserAlertInState; files: Record>; -} +}; -const filesRegex = /((?:(?:[@/](?:[\w-]+\/)+)|(?:\/))?[\w-]+\.[a-z]+)/i; +const filesRegex = /((?:(?:[@\/](?:[\w-]+\/)+)|(?:\/))?[\w-]+\.[a-z]+)/i; /** * Convert internal error language into something more widely accessible. @@ -24,7 +22,7 @@ export const HashCoreConsoleAlert: FC = ({ alert, files, }) => { - const dispatch = useDispatch(); + const { setCurrentFileId } = useFiles(); const message = useMemo( () => @@ -36,7 +34,7 @@ export const HashCoreConsoleAlert: FC = ({ onClick={(evt) => { evt.preventDefault(); - dispatch(setCurrentFileId(files[piece].id)); + setCurrentFileId(files[piece].id); }} > {piece} @@ -45,7 +43,7 @@ export const HashCoreConsoleAlert: FC = ({ {makeErrorMessageFriendlier(piece)} ), ), - [files, alert.message, dispatch], + [files, alert.message, setCurrentFileId], ); const messageIncludesFiles = useMemo( @@ -55,15 +53,6 @@ export const HashCoreConsoleAlert: FC = ({ [alert.message, files], ); - useEffect(() => { - dispatch( - trackEvent({ - action: "User Alert", - label: Object.values(alert).join(" - "), - }), - ); - }, [alert, dispatch]); - return ( <> {alert.type === "complete" ? null : ( diff --git a/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.spec.tsx b/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.spec.tsx index 82729014..35c3652e 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.spec.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { HashCoreContextMenu } from "./HashCoreContextMenu"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.tsx b/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.tsx index b880504f..22623f83 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/ContextMenu/HashCoreContextMenu.tsx @@ -1,15 +1,14 @@ -import React, { FC, CSSProperties } from "react"; +import React, { FC, PropsWithChildren, CSSProperties } from "react"; import "./HashCoreContextMenu.css"; -interface HashCoreContextMenuProps { +type HashCoreContextMenuProps = { style: Pick; -} +}; -export const HashCoreContextMenu: FC = ({ - children, - style, -}) => ( +export const HashCoreContextMenu: FC< + PropsWithChildren +> = ({ children, style }) => (
    {children}
diff --git a/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.scss b/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.scss index 1066dee6..8c35cea1 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.scss +++ b/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.scss @@ -4,8 +4,9 @@ --conflict-marker-divider-bg: transparent; --conflict-marker-end-heading-bg: #156092; --conflict-marker-end-bg: #1a364c; - --conflict-label-font: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", - "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace; + --conflict-label-font: + "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", + "DejaVu Sans Mono", "Courier New", monospace; } .HashCoreEditor { diff --git a/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.tsx b/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.tsx index ef64b47c..7224f0ee 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditor.tsx @@ -6,11 +6,13 @@ import React, { useRef, useState, } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { Tab, TabPanel } from "react-tabs"; import { useModal } from "react-modal-hook"; -import { AppDispatch } from "../../../features/types"; +import { + useFiles, + useFilesSelector, +} from "../../../features/files/FilesContext"; import { HashCoreContextMenu } from "../ContextMenu"; import { HashCoreEditorBehaviorKeysFileAction } from "./HashCoreEditorBehaviorKeysFileAction"; import { HashCoreEditorFile } from "./HashCoreEditorFile"; @@ -23,12 +25,7 @@ import { } from "../../Icon"; import { IconCodeTagsCheck } from "../../Icon/CodeTagsCheck"; import { MonacoContainer } from "../../MonacoContainer"; -import { - Scope, - selectCanToggleVisualGlobals, - selectVisualGlobalsVisible, - useScope, -} from "../../../features/scopes"; +import { Scope, useScope } from "../../../features/scopes"; import { SimpleTooltip } from "../../SimpleTooltip"; import { TabActionBar } from "../../TabActionBar/TabActionBar"; import { @@ -37,33 +34,18 @@ import { } from "../../TabbedEditor"; import { ViewStates } from "../../TabbedEditor/Panel/TabbedEditorPanel"; import { analysisFileId, globalsFileId } from "../../../features/files/utils"; -import { - closeAllFiles, - closeFile, - closeFilesToTheRight, - closeOtherFiles, - setCurrentFileId, - toggleVisualGlobals, -} from "../../../features/files/slice"; import { fileActionSize, getDocsSection, validateAnalysisJsonAndDispatchErrorsIfAny, } from "./utils"; import { - selectCurrentFile, - selectCurrentFileId, - selectOpenFileIds, - selectOpenFiles, selectParsedAnalysis, selectReplaceProposal, selectShouldShowBehaviorKeys, } from "../../../features/files/selectors"; -import { - selectEditorVisible, - selectEmbedded, -} from "../../../features/viewer/selectors"; -import { trackEvent } from "../../../features/analytics"; + +import { useViewer } from "../../../features/viewer/ViewerContext"; import { useNameNewBehaviorModal } from "../Files"; import { useOnClickOutside } from "../../../hooks/useOnClickOutside"; import { useResizeObserver } from "../../../hooks/useResizeObserver/useResizeObserver"; @@ -76,13 +58,21 @@ import "./HashCoreEditor.scss"; export const HashCoreEditor: FC = () => { const [, setMonacoContainer] = useMonacoContainerFromContext(); - const dispatch = useDispatch(); - const openFiles = useSelector(selectOpenFiles); - const openFileIds = useSelector(selectOpenFileIds); - const currentFileId = useSelector(selectCurrentFileId); - const currentFile = useSelector(selectCurrentFile); - const replaceProposal = useSelector(selectReplaceProposal); - const analysis = useSelector(selectParsedAnalysis); + const { + openFiles, + openFileIds, + currentFileId, + currentFile, + setCurrentFileId, + closeFile, + closeOtherFiles, + closeFilesToTheRight, + closeAllFiles, + toggleVisualGlobals, + visualGlobals: shouldShowGlobalEditor, + } = useFiles(); + const replaceProposal = useFilesSelector(selectReplaceProposal); + const analysis = useFilesSelector(selectParsedAnalysis); const [diffEditorInstance, monacoDiffContainerRef] = useMonacoContainerFromContext(true); @@ -149,14 +139,14 @@ export const HashCoreEditor: FC = () => { ], ); - const editorVisible = useSelector(selectEditorVisible); - const shouldShowGlobalEditor = useSelector(selectVisualGlobalsVisible); - const shouldShowBehaviorKeys = useSelector(selectShouldShowBehaviorKeys); + const { editorVisible, embedded, addUserAlert, clearUserAlerts } = + useViewer(); + const shouldShowBehaviorKeys = useFilesSelector(selectShouldShowBehaviorKeys); const section = getDocsSection(currentFile, shouldShowBehaviorKeys); const editorViewStates = useRef({}); - const canToggleVisualGlobals = useSelector(selectCanToggleVisualGlobals); + const canToggleVisualGlobals = currentFileId === globalsFileId; const [contextMenuStyle, setContextMenuStyle] = useState< Pick @@ -173,7 +163,7 @@ export const HashCoreEditor: FC = () => { onClick={(evt) => { evt.preventDefault(); evt.stopPropagation(); - dispatch(closeFile(currentOpenFileInEditor)); + closeFile(currentOpenFileInEditor); }} > Close @@ -184,7 +174,7 @@ export const HashCoreEditor: FC = () => { onClick={(evt) => { evt.preventDefault(); evt.stopPropagation(); - dispatch(closeOtherFiles(currentOpenFileInEditor)); + closeOtherFiles(currentOpenFileInEditor); }} > Close Others @@ -195,7 +185,7 @@ export const HashCoreEditor: FC = () => { onClick={(evt) => { evt.preventDefault(); evt.stopPropagation(); - dispatch(closeFilesToTheRight(currentOpenFileInEditor)); + closeFilesToTheRight(currentOpenFileInEditor); }} > Close to the Right @@ -206,7 +196,7 @@ export const HashCoreEditor: FC = () => { onClick={(evt) => { evt.preventDefault(); evt.stopPropagation(); - dispatch(closeAllFiles(currentOpenFileInEditor)); + closeAllFiles(); }} > Close All @@ -214,13 +204,18 @@ export const HashCoreEditor: FC = () => {
), - [contextMenuStyle, dispatch, currentOpenFileInEditor], + [ + contextMenuStyle, + closeFile, + closeOtherFiles, + closeFilesToTheRight, + closeAllFiles, + currentOpenFileInEditor, + ], ); useOnClickOutside(tabsRef, hideContextMenu); - const embedded = useSelector(selectEmbedded); - const canSave = useScope(Scope.save); const canShowBehaviorKeys = currentFile?.kind === HcFileKind.Behavior || @@ -238,7 +233,7 @@ export const HashCoreEditor: FC = () => { { - dispatch(setCurrentFileId(file.id)); + setCurrentFileId(file.id); }} className={`react-tabs__tab tab-${file.id}`} onContextMenu={(evt) => { @@ -257,7 +252,7 @@ export const HashCoreEditor: FC = () => { className="tab-button" onClick={(evt) => { evt.stopPropagation(); - dispatch(closeFile(file.id)); + closeFile(file.id); }} > @@ -298,7 +293,7 @@ export const HashCoreEditor: FC = () => {
); }; - -// HashCoreEditor.whyDidYouRender = { -// // this is needed because the compenent is wrapped in `memo` so it's -// // `displayName` is `undefined` ... apparently `@welldone-software/why-did- -// // you-render`'s types are somewhat incomplete -// // -// // @ts-expect-error -// customName: "HashCoreEditor" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditorBehaviorKeysFileAction.tsx b/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditorBehaviorKeysFileAction.tsx index 6bbd51b1..35b237fb 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditorBehaviorKeysFileAction.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Editor/HashCoreEditorBehaviorKeysFileAction.tsx @@ -1,17 +1,13 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { AppDispatch } from "../../../features/types"; import { HcFileKind } from "../../../features/files/enums"; import { IconBrain } from "../../Icon"; import { SimpleTooltip } from "../../SimpleTooltip"; import { fileActionSize } from "./utils"; -import { selectCurrentFile } from "../../../features/files/selectors"; -import { toggleBehaviorKeysEditor } from "../../../features/files/slice"; +import { useFiles } from "../../../features/files/FilesContext"; export const HashCoreEditorBehaviorKeysFileAction = () => { - const currentFile = useSelector(selectCurrentFile); - const dispatch = useDispatch(); + const { currentFile, toggleBehaviorKeysEditor } = useFiles(); if ( currentFile?.kind !== HcFileKind.Behavior && @@ -22,10 +18,10 @@ export const HashCoreEditorBehaviorKeysFileAction = () => { return (
); }; - -// // @ts-expect-error -// HashCoreFiles.whyDidYouRender = { -// customName: "HashCoreFiles" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/HashCoreFilesHeaderAction.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/HashCoreFilesHeaderAction.tsx index f971accf..db23cfea 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/HashCoreFilesHeaderAction.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/HashCoreFilesHeaderAction.tsx @@ -1,6 +1,7 @@ import React, { ButtonHTMLAttributes, FC, + PropsWithChildren, RefObject, useRef, useState, @@ -12,9 +13,11 @@ import { SimpleTooltip } from "../../SimpleTooltip"; import "./HashCoreFilesHeaderAction.scss"; export const HashCoreFilesHeaderAction: FC< - ButtonHTMLAttributes & { - paneRef?: RefObject; - } + PropsWithChildren< + ButtonHTMLAttributes & { + paneRef?: RefObject; + } + > > = ({ title, children, className, paneRef, ...props }) => { const buttonRef = useRef(null); const tooltipRef = useRef(null); diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/ListItemFile/HashCoreFilesListItemFile.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/ListItemFile/HashCoreFilesListItemFile.tsx index ea0ac2e7..112435da 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/ListItemFile/HashCoreFilesListItemFile.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/ListItemFile/HashCoreFilesListItemFile.tsx @@ -6,11 +6,10 @@ import React, { useRef, useState, } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { useModal } from "react-modal-hook"; import urljoin from "url-join"; -import { AppDispatch } from "../../../../features/types"; +import { useFiles } from "../../../../features/files/FilesContext"; import { Ext } from "../../../../util/files/enums"; import { FileNameWithShortnameIcon } from "../../../FileName/FileNameWithShortnameIcon"; import { HashCoreContextMenu } from "../../ContextMenu"; @@ -18,22 +17,11 @@ import { HashCoreFilesListItem } from "../ListItem/HashCoreFilesListItem"; import { HcFileKind } from "../../../../features/files/enums"; import { IconAccountMultiple, IconTrash } from "../../../Icon"; import { LinkBehavior } from "../../../Link/LinkBehavior"; -import { ModalConfirmFileDelete, ModalReleaseBehavior } from "../../../Modal"; -import { ReleaseMeta } from "../../../../util/api/types"; +import { ModalConfirmFileDelete } from "../../../Modal"; import { SITE_URL } from "../../../../util/api/paths"; import { Scope, useScope } from "../../../../features/scopes"; -import { - deleteFile, - renameInitFile, - setCurrentFileId, - updateFile, -} from "../../../../features/files/slice"; -import { getReleaseMeta } from "../../../../util/api"; import { isSharedDependency } from "../../../../features/files/utils"; -import { - selectCurrentProject, - selectProjectPublishedFiles, -} from "../../../../features/project/selectors"; +import { useProject } from "../../../../features/project/ProjectContext"; import { useClipboardWriteText } from "../../../../hooks/useClipboardWriteText"; import { useFileIsCurrent, @@ -44,11 +32,11 @@ import { useRenameBehaviorModal } from ".."; import "./HashCoreFilesListItemFile.scss"; -interface HashCoreFilesListItemFileProps { +type HashCoreFilesListItemFileProps = { fileId: string; scrollIntoViewRef?: MutableRefObject; depth?: number; -} +}; export const getDomIdByFileId = (id: string) => `HashCoreFilesListItem-${id}`; @@ -58,10 +46,10 @@ export const HashCoreFilesListItemFile: FC = ({ depth = 1, }) => { const file = useSelectFileById(fileId); - const dispatch = useDispatch(); - const publishedFiles = useSelector(selectProjectPublishedFiles); + const { setCurrentFileId, deleteFile, updateFile, renameInitFile } = + useFiles(); + const { projectPublishedFiles: publishedFiles } = useProject(); const canSave = useScope(Scope.save); - const project = useSelector(selectCurrentProject); const current = useFileIsCurrent(fileId); const clipboardWriteText = useClipboardWriteText(); @@ -69,7 +57,6 @@ export const HashCoreFilesListItemFile: FC = ({ const filePublished = fileIsBehavior && publishedFiles.includes(file.path.formatted); const canRename = canSave && fileIsBehavior && !filePublished; - const canPublish = canRename && project?.visibility === "public"; const canDelete = canSave && file.kind !== HcFileKind.Required && @@ -83,27 +70,14 @@ export const HashCoreFilesListItemFile: FC = ({ fileName={title} onAnswer={(confirm) => { if (confirm) { - dispatch(deleteFile(file.id)); + deleteFile(file.id); } else { hideConfirmDelete(); } }} /> ), - [title, dispatch, file.id], - ); - - const [data, setData] = useState(null); - const [showReleaseBehaviorModal, hideReleaseBehaviorModal] = useModal( - () => - data && file.kind === HcFileKind.Behavior ? ( - - ) : null, - [data, file], + [title, deleteFile, file.id], ); const showNameBehavior = useRenameBehaviorModal(file.id, file.path); @@ -117,25 +91,12 @@ export const HashCoreFilesListItemFile: FC = ({ const [showContextMenu, hideContextMenu] = useModal( () => ( - {canSave && canPublish && ( -
  • - -
  • - )} {isSharedDependency(file) ? ( <>
  • View in HASH @@ -178,9 +139,9 @@ export const HashCoreFilesListItemFile: FC = ({ onClick={() => { if (file.path.ext === Ext.Json) { const contents = initJsonToJs(file.contents); - dispatch(updateFile({ id: file.id, contents })); + updateFile(file.id, contents); } - dispatch(renameInitFile({ id: file.id, newName: "init.js" })); + renameInitFile(file.id, "init.js"); }} > Convert to init.js @@ -193,9 +154,9 @@ export const HashCoreFilesListItemFile: FC = ({ onClick={() => { if (file.path.ext === Ext.Json) { const contents = initJsonToPy(file.contents); - dispatch(updateFile({ id: file.id, contents })); + updateFile(file.id, contents); } - dispatch(renameInitFile({ id: file.id, newName: "init.py" })); + renameInitFile(file.id, "init.py"); }} > Convert to init.py @@ -206,7 +167,7 @@ export const HashCoreFilesListItemFile: FC = ({
  • ); }; - -// HashCoreFilesListItemFolder.whyDidYouRender = { -// // @ts-expect-error -// customName: "HashCoreFilesListItemFolder" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchContainer.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchContainer.tsx index 52a47a03..0cad65be 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchContainer.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchContainer.tsx @@ -1,12 +1,7 @@ import React, { useEffect, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { HashCoreFilesSearch } from "./HashCoreFilesSearch"; -import { - closeSearch, - openSearch, - selectSearchOpen, -} from "../../../../features/search"; +import { useSearch } from "../../../../features/search/SearchContext"; import { focusAndSelect } from "./util"; import { useKeyboardShortcuts } from "../../../../hooks/useKeyboardShortcuts"; import { useSearchReducer } from "./reducer"; @@ -19,13 +14,12 @@ import { useSearchReducer } from "./reducer"; export const HashCoreFilesSearchContainer = () => { const searchInputRef = useRef(null); const replaceInputRef = useRef(null); - const searchOpen = useSelector(selectSearchOpen); - const appDispatch = useDispatch(); + const { searchOpen, openSearch, closeSearch } = useSearch(); const [searchState, searchDispatch] = useSearchReducer(); const ensureOpen = () => { if (!searchOpen) { - appDispatch(openSearch()); + openSearch(); } }; @@ -54,7 +48,7 @@ export const HashCoreFilesSearchContainer = () => { single: { Escape: () => { if (searchOpen) { - appDispatch(closeSearch()); + closeSearch(); } }, }, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchFile.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchFile.tsx index d9d24925..e6f9969d 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchFile.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchFile.tsx @@ -9,7 +9,7 @@ import type { SearchMatch } from "./types"; import "./HashCoreFilesSearchFile.scss"; -interface SearchFileProps { +type SearchFileProps = { file: HcFile; matches: SearchMatch[]; onClick: (match: SearchMatch) => void; @@ -18,7 +18,7 @@ interface SearchFileProps { onReplaceAllInFile: () => Promise; replacing: boolean; pending: boolean; -} +}; export const HashCoreFilesSearchFile: FC = ({ file, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchForm.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchForm.tsx index 1a3620a7..9d02cdf3 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchForm.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchForm.tsx @@ -1,7 +1,5 @@ import React, { FC, RefObject } from "react"; -import { useDispatch } from "react-redux"; -import type { AppDispatch } from "../../../../features/types"; import { HashCoreFilesSearchInput } from "./HashCoreFilesSearchInput"; import { MonacoIconButton, @@ -10,7 +8,7 @@ import { } from "./MonacoIconComponents"; import { Scope, useScope } from "../../../../features/scopes"; import { SearchDispatch, SearchState } from "./reducer"; -import { closeSearch } from "../../../../features/search/slice"; +import { useSearch } from "../../../../features/search/SearchContext"; import { replace } from "./util"; import { useCanHover } from "../../../../hooks/useCanHover"; @@ -23,7 +21,7 @@ export const HashCoreFilesSearchForm: FC<{ replaceInputRef: RefObject; }> = ({ searchState, searchDispatch, searchInputRef, replaceInputRef }) => { const { query, pending, results } = searchState; - const appDispatch = useDispatch(); + const { closeSearch } = useSearch(); const canEdit = useScope(Scope.edit); const canHover = useCanHover(); @@ -95,7 +93,7 @@ export const HashCoreFilesSearchForm: FC<{ iconName="close" title="Close" onClick={() => { - appDispatch(closeSearch()); + closeSearch(); }} />
    diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchMatch.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchMatch.tsx index a86170b0..9263439d 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchMatch.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/HashCoreFilesSearchMatch.tsx @@ -7,13 +7,13 @@ import { useKeepInView } from "../../../KeepInView"; import "./HashCoreFilesSearchMatch.scss"; -interface SearchResultProps { +type SearchResultProps = { match: SearchMatch; onClick: (match: SearchMatch) => void; onReplace: (match: SearchMatch) => Promise; replacing: boolean; pending: boolean; -} +}; export const HashCoreFilesSearchMatch: FC = ({ match, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/hooks.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/hooks.ts index 77bb027f..7ce8fb97 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/hooks.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/hooks.ts @@ -1,17 +1,9 @@ import { RefObject, useCallback, useEffect, useMemo, useRef } from "react"; -import { useDispatch, useSelector, useStore } from "react-redux"; -import produce from "immer"; +import { produce } from "immer"; import { IRange, editor } from "monaco-editor"; -import { Observable, Subject, merge } from "rxjs"; -import { - buffer, - distinctUntilChanged, - filter, - map, - pairwise, -} from "rxjs/operators"; - -import type { AppDispatch, RootState } from "../../../../features/types"; +import { Subject, merge } from "rxjs"; +import { map } from "rxjs/operators"; + import type { HcFile } from "../../../../features/files/types"; import { Replacement, @@ -20,87 +12,77 @@ import { SearchResultsDictionary, } from "./types"; import { SearchDispatch, SearchState } from "./reducer"; -import { fromStore } from "../../../../util/fromStore"; import { getDiffModel } from "../../../TabbedEditor/DiffPanel"; import { getNextContents, searchDebounce, triggerSearch } from "./util"; import { isReadOnly } from "../../../../features/files/utils"; import { parseReplaceString } from "./monaco"; import { - selectAllFiles, - selectFileEntities, selectFileIds, selectReplaceProposal, } from "../../../../features/files/selectors"; -import { selectCurrentProjectUrl } from "../../../../features/project/selectors"; import { - setCurrentFileId, - setReplaceProposal, -} from "../../../../features/files/slice"; + useFiles, + useFilesSelector, +} from "../../../../features/files/FilesContext"; +import { useProject } from "../../../../features/project/ProjectContext"; import { setMonacoModel } from "../../../../features/monaco"; import { useMonacoContainerFromContext } from "../../../TabbedEditor/hooks"; const useFileChangeObservable = () => { - const store = useStore(); - - return useMemo(() => { - const observable = new Observable((subscriber) => { - const cache = new Map(); + const { allFiles } = useFiles(); + const subject = useMemo(() => new Subject(), []); + const cacheRef = useRef(new Map()); - const emitChangedFiles = (state: RootState) => { - const files = selectAllFiles(state); - - for (const file of files) { - if (cache.get(file.id) !== file.contents) { - cache.set(file.id, file.contents); + useEffect(() => { + const cache = cacheRef.current; + const changedIds: string[] = []; - subscriber.next(file.id); - } - } - }; + const currentIds = new Set(allFiles.map((file) => file.id)); + for (const key of cache.keys()) { + if (!currentIds.has(key)) { + cache.delete(key); + } + } - const unsubscribeStore = store.subscribe(() => { - const state = store.getState(); - const ids = selectFileIds(state); + for (const file of allFiles) { + if (cache.get(file.id) !== file.contents) { + cache.set(file.id, file.contents); + changedIds.push(file.id); + } + } - for (const key of cache.keys()) { - if (!ids.includes(key)) { - cache.delete(key); - } - } + if (changedIds.length > 0) { + subject.next(changedIds); + } + }, [allFiles, subject]); - emitChangedFiles(state); - }); + return useMemo(() => subject.pipe(searchDebounce()), [subject]); +}; - emitChangedFiles(store.getState()); +export const useFilesRemovedObservable = () => { + const { allFiles } = useFiles(); + const fileIds = useMemo(() => allFiles.map((file) => file.id), [allFiles]); + const subject = useMemo(() => new Subject(), []); + const prevIdsRef = useRef(fileIds); - return () => { - unsubscribeStore(); - }; - }); + useEffect(() => { + const prevIds = prevIdsRef.current; + prevIdsRef.current = fileIds; - return observable.pipe(buffer(observable.pipe(searchDebounce()))); - }, [store]); -}; + const removedIds = prevIds.filter((id) => !fileIds.includes(id)); + if (removedIds.length > 0) { + subject.next(removedIds); + } + }, [fileIds, subject]); -export const useFilesRemovedObservable = () => { - const store = useStore(); - return useMemo(() => { - return fromStore(store).pipe( - map(selectFileIds), - distinctUntilChanged(), - pairwise(), - map(([firstIds, secondIds]) => - firstIds - .filter((id) => !secondIds.includes(id)) - .map((id) => id.toString()), - ), - filter((ids) => ids.length > 0), - ); - }, [store]); + return subject.asObservable(); }; const useQueryChangeObservable = (query: SearchQuery) => { - const store = useStore(); + const { allFiles } = useFiles(); + const fileIdsRef = useRef(allFiles.map((file) => file.id)); + fileIdsRef.current = allFiles.map((file) => file.id); + const subject = useMemo(() => new Subject(), []); useEffect(() => { @@ -111,9 +93,9 @@ const useQueryChangeObservable = (query: SearchQuery) => { () => subject.pipe( searchDebounce(), - map(() => selectFileIds(store.getState()) as string[]), + map(() => fileIdsRef.current), ), - [subject, store], + [subject], ); }; @@ -121,7 +103,7 @@ const useRemoveDeletedFilesFromResults = ( resultsRef: RefObject, searchDispatch: SearchDispatch, ) => { - const fileIds = useSelector(selectFileIds); + const fileIds = useFilesSelector(selectFileIds); useEffect(() => { if (!resultsRef.current) { @@ -167,10 +149,13 @@ export const useSearch = ( queryRef.current = searchState.query; }); - const store = useStore(); + const { fileEntities } = useFiles(); + const fileEntitiesRef = useRef(fileEntities); + fileEntitiesRef.current = fileEntities; + const filesToSearchObserver = useFilesToSearchObserver(searchState); - const projectUrl = useSelector(selectCurrentProjectUrl); + const { currentProjectUrl: projectUrl } = useProject(); useRemoveDeletedFilesFromResults(resultsRef, searchDispatch); @@ -183,63 +168,64 @@ export const useSearch = ( useEffect(() => { let controller: AbortController | null = null; - const subscription = filesToSearchObserver.subscribe((filesToSearch) => { - controller?.abort(); - - const query = queryRef.current; + const subscription = (filesToSearchObserver as any).subscribe( + (filesToSearch: string[]) => { + controller?.abort(); - /** - * We don't want to do the search if we don't have a search term, but we - * didn't filter the event from the observer because we do want to ensure - * we abort the pending search - */ - if (!query.searchTerm) { - controller = null; + const query = queryRef.current; - return; - } + /** + * We don't want to do the search if we don't have a search term, but we + * didn't filter the event from the observer because we do want to ensure + * we abort the pending search + */ + if (!query.searchTerm) { + controller = null; - controller = new AbortController(); - - const files = selectFileEntities(store.getState()); - - // This parses the replace term for any regex group tokens ($1, $2, etc) - const pattern = query.replaceTerm - ? parseReplaceString(query.replaceTerm) - : null; - - triggerSearch( - query, - filesToSearch, - projectUrl, - files, - pattern, - resultsRef.current, - controller.signal, - ) - .then((nextResults) => { - if (controller!.signal.aborted) { - throw new Error("Aborted"); - } + return; + } - controller = null; - searchDispatch({ - type: "results", - payload: nextResults, + controller = new AbortController(); + + const files = fileEntitiesRef.current; + + const pattern = query.replaceTerm + ? parseReplaceString(query.replaceTerm) + : null; + + triggerSearch( + query, + filesToSearch, + projectUrl, + files, + pattern, + resultsRef.current, + controller.signal, + ) + .then((nextResults) => { + if (controller!.signal.aborted) { + throw new Error("Aborted"); + } + + controller = null; + searchDispatch({ + type: "results", + payload: nextResults, + }); + }) + .catch((err) => { + if (err.message !== "Aborted") { + throw err; + } }); - }) - .catch((err) => { - if (err.message !== "Aborted") { - throw err; - } - }); - }); + }, + ); return () => { controller?.abort(); subscription.unsubscribe(); }; - }, [filesToSearchObserver, projectUrl, searchDispatch, store]); + }, [filesToSearchObserver, projectUrl, searchDispatch]); }; /** @@ -272,7 +258,6 @@ export const useMonacoSearchHighlightDecorator = ( return () => { for (const [model, decorations] of newDecorationsWithModel) { - // It may have been disposed by this point – if we've changed project if (!model.isDisposed()) { model.deltaDecorations(decorations, []); } @@ -285,8 +270,8 @@ export const useReplaceProposal = ( replacing: boolean, results: SearchFileResult[], ) => { - const appDispatch = useDispatch(); - const replaceProposal = useSelector(selectReplaceProposal); + const { setReplaceProposal } = useFiles(); + const replaceProposal = useFilesSelector(selectReplaceProposal); const replacingFileId = replaceProposal.proposal?.fileId; const replacingFileIdRef = useRef(replacingFileId); @@ -309,7 +294,7 @@ export const useReplaceProposal = ( ); if (!resultsForCurrentFile) { - appDispatch(setReplaceProposal(null)); + setReplaceProposal(null); return; } @@ -319,13 +304,11 @@ export const useReplaceProposal = ( throw new Error("Found read only file in replaceProposal"); } - appDispatch( - setReplaceProposal({ - fileId: file.id, - nextContents: getNextContents(file, model, matches), - }), - ); - }, [appDispatch, replacing, results]); + setReplaceProposal({ + fileId: file.id, + nextContents: getNextContents(file, model, matches), + }); + }, [setReplaceProposal, replacing, results]); /** * This effect removes the visible replace proposal tab when swapping from @@ -335,18 +318,18 @@ export const useReplaceProposal = ( if (replacing) { return () => { if (replacingFileIdRef.current) { - appDispatch(setReplaceProposal(null)); + setReplaceProposal(null); } }; } - }, [appDispatch, replacing]); + }, [setReplaceProposal, replacing]); }; export const useRevealMatchInEditor = () => { - const projectUrl = useSelector(selectCurrentProjectUrl); + const { currentProjectUrl: projectUrl } = useProject(); const [editorInstance] = useMonacoContainerFromContext(); const [diffEditorInstance] = useMonacoContainerFromContext(true); - const appDispatch = useDispatch(); + const { setReplaceProposal, setCurrentFileId } = useFiles(); return useCallback( ( @@ -368,7 +351,7 @@ export const useRevealMatchInEditor = () => { } const nextContents = getNextContents(file, model, matches); - appDispatch(setReplaceProposal({ fileId: file.id, nextContents })); + setReplaceProposal({ fileId: file.id, nextContents }); diffEditorInstance.setModel( getDiffModel(projectUrl, file, nextContents), ); @@ -380,7 +363,7 @@ export const useRevealMatchInEditor = () => { if (!editorInstance) { throw new Error("Cannot find editor instance to reveal file in"); } - appDispatch(setCurrentFileId(file.id)); + setCurrentFileId(file.id); setMonacoModel(editorInstance, model); @@ -389,6 +372,12 @@ export const useRevealMatchInEditor = () => { } } }, - [appDispatch, diffEditorInstance, editorInstance, projectUrl], + [ + setReplaceProposal, + diffEditorInstance, + editorInstance, + projectUrl, + setCurrentFileId, + ], ); }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/replacePattern.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/replacePattern.ts index 8a0dbaa5..74bfb02a 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/replacePattern.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/replacePattern.ts @@ -81,8 +81,12 @@ export class ReplacePattern { if (piece.caseOps !== null && piece.caseOps.length > 0) { const repl: string[] = []; const lenOps: number = piece.caseOps.length; - let opIdx = 0; - for (let idx = 0, len: number = match.length; idx < len; idx++) { + let opIdx: number = 0; + for ( + let idx: number = 0, len: number = match.length; + idx < len; + idx++ + ) { if (opIdx >= lenOps) { repl.push(match.slice(idx)); break; @@ -291,13 +295,10 @@ export function parseReplaceString(replaceString: string): ReplacePattern { // to the replacement text, not subsequent content. case CharCode.u: // \u => upper-cases one character. - // falls through case CharCode.U: // \U => upper-cases ALL following characters. - // falls through case CharCode.l: // \l => lower-cases one character. - // falls through case CharCode.L: // \L => lower-cases ALL following characters. result.emitUnchanged(idx - 1); diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/search.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/search.ts index 7aa0d497..ed9cb980 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/search.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/monaco/search.ts @@ -72,7 +72,7 @@ function buildReplaceStringForSpecificSpecialCharacter( ): string { const splitPatternAtSpecialCharacter = pattern.split(specialCharacter); const splitMatchAtSpecialCharacter = matches[0].split(specialCharacter); - let replaceString = ""; + let replaceString: string = ""; splitPatternAtSpecialCharacter.forEach((splitValue, index) => { replaceString += buildReplaceStringWithCasePreserved( diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/reducer.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/reducer.ts index 13fc7e51..7746e8a6 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/reducer.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/reducer.ts @@ -1,14 +1,13 @@ import { Dispatch, useEffect, useReducer } from "react"; -import { useSelector } from "react-redux"; -import produce, { Draft } from "immer"; +import { produce, Draft } from "immer"; import { SearchFileResult, SearchQuery, SearchResultsDictionary, } from "./types"; -import { selectCurrentProjectUrl } from "../../../../features/project/selectors"; import { useFilesRemovedObservable } from "./hooks"; +import { useProject } from "../../../../features/project/ProjectContext"; export type SearchAction = | { @@ -37,14 +36,14 @@ export type SearchAction = | { type: "reset"; payload: { projectUrl: string | null } } | { type: "filesRemoved"; payload: string[] }; -export interface SearchState { +export type SearchState = { query: SearchQuery; resultsMap: SearchResultsDictionary; results: SearchFileResult[]; noResults: boolean; pending: boolean; projectUrl: string | null; -} +}; export type SearchDispatch = Dispatch; @@ -183,7 +182,7 @@ export const useSearchReducer = () => { searchInitialState, ); - const projectUrl = useSelector(selectCurrentProjectUrl); + const { currentProjectUrl: projectUrl } = useProject(); if (searchState.projectUrl !== projectUrl) { searchDispatch({ type: "reset", payload: { projectUrl } }); } diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/types.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/types.ts index 0a94de8f..b74304fd 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/types.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/types.ts @@ -2,19 +2,16 @@ import { Range, editor } from "monaco-editor"; import type { HcFile } from "../../../../features/files/types"; -export interface SearchQuery { +export type SearchQuery = { searchTerm: string; replacing: boolean; replaceTerm: string; caseSensitive: boolean; regex: boolean; preserveCase: boolean; -} +}; -export interface Replacement { - range: Range; - replaceTerm: string; -} +export type Replacement = { range: Range; replaceTerm: string }; export type SearchMatch = Replacement & { id: string; @@ -23,11 +20,13 @@ export type SearchMatch = Replacement & { afterText: string; }; -export interface SearchFileResult { +export type SearchFileResult = { file: HcFile; model: editor.ITextModel; matches: SearchMatch[]; replacing: boolean; -} +}; -export type SearchResultsDictionary = Record; +export type SearchResultsDictionary = { + [index: string]: SearchFileResult; +}; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/util.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/util.ts index cd77d8af..ddbbee27 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/Search/util.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/Search/util.ts @@ -1,5 +1,4 @@ -import { Dictionary } from "@reduxjs/toolkit"; -import produce from "immer"; +import { produce } from "immer"; import { Range, editor } from "monaco-editor"; import { debounceTime } from "rxjs/operators"; @@ -46,7 +45,7 @@ export const getNextContents = ( return nextContents; }; -export const replace = ( +export const replace = async ( model: editor.ITextModel, file: HcFile, replacements: { range: Range; replaceTerm: string }[], @@ -79,7 +78,7 @@ export const triggerSearch = async ( query: SearchQuery, filesToSearch: string[], manifestId: string | null, - allFiles: Dictionary, + allFiles: Record, pattern: ReplacePattern | null, prevResults: SearchResultsDictionary, signal: AbortSignal, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useModalNameBehavior.tsx b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useModalNameBehavior.tsx index 228d4549..f02c89f9 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useModalNameBehavior.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useModalNameBehavior.tsx @@ -1,11 +1,10 @@ import React, { useRef } from "react"; -import { useDispatch } from "react-redux"; import { useModal } from "react-modal-hook"; import { ModalNameBehavior } from "../../../Modal/NameBehavior"; import type { ParsedPath } from "../../../../util/files/types"; import { parse } from "../../../../util/files"; -import { trackEvent } from "../../../../features/analytics"; + import { useName } from "./useName"; export const useModalNameBehavior = ( @@ -21,7 +20,6 @@ export const useModalNameBehavior = ( path?: ParsedPath, id?: string, ) => { - const dispatch = useDispatch(); const onSubmitRef = useRef(onSubmit); onSubmitRef.current = onSubmit; @@ -50,9 +48,6 @@ export const useModalNameBehavior = ( const path = parse({ name, ext: selectedLanguage.value }); onSubmitRef.current(path); - dispatch( - trackEvent({ action: "New behavior", label: path.formatted }), - ); done(); }} diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useName.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useName.ts index 4523ba88..5a1c6978 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useName.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useName.ts @@ -1,6 +1,5 @@ import { useCallback, useEffect, useReducer, useRef, useState } from "react"; -import { useSelector } from "react-redux"; -import produce, { Draft } from "immer"; +import { produce, Draft } from "immer"; import { Ext } from "../../../../util/files/enums"; import type { HcFile } from "../../../../features/files/types"; @@ -9,6 +8,7 @@ import type { ParsedPath } from "../../../../util/files/types"; import type { ReactSelectOption } from "../../../Dropdown/types"; import { destinationPathInUse, parse } from "../../../../util/files"; import { selectIdKindAndPathFromFiles } from "../../../../features/files/selectors"; +import { useFilesSelector } from "../../../../features/files/FilesContext"; import { validateFileName } from "../../../../util/validation"; const extensionMap = { @@ -24,7 +24,7 @@ type LanguageOption = ReactSelectOption & { }; const isAllowedExtensionString = (str: string): str is allowedExtensionString => - Object.prototype.hasOwnProperty.call(extensionMap, str); + extensionMap.hasOwnProperty(str); const isReactOptionAllowable = ( option: ReactSelectOption, @@ -55,10 +55,7 @@ const getLanguageForLanguageStr = ( return languageOptionByValue[lowerCase]; }; -interface NameReducerState { - name: string; - selectedLanguage: LanguageOption; -} +type NameReducerState = { name: string; selectedLanguage: LanguageOption }; type SetName = (name: NameReducerState["name"]) => void; type SetSelectedLanguage = (language: ReactSelectOption) => void; @@ -75,7 +72,7 @@ const nameReducer = produce( state.selectedLanguage = action.language; break; - case "setName": { + case "setName": const matches = action.name.trim().match(/^(.*?)(\..*)?$/); const selectedLanguage = getLanguageForLanguageStr(matches?.[2]); @@ -86,7 +83,7 @@ const nameReducer = produce( state.name = action.name; } break; - } + case "set": return action.value; } @@ -124,7 +121,7 @@ const useValidate = (args: { id?: string; value: NameReducerState; }): ValidateHook => { - const files = useSelector(selectIdKindAndPathFromFiles); + const files = useFilesSelector(selectIdKindAndPathFromFiles); const [errorMessage, setErrorMessage] = useState(null); const argsRef = useRef(args); diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useNameNewBehaviorModal.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useNameNewBehaviorModal.ts index 041a16ac..fe5b8af8 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useNameNewBehaviorModal.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useNameNewBehaviorModal.ts @@ -1,24 +1,20 @@ -import { useDispatch, useStore } from "react-redux"; - -import { createBehavior } from "../../../../features/files/slice"; -import { selectCurrentProject } from "../../../../features/project/selectors"; +import { useFiles } from "../../../../features/files/FilesContext"; +import { useProject } from "../../../../features/project/ProjectContext"; import { useModalNameBehavior } from "./useModalNameBehavior"; export const useNameNewBehaviorModal = () => { - const dispatch = useDispatch(); - const store = useStore(); + const { createBehavior } = useFiles(); + const { currentProject } = useProject(); return useModalNameBehavior({ action: "Create", placeholder: "Name your new file", onSubmit(path) { - const project = selectCurrentProject(store.getState()); - - if (!project) { + if (!currentProject) { throw new Error("Cannot create behavior without a project"); } - dispatch(createBehavior({ path, project })); + createBehavior({ path, project: currentProject }); }, }); }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useRenameBehaviorModal.ts b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useRenameBehaviorModal.ts index 79c43386..0f9d1627 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useRenameBehaviorModal.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Files/hooks/useRenameBehaviorModal.ts @@ -1,23 +1,16 @@ -import { useDispatch } from "react-redux"; - import type { ParsedPath } from "../../../../util/files/types"; -import { renameBehavior } from "../../../../features/files/slice"; +import { useFiles } from "../../../../features/files/FilesContext"; import { useModalNameBehavior } from "./useModalNameBehavior"; export const useRenameBehaviorModal = (id: string, source: ParsedPath) => { - const dispatch = useDispatch(); + const { renameBehavior } = useFiles(); return useModalNameBehavior( { action: "Rename", placeholder: "Rename your file", onSubmit(path) { - dispatch( - renameBehavior({ - id, - newName: path.base, - }), - ); + renameBehavior(id, path.base); }, }, source, diff --git a/apps/sim-core/packages/core/src/components/HashCore/HashCore.tsx b/apps/sim-core/packages/core/src/components/HashCore/HashCore.tsx index 5e7fede1..74047866 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/HashCore.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/HashCore.tsx @@ -1,8 +1,6 @@ -import React, { FC, memo, useEffect, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { navigate } from "hookrouter"; +import React, { FC, memo, useEffect } from "react"; +import { navigate } from "../../util/navigation"; -import { DiscordWidget } from "../DiscordWidget"; import { HashCoreAccessGate } from "./AccessGate/HashCoreAccessGate"; import { HashCoreHeader, HashCoreMain } from "."; import { HashCoreTour } from "./Tour"; @@ -13,50 +11,28 @@ import { image as defaultMetaImage, } from "../../metaTags.json"; import { localStorageProjectKey } from "../../util/localStorageProjectKey"; -import { - selectAccessGate, - selectCurrentProject, -} from "../../features/project/selectors"; import { selectDidSave, selectFileIds } from "../../features/files/selectors"; -import { setProjectWithMeta } from "../../features/actions"; -import { - toggleActivity, - toggleEditor, - toggleViewer, -} from "../../features/viewer/slice"; -import { trackEvent } from "../../features/analytics"; +import { useFilesSelector } from "../../features/files/FilesContext"; +import { useProject } from "../../features/project/ProjectContext"; + import { urlFromProject } from "../../routes"; import { useKeyboardShortcuts } from "../../hooks/useKeyboardShortcuts"; import { useParameterisedUi } from "../../hooks/useParameterisedUi"; import { useSaveOrFork } from "../../hooks/useSaveOrFork"; import { useShouldUnload } from "../../hooks/shouldUnload"; +import { useViewer } from "../../features/viewer/ViewerContext"; export const HashCore: FC = memo(function HashCore() { - const dispatch = useDispatch(); - - const project = useSelector(selectCurrentProject); - const fileIds = useSelector(selectFileIds); - const accessGate = useSelector(selectAccessGate); - const didSave = useSelector(selectDidSave); + const { + currentProject: project, + accessGate, + setProjectWithMeta, + } = useProject(); + const fileIds = useFilesSelector(selectFileIds); + const didSave = useFilesSelector(selectDidSave); useParameterisedUi(); - const firstLoadTracked = useRef(false); - useEffect(() => { - if (project && !firstLoadTracked.current) { - dispatch( - trackEvent({ - action: "Open Project", - label: `${project.type} - ${project.pathWithNamespace} - ${project.ref} - From direct link`, - context: { - type: project.type, - }, - }), - ); - firstLoadTracked.current = true; - } - }, [dispatch, project]); - useEffect(() => { const onStorage = (event: StorageEvent) => { if ( @@ -88,7 +64,7 @@ export const HashCore: FC = memo(function HashCore() { event.newValue, ); - dispatch(setProjectWithMeta(nextProject, { replaceTabs: false })); + setProjectWithMeta(nextProject, { replaceTabs: false }); navigate(urlFromProject(nextProject), true, {}, false); }; @@ -96,12 +72,14 @@ export const HashCore: FC = memo(function HashCore() { return () => { window.removeEventListener("storage", onStorage); }; - }, [dispatch, project, fileIds]); + }, [setProjectWithMeta, project, fileIds]); useShouldUnload(didSave); const [saveOrFork] = useSaveOrFork(); + const { toggleActivity, toggleEditor, toggleViewer } = useViewer(); + useKeyboardShortcuts({ meta: { async s() { @@ -110,13 +88,13 @@ export const HashCore: FC = memo(function HashCore() { }, metaShift: { a() { - dispatch(toggleActivity()); + toggleActivity(); }, e() { - dispatch(toggleEditor()); + toggleEditor(); }, y() { - dispatch(toggleViewer()); + toggleViewer(); }, }, }); @@ -149,7 +127,6 @@ export const HashCore: FC = memo(function HashCore() { ) : project ? ( ) : null} - ); }); diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/HashCoreHeader.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/HashCoreHeader.tsx index ac681a91..b8ffa5ea 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/HashCoreHeader.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/HashCoreHeader.tsx @@ -1,73 +1,21 @@ -import React, { FC, lazy, Suspense, useEffect, useState } from "react"; -import { useSelector } from "react-redux"; +import React, { FC } from "react"; import TimeAgo from "react-timeago"; -import { useModal } from "react-modal-hook"; -import urljoin from "url-join"; import { HashCoreHeaderMenu } from ".."; -import { IS_STAGING } from "../../../util/api"; import { IconBrain } from "../../Icon/Brain"; import { IconLock } from "../../Icon/Lock"; import { Logo } from "../../Logo"; -import { ModalPrivateDependencies } from "../../Modal/PrivateDependencies"; -import { ModalReleaseCreate, ModalReleaseUpdate } from "../../Modal"; -import type { ReleaseMeta } from "../../../util/api/types"; -import { Scope, useScopes } from "../../../features/scopes"; -import { coreVersions } from "../../../util/api/queries"; +import { Scope, useScope } from "../../../features/scopes"; import { projectIsPrivate } from "../../../features/project/utils"; -import { selectCurrentProject } from "../../../features/project/selectors"; -import { - selectDidSave, - selectProjectHasPrivateDependencies, -} from "../../../features/files/selectors"; +import { selectDidSave } from "../../../features/files/selectors"; +import { useFilesSelector } from "../../../features/files/FilesContext"; +import { useProject } from "../../../features/project/ProjectContext"; import "./HashCoreHeader.css"; -const shouldShowVersionPicker = IS_STAGING; - -const HashVersionPicker = shouldShowVersionPicker - ? lazy(() => - import( - /* webpackChunkName: "HashVersionPicker" */ "./HashVersionPicker" - ).then((module) => ({ - default: module.HashVersionPicker, - })), - ) - : null; - export const HashCoreHeader: FC = () => { - const project = useSelector(selectCurrentProject); - const [versions, setVersions] = useState([]); - const isSaved = useSelector(selectDidSave); - - useEffect(() => { - const controller = new AbortController(); - if (shouldShowVersionPicker) { - coreVersions(undefined, controller.signal).then((vs) => - setVersions(vs.coreVersions), - ); - } - return controller.abort.bind(controller); - }, []); - - const [data, _setData] = useState(); - const [_showCreateReleaseModal, hideCreateReleaseModal] = useModal( - () => , - [data], - ); - - const [_showUpdateInIndex, hideUpdateInIndex] = useModal( - () => , - [], - ); - - const [_showPrivateDependencies, hidePrivateDependencies] = useModal(() => ( - - )); - - const _hasPrivateDependencies = useSelector( - selectProjectHasPrivateDependencies, - ); + const { currentProject: project } = useProject(); + const isSaved = useFilesSelector(selectDidSave); const projectUpdatedAtDate = project ? new Date(project.updatedAt) @@ -81,19 +29,7 @@ export const HashCoreHeader: FC = () => { const isBehaviorProject = project?.type === "Behavior"; - const { - canLogin: _canLogin, - canRelease: _canRelease, - canSave, - canUseAccount: _canUseAccount, - canLinkToProjectInIndex, - } = useScopes( - Scope.login, - Scope.release, - Scope.save, - Scope.useAccount, - Scope.linkToProjectInIndex, - ); + const canSave = useScope(Scope.save); /** * These svg icons have fractional sizes to ensure they don't have @@ -123,16 +59,7 @@ export const HashCoreHeader: FC = () => {
    - {project && canLinkToProjectInIndex ? ( - - {title} - - ) : ( - title - )} + {title} {project?.updatedAt && (  - last{" "} @@ -147,70 +74,7 @@ export const HashCoreHeader: FC = () => { )}
    -
    - {!!versions?.length && HashVersionPicker ? ( - - - - ) : null} -
    - { - /** - * We only show last published if you are on main and are able to - * edit / publish - */ - // project?.latestRelease && canRelease ? ( - // - // Last released - // - // ) : null - } - {/* {project ? : null} */} - {/* {project && canRelease ? ( - - ) : null} */} - {/* {canLogin ? ( - - Sign up / Sign in - - ) : null} */} -
    - - {/* {canUseAccount ? : null} */} -
    +
    ); }; - -// // @ts-expect-error -// HashCoreHeader.whyDidYouRender = { -// customName: "HashCoreHeader" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Experiments/HashCoreHeaderMenuExperiments.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Experiments/HashCoreHeaderMenuExperiments.tsx index 524ddf65..20e41ed9 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Experiments/HashCoreHeaderMenuExperiments.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Experiments/HashCoreHeaderMenuExperiments.tsx @@ -1,5 +1,4 @@ import React, { FC, Fragment, memo, MouseEvent } from "react"; -import { useSelector } from "react-redux"; import { useModal } from "react-modal-hook"; import { DisabledExperimentTooltip } from "../../../../SimulationRunner/Controls/Experiments/ExperimentsList"; @@ -10,18 +9,19 @@ import { Scope, useScope } from "../../../../../features/scopes"; import { queueExperiment } from "../../../../../features/simulator/simulate/queueExperiment"; import { selectExperiments } from "../../../../SimulationRunner/Controls/Experiments/selectors"; import { selectProviderTarget } from "../../../../../features/simulator/simulate/selectors"; -import { trackEvent } from "../../../../../features/analytics"; + +import { useFilesSelector } from "../../../../../features/files/FilesContext"; import { useSimulatorDispatch, useSimulatorSelector, } from "../../../../../features/simulator/context"; -interface HashCoreHeaderMenuExperimentsProps { +type HashCoreHeaderMenuExperimentsProps = { openMenuItem: string; onClickMenuItemLabel: ({ target }: MouseEvent) => void; onMouseEnterMenuItemLabel: ({ target }: MouseEvent) => void; clearAll: () => void; -} +}; export const HashCoreHeaderMenuExperiments: FC = memo( @@ -33,7 +33,7 @@ export const HashCoreHeaderMenuExperiments: FC { const dispatch = useSimulatorDispatch(); const canEdit = useScope(Scope.edit); - const experiments = useSelector(selectExperiments); + const experiments = useFilesSelector(selectExperiments); const target = useSimulatorSelector(selectProviderTarget); const [openCreateExperimentModal, hideCreateExperimentModal] = useModal( () => , @@ -94,10 +94,6 @@ export const HashCoreHeaderMenuExperiments: FC { clearAll(); openCreateExperimentModal(); - trackEvent({ - action: "Experiment wizard opened", - label: "Menu", - }); }} > Create new experiment diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.spec.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.spec.tsx index 1e203a24..899106c4 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.spec.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.spec.tsx @@ -1,31 +1,30 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { ModalProvider } from "react-modal-hook"; import { HashCoreHeaderMenuFiles } from "./HashCoreHeaderMenuFiles"; -import { store } from "../../../../../features/store"; +import { ProjectProvider } from "../../../../../features/project/ProjectContext"; +import { UserProvider } from "../../../../../features/user/UserContext"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( - - - {}} - onClickMenuItemLabel={() => {}} - onMouseEnterMenuItemLabel={() => {}} - onMouseEnterSubmenuItemLabel={() => {}} - onMouseEnterSubmenuItem={() => {}} - onMouseLeaveSubmenuItem={() => {}} - userProjects={[]} - exampleProjects={[]} - /> - - , - div, + render( + + + + {}} + onClickMenuItemLabel={() => {}} + onMouseEnterMenuItemLabel={() => {}} + onMouseEnterSubmenuItemLabel={() => {}} + onMouseEnterSubmenuItem={() => {}} + onMouseLeaveSubmenuItem={() => {}} + userProjects={[]} + exampleProjects={[]} + /> + + + , ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.tsx index f548978a..a81fbada 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Files/HashCoreHeaderMenuFiles.tsx @@ -1,5 +1,4 @@ import React, { ChangeEvent, FC, memo, MouseEvent, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { useModal } from "react-modal-hook"; import { IconBrain } from "../../../../Icon/Brain"; @@ -10,18 +9,19 @@ import { ModalNewDataset } from "../../../../Modal/NewDataset/ModalNewDataset"; import { PartialSimulationProject } from "../../../../../features/project/types"; import { Scope } from "../../../../../features/scopes"; import { descByUpdatedAt } from "../../../../../util/descByUpdatedAt"; -import { mainProjectPath, urlFromProject } from "../../../../../routes"; -import { selectCurrentProject } from "../../../../../features/project/selectors"; -import { selectUserProfileUrl } from "../../../../../features/user/selectors"; -import { trackEvent } from "../../../../../features/analytics"; +import { mainProjectPath } from "../../../../../routes"; + +import { useProject } from "../../../../../features/project/ProjectContext"; +import { useUser } from "../../../../../features/user/UserContext"; import { useExportFiles, useImportFiles, + useImportProjectFromUrl, } from "../../../../../features/files/hooks"; import { useNameNewBehaviorModal } from "../../../Files/hooks"; import { useSaveOrFork } from "../../../../../hooks/useSaveOrFork"; -interface HashCoreHeaderMenuFilesProps { +type HashCoreHeaderMenuFilesProps = { openMenuItem: string; openSubmenuItem: string; clearAll: () => void; @@ -34,7 +34,7 @@ interface HashCoreHeaderMenuFilesProps { onMouseLeaveSubmenuItem: ({ target }: MouseEvent) => void; userProjects: PartialSimulationProject[]; exampleProjects: PartialSimulationProject[]; -} +}; /** * @todo most of these props do not need to be props – define them locally instead @@ -50,11 +50,10 @@ export const HashCoreHeaderMenuFiles: FC = memo( onMouseEnterSubmenuItem, onMouseLeaveSubmenuItem, userProjects, - exampleProjects: _exampleProjects, + exampleProjects, }) => { - const userProfileUrl = useSelector(selectUserProfileUrl); - const project = useSelector(selectCurrentProject); - const dispatch = useDispatch(); + const { userProfileUrl } = useUser(); + const { currentProject: project } = useProject(); // const forkUrl = project ? forkUrlFromProject(project) : null; const showModalNewBehavior = useNameNewBehaviorModal(); @@ -65,6 +64,7 @@ export const HashCoreHeaderMenuFiles: FC = memo( const exportFiles = useExportFiles(); const importFiles = useImportFiles(); + const importProjectFromUrl = useImportProjectFromUrl(); const importFileRef = useRef(null); const onImportClick = () => { importFileRef.current?.click(); @@ -82,26 +82,33 @@ export const HashCoreHeaderMenuFiles: FC = memo( const toListItem = (type: "Example" | "User") => (item: PartialSimulationProject) => { - const href = - type === "User" - ? mainProjectPath(item.pathWithNamespace) - : urlFromProject(item); + if (type === "Example") { + const slug = item.pathWithNamespace.split("/").pop() ?? ""; + const zipUrl = `/example_projects/${slug}.zip`; + return ( +
  • + { + evt.preventDefault(); + clearAll(); + importProjectFromUrl(zipUrl, item.name).catch((err) => + console.error(`Error importing example: ${err instanceof Error ? err.message : String(err)}`), + ); + }} + className="HashCoreHeaderMenuProjectLink" + > + {item.name} + +
  • + ); + } + const href = mainProjectPath(item.pathWithNamespace); return (
  • { - dispatch( - trackEvent({ - action: "Open project", - label: `${type} - ${item.pathWithNamespace} - ${item.ref} - From menu`, - context: { - type, - }, - }), - ); - clearAll(); }} className="HashCoreHeaderMenuProjectLink" @@ -154,7 +161,7 @@ export const HashCoreHeaderMenuFiles: FC = memo(
  • )} -
  • + {/*
  • = memo( From starter template -
  • + */}

  • @@ -200,6 +207,7 @@ export const HashCoreHeaderMenuFiles: FC = memo(
      {[...userProjects] .sort(descByUpdatedAt) + .slice(0, 10) .map(toListItem("User"))} {userProfileUrl ? ( <> @@ -218,7 +226,7 @@ export const HashCoreHeaderMenuFiles: FC = memo(
    ) : null} - {/* {exampleProjects.length ? ( + {exampleProjects.length ? (
  • = memo( .map(toListItem("Example"))}
  • - ) : null} */} + ) : null}
  • ) => { + onChange={async (evt: ChangeEvent) => { evt.preventDefault(); clearAll(); const files = evt.currentTarget.files; if (files) { importFiles(files).catch((err) => console.error( - `Error importing project files: ${err.message}`, + `Error importing project files: ${err instanceof Error ? err.message : String(err ?? "Unknown error")}`, ), ); } @@ -300,12 +308,12 @@ export const HashCoreHeaderMenuFiles: FC = memo(
  • ) => { + onClick={async (evt: MouseEvent) => { evt.preventDefault(); clearAll(); exportFiles().catch((err) => console.error( - `Error exporting project files: ${err.message}`, + `Error exporting project files: ${err instanceof Error ? err.message : String(err ?? "Unknown error")}`, ), ); }} @@ -339,8 +347,3 @@ export const HashCoreHeaderMenuFiles: FC = memo( ); }, ); - -// // @ts-expect-error -// HashCoreHeaderMenuFiles.whyDidYouRender = { -// customName: "HashCoreHeaderMenuFiles" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/HashCoreHeaderMenu.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/HashCoreHeaderMenu.tsx index 5834cc3f..5b72c618 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/HashCoreHeaderMenu.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/HashCoreHeaderMenu.tsx @@ -1,15 +1,14 @@ import React, { FC, memo, useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { AppDispatch } from "../../../../features/types"; import { HashCoreHeaderMenuExperiments } from "./Experiments"; import { HashCoreHeaderMenuFiles } from "./Files"; import { HashCoreHeaderMenuHelp } from "./Help"; import { HashCoreHeaderMenuView } from "./View"; -import { openTab } from "../../../../features/viewer/slice"; -import { selectExamples } from "../../../../features/examples/selectors"; -import { selectUserProjects } from "../../../../features/user/selectors"; +import { TabKind } from "../../../../features/viewer/enums"; +import { useExamples } from "../../../../features/examples/ExamplesContext"; import { useMenu } from "./hooks"; +import { useUser } from "../../../../features/user/UserContext"; +import { useViewer } from "../../../../features/viewer/ViewerContext"; import "./HashCoreHeaderMenu.scss"; @@ -17,9 +16,9 @@ import "./HashCoreHeaderMenu.scss"; * @todo nathggns: Look into removing memo and useCallback in here */ export const HashCoreHeaderMenu: FC = memo(() => { - const dispatch = useDispatch(); - const userProjects = useSelector(selectUserProjects); - const examples = useSelector(selectExamples); + const { userProjects } = useUser(); + const { examples } = useExamples(); + const { openTab } = useViewer(); const { menuRef, @@ -34,10 +33,10 @@ export const HashCoreHeaderMenu: FC = memo(() => { } = useMenu(); const onAddView = useCallback( - (tab) => { - dispatch(openTab(tab)); + (tab: TabKind) => { + openTab(tab); }, - [dispatch], + [openTab], ); return ( @@ -87,8 +86,3 @@ export const HashCoreHeaderMenu: FC = memo(() => { ); }); - -// // @ts-expect-error -// HashCoreHeaderMenu.whyDidYouRender = { -// customName: "HashCoreHeaderMenu" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Help/HashCoreHeaderMenuHelp.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Help/HashCoreHeaderMenuHelp.tsx index bc534ae7..8f21ff23 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Help/HashCoreHeaderMenuHelp.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/Help/HashCoreHeaderMenuHelp.tsx @@ -1,18 +1,16 @@ import React, { FC, memo, MouseEvent } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { DISCORD_URL } from "../../../../DiscordWidget/DiscordWidget"; import { LabeledInputRadio } from "../../../../LabeledInputRadio"; -import { selectHasProject } from "../../../../../features/project/selectors"; -import { trackEvent } from "../../../../../features/analytics"; + +import { useProject } from "../../../../../features/project/ProjectContext"; import { useTour } from "../../../Tour"; -interface HashCoreHeaderMenuHelpProps { +type HashCoreHeaderMenuHelpProps = { openMenuItem: string; onClickMenuItemLabel: ({ target }: MouseEvent) => void; onMouseEnterMenuItemLabel: ({ target }: MouseEvent) => void; clearAll: () => void; -} +}; export const HashCoreHeaderMenuHelp: FC = memo( ({ @@ -23,8 +21,7 @@ export const HashCoreHeaderMenuHelp: FC = memo( }) => { const tour = useTour(); // const canUseAccount = useScope(Scope.useAccount); - const hasProject = useSelector(selectHasProject); - const dispatch = useDispatch(); + const { hasProject } = useProject(); return ( <> @@ -36,27 +33,24 @@ export const HashCoreHeaderMenuHelp: FC = memo( onMouseEnter={onMouseEnterMenuItemLabel} /> ); }, ); - -// // @ts-expect-error -// HashCoreHeaderMenuHelp.whyDidYouRender = { -// customName: "HashCoreHeaderMenuHelp" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/View/HashCoreHeaderMenuView.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/View/HashCoreHeaderMenuView.tsx index 88131ed5..770cf6a1 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/View/HashCoreHeaderMenuView.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/View/HashCoreHeaderMenuView.tsx @@ -1,33 +1,22 @@ import React, { FC, Fragment, memo, MouseEvent } from "react"; -import { useDispatch, useSelector } from "react-redux"; import classNames from "classnames"; import { LabeledInputRadio } from "../../../../LabeledInputRadio"; -import { Link } from "../../../../Link/Link"; -import { Scope, useScopes } from "../../../../../features/scopes"; +import { Scope, useScope } from "../../../../../features/scopes"; import { TabKind } from "../../../../../features/viewer/enums"; import { getMetaCharacter } from "../../../../../hooks/useKeyboardShortcuts"; -import { openSearch } from "../../../../../features/search/slice"; -import { - selectActivityVisible, - selectEditorVisible, - selectViewerVisible, -} from "../../../../../features/viewer/selectors"; -import { selectHasProject } from "../../../../../features/project/selectors"; -import { - toggleActivity, - toggleEditor, - toggleViewer, -} from "../../../../../features/viewer/slice"; +import { useProject } from "../../../../../features/project/ProjectContext"; +import { useSearch } from "../../../../../features/search/SearchContext"; +import { useViewer } from "../../../../../features/viewer/ViewerContext"; import { viewerTabs } from "../../../../../features/viewer/utils"; -interface HashCoreHeaderMenuViewProps { +type HashCoreHeaderMenuViewProps = { openMenuItem: string; onClickMenuItemLabel: ({ target }: MouseEvent) => void; onMouseEnterMenuItemLabel: ({ target }: MouseEvent) => void; onAddView: (tabName: TabKind) => void; clearAll: () => void; -} +}; export const HashCoreHeaderMenuView: FC = memo( ({ @@ -37,12 +26,17 @@ export const HashCoreHeaderMenuView: FC = memo( onAddView, clearAll, }) => { - const dispatch = useDispatch(); - const { canEdit, canLogin } = useScopes(Scope.edit, Scope.login); - const hasProject = useSelector(selectHasProject); - const editorVisible = useSelector(selectEditorVisible); - const activityVisible = useSelector(selectActivityVisible); - const viewerVisible = useSelector(selectViewerVisible); + const { openSearch } = useSearch(); + const canEdit = useScope(Scope.edit); + const { hasProject } = useProject(); + const { + editorVisible, + activityVisible, + viewerVisible, + toggleActivity, + toggleEditor, + toggleViewer, + } = useViewer(); const items = []; @@ -68,7 +62,7 @@ export const HashCoreHeaderMenuView: FC = memo( { clearAll(); - dispatch(openSearch()); + openSearch(); }} > {canEdit ? <>Search & Replace : <>Search} @@ -89,7 +83,7 @@ export const HashCoreHeaderMenuView: FC = memo( { clearAll(); - dispatch(toggleEditor()); + toggleEditor(); }} >
  • -
    -
  • - ) : null} -
  • - - Sign up - -
  • -
  • - - Sign in - -
  • - , - ); - } - return ( <> = memo( ); }, ); - -// // @ts-expect-error -// HashCoreHeaderMenuView.whyDidYouRender = { -// customName: "HashCoreHeaderMenuView" -// }; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/hooks/useMenu.ts b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/hooks/useMenu.ts index 902e0f05..a4f4b04c 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/hooks/useMenu.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/Menu/hooks/useMenu.ts @@ -1,9 +1,9 @@ import { useState, MouseEvent, RefObject, useMemo } from "react"; -import { debounce } from "lodash"; +import { debounce } from "lodash-es"; import { useClickOutside } from "./useClickOutside"; -interface MenuInterface { +type MenuInterface = { menuRef: RefObject; openMenuItem: string; openSubmenuItem: string; @@ -15,7 +15,7 @@ interface MenuInterface { }: MouseEvent) => void; onMouseEnterSubmenuItem: ({ target }: MouseEvent) => void; onMouseLeaveSubmenuItem: ({ target }: MouseEvent) => void; -} +}; function isHtmlLabelElement(target: EventTarget): target is HTMLLabelElement { return target instanceof HTMLLabelElement; diff --git a/apps/sim-core/packages/core/src/components/HashCore/Header/UserImage/HashCoreHeaderUserImage.tsx b/apps/sim-core/packages/core/src/components/HashCore/Header/UserImage/HashCoreHeaderUserImage.tsx index 1105ee66..0269381e 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Header/UserImage/HashCoreHeaderUserImage.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Header/UserImage/HashCoreHeaderUserImage.tsx @@ -1,16 +1,11 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; -import { - selectUserImage, - selectUserProfileUrl, -} from "../../../../features/user/selectors"; +import { useUser } from "../../../../features/user/UserContext"; import "./HashCoreHeaderUserImage.css"; export const HashCoreHeaderUserImage: FC = () => { - const url = useSelector(selectUserProfileUrl); - const image = useSelector(selectUserImage); + const { userProfileUrl: url, userImage: image } = useUser(); if (!url) { throw new Error("Cannot display user image without profile to link to"); @@ -22,7 +17,6 @@ export const HashCoreHeaderUserImage: FC = () => { target="_blank" className="HashCoreHeaderUserImage" title="My account" - rel="noreferrer" > {image ? User profile image : null}
    diff --git a/apps/sim-core/packages/core/src/components/HashCore/Main/HashCoreMain.tsx b/apps/sim-core/packages/core/src/components/HashCore/Main/HashCoreMain.tsx index c71bc892..7cc743ee 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Main/HashCoreMain.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Main/HashCoreMain.tsx @@ -1,10 +1,9 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; import { HashCoreAside, HashCoreSection } from ".."; import { WrappedSplitterLayout } from "../../WrappedSplitterLayout/WrappedSplitterLayout"; -import { selectEditorVisible } from "../../../features/viewer/selectors"; import { useAddClassOnClick } from "./util"; +import { useViewer } from "../../../features/viewer/ViewerContext"; import "./HashCoreMain.css"; @@ -27,7 +26,7 @@ export const HashCoreMain: FC = () => { ); }; - const editorVisible = useSelector(selectEditorVisible); + const { editorVisible } = useViewer(); return (
    diff --git a/apps/sim-core/packages/core/src/components/HashCore/Resources/List/HashCoreResourcesList.tsx b/apps/sim-core/packages/core/src/components/HashCore/Resources/List/HashCoreResourcesList.tsx index 94b74c49..a60bbc89 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Resources/List/HashCoreResourcesList.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Resources/List/HashCoreResourcesList.tsx @@ -5,9 +5,9 @@ import { ResourceProject } from "../../../../features/project/types"; import "./HashCoreResourcesList.css"; -export interface HashCoreResourcesListProps { +export type HashCoreResourcesListProps = { results: ResourceProject[]; -} +}; export const HashCoreResourcesList: FC = ({ results, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Resources/SearchableIndex/hooks.ts b/apps/sim-core/packages/core/src/components/HashCore/Resources/SearchableIndex/hooks.ts index f18f7076..20ab0190 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Resources/SearchableIndex/hooks.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Resources/SearchableIndex/hooks.ts @@ -1,6 +1,10 @@ -import { useReducer } from "react"; +import { useEffect, useReducer, useRef } from "react"; import type { ResourceProject } from "../../../../features/project/types"; +import { Scope, useScope } from "../../../../features/scopes"; +import { searchResourceProjects } from "../../../../util/api/queries/searchResourceProjects"; + +import { useProject } from "../../../../features/project/ProjectContext"; export const useSearchIndex = (): { loading: boolean; @@ -8,9 +12,9 @@ export const useSearchIndex = (): { onChange: (term: string) => void; searchTerm: string; } => { - // const searchTermSubjectRef = useRef(new Subject()); - // const appDispatch = useDispatch(); - // const store = useStore(); + const { currentProject, projectLoaded } = useProject(); + const canSave = useScope(Scope.save); + const latestReleaseTag = currentProject?.latestRelease?.tag; const [{ loading, results, searchTerm }, dispatch] = useReducer( ( @@ -42,88 +46,49 @@ export const useSearchIndex = (): { { loading: true, results: [], searchTerm: "" }, ); - // migration shim - // useEffect(() => { - // const search = async (searchTerm: string, signal: AbortSignal) => { - // try { - // dispatch({ type: "BEGIN_SEARCH" }); - - // const results = await searchResourceProjects(searchTerm, signal); - - // // Search is triggered on page load - we don't want to track those as events - // if (searchTerm) { - // appDispatch( - // trackEvent({ action: "Index Search: Core", label: searchTerm }) - // ); - // } - - // if (signal.aborted) { - // return; - // } - - // dispatch({ type: "FINISHED_SEARCHING", payload: results }); - // } catch (err) { - // if (err.name !== "AbortError") { - // console.error("Could not fetch resources", err); - - // dispatch({ type: "ERROR" }); - // } - // } - // }; - - // let controller: AbortController | null = null; - - // const storeObs = fromStore(store); - // const subscription = combineLatest([ - // merge( - // searchTermSubjectRef.current.pipe(skip(1), debounceTime(500)), - // searchTermSubjectRef.current.pipe(take(1)), - // projectChangeObservable(store).pipe( - // withLatestFrom(searchTermSubjectRef.current), - // map((pair) => pair[1] ?? "") - // ), - // storeObs.pipe( - // filter(selectProjectLoaded), - // map(selectLatestReleaseTag), - // distinctUntilChanged(), - // withLatestFrom(searchTermSubjectRef.current), - // map((pair) => pair[1] ?? "") - // ) - // ), - // storeObs.pipe( - // filter(selectProjectLoaded), - // map(selectScope[Scope.save]), - // distinctUntilChanged() - // ), - // ]) - // .pipe(debounceTime(0)) - // .subscribe(([searchTerm, canSave]) => { - // controller?.abort(); - - // if (canSave) { - // controller = new AbortController(); - - // search(searchTerm, controller.signal).catch((err) => { - // if (err.name !== "AbortError") { - // console.error(err); - // } - // }); - // } - // }); - - // return () => { - // controller?.abort(); - // subscription.unsubscribe(); - // }; - // }, [appDispatch, store]); - - // useEffect(() => { - // searchTermSubjectRef.current.next(searchTerm); - // }, [searchTerm]); + const searchTermRef = useRef(searchTerm); + searchTermRef.current = searchTerm; + + useEffect(() => { + if (!projectLoaded || !canSave) { + return; + } + + let controller: AbortController | null = null; + + const doSearch = async () => { + const term = searchTermRef.current; + + controller?.abort(); + controller = new AbortController(); + + try { + dispatch({ type: "BEGIN_SEARCH" }); + const searchResults = await searchResourceProjects( + term, + controller.signal, + ); + + if (!controller.signal.aborted) { + dispatch({ type: "FINISHED_SEARCHING", payload: searchResults }); + } + } catch (err: any) { + if (err.name !== "AbortError") { + console.error("Could not fetch resources", err); + dispatch({ type: "ERROR" }); + } + } + }; + + doSearch(); + + return () => { + controller?.abort(); + }; + }, [projectLoaded, canSave, searchTerm, latestReleaseTag]); return { - onChange: (searchTerm: string) => - dispatch({ type: "SEARCH", payload: searchTerm }), + onChange: (term: string) => dispatch({ type: "SEARCH", payload: term }), loading, results, searchTerm, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Resources/selectors.ts b/apps/sim-core/packages/core/src/components/HashCore/Resources/selectors.ts index f976a135..1385f1d1 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Resources/selectors.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/Resources/selectors.ts @@ -1,7 +1,6 @@ -import { createSelector } from "@reduxjs/toolkit"; +import { createSelector } from "reselect"; import { ResourceProject } from "../../../features/project/types"; -import type { RootState } from "../../../features/types"; import { mapLegacyDependencyFormat } from "../../../features/project/utils"; import { selectParsedDependencies, @@ -30,7 +29,7 @@ export const selectPathsForDependencies = createSelector( export const makeSelectPresentItemsFromResource = () => createSelector( selectPathsForDependencies, - (_: RootState, resource: ResourceProject) => resource, + (_: any, resource: ResourceProject) => resource, (paths, resource) => resource.files.reduce((result, file) => { if (paths.includes(file.path.formatted)) { diff --git a/apps/sim-core/packages/core/src/components/HashCore/Section/HashCoreSection.tsx b/apps/sim-core/packages/core/src/components/HashCore/Section/HashCoreSection.tsx index a91ecb82..adfc5b16 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Section/HashCoreSection.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Section/HashCoreSection.tsx @@ -1,25 +1,37 @@ import React, { FC, useState } from "react"; -import { useSelector } from "react-redux"; import { HashCoreEditorContainer } from "../EditorContainer/HashCoreEditorContainer"; import { HashCoreViewer } from "../Viewer/HashCoreViewer"; import { WrappedSplitterLayout } from "../../WrappedSplitterLayout/WrappedSplitterLayout"; -import { selectDisplayEditorSection } from "../../../features/selectors"; -import { - selectEditorVisible, - selectEmbedded, - selectViewerVisible, -} from "../../../features/viewer/selectors"; +import { useFiles } from "../../../features/files/FilesContext"; import { useResizeObserver } from "../../../hooks/useResizeObserver/useResizeObserver"; +import { useViewer } from "../../../features/viewer/ViewerContext"; import "./HashCoreSection.css"; export const HashCoreSection: FC = () => { - const editorVisible = useSelector(selectEditorVisible); - const displayEditorSection = useSelector(selectDisplayEditorSection); - const embedded = useSelector(selectEmbedded); + const { editorVisible, embedded, viewerVisible, userAlerts } = useViewer(); + const { globalsSrc } = useFiles(); + + const displayEditorSection = (() => { + if (editorVisible || userAlerts.length > 0) { + return true; + } + if (!globalsSrc) { + return false; + } + try { + const parsed = JSON.parse(globalsSrc); + if (!parsed) { + return false; + } + return Object.keys(parsed).length > 0; + } catch { + return true; + } + })(); + const [vertical, setVertical] = useState(false); - const viewerVisible = useSelector(selectViewerVisible); const ref = useResizeObserver(({ width }) => setVertical(width <= 700), { onObserve: null, diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/HashCoreTour.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/HashCoreTour.tsx index 9f3ac354..c1801b0f 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/HashCoreTour.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/HashCoreTour.tsx @@ -1,6 +1,7 @@ import React, { FC, Fragment, + PropsWithChildren, useEffect, useMemo, useReducer, @@ -8,7 +9,6 @@ import React, { useState, } from "react"; import { createPortal } from "react-dom"; -import { useDispatch, useSelector } from "react-redux"; import { Avatar, CloseButton, VERSION, steps } from "./Step"; import { @@ -22,16 +22,12 @@ import { } from "./react-shepherd-wrapper"; import type { TourProgress } from "../../../util/api/types"; import { getTourShowcase } from "../../../util/api"; -import { - selectCurrentProject, - selectProjectLoaded, -} from "../../../features/project/selectors"; -import { selectTourProgress } from "../../../features/user/selectors"; -import { tourProgress } from "../../../features/user/thunks"; import { urlFromProject } from "../../../routes"; +import { useProject } from "../../../features/project/ProjectContext"; import { useGettingStartedProject } from "./util"; import { usePromise } from "../../../hooks/usePromise"; import { useSafeQueryParams } from "../../../hooks/useSafeQueryParams"; +import { useUser } from "../../../features/user/UserContext"; const tourOptions = { defaultStepOptions: { @@ -106,9 +102,8 @@ const useTourPosition = (tour: Tour): [number, number, boolean] => { }; const useAutoTriggerTour = (tour: Tour, isVisible: boolean) => { - const project = useSelector(selectCurrentProject); - const projectLoaded = useSelector(selectProjectLoaded); - const tourProgress = useSelector(selectTourProgress); + const { currentProject: project, projectLoaded } = useProject(); + const { tourProgress } = useUser(); const [{ triggerTour, fromOnboardingRoute }, setQueryParams] = useSafeQueryParams(); @@ -264,33 +259,27 @@ const useTrackProgress = ( prevIdx: number, isCompleted: boolean, ) => { - const dispatch = useDispatch(); + const { updateTourProgress } = useUser(); useEffect(() => { if (!tour.isActive() || (activeIdx === 0 && prevIdx <= activeIdx)) { return; } - /** - * If we've previously completed it and are now just reviewing it, - * we don't want to overwrite their progress - */ const currentStep = isCompleted ? tour.steps.length - 1 : activeIdx; const lastStepViewed = tour.steps[currentStep].options.id ?? ""; - dispatch( - tourProgress({ - completed: isCompleted, - version: VERSION, - lastStepViewed, - }), - ); - }, [activeIdx, dispatch, isCompleted, prevIdx, tour]); + updateTourProgress({ + completed: isCompleted, + version: VERSION, + lastStepViewed, + }); + }, [activeIdx, updateTourProgress, isCompleted, prevIdx, tour]); }; const TourWithBackdrop: FC = () => { const tour = useTour(); - const tourProgress = useSelector(selectTourProgress); + const { tourProgress } = useUser(); const [activeIdx, prevIdx, isVisible] = useTourPosition(tour); const hashTourConfig = useHashTourConfig(isVisible); const isCompleted = useIsCompleted(tour, tourProgress, activeIdx); @@ -349,7 +338,7 @@ const TourWithBackdrop: FC = () => { ); }; -export const HashCoreTour: FC = ({ children }) => ( +export const HashCoreTour: FC = ({ children }) => ( {children} diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepAgents.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepAgents.tsx index f23bb084..fe76c01b 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepAgents.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepAgents.tsx @@ -1,5 +1,4 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; import { BackButton, @@ -10,11 +9,11 @@ import { useDomElementForFileId, useKeyboardSupport, } from "./util"; -import { selectCurrentFileId } from "../../../../features/files/selectors"; +import { useFiles } from "../../../../features/files/FilesContext"; export const HashCoreTourStepAgents: FC = () => { const initialStateFile = useDomElementForFileId("initialState"); - const currentFileId = useSelector(selectCurrentFileId); + const { currentFileId } = useFiles(); const initialStateSelected = currentFileId === "initialState"; useKeyboardSupport(); @@ -30,7 +29,6 @@ export const HashCoreTourStepAgents: FC = () => { HASH is oriented around agents. {" "} diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDatasets.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDatasets.tsx index 3aeb0fc3..0854339f 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDatasets.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDatasets.tsx @@ -1,5 +1,4 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; import urljoin from "url-join"; import { @@ -10,10 +9,10 @@ import { ProgressIndicator, } from "./util"; import { SITE_URL } from "../../../../util/api/paths"; -import { selectUserProfileUrl } from "../../../../features/user/selectors"; +import { useUser } from "../../../../features/user/UserContext"; export const HashCoreTourStepDatasets: FC = () => { - const url = useSelector(selectUserProfileUrl); + const { userProfileUrl: url } = useUser(); return ( <> @@ -23,13 +22,12 @@ export const HashCoreTourStepDatasets: FC = () => { you can import datasets to customize your agents and behaviors . Data, behaviors, and simulations that others users have shared are also available in{" "} - + HASH , which you can add to your own simulations. @@ -37,7 +35,7 @@ export const HashCoreTourStepDatasets: FC = () => {

    Your simulation and datasets are auto-saved to your{" "} {url ? ( - + profile ) : ( diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDone.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDone.tsx index b506122f..9cdcc4e5 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDone.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepDone.tsx @@ -51,7 +51,6 @@ export const HashCoreTourStepDone: FC = () => { Getting Started tutorial {" "} diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlay.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlay.tsx index ba6a308d..26413222 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlay.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlay.tsx @@ -31,7 +31,9 @@ export const HashCoreTourStepPlay: FC = () => { the simulation.

    - Click the run button below the view pane to continue.{" "} + + Click the run button below the view pane to continue. + {" "}

    diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlots.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlots.tsx index f32fa996..104b4647 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlots.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepPlots.tsx @@ -1,5 +1,4 @@ import React, { FC, useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { BackButton, @@ -11,11 +10,10 @@ import { useSimulationPause, } from "./util"; import { TabKind } from "../../../../features/viewer/enums"; -import { addTab, selectCurrentTab } from "../../../../features/viewer"; +import { useViewer } from "../../../../features/viewer/ViewerContext"; const usePlotTab = (): [boolean, HTMLElement | null] => { - const dispatch = useDispatch(); - const currentTab = useSelector(selectCurrentTab); + const { currentTab, addTab } = useViewer(); const currentTabKind = currentTab === TabKind.Analysis; /** @@ -26,7 +24,7 @@ const usePlotTab = (): [boolean, HTMLElement | null] => { // @todo this may call too frequently useEffect(() => { - dispatch(addTab(TabKind.Analysis)); + addTab(TabKind.Analysis); const elem = Array.from( document.querySelectorAll( diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepRefresh.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepRefresh.tsx index 4a83efa3..681964fe 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepRefresh.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/HashCoreTourStepRefresh.tsx @@ -1,5 +1,4 @@ import React, { FC, useEffect, useMemo, useReducer } from "react"; -import { useSelector } from "react-redux"; import { BackButton, @@ -14,7 +13,7 @@ import { useOnSimulationReset, } from "./util"; import { globalsFileId } from "../../../../features/files/utils"; -import { selectCurrentFileId } from "../../../../features/files/selectors"; +import { useFiles } from "../../../../features/files/FilesContext"; const refreshInitialState = { hasOpenedProperties: false, @@ -47,7 +46,7 @@ function refreshReducer( export const HashCoreTourStepRefresh: FC = () => { const [state, dispatch] = useReducer(refreshReducer, refreshInitialState); const propertiesFile = useDomElementForFileId(globalsFileId); - const currentFileId = useSelector(selectCurrentFileId); + const { currentFileId } = useFiles(); useKeyboardSupport(); @@ -87,7 +86,6 @@ export const HashCoreTourStepRefresh: FC = () => { globals.json is where you define global properties {" "} @@ -98,7 +96,6 @@ export const HashCoreTourStepRefresh: FC = () => { context diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/steps.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/steps.tsx index 13083364..a751135f 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/steps.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/steps.tsx @@ -62,7 +62,6 @@ export const steps = [ Behaviors are “actions” that, if attached to an agent, run every timestep. @@ -103,7 +102,6 @@ export const steps = [ HASH plots charts from your simulation diff --git a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/util.tsx b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/util.tsx index 7f0994a0..fe57b5f7 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/util.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Tour/Step/util.tsx @@ -1,4 +1,11 @@ -import React, { FC, useEffect, useMemo, useRef, useState } from "react"; +import React, { + FC, + PropsWithChildren, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { getDomIdByFileId } from "../../Files/ListItemFile"; import { pauseSimulator } from "../../../../features/simulator/simulate/slice"; @@ -32,14 +39,14 @@ export const Indicator: FC<{ position: "left-overlap" | "right-overlap" | "right"; }> = ({ element, show, position }) => { const calculateRef = useRef(null); - const immediateIdRef = useRef(null); + const immediateIdRef = useRef | null>(null); const calculateOnResize = () => { if (immediateIdRef.current !== null) { - clearTimeout(immediateIdRef.current); + clearImmediate(immediateIdRef.current); } - immediateIdRef.current = setTimeout(() => calculateRef.current?.()); + immediateIdRef.current = setImmediate(() => calculateRef.current?.()); }; const setTargetObserverRef = useResizeObserver(calculateOnResize, { @@ -180,15 +187,17 @@ export const ProgressIndicator: FC = () => { ); }; -export const Buttons: FC = ({ children }) => ( +export const Buttons: FC = ({ children }) => (
    {children}
    ); -export const Button: FC<{ - type: "back" | "next"; - className?: string; - disabled?: boolean; -}> = ({ type, className = "", disabled = false, children }) => { +export const Button: FC< + PropsWithChildren<{ + type: "back" | "next"; + className?: string; + disabled?: boolean; + }> +> = ({ type, className = "", disabled = false, children }) => { const tour = useTour(); return ( diff --git a/apps/sim-core/packages/core/src/components/HashCore/Viewer/HashCoreViewer.tsx b/apps/sim-core/packages/core/src/components/HashCore/Viewer/HashCoreViewer.tsx index 754879f8..8029e104 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/Viewer/HashCoreViewer.tsx +++ b/apps/sim-core/packages/core/src/components/HashCore/Viewer/HashCoreViewer.tsx @@ -1,27 +1,24 @@ import React, { FC, lazy, Suspense } from "react"; -import { useSelector } from "react-redux"; +import { ActivityHistory } from "../../ActivityHistory"; import { Scope, useScope } from "../../../features/scopes"; import { SimulationRunner } from "../../SimulationRunner/SimulationRunner"; import { SimulationViewer } from "../../SimulationViewer"; import { WrappedSplitterLayout } from "../../WrappedSplitterLayout/WrappedSplitterLayout"; -import { selectActivityVisible } from "../../../features/viewer/selectors"; import { useInstructionReceiver } from "../useInstructionReceiver"; import { useResizeObserver } from "../../../hooks/useResizeObserver/useResizeObserver"; +import { useViewer } from "../../../features/viewer/ViewerContext"; import "./HashCoreViewer.css"; -import { AgentInspectorSplitterLayout } from "../../ActivityHistory/Inspector/Inspector"; const LazyOpenInCore = lazy(() => - import( - /* webpackChunkName: "OpenInCore" */ "../../OpenInCore/OpenInCore" - ).then((module) => ({ + import("../../OpenInCore/OpenInCore").then((module) => ({ default: module.OpenInCore, })), ); export const HashCoreViewer: FC = () => { - const activityVisible = useSelector(selectActivityVisible); + const { activityVisible } = useViewer(); const canShowOpenInCore = useScope(Scope.showOpenInCore); const onSecondaryPaneSizeChange = (size: number) => { @@ -59,8 +56,7 @@ export const HashCoreViewer: FC = () => { ) : null}

    - {/* */} - + ); diff --git a/apps/sim-core/packages/core/src/components/HashCore/useInstructionReceiver.ts b/apps/sim-core/packages/core/src/components/HashCore/useInstructionReceiver.ts index 3af03f2a..3958d4b7 100644 --- a/apps/sim-core/packages/core/src/components/HashCore/useInstructionReceiver.ts +++ b/apps/sim-core/packages/core/src/components/HashCore/useInstructionReceiver.ts @@ -1,18 +1,12 @@ import { useCallback, useEffect, useMemo, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { maxBy } from "lodash"; +import { maxBy } from "lodash-es"; import { AnalysisObject, Plot } from "../Analysis/types"; import { HcFile } from "../../features/files/types"; -import { - addDependencies, - createBehavior, - updateFile, -} from "../../features/files/slice"; import { parse } from "../../util/files"; import { pauseAndNew } from "../../features/simulator/simulate/thunks"; -import { selectAllFiles } from "../../features/files/selectors"; -import { selectCurrentProject } from "../../features/project/selectors"; +import { useFiles } from "../../features/files/FilesContext"; +import { useProject } from "../../features/project/ProjectContext"; import { selectCurrentSimulationData } from "../../features/simulator/simulate/selectors"; import { toggleCurrentSimulator } from "../../features/simulator/simulate/slice"; import { @@ -20,46 +14,37 @@ import { useSimulatorStore, } from "../../features/simulator/context"; -interface InstructionUpdateFile { +type InstructionUpdateFile = { contents: string; file: string; id: string; type: "updateFile"; -} +}; -interface InstructionUpsertCreatorAgent { +type InstructionUpsertCreatorAgent = { contents: string; file: string; id: string; type: "upsertCreatorAgent"; -} +}; -interface InstructionAddDependencies { - contents: Record; +type InstructionAddDependencies = { + contents: { [key: string]: string }; id: string; type: "addDependencies"; -} +}; -interface InstructionIntialize { - id: string; - type: "initialize"; -} +type InstructionIntialize = { id: string; type: "initialize" }; -interface InstructionResetAndRun { - id: string; - type: "resetAndRun"; -} +type InstructionResetAndRun = { id: string; type: "resetAndRun" }; -interface InstructionSendState { - id: string; - type: "sendState"; -} +type InstructionSendState = { id: string; type: "sendState" }; -interface InstructionUpdateAnalysis { +type InstructionUpdateAnalysis = { contents: AnalysisObject; id: string; type: "updateAnalysis"; -} +}; type InstructionData = | InstructionAddDependencies @@ -89,10 +74,14 @@ const isPluginMessage = ( * (b) embedding hCore. */ export const useInstructionReceiver = () => { - const dispatch = useDispatch(); - const files = useSelector(selectAllFiles); + const { + allFiles: files, + updateFile, + createBehavior, + handleAddDependencies, + } = useFiles(); const handledMessages = useRef([]); - const project = useSelector(selectCurrentProject); + const { currentProject: project } = useProject(); const simulatorDispatch = useSimulatorDispatch(); @@ -132,7 +121,7 @@ export const useInstructionReceiver = () => { switch (event.data.type) { case "addDependencies": - dispatch(addDependencies(event.data.contents)); + handleAddDependencies(event.data.contents); return; case "resetAndRun": @@ -140,33 +129,27 @@ export const useInstructionReceiver = () => { simulatorDispatch(toggleCurrentSimulator()); return; - case "updateFile": { + case "updateFile": const { file, contents } = event.data; const foundFile = Object.values(files)?.find( (fileOption) => fileOption?.path.formatted === file, ); if (foundFile) { - dispatch(updateFile({ id: foundFile.id, contents })); + updateFile(foundFile.id, contents); } else { console.error(`Could not find file at path ${file} to update`); } return; - } + case "upsertCreatorAgent": { const { file, contents } = event.data; const foundFile = Object.values(files)?.find( (fileOption) => fileOption?.path.formatted === file, ); if (foundFile) { - dispatch(updateFile({ id: foundFile.id, contents })); + updateFile(foundFile.id, contents); } else { - dispatch( - createBehavior({ - contents, - path: parse(file), - project: project!, - }), - ); + createBehavior({ contents, path: parse(file), project: project! }); const initJson = Object.values(files)?.find( (file) => file.path.base === "init.json", ); @@ -175,13 +158,8 @@ export const useInstructionReceiver = () => { initParsed.push({ behaviors: [file], }); - dispatch( - updateFile({ - id: initJson!.id, - contents: JSON.stringify(initParsed, null, 2), - }), - ); - } catch (err) { + updateFile(initJson!.id, JSON.stringify(initParsed, null, 2)); + } catch { console.error("init.json is not valid JSON - could not update."); } } @@ -190,7 +168,7 @@ export const useInstructionReceiver = () => { // Add supplied outputs and plots to analysis.json // Replace if we can find a match for the name / key - case "updateAnalysis": { + case "updateAnalysis": const analysisJson = Object.values(files)?.find( (file) => file.path.base === "analysis.json", ); @@ -230,19 +208,17 @@ export const useInstructionReceiver = () => { )) { outputs[title] = data; } - dispatch( - updateFile({ - id: analysisJson!.id, - contents: JSON.stringify(analysisParsed, null, 2), - }), + updateFile( + analysisJson!.id, + JSON.stringify(analysisParsed, null, 2), ); - } catch (err) { + } catch { console.error( "analysis.json is not valid JSON - could not update.", ); } return; - } + // A message to indicate that a plugin is embedding hCore, // so that we don't attempt to communicate with one otherwise. case "initialize": @@ -267,7 +243,15 @@ export const useInstructionReceiver = () => { return; } }, - [dispatch, files, project, sendFiles, simStore], + [ + updateFile, + createBehavior, + handleAddDependencies, + files, + project, + sendFiles, + simStore, + ], ); useEffect(() => { diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/DefaultProject.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/DefaultProject.tsx index b72fd271..f8d7176b 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/DefaultProject.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/DefaultProject.tsx @@ -1,32 +1,64 @@ import { FC, useEffect, useRef } from "react"; -import { useSelector, useStore } from "react-redux"; -import { navigate } from "hookrouter"; +import { navigate } from "../../../util/navigation"; +import orderBy from "lodash-es/orderBy"; -import { selectBootstrapped } from "../../../features/user/selectors"; -import { selectDefaultLinkableProject } from "../../../features/selectors"; import { urlFromProject } from "../../../routes"; +import { useUser } from "../../../features/user/UserContext"; +import { useExamples } from "../../../features/examples/ExamplesContext"; +import { fetchAndParseProject } from "../../../features/files/hooks"; +import { preparePartialSimulationProject } from "../../../features/project/utils"; +import { useProject } from "../../../features/project/ProjectContext"; +import { + fetchExampleManifest, + exampleZipUrl, +} from "../../../util/exampleProjects"; export const HashRouterEffectDefaultProject: FC = () => { - const store = useStore(); - const storeRef = useRef(store); - - storeRef.current = store; - - const bootstrapped = useSelector(selectBootstrapped); + const { bootstrapped, userProjects, addUserProject } = useUser(); + const { examples } = useExamples(); + const { setProjectWithMeta } = useProject(); + const importingRef = useRef(false); useEffect(() => { - if (bootstrapped) { - const defaultProject = selectDefaultLinkableProject( - storeRef.current.getState(), + if (!bootstrapped) return; + + if (userProjects.length) { + const project = orderBy(userProjects, "updatedAt", "desc")[0]; + navigate( + urlFromProject({ pathWithNamespace: project.pathWithNamespace, ref: "main" }), ); + return; + } - if (!defaultProject) { - throw new Error("Could not find a default project"); - } + if (importingRef.current) return; + importingRef.current = true; - navigate(urlFromProject(defaultProject)); - } - }, [bootstrapped]); + (async () => { + try { + const manifest = await fetchExampleManifest(); + const defaultEntry = + manifest.find((entry) => entry.default) ?? manifest[0]; + + if (!defaultEntry) { + console.warn("No example projects available"); + return; + } + + const url = exampleZipUrl(defaultEntry); + const project = await fetchAndParseProject( + url, + defaultEntry.name, + "@example", + ); + + addUserProject(preparePartialSimulationProject(project)); + setProjectWithMeta(project); + navigate(urlFromProject(project), false, {}, true); + } catch (err) { + console.error("Failed to load default example project:", err); + } + })(); + }, [bootstrapped, userProjects, examples, addUserProject, setProjectWithMeta]); return null; }; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/Fork.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/Fork.tsx index e086eaf8..7c512166 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/Fork.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/Fork.tsx @@ -1,31 +1,28 @@ import React, { FC, useEffect, useLayoutEffect, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { useModal } from "react-modal-hook"; -import { unwrapResult } from "@reduxjs/toolkit"; -import { AppDispatch } from "../../../features/types"; import { LinkableProject, SimulationProject, } from "../../../features/project/types"; import { ModalNewProject } from "../../Modal/NewProject/ModalNewProject"; import { Scope, useScopes } from "../../../features/scopes"; -import { fetchProject } from "../../../features/project/slice"; import { forceLogIn } from "../../../features/user/utils"; -import { forkProject } from "../../../features/project/thunks"; -import { selectBootstrapped } from "../../../features/user/selectors"; -import { selectCurrentProject } from "../../../features/project/selectors"; +import { prepareForkProject } from "../../../features/project/thunks"; +import { navigate } from "../../../util/navigation"; import { urlFromProject } from "../../../routes"; import { useFatalError } from "../../ErrorBoundary/ErrorBoundary"; import { useNavigateAway } from "./hooks"; +import { useProject } from "../../../features/project/ProjectContext"; +import { useUser } from "../../../features/user/UserContext"; +import { useToast } from "../../../features/toast/ToastContext"; const useEnsureProject = ( project: LinkableProject, onCancel: VoidFunction, ): SimulationProject | null => { - const dispatch = useDispatch(); - const currentProject = useSelector(selectCurrentProject); - const bootstrapped = useSelector(selectBootstrapped); + const { currentProject, fetchProject } = useProject(); + const { bootstrapped } = useUser(); const fatalError = useFatalError(); const isCurrentProject = !!(project && currentProject) && @@ -38,29 +35,20 @@ const useEnsureProject = ( useEffect(() => { if (bootstrapped && !isCurrentProject && project) { - //@ts-expect-error redux problems - const promise = dispatch(fetchProject({ project, redirect: false })); - (async () => { try { - //@ts-expect-error redux problems - const result = unwrapResult(await promise); - + const result = await fetchProject({ project, redirect: false }); if (!result) { onCancelRef.current(); } - } catch (err) { - if (err instanceof Error && err?.name !== "AbortError") { + } catch (err: any) { + if (err?.name !== "AbortError") { fatalError(err); } } })(); - - return () => { - promise.abort(); - }; } - }, [bootstrapped, dispatch, fatalError, isCurrentProject, project]); + }, [bootstrapped, fetchProject, fatalError, isCurrentProject, project]); return isCurrentProject ? currentProject : null; }; @@ -69,12 +57,14 @@ export const HashRouterEffectFork: FC<{ project: LinkableProject; }> = ({ project: targetProject }) => { const navigateAway = useNavigateAway(targetProject); - const dispatch = useDispatch(); const { canFork, canForkIfSignedIn, canLogin } = useScopes( Scope.fork, Scope.forkIfSignedIn, Scope.login, ); + const { setProjectWithMeta } = useProject(); + const { addUserProject } = useUser(); + const { displayToast } = useToast(); const project = useEnsureProject(targetProject, () => canLogin ? forceLogIn(true) : navigateAway(true), ); @@ -88,8 +78,13 @@ export const HashRouterEffectFork: FC<{ { - //@ts-expect-error redux problems - await dispatch(forkProject(project, values)); + const result = prepareForkProject(project, values); + if (result.partialProject) { + addUserProject(result.partialProject); + } + setProjectWithMeta(result.nextProject); + navigate(urlFromProject(result.nextProject)); + displayToast(result.toastData); hideForkModal(); }} defaultName={projectName} @@ -98,14 +93,21 @@ export const HashRouterEffectFork: FC<{ visibilityDisabled={projectVisibility === "private"} /> ) : null, - [dispatch, navigateAway, project, projectName, projectVisibility], + [ + navigateAway, + project, + projectName, + projectVisibility, + setProjectWithMeta, + addUserProject, + displayToast, + ], ); useEffect(() => { if (canForkIfSignedIn || canFork) { if (canFork) { showForkModal(); - return () => { hideForkModal(); }; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/LegacySimulation.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/LegacySimulation.tsx index 939d347e..4383818c 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/LegacySimulation.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/LegacySimulation.tsx @@ -1,17 +1,16 @@ import { FC, useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { navigate } from "hookrouter"; +import { navigate } from "../../../util/navigation"; import { HashCoreAccessGateKind } from "../../HashCore/AccessGate"; import { linkableProjectByLegacyId } from "../../../util/api/queries"; -import { setAccessGate } from "../../../features/project/slice"; import { urlFromProject } from "../../../routes"; import { useHandlePromiseRejection } from "../../ErrorBoundary"; +import { useProject } from "../../../features/project/ProjectContext"; export const HashRouterEffectLegacySimulation: FC<{ id: string }> = ({ id, }) => { - const dispatch = useDispatch(); + const { setAccessGate } = useProject(); const handlePromiseRejection = useHandlePromiseRejection(); useEffect(() => { @@ -25,16 +24,14 @@ export const HashRouterEffectLegacySimulation: FC<{ id: string }> = ({ ); navigate(urlFromProject(simulation), true, { fromLegacy: true }, false); - } catch (err) { - dispatch( - setAccessGate({ - accessGate: { - kind: HashCoreAccessGateKind.NotFound, - props: { requestedProject: null }, - }, - url: window.location.pathname, - }), - ); + } catch { + setAccessGate({ + accessGate: { + kind: HashCoreAccessGateKind.NotFound, + props: { requestedProject: null }, + }, + url: window.location.pathname, + }); } } @@ -43,7 +40,7 @@ export const HashRouterEffectLegacySimulation: FC<{ id: string }> = ({ return () => { controller.abort(); }; - }, [dispatch, handlePromiseRejection, id]); + }, [setAccessGate, handlePromiseRejection, id]); return null; }; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/NewProject.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/NewProject.tsx index c7eee2a8..d2d43ca9 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/NewProject.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/NewProject.tsx @@ -1,92 +1,62 @@ -import { navigate } from "hookrouter"; import React, { FC, useEffect } from "react"; import { useModal } from "react-modal-hook"; -import { useDispatch } from "react-redux"; +import { navigate } from "../../../util/navigation"; -import { setProjectWithMeta } from "../../../features/actions"; -import { trackEvent } from "../../../features/analytics"; +import { ModalNewProject } from "../../Modal/NewProject/ModalNewProject"; +import { createLocalProjectFromTemplate } from "../../../util/api/queries/createLocalProjectFromTemplate"; import { preparePartialSimulationProject } from "../../../features/project/utils"; -import { Scope, useScopes } from "../../../features/scopes"; -import { AppDispatch } from "../../../features/types"; -import { addUserProject } from "../../../features/user/slice"; -import { forceLogIn } from "../../../features/user/utils"; -import { useSafeQueryParams } from "../../../hooks/useSafeQueryParams"; +import { templates } from "./templates/templates"; + import { urlFromProject } from "../../../routes"; -import { useFatalError } from "../../ErrorBoundary/ErrorBoundary"; -import { ModalNewProject } from "../../Modal/NewProject/ModalNewProject"; import { useNavigateAway } from "./hooks"; -import { createNewSimulationProjectFromTemplate } from "./templates/templates"; +import { useSafeQueryParams } from "../../../hooks/useSafeQueryParams"; +import { useUser } from "../../../features/user/UserContext"; +import { useProject } from "../../../features/project/ProjectContext"; export const HashRouterEffectNewProject: FC<{ template?: string }> = ({ template = "empty", }) => { - const dispatch = useDispatch(); + const { addUserProject } = useUser(); + const { setProjectWithMeta } = useProject(); const navigateAway = useNavigateAway(); const [{ namespace }] = useSafeQueryParams(); - const { canNewProject, canNewProjectIfSignedIn } = useScopes( - Scope.newProject, - Scope.newProjectIfSignedIn, - ); - const fatalError = useFatalError(); + + const actions = templates[template]; + if (!actions) { + throw new Error(`Unrecognised template ${template}`); + } const [showModal, hideModal] = useModal( () => ( { - //migration shim - - const project = createNewSimulationProjectFromTemplate( + const project = createLocalProjectFromTemplate( values.namespace, values.path, values.name, values.visibility, - template, + actions, ); - dispatch( - //@ts-expect-error redux problems - trackEvent({ - action: "New Project: Core", - label: project.pathWithNamespace, - }), - ); + if (!values.namespace) { + addUserProject(preparePartialSimulationProject(project)); + } - dispatch(addUserProject(preparePartialSimulationProject(project))); - //@ts-expect-error redux problems - dispatch(setProjectWithMeta(project)); + setProjectWithMeta(project); navigate(urlFromProject(project), false, {}, true); }} action="Create New Simulation" defaultNamespace={namespace} /> ), - [dispatch, namespace, navigateAway], + [actions, addUserProject, setProjectWithMeta, namespace, navigateAway], ); useEffect(() => { - if (canNewProject) { - showModal(); - - return () => { - hideModal(); - }; - } else if (canNewProjectIfSignedIn) { - forceLogIn(true); - } else { - fatalError( - "Should never not be able to new project if signed while at /new", - ); - } - }, [ - dispatch, - showModal, - hideModal, - canNewProject, - canNewProjectIfSignedIn, - fatalError, - ]); + showModal(); + return () => hideModal(); + }, [showModal, hideModal]); return null; }; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/NotFound.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/NotFound.tsx index d927e3d4..0c8a1f65 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/NotFound.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/NotFound.tsx @@ -1,23 +1,20 @@ import { FC, useEffect } from "react"; -import { useDispatch } from "react-redux"; import { HashCoreAccessGateKind } from "../../HashCore/AccessGate"; -import { setAccessGate } from "../../../features/project/slice"; +import { useProject } from "../../../features/project/ProjectContext"; export const HashRouterEffectNotFound: FC = () => { - const dispatch = useDispatch(); + const { setAccessGate } = useProject(); useEffect(() => { - dispatch( - setAccessGate({ - accessGate: { - kind: HashCoreAccessGateKind.NotFound, - props: { requestedProject: null }, - }, - url: window.location.pathname, - }), - ); - }, [dispatch]); + setAccessGate({ + accessGate: { + kind: HashCoreAccessGateKind.NotFound, + props: { requestedProject: null }, + }, + url: window.location.pathname, + }); + }, [setAccessGate]); return null; }; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/Onboard.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/Onboard.tsx index b675f89a..e346e6b2 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/Onboard.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/Onboard.tsx @@ -1,5 +1,5 @@ import { FC, useEffect } from "react"; -import { navigate } from "hookrouter"; +import { navigate } from "../../../util/navigation"; import { urlFromProject } from "../../../routes"; import { useGettingStartedProject } from "../../HashCore/Tour/util"; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/Project.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/Project.tsx index 5c0a8841..2521db7d 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/Project.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/Project.tsx @@ -1,33 +1,29 @@ import React, { FC, useEffect, useMemo } from "react"; -import { useDispatch, useSelector, useStore } from "react-redux"; -import { HookRouter, setQueryParams, useRoutes } from "hookrouter"; -import type { AppDispatch } from "../../../features/types"; +import { RouteMap, usePathRouter } from "../../../util/usePathRouter"; +import { setQueryParams } from "../../../util/navigation"; + import { HashRouterEffectFork } from "./Fork"; import { HashRouterEffectNotFound } from "./NotFound"; import { LinkableProject } from "../../../features/project/types"; -import { ProjectAccessScope } from "../../../shared/scopes"; -import { fetchProject } from "../../../features/project/slice"; import { getSafeQueryParams } from "../../../util/getSafeQueryParams"; -import { parseAccessCodeInParams } from "../../../util/parseAccessCodeInParams"; -import { selectBootstrapped } from "../../../features/user/selectors"; -import { selectCurrentProjectUrl } from "../../../features/project/selectors"; import { urlFromProject } from "../../../routes"; import { useHandlePromiseRejection } from "../../ErrorBoundary"; -import { withSignal } from "../../../util/withSignal"; +import { useProject } from "../../../features/project/ProjectContext"; +import { useUser } from "../../../features/user/UserContext"; -interface ProjectParams { +type ProjectParams = { namespace: string; path: string; ref: string; fork?: boolean; -} +}; const routeHandler = ({ namespace, path, ref = "main", -}: HookRouter.QueryParams): ProjectParams => ({ +}: Record): ProjectParams => ({ namespace: `@${namespace}`, path, ref, @@ -36,75 +32,71 @@ const routeHandler = ({ const HashRouterEffectProjectFetch: FC<{ project: LinkableProject; }> = ({ project }) => { - const dispatch = useDispatch(); const handlePromiseRejection = useHandlePromiseRejection(); - const bootstrapped = useSelector(selectBootstrapped); - const store = useStore(); + const { bootstrapped } = useUser(); + const { currentProjectUrl, fetchProject } = useProject(); useEffect(() => { const projectUrl = urlFromProject(project); - if ( - !bootstrapped || - selectCurrentProjectUrl(store.getState()) === projectUrl - ) { + if (!bootstrapped || currentProjectUrl === projectUrl) { return; } - const { fromLegacy, file, ...params } = getSafeQueryParams(); - const { access, ...otherParams } = parseAccessCodeInParams( - params, - ProjectAccessScope.Read, - ); + const { + fromLegacy, + file, + accessCode: _ac, + ...otherParams + } = getSafeQueryParams(); setQueryParams( { ...otherParams, fromLegacy: undefined, file: undefined, - accessCode: access?.code ?? undefined, + accessCode: undefined, }, true, ); - // Assigning here due to a bug in TS typing const controller = new AbortController(); - async function fetch() { - await withSignal( - //@ts-expect-error redux problems - dispatch( - //@ts-expect-error redux problems - fetchProject({ - project, - fromLegacy: !!fromLegacy, - file, - access, - }), - ), - controller.signal, - ); + async function doFetch() { + await fetchProject({ + project, + fromLegacy: !!fromLegacy, + file, + }); } - handlePromiseRejection(fetch()); + handlePromiseRejection(doFetch()); return () => { controller.abort(); }; - }, [dispatch, bootstrapped, handlePromiseRejection, project, store]); + }, [ + bootstrapped, + handlePromiseRejection, + project, + currentProjectUrl, + fetchProject, + ]); return null; }; +const projectRoutes: RouteMap = { + "/@:namespace/:path": routeHandler, + "/@:namespace/:path/:ref": routeHandler, + "/@:namespace/:path/:ref/fork": (args: Record) => ({ + ...routeHandler(args), + fork: true, + }), +}; + export const HashRouterEffectProject: FC = () => { - const routeResult: ProjectParams | null = useRoutes({ - ":namespace/:path": routeHandler, - ":namespace/:path/:ref": routeHandler, - ":namespace/:path/:ref/fork": (args) => ({ - ...routeHandler(args), - fork: true, - }), - }); + const routeResult: ProjectParams | null = usePathRouter(projectRoutes); const pathWithNamespace = routeResult ? `${routeResult.namespace}/${routeResult.path}` diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/RedirectToRoot.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/RedirectToRoot.tsx new file mode 100644 index 00000000..8a75d316 --- /dev/null +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/RedirectToRoot.tsx @@ -0,0 +1,9 @@ +import { FC, useEffect } from "react"; +import { navigate } from "../../../util/navigation"; + +export const HashRouterEffectRedirectToRoot: FC = () => { + useEffect(() => { + navigate("/", true); + }, []); + return null; +}; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/hooks.ts b/apps/sim-core/packages/core/src/components/HashRouter/Effect/hooks.ts index 04545506..fa47bea1 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/hooks.ts +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/hooks.ts @@ -1,14 +1,10 @@ import { useCallback, useEffect, useRef } from "react"; -import { useSelector } from "react-redux"; -import { navigate, setQueryParams } from "hookrouter"; + +import { navigate, setQueryParams } from "../../../util/navigation"; import { LinkableProject } from "../../../features/project/types"; -import { - selectAccessGate, - selectCurrentProjectUrl, - selectProjectAccess, -} from "../../../features/project/selectors"; import { urlFromProject } from "../../../routes"; +import { useProject } from "../../../features/project/ProjectContext"; /** * Ideally we'd be able to know if we've navigated in-app or loaded this URL @@ -20,12 +16,10 @@ import { urlFromProject } from "../../../routes"; */ export const useNavigateAway = (defaultProject?: LinkableProject | null) => { const defaultUrl = defaultProject ? urlFromProject(defaultProject) : null; - const accessGateUrl = useSelector(selectAccessGate)?.url; - const projectUrl = useSelector(selectCurrentProjectUrl); - const access = useSelector(selectProjectAccess); + const { accessGate, currentProjectUrl: projectUrl } = useProject(); + const accessGateUrl = accessGate?.url; const url = accessGateUrl ?? projectUrl ?? defaultUrl ?? "/"; - const queryParams = - url === projectUrl && access ? { accessCode: access.code } : {}; + const queryParams = {}; const dataRef = useRef({ url, queryParams }); diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/routes.tsx b/apps/sim-core/packages/core/src/components/HashRouter/Effect/routes.tsx index de75aac4..2262aa53 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/routes.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/routes.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { HookRouter, navigate, useRoutes } from "hookrouter"; import { HashRouterEffectDefaultProject } from "./DefaultProject"; import { HashRouterEffectLegacySimulation } from "./LegacySimulation"; @@ -7,45 +6,39 @@ import { HashRouterEffectNewProject } from "./NewProject"; import { HashRouterEffectNotFound } from "./NotFound"; import { HashRouterEffectOnboard } from "./Onboard"; import { HashRouterEffectProject } from "./Project"; -import { HashRouterEffectSignin } from "./Signin"; -import { HashRouterEffectSignup } from "./Signup"; +import { HashRouterEffectRedirectToRoot } from "./RedirectToRoot"; +import { RouteMap, usePathRouter } from "../../../util/usePathRouter"; import { getRouteFromQuery } from "../../../routes"; +import { navigate } from "../../../util/navigation"; -const routes: HookRouter.RouteObject = { +const routes: RouteMap = { "/": () => , "/new": () => , - "/new/:template": ({ template }) => ( + "/new/:template": ({ template }: { template: string }) => ( ), "/onboard": () => , - "/onboard/:step": ({ step }) => , + "/onboard/:step": ({ step }: { step: string }) => ( + + ), - "/simulation/:id": ({ id }) => , - "/simulation/:id/:name": ({ id }) => ( + "/simulation/:id": ({ id }: { id: string }) => ( + + ), + "/simulation/:id/:name": ({ id }: { id: string }) => ( ), - "/signup": () => , - "/signin": () => , + "/signup": () => , + "/signin": () => , "/@*": () => , - /** - * @todo route handlers should be side effect free – handle this elsewhere - */ "/:buildstamp/index.html": () => { - setTimeout(() => { - /** - * hookrouter's navigate has a bug where it incorrectly strips queries - * included in the path, so we have to separate them. - * - * @see https://github.com/Paratron/hookrouter/issues/70 - * @todo consider moving to https://github.com/kyeotic/raviger - */ + setImmediate(() => { const { path, query } = getRouteFromQuery(); - navigate(path, true, query, true); }); }, @@ -53,4 +46,4 @@ const routes: HookRouter.RouteObject = { "*": () => , }; -export const useRouteEffect = () => useRoutes(routes); +export const useRouteEffect = () => usePathRouter(routes); diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/empty.ts b/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/empty.ts index cd741525..c1f8bc37 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/empty.ts +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/empty.ts @@ -1,4 +1,4 @@ -import { CommitActionVerb } from "../../../../util/api/types"; +import { CommitActionVerb } from "../../../../util/api/apiTypes"; import { ProjectTemplate } from "./types"; import { defaultJsBehaviorSrc } from "../../../../util/defaultJsBehaviorSrc"; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/starter.ts b/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/starter.ts index 9fa1865d..d0a87f18 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/starter.ts +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/starter.ts @@ -1,4 +1,4 @@ -import { CommitActionVerb } from "../../../../util/api/types"; +import { CommitActionVerb } from "../../../../util/api/apiTypes"; import { ProjectTemplate } from "./types"; const starterSimAnalysis = `\ diff --git a/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/templates.ts b/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/templates.ts index 6906ad37..dceb22ae 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/templates.ts +++ b/apps/sim-core/packages/core/src/components/HashRouter/Effect/templates/templates.ts @@ -1,52 +1,8 @@ -import { - LocalStorageProject, - ProjectVisibility, - SimulationProjectWithHcFiles, -} from "../../../../features/project/types"; +import { ProjectTemplate } from "./types"; +import { emptyTemplate } from "./empty"; +import { starterTemplate } from "./starter"; -// Migration shim. -// This is meant to be stored as a template project that we can fork from. -// We can bring that back once github integration is online. -// In the meantime, we store it inline as a blob of json. -// (to update, import the project and copy the json to here.) -const EMPTY_PROJECT_JSON = - '{"id":"empty-project","name":"empty-project","description":"","image":null,"thumbnail":null,"createdAt":"2023-10-21T00:38:19.622Z","updatedAt":"2023-10-21T00:38:19.622Z","canUserEdit":true,"pathWithNamespace":"@imported/empty-project","namespace":"@imported","type":"Simulation","ref":"main","visibility":"public","ownerType":"User","forkOf":null,"latestRelease":null,"license":{"id":"5dc3da73cc0cf804dcc66a51","name":"MIT License"},"keywords":[],"config":{"files":["README.md","src/globals.json","views/analysis.json","dependencies.json","experiments.json","src/init.json","src/behaviors/custom.js"],"keywords":[],"type":"Simulation"},"access":null,"actions":[],"files":[{"id":"description","path":{"root":"","dir":"","ext":".md","name":"README","base":"README.md","formatted":"README.md"},"repoPath":"README.md","contents":"This is a new simulation - it\'s an empty scaffold to build from.\\n\\n## Create agents for the simulation:\\n\\nDefine initial agents in init.json by adding objects to the array\\n Ex. ```[{“position”:[0,0], “behaviors”: [‘custom.js’’}]```\\nOR convert init.json to a JavaScript or Python file by right clicking on init.json and return an array of agents\\nAgents will run each of their behaviors on each step of the simulation\\n\\n## Add behaviors to the agents\\n\\nCreate new behavior files by clicking the new file indicator in the top left panel.\\nSelect python or javascript.\\nAttach the behaviors to the agent by adding them to the agents behavior array\\n Ex. ```[{“position”:[0,0], “behaviors”: [‘custom.js’’}]```\\nBehaviors can access and modify the agent state\\nThey can allow the agent to view other agents with neighbors: Neighbors = context.neighbors()\\nOr allow agents to interact by sending messages state.addMessage(...)\\n\\n## Run the simulation\\n\\nClick the Play button or the Step Simulation button in the bottom right under the viewer\\nIf you’ve defined a position on the agent, you’ll see the agent appear in the 3d viewer\\nClick reset to reset the simulation to the initial state.\\n","kind":"Required"},{"id":"properties","path":{"root":"","dir":"","ext":".json","name":"globals","base":"globals.json","formatted":"globals.json"},"repoPath":"src/globals.json","contents":"{}","kind":"Required"},{"id":"analysis","path":{"root":"","dir":"","ext":".json","name":"analysis","base":"analysis.json","formatted":"analysis.json"},"repoPath":"views/analysis.json","contents":"{\\n \\"outputs\\": {}, \\n \\"plots\\": []\\n}","kind":"Required"},{"id":"dependencies","path":{"root":"","dir":"","ext":".json","name":"dependencies","base":"dependencies.json","formatted":"dependencies.json"},"repoPath":"dependencies.json","contents":"{}","kind":"Required"},{"id":"experiments","path":{"root":"","dir":"","ext":".json","name":"experiments","base":"experiments.json","formatted":"experiments.json"},"repoPath":"experiments.json","contents":"{}","kind":"Required"},{"id":"initialState","path":{"root":"","dir":"","ext":".json","name":"init","base":"init.json","formatted":"init.json"},"repoPath":"src/init.json","contents":"[]","kind":"Init"},{"id":"custom_js_1_0","path":{"root":"","dir":"","ext":".js","name":"custom","base":"custom.js","formatted":"custom.js"},"repoPath":"src/behaviors/custom.js","contents":"/**\\n * @param {AgentState} state of the agent\\n * @param {AgentContext} context of the agent\\n */\\nconst behavior = (state, context) => {\\n\\n};\\n","kind":"Behavior","keys":{"keys":{"uuid":"e197580e-7a90-4056-bec4-e803601f9ae9","meta":{"nullable":false,"type":"struct"},"key":"fields","rows":[],"version":"2"},"built_in_key_use":null,"dynamic_access":false,"_trackCreation":false}}]}'; - -const STARTER_PROJECT_JSON = `{"id":"empty-template-project","name":"empty-template-project","description":"","image":null,"thumbnail":null,"createdAt":"2023-10-21T00:35:27.289Z","updatedAt":"2023-10-21T00:35:27.289Z","canUserEdit":true,"pathWithNamespace":"@imported/empty-template-project","namespace":"@imported","type":"Simulation","ref":"main","visibility":"public","ownerType":"User","forkOf":null,"latestRelease":null,"license":{"id":"5dc3da73cc0cf804dcc66a51","name":"MIT License"},"keywords":[],"config":{"files":["README.md","src/globals.json","views/analysis.json","dependencies.json","experiments.json","src/init.json","src/behaviors/new_behavior.js","dependencies/@hash/create-agents/src/behaviors/create_agents.js","dependencies/@hash/create-scatters/src/behaviors/create_scatters.js","dependencies/@hash/random-movement/src/behaviors/random_movement.rs","dependencies/@hash/remove-self/src/behaviors/remove_self.js"],"keywords":[],"type":"Simulation"},"access":null,"actions":[],"files":[{"id":"description","path":{"root":"","dir":"","ext":".md","name":"README","base":"README.md","formatted":"README.md"},"repoPath":"README.md","contents":"This starter template shows how to use basic features of HASH.\\nIt should provide direction in using common patterns and tools on the platform.\\n","kind":"Required"},{"id":"properties","path":{"root":"","dir":"","ext":".json","name":"globals","base":"globals.json","formatted":"globals.json"},"repoPath":"src/globals.json","contents":"{\\n \\"topology\\": {\\n \\"x_bounds\\": [0, 10],\\n \\"y_bounds\\": [0, 10]\\n },\\n \\"max_height\\": 10\\n}","kind":"Required"},{"id":"analysis","path":{"root":"","dir":"","ext":".json","name":"analysis","base":"analysis.json","formatted":"analysis.json"},"repoPath":"views/analysis.json","contents":"{\\n \\"outputs\\": {}, \\n \\"plots\\": []\\n}","kind":"Required"},{"id":"dependencies","path":{"root":"","dir":"","ext":".json","name":"dependencies","base":"dependencies.json","formatted":"dependencies.json"},"repoPath":"dependencies.json","contents":"{\\n \\"@hash/create-agents/create_agents.js\\": \\"2.1.1\\",\\n \\"@hash/create-scatters/create_scatters.js\\": \\"3.1.1\\",\\n \\"@hash/random-movement/random_movement.rs\\": \\"1.0.0\\",\\n \\"@hash/remove-self/remove_self.js\\": \\"2.1.0\\"\\n}","kind":"Required"},{"id":"experiments","path":{"root":"","dir":"","ext":".json","name":"experiments","base":"experiments.json","formatted":"experiments.json"},"repoPath":"experiments.json","contents":"{}","kind":"Required"},{"id":"initialState","path":{"root":"","dir":"","ext":".json","name":"init","base":"init.json","formatted":"init.json"},"repoPath":"src/init.json","contents":"[\\n {\\n \\"position\\": [0, 0],\\n \\"color\\": \\"blue\\",\\n \\"height\\": 1,\\n \\"behaviors\\": [\\"new_behavior.js\\"]\\n },\\n {\\n \\"name\\": \\"creator\\",\\n \\"behaviors\\": [\\n \\"@hash/create-scatters/create_scatters.js\\",\\n \\"@hash/create-agents/create_agents.js\\",\\n \\"@hash/remove-self/remove_self.js\\"\\n ],\\n \\"scatter_templates\\": [\\n {\\n \\"template_name\\": \\"randomly_moving\\",\\n \\"template_count\\": 10,\\n \\"color\\": \\"green\\",\\n \\"height\\": 1,\\n \\"behaviors\\": [\\"@hash/random-movement/random_movement.rs\\"]\\n }\\n ]\\n }\\n]","kind":"Init"},{"id":"create__agents_js_1_0","path":{"root":"","dir":"","ext":".js","name":"create_agents","base":"create_agents.js","formatted":"create_agents.js"},"repoPath":"dependencies/@hash/create-agents/src/behaviors/create_agents.js","contents":"/**\\n * This behavior generates all the agents that have been defined by other behaviors.\\n * \\n * agents {: [agent definitions]} - stores lists of agent definitions\\n */\\nfunction behavior(state, context) {\\n let messages = state.get(\\"messages\\");\\n const agents = state.get(\\"agents\\");\\n\\n for (agent_name in agents) {\\n const agent_list = agents[agent_name];\\n\\n for (agent of agent_list) {\\n messages.push({\\n \\"to\\": \\"hash\\", \\n \\"type\\": \\"create_agent\\",\\n \\"data\\": agent\\n })\\n }\\n }\\n\\n state.set(\\"messages\\", messages)\\n}","kind":"Behavior","keys":{"keys":{"uuid":"8d0e8a17-617d-4179-8e4d-3a955e339c81","meta":{"nullable":false,"type":"struct"},"key":"fields","rows":[["agents",{"uuid":"bf465ce0-7da6-4df7-bca1-415dab5ded99","meta":{"type":"any","nullable":true},"key":"scalar"}]],"version":"2"},"built_in_key_use":null,"_trackCreation":false}},{"id":"create__scatters_js_1_0","path":{"root":"","dir":"","ext":".js","name":"create_scatters","base":"create_scatters.js","formatted":"create_scatters.js"},"repoPath":"dependencies/@hash/create-scatters/src/behaviors/create_scatters.js","contents":"/**\\n * This behavior creates agents with random placements based an the defined templates in the creator agent.\\n * \\n * scatter_templates [{\\n * \\"template_name\\": name,\\n * \\"template_count\\": count,\\n * ...other properties\\n * }] - stores sets of unique properties that will be added to each type of agent.\\n */\\nfunction behavior(state, context) {\\n const { x_bounds, y_bounds } = context.globals()[\\"topology\\"];\\n\\n const width = x_bounds[1] - x_bounds[0];\\n const height = y_bounds[1] - y_bounds[0];\\n\\n const scatter_templates = state.get(\\"scatter_templates\\");\\n let agents = state.get(\\"agents\\")\\n // Make sure to not overwrite existing agents\\n agents = agents ? agents : {};\\n\\n // Create scatter for each defined template\\n for (template of scatter_templates) { //scatter_templates.forEach(template => {\\n const name = template[\\"template_name\\"];\\n const count = template[\\"template_count\\"];\\n\\n // Store agents in an array in the creator agent\\n agents[name] = [...Array(count)].map(_ => {\\n // Choose random position within topology\\n const x = Math.floor(Math.random() * width) + x_bounds[0];\\n const y = Math.floor(Math.random() * height) + y_bounds[0];\\n \\n let agent = {\\n ...template,\\n position: [x, y]\\n };\\n\\n delete agent.template_name;\\n delete agent.template_count;\\n return agent;\\n })\\n };\\n\\n state.set(\\"agents\\", agents);\\n}","kind":"Behavior","keys":{"keys":{"uuid":"7a83585e-04ad-48ce-bc24-ba5da50357af","meta":{"nullable":false,"type":"struct"},"key":"fields","rows":[["scatter_templates",{"uuid":"f06985c8-27f9-444b-8b96-8f1a785c4434","meta":{"type":"any","nullable":false},"key":"scalar"}],["agents",{"uuid":"96ec0a89-cf35-4db7-9ab0-bb8ccadaac5b","meta":{"type":"any","nullable":true},"key":"scalar"}]],"version":"2"},"built_in_key_use":null,"_trackCreation":false}},{"id":"new__behavior_js_1_0","path":{"root":"","dir":"","ext":".js","name":"new_behavior","base":"new_behavior.js","formatted":"new_behavior.js"},"repoPath":"src/behaviors/new_behavior.js","contents":"/**\\n * Write a brief description of your behavior here.\\n * \\n * This behavior will cause an agent to increase its height each step\\n * until it reaches the max_height defined in globals.json\\n */\\nconst behavior = (state, context) => {\\n // You can access agent properties by using state.get()\\n let height = state.get(\\"height\\");\\n \\n if (height < context.globals()[\\"max_height\\"]) {\\n height += 1;\\n }\\n\\n // You can set agent properties using state.set()\\n state.set(\\"height\\", height);\\n};","kind":"Behavior","keys":{"keys":{"uuid":"658833c5-908d-482b-a93e-b57ab3aed041","meta":{"nullable":false,"type":"struct"},"key":"fields","rows":[],"version":"2"},"built_in_key_use":null,"dynamic_access":false,"_trackCreation":false}},{"id":"random__movement_rs_1_0","path":{"root":"","dir":"","ext":".rs","name":"random_movement","base":"random_movement.rs","formatted":"random_movement.rs"},"repoPath":"dependencies/@hash/random-movement/src/behaviors/random_movement.rs","contents":"use crate::{\\n behaviors::get_state_or_property,\\n prelude::{AgentState, Context, SimulationResult},\\n};\\nuse rand::Rng;\\n\\npub fn random_movement(mut state: AgentState, context: &Context) -> SimulationResult {\\n // If min and/or max neighbors are defined, move until our neighbor count is within those bounds.\\n // if one or the other is undefined, it's open-ended.\\n let neighbor_count = context.neighbors.len() as i64;\\n let min_neighbors: i64 =\\n get_state_or_property(&state, &context, \\"random_movement_seek_min_neighbors\\", -1);\\n\\n let max_neighbors: i64 =\\n get_state_or_property(&state, &context, \\"random_movement_seek_max_neighbors\\", -1);\\n\\n fn get_satisfaction(neighbor_count: i64, min_neighbors: i64, max_neighbors: i64) -> bool {\\n let min_satisfied = neighbor_count >= min_neighbors;\\n let min_defined = min_neighbors >= 0;\\n let max_satisfied = neighbor_count <= max_neighbors;\\n let max_defined = max_neighbors >= 0;\\n\\n // Both defined; both need to be satisfied.\\n if min_defined && max_defined {\\n return min_satisfied && max_satisfied;\\n }\\n\\n // only min defined? only need to satisfy it.\\n if min_defined {\\n return min_satisfied;\\n }\\n\\n // only max defined? only need to satisfy it.\\n if max_defined {\\n return max_satisfied;\\n }\\n\\n // No checks defined; can't get no satisfaction.\\n false\\n }\\n\\n if get_satisfaction(neighbor_count, min_neighbors, max_neighbors) {\\n // Our neighbor metrics are satisfied, no need to move.\\n return Ok(state);\\n }\\n\\n let step_size: f64 = get_state_or_property(&state, &context, \\"random_movement_step_size\\", 1.0);\\n\\n // Take a step forward, backwards, or nowhere by step_size.\\n fn step(step_size: f64) -> f64 {\\n let mod3 = rand::thread_rng().gen::() % 3;\\n if mod3 == 0 {\\n step_size\\n } else if mod3 == 1 {\\n -step_size\\n } else {\\n 0.0\\n }\\n }\\n\\n let pos = state.get_pos_mut()?;\\n pos[\\"x\\"] += step(step_size);\\n pos[\\"y\\"] += step(step_size);\\n\\n Ok(state)\\n}\\n","kind":"Behavior","keys":{"keys":{"uuid":"342969f1-d497-4429-b86f-e4baf170f436","meta":{"nullable":false,"type":"struct"},"key":"fields","rows":[["random_movement_step_size",{"uuid":"f7a96d22-a511-4854-aa2f-2d2ef884092f","meta":{"type":"number","nullable":true},"key":"scalar"}],["random_movement_seek_min_neighbors",{"uuid":"4e7d5caa-01dd-4421-954c-263155c990e6","meta":{"type":"number","nullable":true},"key":"scalar"}],["random_movement_seek_max_neighbors",{"uuid":"d116258f-4143-4db0-bd77-70a148321388","meta":{"type":"number","nullable":true},"key":"scalar"}]],"version":"2"},"built_in_key_use":{"selected":["position"]},"_trackCreation":false}},{"id":"remove__self_js_1_0","path":{"root":"","dir":"","ext":".js","name":"remove_self","base":"remove_self.js","formatted":"remove_self.js"},"repoPath":"dependencies/@hash/remove-self/src/behaviors/remove_self.js","contents":"/**\\n * This behavior removes its agent from the simulation\\n * after one time step.\\n */\\nfunction behavior(state, context) {\\n // Not specifying an agent_id automatically causes the\\n // sender to be the target of the remove action\\n state.addMessage(\\"HASH\\", \\"remove_agent\\");\\n}","kind":"Behavior","keys":{"keys":{"uuid":"3b99a97f-03df-4d25-9d34-37296626bd57","meta":{"nullable":false,"type":"struct"},"key":"fields","rows":[],"version":"2"},"built_in_key_use":null,"_trackCreation":false}}]}`; - -const templates: Record = { - empty: JSON.parse(EMPTY_PROJECT_JSON), - starter: JSON.parse(STARTER_PROJECT_JSON), -}; - -export const createNewSimulationProjectFromTemplate = ( - namespace: string, - path: string, - name: string, - visibility: ProjectVisibility, - template: string, -): SimulationProjectWithHcFiles => { - if (!(namespace && path && name)) { - throw Error( - "Namespace, path, and name must be specified when creating a project.", - ); - } - - const templateProject = templates[template]; - if (!templateProject) { - throw new Error(`Unrecognized template ${template}`); - } - const project: SimulationProjectWithHcFiles = { - ...templateProject, - id: path, - name, - pathWithNamespace: `${namespace}/${path}`, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - namespace, - visibility: visibility, - access: null, - }; - - return project; +export const templates: Record = { + empty: emptyTemplate, + starter: starterTemplate, }; diff --git a/apps/sim-core/packages/core/src/components/HashRouter/HashRouter.tsx b/apps/sim-core/packages/core/src/components/HashRouter/HashRouter.tsx index 22658fb1..436f5119 100644 --- a/apps/sim-core/packages/core/src/components/HashRouter/HashRouter.tsx +++ b/apps/sim-core/packages/core/src/components/HashRouter/HashRouter.tsx @@ -1,24 +1,35 @@ import React, { FC, memo, useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import type { AppDispatch } from "../../features/types"; import { HashCore } from "../HashCore"; import { LoadingIcon } from "../LoadingIcon"; -import { bootstrapApp } from "../../features/thunks"; -import { selectBootstrapped } from "../../features/user/selectors"; +import { runBootstrap } from "../../features/bootstrap"; import { useHandlePromiseRejection } from "../ErrorBoundary"; import { useRouteEffect } from "./Effect"; +import { useUser } from "../../features/user/UserContext"; +import { useExamples } from "../../features/examples/ExamplesContext"; +import { useToast } from "../../features/toast/ToastContext"; +import { useProject } from "../../features/project/ProjectContext"; export const HashRouter: FC = memo(function HashApp() { - const dispatch = useDispatch(); - const bootstrapped = useSelector(selectBootstrapped); + const { bootstrapped, bootstrapUser } = useUser(); + const { setExamples } = useExamples(); + const { setToastForProject } = useToast(); + const { currentProject } = useProject(); const handlePromiseRejection = useHandlePromiseRejection(); const routeEffect = useRouteEffect(); useEffect(() => { - //@ts-expect-error redux problems - handlePromiseRejection(dispatch(bootstrapApp())); - }, [handlePromiseRejection, dispatch]); + handlePromiseRejection( + runBootstrap({ + bootstrapUser, + setExamples, + setToastForProject, + currentProject, + }), + ); + // Only run once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [handlePromiseRejection]); if (!(bootstrapped && routeEffect)) { return ; @@ -31,8 +42,3 @@ export const HashRouter: FC = memo(function HashApp() { ); }); - -// // @ts-expect-error -// HashApp.whyDidYouRender = { -// customName: "HashApp" -// }; diff --git a/apps/sim-core/packages/core/src/components/Icon/AccountMultiple/IconAccountMultiple.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/AccountMultiple/IconAccountMultiple.spec.tsx index eb64291e..6aa85a6e 100644 --- a/apps/sim-core/packages/core/src/components/Icon/AccountMultiple/IconAccountMultiple.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/AccountMultiple/IconAccountMultiple.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconAccountMultiple } from "./IconAccountMultiple"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/AddDatapoint/IconAddDatapoint.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/AddDatapoint/IconAddDatapoint.spec.tsx index bd58629c..4cad5916 100644 --- a/apps/sim-core/packages/core/src/components/Icon/AddDatapoint/IconAddDatapoint.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/AddDatapoint/IconAddDatapoint.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconAddDatapoint } from "./IconAddDatapoint"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Alert/IconAlert.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Alert/IconAlert.spec.tsx index ea67e62c..5922e723 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Alert/IconAlert.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Alert/IconAlert.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconAlert } from "./IconAlert"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/AlertOutline/IconAlertOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/AlertOutline/IconAlertOutline.spec.tsx index c0497dcc..16a0c0a7 100644 --- a/apps/sim-core/packages/core/src/components/Icon/AlertOutline/IconAlertOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/AlertOutline/IconAlertOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconAlertOutline } from "./IconAlertOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ArrowDownDrop/IconArrowDownDrop.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ArrowDownDrop/IconArrowDownDrop.spec.tsx index 89cbdac1..1f42a980 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ArrowDownDrop/IconArrowDownDrop.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ArrowDownDrop/IconArrowDownDrop.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconArrowDownDrop } from "./IconArrowDownDrop"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ArrowLeftBold/IconArrowLeftBold.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ArrowLeftBold/IconArrowLeftBold.spec.tsx index 7b5890ad..a98e48df 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ArrowLeftBold/IconArrowLeftBold.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ArrowLeftBold/IconArrowLeftBold.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconArrowLeftBold } from "./IconArrowLeftBold"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ArrowRightBold/IconArrowRightBold.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ArrowRightBold/IconArrowRightBold.spec.tsx index 62a50e3c..2f56b946 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ArrowRightBold/IconArrowRightBold.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ArrowRightBold/IconArrowRightBold.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconArrowRightBold } from "./IconArrowRightBold"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/AutoFix/IconAutoFix.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/AutoFix/IconAutoFix.spec.tsx index 64831096..96419bc6 100644 --- a/apps/sim-core/packages/core/src/components/Icon/AutoFix/IconAutoFix.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/AutoFix/IconAutoFix.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconAutoFix } from "./IconAutoFix"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Beaker/IconBeaker.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Beaker/IconBeaker.spec.tsx index 4036d149..83aef63f 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Beaker/IconBeaker.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Beaker/IconBeaker.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconBeaker } from "./IconBeaker"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Brain/IconBrain.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Brain/IconBrain.spec.tsx index c63aee90..1f74b88c 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Brain/IconBrain.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Brain/IconBrain.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconBrain } from "./IconBrain"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Cancel/IconCancel.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Cancel/IconCancel.spec.tsx index 8b7d873e..0e95576b 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Cancel/IconCancel.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Cancel/IconCancel.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCancel } from "./IconCancel"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ChartBarStacked/IconChartBarStacked.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ChartBarStacked/IconChartBarStacked.spec.tsx index e074f900..4940a45a 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ChartBarStacked/IconChartBarStacked.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ChartBarStacked/IconChartBarStacked.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconChartBarStacked } from "./IconChartBarStacked"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ChartLine/IconChartLine.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ChartLine/IconChartLine.spec.tsx index 4ca4c857..8b432e08 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ChartLine/IconChartLine.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ChartLine/IconChartLine.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconChartLine } from "./IconChartLine"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ChartLineVariant/IconChartLineVariant.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ChartLineVariant/IconChartLineVariant.spec.tsx index a85f829c..466ff0ca 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ChartLineVariant/IconChartLineVariant.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ChartLineVariant/IconChartLineVariant.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconChartLineVariant } from "./IconChartLineVariant"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Check/IconCheck.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Check/IconCheck.spec.tsx index 71aa1c6f..498a6dfd 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Check/IconCheck.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Check/IconCheck.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCheck } from "./IconCheck"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedCircleOutline/IconCheckboxMarkedCircleOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedCircleOutline/IconCheckboxMarkedCircleOutline.spec.tsx index 1f5c03f5..5dcbad8c 100644 --- a/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedCircleOutline/IconCheckboxMarkedCircleOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedCircleOutline/IconCheckboxMarkedCircleOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCheckboxMarkedCircleOutline } from "./IconCheckboxMarkedCircleOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedOutline/IconCheckboxMarkedOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedOutline/IconCheckboxMarkedOutline.spec.tsx index 29330c57..42004207 100644 --- a/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedOutline/IconCheckboxMarkedOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/CheckboxMarkedOutline/IconCheckboxMarkedOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCheckboxMarkedOutline } from "./IconCheckboxMarkedOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ChevronRight/IconChevronRight.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ChevronRight/IconChevronRight.spec.tsx index 151f0e5d..a39fb65a 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ChevronRight/IconChevronRight.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ChevronRight/IconChevronRight.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconChevronRight } from "./IconChevronRight"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Cloud/IconCloud.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Cloud/IconCloud.spec.tsx index 879c1d11..3fd70ffa 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Cloud/IconCloud.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Cloud/IconCloud.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCloud } from "./IconCloud"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/CodeTagsCheck/IconCodeTagsCheck.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/CodeTagsCheck/IconCodeTagsCheck.spec.tsx index 993e222b..c769a6bf 100644 --- a/apps/sim-core/packages/core/src/components/Icon/CodeTagsCheck/IconCodeTagsCheck.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/CodeTagsCheck/IconCodeTagsCheck.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCodeTagsCheck } from "./IconCodeTagsCheck"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ContentCopy/IconContentCopy.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ContentCopy/IconContentCopy.spec.tsx index af1c2f29..3486045b 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ContentCopy/IconContentCopy.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ContentCopy/IconContentCopy.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconContentCopy } from "./IconContentCopy"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ContentDuplicate/IconContentDuplicate.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ContentDuplicate/IconContentDuplicate.spec.tsx index f3d7b0a8..55380fb4 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ContentDuplicate/IconContentDuplicate.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ContentDuplicate/IconContentDuplicate.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconContentDuplicate } from "./IconContentDuplicate"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/CreateDashboard/IconCreateDashboard.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/CreateDashboard/IconCreateDashboard.spec.tsx index ff0d6d1c..62978de1 100644 --- a/apps/sim-core/packages/core/src/components/Icon/CreateDashboard/IconCreateDashboard.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/CreateDashboard/IconCreateDashboard.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCreateDashboard } from "./IconCreateDashboard"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/CreatePlot/IconCreatePlot.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/CreatePlot/IconCreatePlot.spec.tsx index 3f9078f6..73f39fed 100644 --- a/apps/sim-core/packages/core/src/components/Icon/CreatePlot/IconCreatePlot.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/CreatePlot/IconCreatePlot.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCreatePlot } from "./IconCreatePlot"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/CubeUnfolded/IconCubeUnfolded.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/CubeUnfolded/IconCubeUnfolded.spec.tsx index 5ef26231..c78fb779 100644 --- a/apps/sim-core/packages/core/src/components/Icon/CubeUnfolded/IconCubeUnfolded.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/CubeUnfolded/IconCubeUnfolded.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconCubeUnfolded } from "./IconCubeUnfolded"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/DirectionsFork/IconDirectionsFork.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/DirectionsFork/IconDirectionsFork.spec.tsx index 3e8a852f..bc1c7313 100644 --- a/apps/sim-core/packages/core/src/components/Icon/DirectionsFork/IconDirectionsFork.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/DirectionsFork/IconDirectionsFork.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconDirectionsFork } from "./IconDirectionsFork"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/DotsHorizontal/IconDotsHorizontal.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/DotsHorizontal/IconDotsHorizontal.spec.tsx index e5b46818..12004cbf 100644 --- a/apps/sim-core/packages/core/src/components/Icon/DotsHorizontal/IconDotsHorizontal.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/DotsHorizontal/IconDotsHorizontal.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconDotsHorizontal } from "./IconDotsHorizontal"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/DotsVertical/IconDotsVertical.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/DotsVertical/IconDotsVertical.spec.tsx index fc3aceed..5a9af8b0 100644 --- a/apps/sim-core/packages/core/src/components/Icon/DotsVertical/IconDotsVertical.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/DotsVertical/IconDotsVertical.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconDotsVertical } from "./IconDotsVertical"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Download/IconDownload.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Download/IconDownload.spec.tsx index 83cc9fea..14ae2bf2 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Download/IconDownload.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Download/IconDownload.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconDownload } from "./IconDownload"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/DragVertical/IconDragVertical.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/DragVertical/IconDragVertical.spec.tsx index 77487916..5f8917e2 100644 --- a/apps/sim-core/packages/core/src/components/Icon/DragVertical/IconDragVertical.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/DragVertical/IconDragVertical.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconDragVertical } from "./IconDragVertical"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Earth/IconEarth.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Earth/IconEarth.spec.tsx index 5515e860..663d082e 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Earth/IconEarth.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Earth/IconEarth.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconEarth } from "./IconEarth"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ExperimentsCreate/IconExperimentsCreate.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ExperimentsCreate/IconExperimentsCreate.spec.tsx index b4657649..d0a6c3a6 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ExperimentsCreate/IconExperimentsCreate.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ExperimentsCreate/IconExperimentsCreate.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconExperimentsCreate } from "./IconExperimentsCreate"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/ExperimentsRun/IconExperimentsRun.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/ExperimentsRun/IconExperimentsRun.spec.tsx index 1e628f61..70cb843d 100644 --- a/apps/sim-core/packages/core/src/components/Icon/ExperimentsRun/IconExperimentsRun.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/ExperimentsRun/IconExperimentsRun.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconExperimentsRun } from "./IconExperimentsRun"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Eye/IconEye.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Eye/IconEye.spec.tsx index f9f545fd..65ca3e1f 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Eye/IconEye.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Eye/IconEye.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconEye } from "./IconEye"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/EyeOutline/IconEyeOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/EyeOutline/IconEyeOutline.spec.tsx index e8b18030..d26de151 100644 --- a/apps/sim-core/packages/core/src/components/Icon/EyeOutline/IconEyeOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/EyeOutline/IconEyeOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconEyeOutline } from "./IconEyeOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/FileFind/IconFileFind.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/FileFind/IconFileFind.spec.tsx index 67f020e6..75fa1280 100644 --- a/apps/sim-core/packages/core/src/components/Icon/FileFind/IconFileFind.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/FileFind/IconFileFind.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconFileFind } from "./IconFileFind"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/FileOutline/IconFileOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/FileOutline/IconFileOutline.spec.tsx index b18d1403..093f036c 100644 --- a/apps/sim-core/packages/core/src/components/Icon/FileOutline/IconFileOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/FileOutline/IconFileOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconFileOutline } from "./IconFileOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/FilePlus/IconFilePlus.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/FilePlus/IconFilePlus.spec.tsx index dfc6ac39..91ca16c9 100644 --- a/apps/sim-core/packages/core/src/components/Icon/FilePlus/IconFilePlus.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/FilePlus/IconFilePlus.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconFilePlus } from "./IconFilePlus"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Filter/IconFilter.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Filter/IconFilter.spec.tsx index 15ef5b5d..6e6d028c 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Filter/IconFilter.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Filter/IconFilter.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconFilter } from "./IconFilter"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/FolderLock/IconFolderLock.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/FolderLock/IconFolderLock.spec.tsx index d13658b1..4a4c689a 100644 --- a/apps/sim-core/packages/core/src/components/Icon/FolderLock/IconFolderLock.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/FolderLock/IconFolderLock.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconFolderLock } from "./IconFolderLock"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/HCoreMono/IconHCoreMono.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/HCoreMono/IconHCoreMono.spec.tsx index 6f281508..db87f2bd 100644 --- a/apps/sim-core/packages/core/src/components/Icon/HCoreMono/IconHCoreMono.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/HCoreMono/IconHCoreMono.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconHCoreMono } from "./IconHCoreMono"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/HIndex/IconHIndex.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/HIndex/IconHIndex.spec.tsx index 659dcb3e..59795650 100644 --- a/apps/sim-core/packages/core/src/components/Icon/HIndex/IconHIndex.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/HIndex/IconHIndex.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconHIndex } from "./IconHIndex"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/HelpCircle/IconHelpCircle.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/HelpCircle/IconHelpCircle.spec.tsx index d6ccdb50..76da100b 100644 --- a/apps/sim-core/packages/core/src/components/Icon/HelpCircle/IconHelpCircle.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/HelpCircle/IconHelpCircle.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconHelpCircle } from "./IconHelpCircle"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/HelpCircleOutline/IconHelpCircleOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/HelpCircleOutline/IconHelpCircleOutline.spec.tsx index b5ce828f..0386c357 100644 --- a/apps/sim-core/packages/core/src/components/Icon/HelpCircleOutline/IconHelpCircleOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/HelpCircleOutline/IconHelpCircleOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconHelpCircleOutline } from "./IconHelpCircleOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Import/IconImport.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Import/IconImport.spec.tsx index 727a0838..5825ff9d 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Import/IconImport.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Import/IconImport.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconImport } from "./IconImport"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/InformationOutline/IconInformationOutline.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/InformationOutline/IconInformationOutline.spec.tsx index a6e7cc55..c5561b82 100644 --- a/apps/sim-core/packages/core/src/components/Icon/InformationOutline/IconInformationOutline.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/InformationOutline/IconInformationOutline.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconInformationOutline } from "./IconInformationOutline"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/KeyPlus/IconKeyPlus.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/KeyPlus/IconKeyPlus.spec.tsx index 75b3a4e6..a1d29a6d 100644 --- a/apps/sim-core/packages/core/src/components/Icon/KeyPlus/IconKeyPlus.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/KeyPlus/IconKeyPlus.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconKeyPlus } from "./IconKeyPlus"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/KeyboardReturn/IconKeyboardReturn.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/KeyboardReturn/IconKeyboardReturn.spec.tsx index be93a817..62b2d4d3 100644 --- a/apps/sim-core/packages/core/src/components/Icon/KeyboardReturn/IconKeyboardReturn.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/KeyboardReturn/IconKeyboardReturn.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconKeyboardReturn } from "./IconKeyboardReturn"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Loading/IconLoading.tsx b/apps/sim-core/packages/core/src/components/Icon/Loading/IconLoading.tsx index e3d3f5b7..e593f487 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Loading/IconLoading.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Loading/IconLoading.tsx @@ -1,5 +1,5 @@ import React, { FC, memo, useLayoutEffect, useRef } from "react"; -// @ts-expect-error unclear what the issue is here +// @ts-expect-error -- gradient-path has no type declarations import GradientPath from "gradient-path"; import { IconLoadingProps } from "./types"; diff --git a/apps/sim-core/packages/core/src/components/Icon/Loading/LazyIconLoading.tsx b/apps/sim-core/packages/core/src/components/Icon/Loading/LazyIconLoading.tsx index 0a3bb804..442c2720 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Loading/LazyIconLoading.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Loading/LazyIconLoading.tsx @@ -2,9 +2,7 @@ import React, { FC, lazy, Suspense } from "react"; import { IconLoadingProps } from "./types"; -const lazyIconPromise = import( - /* webpackChunkName: "IconLoading", webpackPrefetch: true */ "./IconLoading" -); +const lazyIconPromise = import("./IconLoading"); const LazyIconLoadingInner = lazy(async () => ({ default: (await lazyIconPromise).IconLoading, diff --git a/apps/sim-core/packages/core/src/components/Icon/Lock/IconLock.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Lock/IconLock.spec.tsx index c156bc0e..1c06c049 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Lock/IconLock.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Lock/IconLock.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconLock } from "./IconLock"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/MenuDown/IconMenuDown.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/MenuDown/IconMenuDown.spec.tsx index 466879f6..4b94f526 100644 --- a/apps/sim-core/packages/core/src/components/Icon/MenuDown/IconMenuDown.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/MenuDown/IconMenuDown.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconMenuDown } from "./IconMenuDown"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/More/IconMore.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/More/IconMore.spec.tsx index 11c70fbd..c6c27995 100644 --- a/apps/sim-core/packages/core/src/components/Icon/More/IconMore.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/More/IconMore.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconMore } from "./IconMore"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/OpenInNew/IconOpenInNew.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/OpenInNew/IconOpenInNew.spec.tsx index c66f8de4..07abe873 100644 --- a/apps/sim-core/packages/core/src/components/Icon/OpenInNew/IconOpenInNew.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/OpenInNew/IconOpenInNew.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconOpenInNew } from "./IconOpenInNew"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/PackageDown/IconPackageDown.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/PackageDown/IconPackageDown.spec.tsx index 8da32a55..a5748ff9 100644 --- a/apps/sim-core/packages/core/src/components/Icon/PackageDown/IconPackageDown.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/PackageDown/IconPackageDown.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconPackageDown } from "./IconPackageDown"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Pause/IconPause.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Pause/IconPause.spec.tsx index 5aa060c5..57226d92 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Pause/IconPause.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Pause/IconPause.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconPause } from "./IconPause"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Plus/IconPlus.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Plus/IconPlus.spec.tsx index 9c1a3e35..afa05a6b 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Plus/IconPlus.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Plus/IconPlus.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconPlus } from "./IconPlus"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/PresentationPause/IconPresentationPause.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/PresentationPause/IconPresentationPause.spec.tsx index fe9b6c52..54dc9156 100644 --- a/apps/sim-core/packages/core/src/components/Icon/PresentationPause/IconPresentationPause.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/PresentationPause/IconPresentationPause.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconPresentationPause } from "./IconPresentationPause"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/PresentationPlay/IconPresentationPlay.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/PresentationPlay/IconPresentationPlay.spec.tsx index 1e4aa880..607be9ac 100644 --- a/apps/sim-core/packages/core/src/components/Icon/PresentationPlay/IconPresentationPlay.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/PresentationPlay/IconPresentationPlay.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconPresentationPlay } from "./IconPresentationPlay"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Python/IconPython.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Python/IconPython.spec.tsx index b0d51075..46ae7b63 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Python/IconPython.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Python/IconPython.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconPython } from "./IconPython"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Restart/IconRestart.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Restart/IconRestart.spec.tsx index f3e871ba..661bcaab 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Restart/IconRestart.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Restart/IconRestart.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconRestart } from "./IconRestart"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/RunFast/IconRunFast.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/RunFast/IconRunFast.spec.tsx index aecbc54f..013f81f2 100644 --- a/apps/sim-core/packages/core/src/components/Icon/RunFast/IconRunFast.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/RunFast/IconRunFast.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconRunFast } from "./IconRunFast"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Rust/IconRust.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Rust/IconRust.spec.tsx index 6388d87c..09508109 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Rust/IconRust.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Rust/IconRust.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconRust } from "./IconRust"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Settings/IconSettings.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Settings/IconSettings.spec.tsx index 4dea485e..fbdc918c 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Settings/IconSettings.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Settings/IconSettings.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconSettings } from "./IconSettings"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Spinner/IconSpinner.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Spinner/IconSpinner.spec.tsx index 2708c207..257053d7 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Spinner/IconSpinner.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Spinner/IconSpinner.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconSpinner } from "./IconSpinner"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Stop/IconStop.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Stop/IconStop.spec.tsx index 9f3ca790..bd3273a2 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Stop/IconStop.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Stop/IconStop.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconStop } from "./IconStop"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Sync/IconSync.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Sync/IconSync.spec.tsx index c0331909..e2810b8a 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Sync/IconSync.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Sync/IconSync.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconSync } from "./IconSync"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/TableAdd/IconTableAdd.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/TableAdd/IconTableAdd.spec.tsx index dd6f4aae..1c91c2a4 100644 --- a/apps/sim-core/packages/core/src/components/Icon/TableAdd/IconTableAdd.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/TableAdd/IconTableAdd.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconTableAdd } from "./IconTableAdd"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/TableLarge/IconTableLarge.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/TableLarge/IconTableLarge.spec.tsx index 19f06ab6..f0afa381 100644 --- a/apps/sim-core/packages/core/src/components/Icon/TableLarge/IconTableLarge.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/TableLarge/IconTableLarge.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconTableLarge } from "./IconTableLarge"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Trash/IconTrash.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Trash/IconTrash.spec.tsx index fbce7375..42b3f2ae 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Trash/IconTrash.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Trash/IconTrash.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconTrash } from "./IconTrash"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Tree/IconTree.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Tree/IconTree.spec.tsx index d6f18052..39a24353 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Tree/IconTree.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Tree/IconTree.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconTree } from "./IconTree"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Update/IconUpdate.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Update/IconUpdate.spec.tsx index 0e42c7ee..83c672ab 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Update/IconUpdate.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Update/IconUpdate.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconUpdate } from "./IconUpdate"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/Upload/IconUpload.spec.tsx b/apps/sim-core/packages/core/src/components/Icon/Upload/IconUpload.spec.tsx index 9e3a75ee..830724e9 100644 --- a/apps/sim-core/packages/core/src/components/Icon/Upload/IconUpload.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Icon/Upload/IconUpload.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { IconUpload } from "./IconUpload"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Icon/index.ts b/apps/sim-core/packages/core/src/components/Icon/index.ts index af278a28..e3493f1e 100644 --- a/apps/sim-core/packages/core/src/components/Icon/index.ts +++ b/apps/sim-core/packages/core/src/components/Icon/index.ts @@ -53,6 +53,6 @@ export { IconTableLarge } from "./TableLarge"; export { IconTrash } from "./Trash"; export { IconHCoreMono } from "./HCoreMono"; -export interface IconProps { +export type IconProps = { size?: number; -} +}; diff --git a/apps/sim-core/packages/core/src/components/Inputs/EnumInput/EnumInput.tsx b/apps/sim-core/packages/core/src/components/Inputs/EnumInput/EnumInput.tsx index 09db7f7b..1248bc9f 100644 --- a/apps/sim-core/packages/core/src/components/Inputs/EnumInput/EnumInput.tsx +++ b/apps/sim-core/packages/core/src/components/Inputs/EnumInput/EnumInput.tsx @@ -4,12 +4,12 @@ import { RoundedSelect } from "../Select/RoundedSelect"; import "./EnumInput.scss"; -export interface EnumInputProps { +export type EnumInputProps = { name: string; value: string; options: string[] | string; onChange: (val: string) => void; -} +}; export const EnumInput: FC = memo( ({ name, value, options, onChange }) => { diff --git a/apps/sim-core/packages/core/src/components/Inputs/Select/Select.tsx b/apps/sim-core/packages/core/src/components/Inputs/Select/Select.tsx index 4f4a10ea..77498d83 100644 --- a/apps/sim-core/packages/core/src/components/Inputs/Select/Select.tsx +++ b/apps/sim-core/packages/core/src/components/Inputs/Select/Select.tsx @@ -76,7 +76,7 @@ export const Select = forwardRef< diff --git a/apps/sim-core/packages/core/src/components/KeepInView/KeepInView.tsx b/apps/sim-core/packages/core/src/components/KeepInView/KeepInView.tsx index 4b5c6442..892b9309 100644 --- a/apps/sim-core/packages/core/src/components/KeepInView/KeepInView.tsx +++ b/apps/sim-core/packages/core/src/components/KeepInView/KeepInView.tsx @@ -2,6 +2,7 @@ import React, { createContext, FC, HTMLAttributes, + PropsWithChildren, useCallback, useContext, useEffect, @@ -15,10 +16,9 @@ type Subscribe = (handler: VoidFunction) => Unsubscribe; const KeepInViewContext = createContext(null); -export const KeepInViewProvider: FC> = ({ - children, - ...props -}) => { +export const KeepInViewProvider: FC< + PropsWithChildren> +> = ({ children, ...props }) => { const subscribersRef = useRef([] as VoidFunction[]); const observerRef = useResizeObserver(() => { for (const handler of subscribersRef.current) { diff --git a/apps/sim-core/packages/core/src/components/LabeledInputRadio/LabeledInputRadio.tsx b/apps/sim-core/packages/core/src/components/LabeledInputRadio/LabeledInputRadio.tsx index e0a34b9f..2a626757 100644 --- a/apps/sim-core/packages/core/src/components/LabeledInputRadio/LabeledInputRadio.tsx +++ b/apps/sim-core/packages/core/src/components/LabeledInputRadio/LabeledInputRadio.tsx @@ -3,14 +3,14 @@ import classNames from "classnames"; import "./LabeledInputRadio.css"; -interface LabeledInputRadioProps { +type LabeledInputRadioProps = { label: string; group: string; isChecked: (htmlFor: string) => boolean; onClick?: (event: MouseEvent) => void; onMouseEnter?: (event: MouseEvent) => void; disabled?: boolean; -} +}; export const LabeledInputRadio: FC = ({ label, diff --git a/apps/sim-core/packages/core/src/components/Link/Link.tsx b/apps/sim-core/packages/core/src/components/Link/Link.tsx index 5e890d17..5482ed3d 100644 --- a/apps/sim-core/packages/core/src/components/Link/Link.tsx +++ b/apps/sim-core/packages/core/src/components/Link/Link.tsx @@ -1,7 +1,7 @@ -import React, { FC, forwardRef, HTMLProps } from "react"; -import { navigate } from "hookrouter"; +import React, { FC, forwardRef, HTMLProps, PropsWithChildren } from "react"; +import { navigate } from "../../util/navigation"; -import { Scope, useScope } from "../../features/scopes"; +import { Scope } from "../../features/scopes"; export type LinkProps = Omit< HTMLProps, @@ -21,73 +21,56 @@ const getHref = (route: string | undefined, query: Record) => : "" }`; -export const Link: FC = forwardRef( - function Link( - { - path, - onClick, - query = {}, - children, - replace = false, - scope = null, - forceLogin, - target, - ...props - }, - ref, - ) { - /** - * defaulting to Scope.login because we cannot dynamically call this hook. - * We'll only use the result of this if scope is passed in. - */ - const hasScope = useScope(scope ?? Scope.login); - - const absolute = path?.startsWith("http"); - - if (scope !== null && absolute) { - throw new Error("Cannot scope absolute URL"); - } - - let filteredQuery = Object.fromEntries( - Object.entries(query).filter( - ([_, value]) => value !== null && typeof value !== "undefined", - ), - ); - - let route = path; - let mappedOnClick = onClick; +export const Link: FC> = forwardRef< + HTMLAnchorElement, + LinkProps +>(function Link( + { + path, + onClick, + query = {}, + children, + replace = false, + scope: _scope, + forceLogin: _forceLogin, + target, + ...props + }, + ref, +) { + const absolute = path?.startsWith("http"); - if ((scope !== null && !hasScope) || forceLogin) { - filteredQuery = path ? { route: getHref(path, filteredQuery) } : {}; - route = "/signin"; - mappedOnClick = undefined; - } + const filteredQuery = Object.fromEntries( + Object.entries(query).filter( + ([_, value]) => value !== null && typeof value !== "undefined", + ), + ); - const href = getHref(route, filteredQuery); + const route = path; + const href = getHref(route, filteredQuery); - return ( - { - if (!(evt.metaKey || evt.ctrlKey || evt.altKey)) { - evt.preventDefault(); - if (route) { - navigate(route, replace, filteredQuery); - } + return ( + { + if (!(evt.metaKey || evt.ctrlKey || evt.altKey)) { + evt.preventDefault(); + if (route) { + navigate(route, replace, filteredQuery); } - - mappedOnClick?.(evt); } - } - {...props} - > - {children} - - ); - }, -); + + onClick?.(evt); + } + } + {...props} + > + {children} + + ); +}); diff --git a/apps/sim-core/packages/core/src/components/Link/LinkBehavior.tsx b/apps/sim-core/packages/core/src/components/Link/LinkBehavior.tsx index 3463e45c..33421a74 100644 --- a/apps/sim-core/packages/core/src/components/Link/LinkBehavior.tsx +++ b/apps/sim-core/packages/core/src/components/Link/LinkBehavior.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import { HcSharedBehaviorFile } from "../../features/files/types"; import { Link, LinkProps } from "./Link"; @@ -6,7 +6,9 @@ import { mainProjectPath } from "../../routes"; import { mapFileId } from "../../features/files/utils"; export const LinkBehavior: FC< - Omit & { file: HcSharedBehaviorFile } + PropsWithChildren< + Omit & { file: HcSharedBehaviorFile } + > > = ({ children, file, ...props }) => ( = ({ fullScreen }) => (
    diff --git a/apps/sim-core/packages/core/src/components/Logo/Logo.spec.tsx b/apps/sim-core/packages/core/src/components/Logo/Logo.spec.tsx index b2d3e491..719c96db 100644 --- a/apps/sim-core/packages/core/src/components/Logo/Logo.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Logo/Logo.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { Logo } from "./Logo"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Logo/Logo.tsx b/apps/sim-core/packages/core/src/components/Logo/Logo.tsx index 1a56499b..3ac4784c 100644 --- a/apps/sim-core/packages/core/src/components/Logo/Logo.tsx +++ b/apps/sim-core/packages/core/src/components/Logo/Logo.tsx @@ -1,17 +1,17 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import { ReactSVG } from "react-svg"; import classNames from "classnames"; import "./Logo.css"; -interface LogoProps { +type LogoProps = { size?: number; logoSize?: number; textSize?: number; className?: string; -} +}; -export const Logo: FC = ({ +export const Logo: FC> = ({ size = 1, logoSize = size, textSize = size, diff --git a/apps/sim-core/packages/core/src/components/Modal/Analysis/AnalysisModal.tsx b/apps/sim-core/packages/core/src/components/Modal/Analysis/AnalysisModal.tsx index 4f882717..4b308c41 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Analysis/AnalysisModal.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Analysis/AnalysisModal.tsx @@ -1,4 +1,9 @@ -import React, { FC, FormEventHandler, ReactNode } from "react"; +import React, { + FC, + FormEventHandler, + PropsWithChildren, + ReactNode, +} from "react"; import classNames from "classnames"; import { IconHelpCircleOutline } from "../../Icon/HelpCircleOutline"; @@ -7,7 +12,7 @@ import { ModalExit } from "../ModalExit"; import "./AnalysisModal.scss"; -interface AnalysisModalProps { +type AnalysisModalProps = { onClose?: () => void; cancelButton?: boolean; className?: string; @@ -15,9 +20,9 @@ interface AnalysisModalProps { footerLegend: ReactNode | string | null; submitButtonText: string; onSubmit: FormEventHandler; -} +}; -export const AnalysisModal: FC = ({ +export const AnalysisModal: FC> = ({ onClose, cancelButton = true, children, diff --git a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.spec.tsx index 3093dd2e..f28e0022 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.spec.tsx @@ -1,45 +1,29 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; import { ModalProvider } from "react-modal-hook"; import { render, fireEvent } from "@testing-library/react"; import { ErrorBoundary } from "../../ErrorBoundary"; import { ModalOutputMetrics } from "./ModalOutputMetrics"; -import { mockProject } from "../../../features/project/mocks"; -import { setProjectWithMeta } from "../../../features/actions"; -import { store } from "../../../features/store"; const noop = () => {}; it("renders without crashing", () => { - const div = document.createElement("div"); - - //@ts-expect-error redux problems - store.dispatch(setProjectWithMeta(mockProject)); - - ReactDOM.render( - - - - - - - , - div, + render( + + + + + , ); - ReactDOM.unmountComponentAtNode(div); }); it("renders the right title and headings (create)", () => { const { getByText } = render( - - - - - - - , + + + + + , ); expect(getByText("Define new metric")).toBeDefined(); // title expect(getByText("METRIC NAME")).toBeDefined(); // first input label @@ -55,13 +39,11 @@ it("renders the right title and headings (create)", () => { it("renders the right title and headings (edit)", () => { const { getByText } = render( - - - - - - - , + + + + + , ); expect(getByText("Edit metric")).toBeDefined(); // title expect(getByText("METRIC NAME")).toBeDefined(); // first input label @@ -76,13 +58,11 @@ it("renders the right title and headings (edit)", () => { it("calls onClose when pressing ESCAPE key", () => { const mockFn = jest.fn(); const { baseElement } = render( - - - - - - - , + + + + + , ); fireEvent.keyDown(baseElement, { key: "Escape", code: "Escape" }); expect(mockFn).toHaveBeenCalled(); diff --git a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.tsx b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.tsx index ab223eea..51fc171c 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalOutputMetrics.tsx @@ -1,5 +1,4 @@ import React, { FC, useState } from "react"; -import { shallowEqual, useSelector } from "react-redux"; import { useForm } from "react-hook-form"; import { AnalysisModal } from "./AnalysisModal"; @@ -11,13 +10,14 @@ import { OutputOperation } from "../../../features/analysis/analysisJsonTypes"; import { RESERVED_BUILT_IN_KEYS } from "../../../features/files/validate"; import { ReactSelectOption } from "../../Dropdown/types"; import { selectLocalBehaviorKeyFieldNames } from "../../../features/files/selectors"; +import { useFilesSelector } from "../../../features/files/FilesContext"; import { useSafeOnClose } from "../../../hooks/useSafeOnClose"; import { validateOutput } from "../../../features/analysis/analysisJsonValidation"; import { validateTitle } from "../../../features/analysis/validation"; import "./ModalOutputMetrics.scss"; -interface ModalOutputMetricsProps { +type ModalOutputMetricsProps = { onClose: VoidFunction; onSave: Function; onDelete?: Function; @@ -25,12 +25,12 @@ interface ModalOutputMetricsProps { metricKey?: string; operations?: Operation[]; isCreate?: boolean; -} +}; -interface FormInputs { +type FormInputs = { title: string; operations: Operation[]; -} +}; export const defaultNewOperation: Operation = { op: OperationTypes.get, @@ -96,7 +96,7 @@ export const ModalOutputMetrics: FC = ({ return true; }; - const onSubmit = (values: FormInputs) => { + const onSubmit = async (values: FormInputs) => { const result = { ...values, operations: currentOperations, @@ -155,10 +155,7 @@ export const ModalOutputMetrics: FC = ({ ); const safeOnClose = useSafeOnClose(!isFormDirty, true, onClose); - const localBehaviorKeys = useSelector( - selectLocalBehaviorKeyFieldNames, - shallowEqual, - ); + const localBehaviorKeys = useFilesSelector(selectLocalBehaviorKeyFieldNames); const behaviorKeys = [...localBehaviorKeys, ...RESERVED_BUILT_IN_KEYS].sort(); const behaviorKeysOptions: ReactSelectOption[] = behaviorKeys.map((key) => ({ label: key, diff --git a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.spec.tsx index 4e014a0b..c9c69089 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.spec.tsx @@ -1,30 +1,17 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { ErrorBoundary } from "../../ErrorBoundary"; import { ModalPlots } from "./ModalPlots"; -import { mockProject } from "../../../features/project/mocks"; -import { setProjectWithMeta } from "../../../features/actions"; -import { store } from "../../../features/store"; it("renders without crashing", () => { - const div = document.createElement("div"); - - //@ts-expect-error redux problems - store.dispatch(setProjectWithMeta(mockProject)); - - ReactDOM.render( - - - {}} - onSave={() => {}} - outputs={{ hello: [{ op: "get", field: "bla" }] }} - /> - - , - div, + render( + + {}} + onSave={() => {}} + outputs={{ hello: [{ op: "get", field: "bla" }] }} + /> + , ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.tsx b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.tsx index 5305c24e..0ef339b2 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Analysis/ModalPlots.tsx @@ -21,10 +21,10 @@ import { validatePlot } from "../../../features/analysis/analysisJsonValidation" import "./ModalPlots.scss"; -interface ModalPlotsProps { +type ModalPlotsProps = { onClose: VoidFunction; onSave: Function; - outputs: Record; + outputs: { [index: string]: any[] }; onDelete?: Function; plotKey?: number; plotTitle?: string; @@ -39,14 +39,14 @@ interface ModalPlotsProps { XAxisItems?: XAxisItemType[]; isCreate?: boolean; combinedHeightOfAllPlots?: number; -} +}; -interface FormInputs { +type FormInputs = { title: string; chartType: ChartTypes; YAxisItems: YAxisItemType[]; XAxisItems: XAxisItemType[]; -} +}; const chartTypeOptions: ReactSelectOption[] = [ ChartTypes.area, @@ -127,7 +127,7 @@ export const ModalPlots: FC = ({ const [currentXAxisItems, setCurrentXAxisItems] = useState(XAxisItems); const [chartType, setChartType] = useState( - isCreate ?? !plotChartType + (isCreate ?? !plotChartType) ? chartTypeOptions[0] : { label: String(plotChartType), value: String(plotChartType) }, ); @@ -197,7 +197,7 @@ export const ModalPlots: FC = ({ return true; }; - const onSubmit = (values: FormInputs) => { + const onSubmit = async (values: FormInputs) => { const result = getFormState(values); if (!validate(result)) { return; @@ -238,7 +238,7 @@ export const ModalPlots: FC = ({ const updateAxisItem = ( axisToUpdate: "x" | "y", index: number, - newValues: YAxisItemType, + newValues: YAxisItemType | XAxisItemType, ) => { const items = axisToUpdate === "x" ? currentXAxisItems : currentYAxisItems; const newOps = [...items]; diff --git a/apps/sim-core/packages/core/src/components/Modal/BigModal.tsx b/apps/sim-core/packages/core/src/components/Modal/BigModal.tsx index 9d9dc6d8..ea9fa87f 100644 --- a/apps/sim-core/packages/core/src/components/Modal/BigModal.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/BigModal.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import classNames from "classnames"; import { Modal } from "./Modal"; @@ -6,14 +6,14 @@ import { ModalExit } from "./ModalExit"; import "./BigModal.css"; -interface BigModalProps { +type BigModalProps = { onClose?: () => void; cancelButton?: boolean; className?: string; backdropClassName?: string; -} +}; -export const BigModal: FC = ({ +export const BigModal: FC> = ({ onClose, cancelButton = true, children, diff --git a/apps/sim-core/packages/core/src/components/Modal/Confirm/FileDelete/ModalConfirmFileDelete.tsx b/apps/sim-core/packages/core/src/components/Modal/Confirm/FileDelete/ModalConfirmFileDelete.tsx index 207821a9..b269d92c 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Confirm/FileDelete/ModalConfirmFileDelete.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Confirm/FileDelete/ModalConfirmFileDelete.tsx @@ -6,10 +6,10 @@ import { useModalConfirm } from "../hooks"; import "./ModalConfirmFileDelete.css"; -interface ModalConfirmFileDeleteProps { +type ModalConfirmFileDeleteProps = { onAnswer: (answer: boolean) => void; fileName: string; -} +}; export const ModalConfirmFileDelete: FC = ({ onAnswer, diff --git a/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.spec.tsx index 5c29030a..8c4c748a 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.spec.tsx @@ -14,12 +14,9 @@ export const thisMustBeHereToMakeTheBuildHappyAboutTheFactThatWeDoNotHaveAnImpor // import { setProjectWithMeta } from "../../../features/actions"; // import { store } from "../../../features/store"; -// TODO: Fix these tests. They are failing with the following error: -// ReferenceError: WEBPACK_PUBLIC_PATH is not defined -// import { getLocalStorageSimulatorTarget } from "./target"; -// > const workerUrl = urljoin(WEBPACK_PUBLIC_PATH, "simulationworker.js"); -// Most probably this is happening because the Jest pipeline is not running through webpack, -// or I am missing another layer of Providers +// TODO: Fix these tests. They are failing because the tests need proper +// context providers (FilesProvider, SimulatorProvider, etc.) to render +// the full ExperimentModal. // describe.skip("ExperimentModal tests", () => { // it("renders without crashing", () => { diff --git a/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.tsx b/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.tsx index 309ba790..bdbc3717 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Experiments/ExperimentModal.tsx @@ -6,10 +6,9 @@ import React, { useRef, useState, } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { ProviderTargetEnv } from "@hashintel/engine-web"; import { Result, combine, ok } from "neverthrow"; -import { omit, pick } from "lodash"; +import { omit, pick } from "lodash-es"; import { v4 as uuid } from "uuid"; import { @@ -38,7 +37,11 @@ import { ModalExit } from "../ModalExit"; import { ModalFormEntryLabel } from "../FormEntry/ModalFormEntryLabel"; import { ReactSelectOption } from "../../Dropdown/types"; import { RoundedSelect } from "../../Inputs/Select/RoundedSelect"; -import { addUserAlert } from "../../../features/viewer/slice"; +import { + useFiles, + useFilesSelector, +} from "../../../features/files/FilesContext"; +import { useViewer } from "../../../features/viewer/ViewerContext"; import { convertToReactSelectOption, convertToReactSelectOptions, @@ -65,7 +68,6 @@ import { } from "../../../features/files/selectors"; import { selectProviderTarget } from "../../../features/simulator/simulate/selectors"; import { toggleProviderTarget } from "../../../features/simulator/simulate/thunks"; -import { updateFile } from "../../../features/files/slice"; import { useRefState } from "../../../hooks/useRefState"; import { useSimulatorDispatch, @@ -295,7 +297,7 @@ const prepareExperimentForFormData = ( } if (originalFields?.metricName) { - cloneFields.metricName = { + cloneFields!.metricName = { value: originalFields.metricName, label: originalFields.metricName, }; @@ -306,12 +308,12 @@ const prepareExperimentForFormData = ( originalFields?.metricObjective === FormDataDynamicFieldOptimizationMetricObjective.max ) { - cloneFields.metricObjective = { + cloneFields!.metricObjective = { value: originalFields?.metricObjective, label: originalFields?.metricObjective, }; } else { - cloneFields.metricObjective = + cloneFields!.metricObjective = initialFormDynamicFieldsData[ ExperimentTypes.optimization ].metricObjective; @@ -332,7 +334,7 @@ const prepareExperimentForFormData = ( } else { value = null; } - clone.dynamicFields[experimentType]!.runs = value; + clone.dynamicFields![experimentType]!.runs = value; return clone; } const fieldValue = experiment.dynamicFields?.[experimentType]?.field; @@ -341,11 +343,11 @@ const prepareExperimentForFormData = ( const hasFieldTypeString = typeof fieldValue === "string"; const hasDistributionTypeString = typeof distributionValue === "string"; // monte-carlo case if (hasFieldTypeString) { - clone.dynamicFields[experimentType]!.field = + clone.dynamicFields![experimentType]!.field = convertToReactSelectOption(fieldValue); } if (hasDistributionTypeString) { - clone.dynamicFields["monte-carlo"]!.distribution = + clone.dynamicFields!["monte-carlo"]!.distribution = convertToReactSelectOption(distributionValue); } return clone; @@ -396,7 +398,7 @@ const onSubmitSpecificExperimentHandler = ( clone.runs = clone.runs?.map((run: ReactSelectOption) => run.value) ?? []; return ok(clone); - case ExperimentTypes.optimization: { + case ExperimentTypes.optimization: clone.metricName = clone.metricName.value; clone.metricObjective = clone.metricObjective.value; let formErrors: FormErrorsType | null = null; @@ -436,9 +438,9 @@ const onSubmitSpecificExperimentHandler = ( return res; } return ok(clone); - } + default: - if (clone.field?.value) { + if (clone.field && clone.field.value) { clone.field = clone.field.value; } return ok(clone); @@ -455,21 +457,22 @@ export const ExperimentModal: FC<{ () => prepareExperimentForFormData(experiment) ?? initialFormData, ); const shouldRunExperimentAfterSaving = useRef(false); - const dispatch = useDispatch(); + const { updateFile: contextUpdateFile } = useFiles(); + const { addUserAlert } = useViewer(); const simulationTarget = useSimulatorSelector(selectProviderTarget); const [newSimulationTarget, setNewSimulationTarget, newSimulationTargetRef] = useRefState(simulationTarget); const simulatorDispatch = useSimulatorDispatch(); const experiments: [string, RawExperimentType][] | null = - useSelector(selectExperiments); - const globals = parseGlobals(useSelector(selectGlobals)); + useFilesSelector(selectExperiments); + const globals = parseGlobals(useFilesSelector(selectGlobals)); const fieldOptions = (globals?.globals ? flattenObjectKeysIntoString(globals.globals).map((global: string) => convertToReactSelectOption(global), ) : null) ?? []; - const metricOptions = useSelector(selectParsedAnalysisMetricNames).map( + const metricOptions = useFilesSelector(selectParsedAnalysisMetricNames).map( (name): ReactSelectOption => ({ value: name, label: name }), ); const canUseCloud = false; @@ -515,8 +518,9 @@ export const ExperimentModal: FC<{ const experimentType: ExperimentTypes | string = formData.experimentType.value; let fields = JSON.parse( - //@ts-expect-error tech debt - JSON.stringify(formData.dynamicFields[experimentType]), + JSON.stringify( + formData.dynamicFields[experimentType as ExperimentTypes], + ), ); const res = onSubmitSpecificExperimentHandler( @@ -569,7 +573,7 @@ export const ExperimentModal: FC<{ delete newExperiments[experiment.experimentTitle]; } const contents = stringifyExperiments(newExperiments); - dispatch(updateFile({ id: experimentsFileId, contents })); + contextUpdateFile(experimentsFileId, contents); if (shouldRunExperimentAfterSaving.current) { // switch the simulation environment target if the user changed it if (newSimulationTargetRef.current !== simulationTarget) { @@ -591,8 +595,10 @@ export const ExperimentModal: FC<{ const splitted = fieldName.split("."); if (splitted.length === 1) { clone[fieldName] = value; - // @ts-expect-error tech debt - if (fieldName === "experimentType" && value.value !== clone[fieldName]) { + if ( + fieldName === "experimentType" && + (value as ReactSelectOption).value !== clone[fieldName] + ) { // we changed the experiment type, thus we have to rebuild the dynamicFields const clonedDynamicFields: typeof initialFormData.dynamicFields = JSON.parse(JSON.stringify(initialFormData.dynamicFields)); @@ -621,7 +627,7 @@ export const ExperimentModal: FC<{ if (value !== "") { // clear errors for this field const newErrors: any = JSON.parse(JSON.stringify(formErrors)); - if (newErrors.dynamicFields?.[experimentType]) { + if (newErrors.dynamicFields && newErrors.dynamicFields[experimentType]) { delete newErrors?.dynamicFields[experimentType][field]; } setFormErrors(newErrors); @@ -646,30 +652,28 @@ export const ExperimentModal: FC<{ return; } console.error("experiments.json is malformed, closing modal"); - dispatch( - addUserAlert({ - type: "error", - message: `You can't use the Experiments visual editor because your experiments file has a typo.`, - context: "experiments.json", - timestamp: Date.now(), - simulationId: null, - }), - ); + addUserAlert({ + type: "error", + message: `You can't use the Experiments visual editor because your experiments file has a typo.`, + context: "experiments.json", + timestamp: Date.now(), + simulationId: null, + }); onClose(); - }, [experiments, onClose, dispatch]); + }, [experiments, onClose, addUserAlert]); const hasExperiments = experiments && experiments.length > 0; const experimentTitles = !hasExperiments ? [] - : experiments?.map((item: any) => item[0]) ?? []; + : (experiments?.map((item: any) => item[0]) ?? []); const experimentTitlesMinusGroupsAndMultiparameterAsOptions = !hasExperiments ? [] - : experiments + : (experiments ?.filter( (item: any) => item[1].type !== "multiparameter" && item[1].type !== "group", ) - .map((item: any) => convertToReactSelectOption(item[0])) ?? []; + .map((item: any) => convertToReactSelectOption(item[0])) ?? []); const showValues = shouldShowType(ExperimentTypes.values); const showLinspace = shouldShowType(ExperimentTypes.linspace); @@ -762,7 +766,9 @@ export const ExperimentModal: FC<{ !!formErrors?.dynamicFields?.values?.field, )} options={fieldOptions} - value={formData.dynamicFields?.values?.field} + value={ + formData.dynamicFields?.values?.field as ReactSelectOption + } isSearchable creatable onChange={(selectedOption) => @@ -802,7 +808,9 @@ export const ExperimentModal: FC<{ @@ -867,7 +875,9 @@ export const ExperimentModal: FC<{ @@ -936,7 +946,10 @@ export const ExperimentModal: FC<{ @@ -963,7 +976,10 @@ export const ExperimentModal: FC<{ onChange("monte-carlo.distribution", selectedOption) @@ -1207,7 +1223,7 @@ export const ExperimentModal: FC<{ } // @todo this casting shouldn't be necessary errorMessage={ - formErrors.dynamicFields?.optimization?.maxRuns + formErrors.dynamicFields?.optimization?.maxRuns! } /> { + setImmediate(() => { document .querySelector( `[name="fields.${fields.length - 1}.name"]`, @@ -1429,7 +1445,7 @@ export const ExperimentModal: FC<{ theme="blue" type="submit" > - Save and run in hCloud + Run experiment ) : null ) : canUseCloud ? ( @@ -1456,7 +1472,7 @@ export const ExperimentModal: FC<{ }} > Save and run{" "} - {newSimulationTarget === "cloud" ? "in hCloud" : "locally"} + {newSimulationTarget === "cloud" ? "in cloud" : "locally"} ) : ( ; +> {} const EXPERIMENT_TYPE_HINTS: ExperimentTypeHints = { values: { @@ -55,11 +55,7 @@ export const ExperimentTypeTooltip: FC<{ type: ExperimentTypes }> = ({ type, }) => (
    - + = ({ {EXPERIMENT_TYPE_HINTS[type].description}

    - + Read more.
    diff --git a/apps/sim-core/packages/core/src/components/Modal/Experiments/types.ts b/apps/sim-core/packages/core/src/components/Modal/Experiments/types.ts index 7f249931..14d4a577 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Experiments/types.ts +++ b/apps/sim-core/packages/core/src/components/Modal/Experiments/types.ts @@ -20,48 +20,48 @@ export enum DistributionTypes { gamma = "gamma", } -export interface FormDataDynamicFieldValuesType { +export type FormDataDynamicFieldValuesType = { steps: number; field: ReactSelectOption; values: string; -} -export interface FormDataDynamicFieldValuesErrorsType { +}; +export type FormDataDynamicFieldValuesErrorsType = { steps?: string; field?: string; values?: string; -} +}; -export interface FormDataDynamicFieldLinspaceType { +export type FormDataDynamicFieldLinspaceType = { steps: number; field: ReactSelectOption; start: number; stop: number; samples: number; -} -export interface FormDataDynamicFieldLinspaceErrorsType { +}; +export type FormDataDynamicFieldLinspaceErrorsType = { steps?: string; field?: string; start?: string; stop?: string; samples?: string; -} +}; -export interface FormDataDynamicFieldArangeType { +export type FormDataDynamicFieldArangeType = { steps: number; field: ReactSelectOption; start: number; stop: number; increment: number; -} -export interface FormDataDynamicFieldArangeErrorsType { +}; +export type FormDataDynamicFieldArangeErrorsType = { steps?: string; field?: string; start?: string; stop?: string; increment?: string; -} +}; -export interface FormDataDynamicFieldMonteCarloType { +export type FormDataDynamicFieldMonteCarloType = { steps: number; field: ReactSelectOption; samples: number; @@ -75,8 +75,8 @@ export interface FormDataDynamicFieldMonteCarloType { beta?: number; shape?: number; scale?: number; -} -export interface FormDataDynamicFieldMonteCarloErrorsType { +}; +export type FormDataDynamicFieldMonteCarloErrorsType = { steps?: string; field?: string; samples?: string; @@ -90,43 +90,43 @@ export interface FormDataDynamicFieldMonteCarloErrorsType { beta?: string; shape?: string; scale?: string; -} +}; -export interface FormDataDynamicFieldGroupType { +export type FormDataDynamicFieldGroupType = { steps: number; runs: ReactSelectOption[] | null; -} -export interface FormDataDynamicFieldGroupErrorsType { +}; +export type FormDataDynamicFieldGroupErrorsType = { steps?: string; runs?: string; -} +}; -export interface FormDataDynamicFieldMultiparameterType { +export type FormDataDynamicFieldMultiparameterType = { steps: number; runs: ReactSelectOption[] | null; // Used for react-select multi item -} -export interface FormDataDynamicFieldMultiparameterErrorsType { +}; +export type FormDataDynamicFieldMultiparameterErrorsType = { steps?: string; runs?: string; -} +}; export enum FormDataDynamicFieldOptimizationMetricObjective { min = "min", max = "max", } -export interface FormDataDynamicFieldOptimizationFieldType { +export type FormDataDynamicFieldOptimizationFieldType = { name: string; value: string; uuid: string; -} -export interface FormDataDynamicFieldOptimizationFieldErrorsType { +}; +export type FormDataDynamicFieldOptimizationFieldErrorsType = { name?: string; value?: string; uuid?: string; -} +}; -export interface FormDataDynamicFieldOptimizationType { +export type FormDataDynamicFieldOptimizationType = { maxRuns: number; minSteps: number; maxSteps: number; @@ -136,17 +136,17 @@ export interface FormDataDynamicFieldOptimizationType { label: string; }; fields: FormDataDynamicFieldOptimizationFieldType[]; -} -export interface FormDataDynamicFieldOptimizationErrorsType { +}; +export type FormDataDynamicFieldOptimizationErrorsType = { maxRuns?: string; minSteps?: string; maxSteps?: string; metricName?: string; metricObjective?: string; fields?: FormDataDynamicFieldOptimizationFieldErrorsType[]; -} +}; -export interface FormDataType { +export type FormDataType = { experimentTitle: string; experimentType: ReactSelectOption; // ReactSelectOption | string => Used for react-select single item @@ -159,7 +159,7 @@ export interface FormDataType { [ExperimentTypes.multiparameter]?: FormDataDynamicFieldMultiparameterType; [ExperimentTypes.optimization]?: FormDataDynamicFieldOptimizationType; }; -} +}; export type RawExperimentOptimizationFieldValue = | { range: string } @@ -184,7 +184,7 @@ export type RawExperimentOptimizationType = Omit< * * @todo fix this */ -export interface RawExperimentType { +export type RawExperimentType = { experimentTitle: string; experimentType: string; dynamicFields: { @@ -206,9 +206,9 @@ export interface RawExperimentType { }; optimization?: RawExperimentOptimizationType; }; -} +}; -export interface DynamicFieldsErrorsType { +export type DynamicFieldsErrorsType = { [ExperimentTypes.values]?: FormDataDynamicFieldValuesErrorsType; [ExperimentTypes.linspace]?: FormDataDynamicFieldLinspaceErrorsType; [ExperimentTypes.arange]?: FormDataDynamicFieldArangeErrorsType; @@ -216,19 +216,19 @@ export interface DynamicFieldsErrorsType { [ExperimentTypes.group]?: FormDataDynamicFieldGroupErrorsType; [ExperimentTypes.multiparameter]?: FormDataDynamicFieldMultiparameterErrorsType; [ExperimentTypes.optimization]?: FormDataDynamicFieldOptimizationErrorsType; -} +}; -export interface FormErrorsType { +export type FormErrorsType = { experimentTitle?: string; dynamicFields?: DynamicFieldsErrorsType; -} +}; export type AllFormDataTypeDynamicFieldsType = Required< FormDataType["dynamicFields"] >[keyof FormDataType["dynamicFields"]]; -export interface ParseError { +export type ParseError = { msg?: string; -} +}; export type ParseResult = Result; diff --git a/apps/sim-core/packages/core/src/components/Modal/Experiments/utils.tsx b/apps/sim-core/packages/core/src/components/Modal/Experiments/utils.tsx index 2be50d0e..7d723b87 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Experiments/utils.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Experiments/utils.tsx @@ -1,4 +1,4 @@ -import { isPlainObject } from "lodash"; +import { isPlainObject } from "lodash-es"; import { DynamicFieldsErrorsType, diff --git a/apps/sim-core/packages/core/src/components/Modal/Experiments/valuesParser.ts b/apps/sim-core/packages/core/src/components/Modal/Experiments/valuesParser.ts index 2f5b8a1c..ff6d3e9c 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Experiments/valuesParser.ts +++ b/apps/sim-core/packages/core/src/components/Modal/Experiments/valuesParser.ts @@ -115,9 +115,7 @@ const convertParsedValueFromInput = (value: string): ParseResult => { try { const obj = JSON.parse(value); return ok(obj); - } catch (error) { - // Hide parsing errors. - } + } catch {} return ok(value.trim()); }; @@ -127,7 +125,6 @@ const parseValues = (values: string): ParseResult => { let modified = modifier.modify(values); let parsed: any[]; let remainingRetries = 100; - // eslint-disable-next-line no-constant-condition while (true) { try { parsed = (Parser.parse(modified) as any).body[0].expression.expressions; @@ -153,7 +150,7 @@ const parseValues = (values: string): ParseResult => { const modPos = (error as any).pos as number; const position = modPos - 1; // Account for wrap return err({ - msg: `Invalid value at character '${modified[modPos]}' [${position}]`, + msg: `Invalid value at character \'${modified[modPos]}\' [${position}]`, }); } diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/Dropdown/ModalFormEntryDropdown.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/Dropdown/ModalFormEntryDropdown.spec.tsx index ebe5bbf3..f496e290 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/Dropdown/ModalFormEntryDropdown.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/Dropdown/ModalFormEntryDropdown.spec.tsx @@ -1,18 +1,15 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ModalFormEntryDropdown } from "./ModalFormEntryDropdown"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render( {}} />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.spec.tsx index c9fb54fa..d12941da 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ModalFormEntry } from "./ModalFormEntry"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + render(); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.tsx index 76cd14bb..0f68b453 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntry.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode } from "react"; +import React, { FC, PropsWithChildren, ReactNode } from "react"; import classNames from "classnames"; import classnames from "classnames"; @@ -8,16 +8,16 @@ import { ShrinkWrap } from "../../ShrinkWrap/ShrinkWrap"; import "./ModalFormEntry.css"; -export interface ModalFormEntryProps { +export type ModalFormEntryProps = { label?: ReactNode; optional?: boolean; flex?: boolean; error?: string; errorInline?: boolean; className?: string; -} +}; -export const ModalFormEntry: FC = ({ +export const ModalFormEntry: FC> = ({ label, children, optional = false, diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntryLabel.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntryLabel.tsx index e8c68ca0..d9a5fae9 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntryLabel.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/ModalFormEntryLabel.tsx @@ -1,11 +1,10 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import "./ModalFormEntryLabel.scss"; -export const ModalFormEntryLabel: FC<{ optional?: boolean }> = ({ - optional, - children, -}) => ( +export const ModalFormEntryLabel: FC< + PropsWithChildren<{ optional?: boolean }> +> = ({ optional, children }) => (
    {children}{" "} {optional && OPTIONAL} diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.spec.tsx index 3ecd0e62..4c32dab9 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.spec.tsx @@ -1,24 +1,20 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ModalFormEntryPublishAs } from "./ModalFormEntryPublishAs"; it("renders without crashing", () => { - const div = document.createElement("div"); - const user = { subLabel: "user", value: "", label: "User", }; - ReactDOM.render( + render( {}} />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.tsx index 7d8618a4..3f72c743 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/PublishAs/ModalFormEntryPublishAs.tsx @@ -6,14 +6,14 @@ import type { ReactSelectOption } from "../../../Dropdown/types"; import "./ModalFormEntryPublishAs.css"; -interface ModalFormEntryPublishAsProps { +type ModalFormEntryPublishAsProps = { buttonLabel: string; publishAsOptions: ReactSelectOption[]; selectedPublishAs: ReactSelectOption; setSelectedPublishAs: (publishAs: ReactSelectOption) => void; disabled?: boolean; submitDisabled?: boolean; -} +}; export const ModalFormEntryPublishAs: FC = ({ buttonLabel, diff --git a/apps/sim-core/packages/core/src/components/Modal/FormEntry/RequiredText/ModalFormEntryRequiredText.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/FormEntry/RequiredText/ModalFormEntryRequiredText.spec.tsx index de4354c3..d57e7279 100644 --- a/apps/sim-core/packages/core/src/components/Modal/FormEntry/RequiredText/ModalFormEntryRequiredText.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/FormEntry/RequiredText/ModalFormEntryRequiredText.spec.tsx @@ -1,11 +1,10 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ModalFormEntryRequiredText } from "./ModalFormEntryRequiredText"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render( { onChange={() => undefined} onBlur={() => undefined} />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/Modal.tsx b/apps/sim-core/packages/core/src/components/Modal/Modal.tsx index 79905077..35cb4bc3 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Modal.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Modal.tsx @@ -9,7 +9,7 @@ import classNames from "classnames"; import "./Modal.css"; -export interface ModalProps { +export type ModalProps = { onClose?: () => void; modalClassName?: string; backdropClassName?: string; @@ -18,7 +18,7 @@ export interface ModalProps { containerClassName?: string; children?: ReactNode | null; onClick?: HTMLProps["onClick"]; -} +}; export const Modal = forwardRef( ( @@ -34,14 +34,15 @@ export const Modal = forwardRef( }, ref, ) => { - function handler(evt: KeyboardEvent) { - if (evt.key === "Escape") { - evt.preventDefault(); - onClose?.(); - } - } useEffect(() => { if (esc) { + function handler(evt: KeyboardEvent) { + if (evt.key === "Escape") { + evt.preventDefault(); + onClose?.(); + } + } + window.addEventListener("keydown", handler); return () => { diff --git a/apps/sim-core/packages/core/src/components/Modal/ModalForm.tsx b/apps/sim-core/packages/core/src/components/Modal/ModalForm.tsx index f875c756..626d4328 100644 --- a/apps/sim-core/packages/core/src/components/Modal/ModalForm.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/ModalForm.tsx @@ -1,11 +1,13 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import "./ModalForm.scss"; -export const ModalForm: FC<{ - onSubmit: () => Promise; - disabled: boolean | undefined; -}> = ({ onSubmit, disabled, children }) => ( +export const ModalForm: FC< + PropsWithChildren<{ + onSubmit: () => Promise; + disabled: boolean | undefined; + }> +> = ({ onSubmit, disabled, children }) => (
    { event.preventDefault(); diff --git a/apps/sim-core/packages/core/src/components/Modal/ModalInfoBox.tsx b/apps/sim-core/packages/core/src/components/Modal/ModalInfoBox.tsx index e0108adc..42934277 100644 --- a/apps/sim-core/packages/core/src/components/Modal/ModalInfoBox.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/ModalInfoBox.tsx @@ -1,14 +1,16 @@ -import React, { FC } from "react"; +import React, { FC, PropsWithChildren } from "react"; import classNames from "classnames"; import { IconAlertOutline, IconInformationOutline } from "../Icon"; import "./ModalInfoBox.scss"; -export const ModalInfoBox: FC<{ - type?: "info" | "warning"; - className?: string; -}> = ({ type = "info", children, className }) => ( +export const ModalInfoBox: FC< + PropsWithChildren<{ + type?: "info" | "warning"; + className?: string; + }> +> = ({ type = "info", children, className }) => (
    diff --git a/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.spec.tsx index 65a8d546..0bfad214 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.spec.tsx @@ -1,11 +1,10 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ModalNameBehavior } from "./ModalNameBehavior"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render( {}} onCancel={() => {}} @@ -18,7 +17,5 @@ it("renders without crashing", () => { action="Create" placeholder="Name your new file" />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.tsx b/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.tsx index 00ed7aef..d5207464 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/NameBehavior/ModalNameBehavior.tsx @@ -19,7 +19,7 @@ import { useResizeObserver } from "../../../hooks/useResizeObserver/useResizeObs import "./ModalNameBehavior.css"; -interface ModalNameBehaviorProps { +type ModalNameBehaviorProps = { errorMessage: string | null; languageOptions: ReactSelectOption[]; name: string; @@ -30,7 +30,7 @@ interface ModalNameBehaviorProps { onSelectedLanguageChange: (language: ReactSelectOption) => void; action: string; placeholder: string; -} +}; export const ModalNameBehavior: FC = ({ onSubmit, @@ -141,8 +141,3 @@ export const ModalNameBehavior: FC = ({ ); }; - -// // @ts-expect-error -// ModalNameBehavior.whyDidYouRender = { -// customName: "ModalNameBehavior" -// }; diff --git a/apps/sim-core/packages/core/src/components/Modal/NewDataset/ModalNewDataset.tsx b/apps/sim-core/packages/core/src/components/Modal/NewDataset/ModalNewDataset.tsx index 58b74a05..e7a2fd38 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NewDataset/ModalNewDataset.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/NewDataset/ModalNewDataset.tsx @@ -1,18 +1,18 @@ import React, { FC, useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; import { useDropzone } from "react-dropzone"; import classNames from "classnames"; -import { AppDispatch } from "../../../features/types"; import { BigModal } from "../BigModal"; import { IconAlert, IconSpinner } from "../../Icon"; import { IconUpload } from "../../Icon/Upload"; -import { createDataset } from "../../../features/files/slice"; +import { useFiles } from "../../../features/files/FilesContext"; +import { useProject } from "../../../features/project/ProjectContext"; import "./ModalNewDataset.scss"; export const ModalNewDataset: FC<{ onClose: VoidFunction }> = ({ onClose }) => { - const dispatch = useDispatch(); + const { addPreparedFile } = useFiles(); + const { currentProject } = useProject(); const [state, setState] = useState<"uploading" | "failed" | "initial">( "initial", @@ -29,15 +29,36 @@ export const ModalNewDataset: FC<{ onClose: VoidFunction }> = ({ onClose }) => { setState("uploading"); try { - //@ts-expect-error redux problems - await dispatch(createDataset(file)); + // TODO: createDataset was an async thunk that uploaded to server. + // In local-first mode, read the file locally and add it directly. + const contents = await file.text(); + const ext = file.name.split(".").pop()?.toLowerCase() ?? "json"; + const baseName = file.name.replace(/\.[^/.]+$/, ""); + const repoPath = `data/${baseName}.${ext}`; + + addPreparedFile({ + id: repoPath, + name: file.name, + path: { + formatted: repoPath, + base: baseName, + dir: "data", + root: "", + name: baseName, + ext: `.${ext}`, + }, + repoPath, + kind: 5 as any, // HcFileKind.Dataset + contents, + ref: currentProject?.ref ?? "main", + } as any); onClose(); } catch (err) { console.error("Uploading failed", err); setState("failed"); } }, - [onClose, dispatch], + [onClose, addPreparedFile, currentProject], ); const uploading = state === "uploading"; diff --git a/apps/sim-core/packages/core/src/components/Modal/NewProject/ModalNewProject.tsx b/apps/sim-core/packages/core/src/components/Modal/NewProject/ModalNewProject.tsx index 96c33042..7f3b7e92 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NewProject/ModalNewProject.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/NewProject/ModalNewProject.tsx @@ -112,7 +112,7 @@ export const ModalNewProject: FC<{ await onSubmit({ ...values, namespace: - values.namespace === USER_ORG_VALUE ? "@user" : values.namespace, + values.namespace === USER_ORG_VALUE ? "" : values.namespace, }); }); } catch (err) { diff --git a/apps/sim-core/packages/core/src/components/Modal/NewProject/NewProjectField.tsx b/apps/sim-core/packages/core/src/components/Modal/NewProject/NewProjectField.tsx index 1ebf30ac..f1a787ba 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NewProject/NewProjectField.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/NewProject/NewProjectField.tsx @@ -1,14 +1,16 @@ -import React, { FC, ReactNode } from "react"; +import React, { FC, PropsWithChildren, ReactNode } from "react"; import classNames from "classnames"; -export const NewProjectField: FC<{ - focused: boolean; - error?: boolean; - name: string; - fieldName: string; - tip?: ReactNode; - showTip?: boolean; -}> = ({ +export const NewProjectField: FC< + PropsWithChildren<{ + focused: boolean; + error?: boolean; + name: string; + fieldName: string; + tip?: ReactNode; + showTip?: boolean; + }> +> = ({ focused, error = false, name, diff --git a/apps/sim-core/packages/core/src/components/Modal/NewProject/types.ts b/apps/sim-core/packages/core/src/components/Modal/NewProject/types.ts index 48f924e6..7eba7914 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NewProject/types.ts +++ b/apps/sim-core/packages/core/src/components/Modal/NewProject/types.ts @@ -1,8 +1,8 @@ import { ProjectVisibility } from "../../../features/project/types"; -export interface NewProjectModalValues { +export type NewProjectModalValues = { name: string; path: string; namespace: string; visibility: ProjectVisibility; -} +}; diff --git a/apps/sim-core/packages/core/src/components/Modal/NewProject/utils.ts b/apps/sim-core/packages/core/src/components/Modal/NewProject/utils.ts index 8a49c44c..54e8b0b6 100644 --- a/apps/sim-core/packages/core/src/components/Modal/NewProject/utils.ts +++ b/apps/sim-core/packages/core/src/components/Modal/NewProject/utils.ts @@ -1,11 +1,9 @@ -import { useSelector } from "react-redux"; - import { SITE_URL } from "../../../util/api/paths"; import { getUserOrgs } from "../../HashCore/utils"; -import { selectCurrentUser } from "../../../features/user"; +import { useUser } from "../../../features/user/UserContext"; export const namespacePrefix = SITE_URL.replace(/^(.*?):\/\//, ""); export const USER_ORG_VALUE = "#_USER_ORG_VALUE"; -export const useOrgs = () => getUserOrgs(useSelector(selectCurrentUser)); +export const useOrgs = () => getUserOrgs(useUser().currentUser); diff --git a/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.spec.tsx index 353d571f..8e468608 100644 --- a/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.spec.tsx @@ -1,23 +1,13 @@ import React from "react"; -import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; import { ModalPrivateDependencies } from "./ModalPrivateDependencies"; -import { mockProject } from "../../../features/project/mocks"; -import { setProjectWithMeta } from "../../../features/actions"; -import { store } from "../../../features/store"; +import { ProjectProvider } from "../../../features/project/ProjectContext"; it("renders without crashing", () => { - const div = document.createElement("div"); - - //@ts-expect-error redux problems - store.dispatch(setProjectWithMeta(mockProject)); - - ReactDOM.render( - + render( + {}} /> - , - div, + , ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.tsx b/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.tsx index c47168bd..8a136f37 100644 --- a/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/PrivateDependencies/ModalPrivateDependencies.tsx @@ -1,5 +1,4 @@ import React, { FC } from "react"; -import { useSelector } from "react-redux"; import urljoin from "url-join"; import { FancyButton } from "../../Fancy/Button"; @@ -9,8 +8,9 @@ import { IconAlertOutline } from "../../Icon/AlertOutline"; import { Modal } from "../Modal"; import { SITE_URL } from "../../../util/api/paths"; import { Scope, useScope } from "../../../features/scopes"; -import { selectCurrentProjectRequired } from "../../../features/project/selectors"; import { selectPrivateDependencies } from "../../../features/files/selectors"; +import { useFilesSelector } from "../../../features/files/FilesContext"; +import { useProject } from "../../../features/project/ProjectContext"; import "./ModalPrivateDependencies.css"; @@ -31,9 +31,9 @@ const getPrivateKindsMessage = (files: HcDependencyFile[]) => { export const ModalPrivateDependencies: FC<{ onClose: () => void; }> = ({ onClose }) => { - const privateDependencies = useSelector(selectPrivateDependencies); + const privateDependencies = useFilesSelector(selectPrivateDependencies); const privateKinds = getPrivateKindsMessage(privateDependencies); - const project = useSelector(selectCurrentProjectRequired); + const project = useProject().currentProject!; const canLinkToProjectInIndex = useScope(Scope.linkToProjectInIndex); const makeCurrentProjectPrivateText = ( @@ -51,7 +51,6 @@ export const ModalPrivateDependencies: FC<{ {dependency.path.formatted} @@ -66,7 +65,6 @@ export const ModalPrivateDependencies: FC<{ {makeCurrentProjectPrivateText} diff --git a/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseBehavior.tsx b/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseBehavior.tsx index ad86a17b..4aa700ee 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseBehavior.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseBehavior.tsx @@ -107,28 +107,27 @@ export const ModalReleaseBehavior: FC = ({ try { await handleQueryCodeErrors(values, setError, async () => { - const { forkedBehaviors } = await dispatch( - //@ts-expect-error redux problems - forkAndReleaseBehaviors({ - projectPath: project.pathWithNamespace, - name: values.name, - namespace: - selectedPublishAs.value === "user" - ? "" - : selectedPublishAs.subLabel!, - path: values.path, - behaviors: toPublish.map((file) => ({ - path: file.repoPath, - filename: file.path.base, - })), - projectDescription: values.description, - keywords: selectedKeywords.map((keyword) => keyword.value), - // subjects: selectedSubjects.map((subject) => subject.label), - license: selectedLicense.value ?? "", - // @todo allow for private behavior releases - visibility: "public", - }), - ).then(unwrapResult); + const { forkedBehaviors } = unwrapResult( + await dispatch( + forkAndReleaseBehaviors({ + projectPath: project.pathWithNamespace, + name: values.name, + namespace: + selectedPublishAs.value === "user" + ? "" + : selectedPublishAs.subLabel!, + path: values.path, + behaviors: toPublish.map((file) => ({ + path: file.repoPath, + filename: file.path.base, + })), + projectDescription: values.description, + keywords: selectedKeywords.map((keyword) => keyword.value), + license: selectedLicense.value ?? "", + visibility: "public", + }), + ), + ); dispatch( displayToast({ diff --git a/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseCreate.tsx b/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseCreate.tsx index 291ab48a..906ce152 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseCreate.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Release/ModalReleaseCreate.tsx @@ -85,7 +85,7 @@ export const ModalReleaseCreate: FC = ({ keywords: selectedKeywords.map((keyword) => keyword.value), license: selectedLicense.value ?? "", }, - }), + }) as any, ); onClose(); diff --git a/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.scss b/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.scss index c3603693..05133326 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.scss +++ b/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.scss @@ -20,9 +20,6 @@ } } -/** - * @warning this is used in ModalShare - */ .ModalSplitInner__Top { background-color: var(--top-background); padding: 0 var(--padding-horizontal) 40px; diff --git a/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.tsx b/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.tsx index 392e6fc9..166ea2f1 100644 --- a/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/Split/ModalSplit.tsx @@ -9,11 +9,11 @@ import "./ModalSplit.scss"; type ModalSplitOuterProps = ModalProps & { loading?: boolean }; -interface ModalSplitInnerProps { +type ModalSplitInnerProps = { top?: ReactNode | null; bottom?: ReactNode | null; innerClassName?: string; -} +}; export const ModalSplitOuter = forwardRef( ({ modalClassName, children, loading, onClose, ...props }, ref) => ( diff --git a/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.spec.tsx b/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.spec.tsx index f0d6ce0a..cd4dbcc4 100644 --- a/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.spec.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.spec.tsx @@ -1,11 +1,10 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { ModalTwoColumn } from "./ModalTwoColumn"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render( + render( { leftChildren={null} rightChildren={null} />, - div, ); - ReactDOM.unmountComponentAtNode(div); }); diff --git a/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.tsx b/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.tsx index ebf46a38..e98aa00c 100644 --- a/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.tsx +++ b/apps/sim-core/packages/core/src/components/Modal/TwoColumn/ModalTwoColumn.tsx @@ -5,7 +5,7 @@ import { ModalForm } from "../ModalForm"; import "./ModalTwoColumn.css"; -interface ModalTwoColumnProps { +type ModalTwoColumnProps = { title: ReactNode; intro: ReactNode; onSubmit: () => Promise; @@ -13,7 +13,7 @@ interface ModalTwoColumnProps { rightChildren: ReactNode; className?: string; disabled?: boolean; -} +}; /** * @deprecated diff --git a/apps/sim-core/packages/core/src/components/Modal/index.ts b/apps/sim-core/packages/core/src/components/Modal/index.ts index 01aa1332..b98f3640 100644 --- a/apps/sim-core/packages/core/src/components/Modal/index.ts +++ b/apps/sim-core/packages/core/src/components/Modal/index.ts @@ -11,8 +11,3 @@ export { export { Modal } from "./Modal"; export { ModalNameBehavior } from "./NameBehavior"; export { ModalTwoColumn } from "./TwoColumn"; -export { - ModalReleaseCreate, - ModalReleaseUpdate, - ModalReleaseBehavior, -} from "./Release"; diff --git a/apps/sim-core/packages/core/src/components/MonacoContainer/MonacoContainer.spec.tsx b/apps/sim-core/packages/core/src/components/MonacoContainer/MonacoContainer.spec.tsx index aa61107e..b5e7e590 100644 --- a/apps/sim-core/packages/core/src/components/MonacoContainer/MonacoContainer.spec.tsx +++ b/apps/sim-core/packages/core/src/components/MonacoContainer/MonacoContainer.spec.tsx @@ -1,10 +1,8 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { render } from "@testing-library/react"; import { MonacoContainer } from "./MonacoContainer"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(