Skip to content

upstream#13

Merged
willymwai merged 370 commits intotruehostcloud:masterfrom
plasmicapp:master
May 19, 2025
Merged

upstream#13
willymwai merged 370 commits intotruehostcloud:masterfrom
plasmicapp:master

Conversation

@willymwai
Copy link
Member

@willymwai willymwai commented May 19, 2025

PR Type

Enhancement, Tests, Bug fix, Configuration changes


Description

  • Major enhancements to comment, notification, and CMS row management logic, including improved notification grouping, new notification types, and better error handling for CMS operations.

  • Added a new HTML parser for the web importer, supporting style extraction, CSS parsing, and DOM traversal with style inheritance and sanitization.

  • Refactored loader bundler to remove Rollup support and use esbuild exclusively, with added support for the "tanstack" platform and improved error handling.

  • Enhanced code component prop type validation and custom function registration, improving type safety and code generation.

  • Added support for TanStack Router and server query integration in React code generation.

  • Extensive expansion and modernization of test coverage:

    • Comprehensive Playwright and Cypress test suites for React Aria components, auto-open functionality, Plexus installation, and multiple interaction conflicts.
    • Major refactor and expansion of comment notification tests and utilities, including new edge cases and notification types.
    • Expanded and modernized tests for comment notification email grouping and content.
    • Refactored and improved reliability of existing Cypress tests, including better setup/teardown and utility functions.
    • Updated tests to match new APIs and function signatures.
  • Fixed import path for Analytics types in AmplitudeAnalytics.

  • Added .npmignore to exclude internal README from the rive package.

  • Organized and expanded imports in SlotUtils for better code clarity.


Changes walkthrough 📝

Relevant files
Tests
10 files
send-comments-notifications.spec.ts
Major refactor and expansion of comment notification tests and
utilities

platform/wab/src/wab/server/scripts/send-comments-notifications.spec.ts

  • Refactored test utility functions for comments, thread status, and
    reactions (renamed and enhanced).
  • Replaced legacy notification structure assertions with new, more
    explicit notification map structures using createNotification.
  • Added comprehensive new test cases for notifications, including
    reactions, thread resolution, mentions, and notification preferences.
  • Improved test coverage for edge cases (e.g., self-mentions, reaction
    removals, combined notifications).
  • Updated test assertions to match new notification logic and data
    structures.
  • +1842/-393
    nextjs-plasmic-react-aria.spec.ts
    Add Playwright tests for React Aria code components in Next.js

    platform/loader-tests/src/playwright-tests/nextjs/nextjs-plasmic-react-aria.spec.ts

  • Added a comprehensive Playwright test suite for
    @plasmicpkgs/react-aria code components in Next.js loader projects.
  • Implemented detailed state and interaction checks for various
    components (Button, Checkbox, Radio, Switch, Text Field, Select,
    Dialogs, Slider, etc.).
  • Introduced utility classes (StateChecker, LeafElementStateChecker) for
    reusable, fine-grained state assertions.
  • Included visual and accessibility state checks, keyboard navigation,
    and variant toggling.
  • [link]   
    multiple-interactions-with-conflicts.spec.ts
    Add tests for multiple interaction conflicts in site diffs

    platform/wab/src/wab/shared/site-diffs/tests/multiple-interactions-with-conflicts.spec.ts

  • Added a new test suite to verify detection and resolution of conflicts
    involving multiple interactions in site diffs.
  • Tests both conflict detection and correct merging after conflict
    resolution.
  • Validates the structure and correctness of merged event handlers and
    interactions.
  • +131/-0 
    auto-open.spec.ts
    Add comprehensive Cypress tests for auto-open feature       

    platform/wab/cypress/e2e/auto-open.spec.ts

  • Added a comprehensive new Cypress test suite for auto-open
    functionality.
  • Tests cover code components, Plasmic components, nested/nesting
    scenarios, and various visibility states.
  • Includes utility functions for asserting auto-opened/hidden states and
    simulating user interactions.
  • Covers undo functionality, multi-selection, and interaction with
    live/interactive modes.
  • +799/-0 
    tutorial.spec.ts
    Refactor tutorial Cypress test for better setup/teardown 

    platform/wab/cypress/e2e/tutorial.spec.ts

  • Refactored tutorial test to use beforeEach/afterEach for dev flags and
    project setup/teardown.
  • Ensured dev flags are reset after test completion.
  • Improved test reliability by cleaning up projects before and after
    tests.
  • No changes to the core tutorial steps, but improved test hygiene and
    isolation.
  • +338/-348
    host-app.spec.ts
    Refactor host-app Cypress test to use new utility               

    platform/wab/cypress/e2e/host-app.spec.ts

  • Refactored test utility usage to use configureProjectAppHost instead
    of configureProject.
  • Ensured consistent usage of the new utility for all host configuration
    steps.
  • No changes to test logic, only to helper function usage.
  • +7/-30   
    plexus-installation.spec.ts
    Add Cypress E2E tests for Plexus component installation workflows

    platform/wab/cypress/e2e/plexus-installation.spec.ts

  • Added a comprehensive Cypress E2E test suite for Plexus component
    installation.
  • Tests standalone and installable Plexus components, including
    drag-and-drop, "Install All", and undo functionality.
  • Verifies project panel state, default components, project
    dependencies, and style tokens after installation and undo.
  • Includes utility functions and assertions for component presence,
    dependencies, and project state.
  • +609/-0 
    util.ts
    Extend Cypress test utilities for project panel, props, variants, and
    UI control

    platform/wab/cypress/support/util.ts

  • Added numerous utility functions for Cypress tests, including project
    panel, component prop, variant, and visibility helpers.
  • Enhanced functions for interacting with the UI, such as toggling
    modes, handling frames, and managing undo/redo.
  • Improved project panel and prop manipulation, including new helpers
    for counting components/arenas and configuring app host.
  • Added functions for handling auto-open banners, style tokens tab, and
    more robust element/variant interactions.
  • +322/-40
    comment-notification-email.spec.ts
    Expand and modernize tests for comment notification email grouping and
    content

    platform/wab/src/wab/server/emails/comment-notification-email.spec.ts

  • Rewrote and expanded tests for sendUserNotificationEmail to cover
    multiple notification scenarios.
  • Added tests for grouping notifications by project, multiple
    notifications, multiple projects, and multiple branches.
  • Utilized real project/user setup and notification creation for more
    realistic test coverage.
  • Improved HTML normalization and assertions for email content and
    properties.
  • +568/-105
    prefill-cloudfront.spec.ts
    Update test for prefill-cloudfront to match new publishment API
    signature

    platform/wab/src/wab/server/workers/prefill-cloudfront.spec.ts

  • Updated test to remove minLoaderVersion argument from
    getRecentLoaderPublishmentsMock call.
  • Ensured test reflects updated function signature.
  • +1/-6     
    Enhancement
    6 files
    DbMgr.ts
    Major enhancements to comment, notification, and CMS row management
    logic

    platform/wab/src/wab/server/db/DbMgr.ts

  • Refactored and enhanced comment, thread, and reaction management,
    including notification tracking and update logic.
  • Improved unique field checking for CMS rows, including new error
    handling for unique constraint violations.
  • Updated project and workspace creation logic, including default access
    levels and removal of deprecated fields.
  • Added new helper functions and improved ID stamping for entities,
    supporting explicit IDs and short UUIDs.
  • Adjusted permissions checks and access levels for various operations.
  • Enhanced methods to support new notification and comment history
    features.
  • Added Sentry logging for large query results.
  • Improved handling of auto-open and visibility for CMS and
    comment-related entities.
  • +355/-110
    html-parser.ts
    Add HTML parser for web importer with style extraction     

    platform/wab/src/wab/client/web-importer/html-parser.ts

  • Added a new HTML parser module for the web importer, including style
    extraction, CSS parsing, and DOM traversal.
  • Implements logic to convert HTML/CSS into a structured tree with style
    inheritance, sanitization, and variable resolution.
  • Handles special cases for containers, SVG, text, and component nodes.
  • Provides utility functions for CSS value normalization and style
    safety checks.
  • +852/-0 
    module-bundler.ts
    Remove Rollup, refactor loader bundler to use esbuild only

    platform/wab/src/wab/server/loader/module-bundler.ts

  • Removed deprecated Rollup bundling logic, now only supports esbuild
    for loader bundling.
  • Added support for a new "tanstack" platform in bundle options.
  • Ensured serverQueriesExecFuncFileName is included in component meta.
  • Improved error handling and logging for esbuild bundling failures.
  • Cleaned up imports and removed unused dependencies.
  • +52/-282
    send-comments-notifications.ts
    Refactor and extend comment notification processing for new types and
    grouping

    platform/wab/src/wab/server/scripts/send-comments-notifications.ts

  • Refactored notification processing logic to support new notification
    types (comments, thread histories, reactions).
  • Introduced a Context class to cache and organize thread/comment data
    for efficient notification grouping.
  • Improved grouping of notifications by user, project, and thread, and
    enhanced notification filtering logic.
  • Updated email sending and notification marking to use new grouped
    structures and notification timestamps.
  • Enhanced type safety and modularized helper functions for notification
    processing.
  • +431/-199
    code-components.ts
    Enhance code component prop type validation and custom function
    registration

    platform/wab/src/wab/shared/code-components/code-components.ts

  • Improved type checking for code component prop registrations,
    including more exhaustive and type-safe checks.
  • Added/updated utility types for prop type extraction and type guards.
  • Enhanced function registration and parameter type mapping for custom
    functions.
  • Improved handling of className prop updates and variant prop changes.
  • Added/updated helper functions for prop type handling and code
    generation.
  • +231/-111
    index.ts
    Add TanStack Router support and server query integration to React
    codegen

    platform/wab/src/wab/shared/codegen/react-p/index.ts

  • Added support for TanStack Router platform in code generation,
    including route info and head serialization.
  • Integrated server query handling and RSC metadata into generated
    modules.
  • Improved handling of className prop inclusion for code components with
    variants or style sections.
  • Refactored code to support new platform options and improved
    modularity.
  • +141/-47
    Bug fix
    1 files
    AmplitudeAnalytics.ts
    Fix import path for Analytics types in AmplitudeAnalytics

    platform/wab/src/wab/shared/observability/AmplitudeAnalytics.ts

  • Fixed import path for Analytics and Properties types to use the
    correct observability directory.
  • No functional changes, only import correction.
  • +4/-4     
    Formatting
    1 files
    SlotUtils.ts
    Organize and expand imports in SlotUtils for slot and Tpl utilities

    platform/wab/src/wab/shared/SlotUtils.ts

  • Added and reordered imports for new slot and Tpl utility functions.
  • No functional code changes, only import organization.
  • +29/-26 
    Configuration changes
    1 files
    .npmignore
    Add .npmignore to exclude internal README from rive package

    plasmicpkgs/rive/.npmignore

  • Added .npmignore file to exclude README.internal from npm package.
  • +1/-0     
    Additional files
    101 files
    01-config-tooling.md +0/-16   
    README +0/-5     
    .browserslistrc +0/-16   
    .editorconfig +0/-16   
    README.md +0/-27   
    angular.json +0/-94   
    3rdpartylicenses.txt +0/-256 
    index.html +0/-12   
    main.ef91450b26733d80.js +0/-7360
    polyfills.5cf9041adbc6958d.js +0/-2045
    runtime.6263bc5acf5b193e.js +0/-64   
    styles.ef46db3751d8e999.css [link]   
    karma.conf.js +0/-41   
    package.json +0/-39   
    app.component.css [link]   
    app.component.html +0/-831 
    app.component.spec.ts +0/-31   
    app.component.ts +0/-11   
    app.module.ts +0/-22   
    .gitkeep [link]   
    environment.prod.ts +0/-3     
    environment.ts +0/-16   
    index.html +0/-13   
    main.ts +0/-13   
    polyfills.ts +0/-52   
    styles.css +0/-1     
    test.ts +0/-30   
    tsconfig.app.json +0/-10   
    tsconfig.json +0/-29   
    tsconfig.spec.json +0/-10   
    .eslintrc.json +0/-3     
    AngularTest.tsx +0/-33   
    next.config.js +0/-9     
    ng +0/-1     
    package.json +0/-25   
    plasmic-host.tsx +0/-6     
    test-ng.tsx +0/-5     
    Home.module.css +0/-129 
    globals.css +0/-26   
    tsconfig.json +0/-20   
    .eslintrc.json +3/-0     
    README.md +1/-1     
    next.config.mjs +19/-0   
    package.json +26/-0   
    [[...catchall]].tsx +22/-12 
    hello.ts +1/-1     
    plasmic-host.tsx +7/-0     
    plasmic-init.ts +8/-9     
    Home.module.css +165/-0 
    globals.css +42/-0   
    tsconfig.json +34/-0   
    README.md +0/-46   
    env.d.ts +0/-1     
    package.json +0/-32   
    App.vue +0/-25   
    base.css +0/-75   
    main.css +0/-35   
    HelloWorld.vue +0/-40   
    TheWelcome.vue +0/-86   
    WelcomeItem.vue +0/-86   
    HelloWorld.spec.ts +0/-11   
    IconCommunity.vue +0/-7     
    IconDocumentation.vue +0/-7     
    IconEcosystem.vue +0/-7     
    IconSupport.vue +0/-7     
    IconTooling.vue +0/-19   
    main.ts +0/-14   
    plasmic-init.ts +0/-15   
    index.jsx +0/-47   
    counter.ts +0/-12   
    AboutView.vue +0/-15   
    HomeView.vue +0/-9     
    tsconfig.app.json +0/-12   
    tsconfig.config.json +0/-8     
    tsconfig.json +0/-14   
    tsconfig.vitest.json +0/-9     
    vite.config.ts +0/-15   
    knip.ts +58/-0   
    package.json +9/-3     
    package.json +1/-1     
    file-utils.spec.ts +47/-0   
    export.ts +1/-1     
    init.ts +51/-4   
    sync-components.ts +26/-1   
    sync.ts +3/-3     
    api.ts +15/-2   
    code-utils.ts +103/-2 
    config-utils.ts +22/-2   
    envdetect.ts +9/-0     
    file-utils.ts +29/-3   
    rsc-config.ts +56/-0   
    gatsby-browser.jsx +2/-1     
    gatsby-ssr.jsx +2/-1     
    package.json +4/-4     
    plasmic.json +14/-8   
    Button.jsx +1/-3     
    PlasmicButton.jsx +53/-100
    PlasmicDynamicPage.jsx +36/-30 
    PlasmicGlobalVariant__Screen.jsx +3/-15   
    PlasmicHomepage.jsx +25/-25 
    Additional files not shown

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • abbas-nazar and others added 30 commits March 13, 2025 03:20
    * feat: added user mention email notiifcation and tests
    
    * refactor: added regex for user mention, added test for invalid mention witout preceding @
    
    * refactor: user will all notification-settings will be notified for all comments and history notification in a project
    
    * refactor: moved comments test utils to comments-utils
    
    * refactor: extracted user memtioned emails for comment one time rather tan matching for each user
    
    * refactor: new test added and regex library used for regex
    
    GitOrigin-RevId: 464c3be05b97205b2a2ceb8b0a64769825ad73bf
    … clicking a comment post (#653)
    
    * refactor: changed from centerFocusedFrame to tryZoomToFitSelection on threadSelect, added dialog padding for zoom in tryZoomToFitSelection if comments dialog is opened
    
    * refactor: refactored comments dialog zoom variable name
    
    GitOrigin-RevId: ab8ff5625d95fb2293deffb8c6c815e882a45ab0
    …lassName
    
    GitOrigin-RevId: 287db5c4b4b0c5190b218b2b2e9b7a89d6cc1c8e
    GitOrigin-RevId: f86824a10cb6c44c0d82b1996ad5086244976ea2
    GitOrigin-RevId: 83931c73334f5e43763ebe692a98a7dc8884341e
    GitOrigin-RevId: a788bb98d25e31eaca6950368f2378e803cf66a0
    GitOrigin-RevId: 207bfb8359003ea8757e5a9b45c29555656939e5
     - @plasmicapp/data-sources@0.1.175
     - @plasmicapp/react-web@0.2.376
     - @plasmicapp/react-web-runtime@0.0.96
     - @plasmicpkgs/antd5@0.0.277
     - @plasmicpkgs/fetch@0.0.3
     - @plasmicpkgs/plasmic-rich-components@1.0.207
    
    GitOrigin-RevId: 9ac8f559e516bf86e67de2b73d8c51ae1d66a9b6
    * fix: remove some unwanted imports from client/ in shared/
    
    * refactor: move isPositionManagedFrame to studioCtx
    
    GitOrigin-RevId: 42c5a6225988e12a21a91d3f5700e3a97d10e47c
    GitOrigin-RevId: 98619a2e83195662b9edd9010e169efebe7cbc12
    * fix: Swallow s3 error in dev mode
    
    * fix: Only ignore error in development
    
    GitOrigin-RevId: d94e49f84ecebd52c51dbe630f2dd89b23aca77e
    * feat(comments-filter): add user mentions filter in the Comments Tab
    
    * feat(comments-filter): add user participation logic in the mentions and replies filter
    
    * feat(comments-filter): add comments resolved filter
    
    * chore: plasmic.json and plasmic.lock changes
    
    * chore: reverted an unncessary change
    
    * feat(comments-filter): removed setter and getter for comments filter observable
    
    * feat(comments-filter): fix imports for shared code
    
    GitOrigin-RevId: 7a1524fcc6cb8751a0f348e0bb0e4c158ce885f8
    GitOrigin-RevId: b5fc0391536afd33a5a1130040d13abccad0d2eb
    * [DEVOP-130] Created pipeline to push services to GAR
    
    * Chore(build-and-push-services): Changed trigger condition to test
    
    * Chore(build-and-push-services): Added matrix for services and contexts
    
    * Chore(build-and-push-services): Remove matrix for context and added conditional step for it
    
    * Chore(build-and-push-services): Changed context for wab
    
    * Chore(build-and-push-services): Added Dockerfile path input to env and to docker-buildx action
    
    * Chore(build-and-push-services): Added steps to build plasmic-deployed.json and change Dockerfile
    
    * Chore(build-and-push-services): Added steps to build plasmic-deployed.json and change Dockerfile
    
    * Chore(build-and-push-services): Remove .tmp from copy instructions
    
    * Chore(build-and-push-services): Added working-directory
    
    * Chore(build-and-push-services): Changed path for node command
    
    * Chore(build-and-push-services): Fixed path for Dockerfile commands
    
    * Fix(build-and-push-services): added wab prefix to fix path
    
    * Chore(build-and-push-services): Remove make command
    
    * Chore(build-and-push-services): Fixed Dockerfile
    
    * Chore(build-and-push-services): Changed runs-on
    
    * Chore(build-and-push-services): Changed runs-on
    
    * Chore(build-and-push-services): Changed runs-on
    
    * Test(build-and-push-services): Commented make command
    
    * Test(build-and-push-services): Commented make command
    
    * Chore(build-and-push-services): Remove -f function
    
    * Chore(build-and-push-services): Started tagging from 1
    
    * Chore(build-and-push-services): Remove plasmic-deployed.json file
    
    * Chore(build-and-push-services): Added condition to build plasmic-deployed, changed tagging system and remove debug step
    
    * Chore(build-and-push-services): Remove paths-ignore and added exclude condition in changes job
    
    * Chore(platform): Added new Dockerfile for services
    
    * Chore(Dockerfile): Renamed new dockerfile
    
    * Chore(build-and-push-services): Added new filters
    
    * Feat(build-and-push-services): Added caching capability to workflow
    
    * Chore(build-and-push-services): Added again branch trigger
    
    * Chore(build-and-push-services): Added comments to explain critical steps behavior
    
    * Chore(build-and-push-services): Using base Dockerfile to build image
    
    * Chore(build-and-push-services): Added again branch trigger
    
    * Chore(build-and-push-services): Added pre-build steps to not change Dockerfile
    
    * Chore(build-and-push-services): Simplify pre-build step
    
    * Chore(build-and-push-services): Added working-directory to pre-build
    
    * Chore(build-and-push-services): Changed context
    
    * Chore(build-and-push-services): Removed condition
    
    * Chore(build-and-push-services): Check if plasmic-deployed.json exists in the given path
    
    * Chore(build-and-push-services): Remove debug step
    
    * Chore(build-and-push-services): Remove duplicated ignore condition
    
    * Fix(build-and-push-services): Fixed typos and spaces
    
    * Chore(build-and-push-services): Added condition for matrix to run only outputted jobs
    
    * Chore(build-and-push-services): Remove condition for matrix
    
    * Chore(platform): Remove Dockerfile.v2 since it's not needed
    
    * Chore(platform): Test matrix
    
    * Chore(platform): Fixed output from changes job
    
    * Chore(platform): Test both services
    
    * Chore(build-and-push-services): Remove branch trigger
    
    * Chore(build-and-push-services): Remove exceptions from wab-ci
    
    ---------
    
    Co-authored-by: Andrés Dominguez <schub_andres@plasmic.app>
    GitOrigin-RevId: 0b982f5ed817b0feed9d4ea11cbc7d89c45e0025
    GitOrigin-RevId: c6975b0c14a43997ca5cc5db74504ad3b579ce50
    * refactor: override comment dialog max-height to align top and bottom margins
    
    * refactor: chnaged max-height to percentage for CommentDialog.section
    
    * refactor: class changed from CommentDialog to CommentDialogContainer
    
    GitOrigin-RevId: 305c854d4e3be62007a8b95a9878786481c59362
    * feat: comment thread status indicator added to ThreadCommentsDialog and RootComment
    
    * feat: tooltip component added, tooltip added to ThreadHistoryStatus
    
    * Fix: Prevent dialog from opening when clicking on icon by stopping event propagation
    
    * chore: comments project plasmic sync after rebase
    
    * refactor: element selection refactored in CommentsTab
    
    * sync: comments project synced
    
    * refactor: used SWR mutation in ThreadHistoryStatus
    
    GitOrigin-RevId: 6f4d9119e6ab0ff590ad204b6622b0560644f981
    …ant (#640)
    
    * fix(comments-navigation): comments on variant to focus on correct variant artboard
    
    * fix(comments-navigation): improved logic to set studio focus on tpl for correct frame
    
    * chore(variant-comments): remove unused utility function
    
    * chore(variant-comments): fix typo in assert message
    
    * [PLA-11871]: Comment stats for focused mode (#641)
    
    * fix(comments): added variant specific stats for comments in focus mode
    
    * chore(variant-comments): added requested changes to structure code better
    
    * fix(comments-marker): fixed offset for add comments marker on the canvas
    
    * fix(variant-comments): fixed iid access on undefined address for newly added registered variant in focus mode
    
    * feat(variant-comments): refactor to avoid client specific imports in shared code
    
    * feat(variant-comments): added requested changes to remove fallback logic for comment stats
    
    * feat(variant-comments): find arena frame using frame variants instead of viewCtx
    
    * feat(variant-comments): remove async from getArenaFrameForSetOfVariants function
    
    * feat(variant-comments): remove invalid import after merge conflict resolution
    
    * feat(variant-comments): fix otherVariantsCount prop type on CommentIndicator
    
    * feat(variant-comments): fix get arena frame for set of variants to get correct pinned global variants
    
    * feat(variant-comments): fixed comments utils test cases
    
    * chore(comments-tests): used unique uuid for threads
    
    * feat(comment-stats): better code organization for tpl tree icon comment indicator stats
    
    GitOrigin-RevId: 8ac00fdcf5fd0e6a973c32bf635d9367b24f77ab
    Change-Id: I2162e4c63663806dfd29cb36a82d5ae4caedbdf8
    GitOrigin-RevId: 29ce2c70d47267b372cc105ca39dbd297b408cd9
     - @plasmicapp/loader-gatsby@1.0.380
     - @plasmicapp/loader-nextjs@1.0.416
     - @plasmicapp/loader-react@1.0.377
     - @plasmicapp/loader-svelte@0.0.357
     - @plasmicapp/loader-vue@0.1.371
    
    GitOrigin-RevId: 09fd9d6edc90496b0151eac0611d8105670cc202
    GitOrigin-RevId: b6d7a53ab7ae55b71d8438755bce01c98f64752b
    GitOrigin-RevId: 81fa66f16885736173eac6bd1fc552ae30928bb3
    GitOrigin-RevId: 472c0f3db78ddbdd70dc4f9c769fc53a1efd92d3
    GitOrigin-RevId: 5f767e987faaa415da095de0e34a3834086ed0ca
    GitOrigin-RevId: f30b983089ea2e1816137d94628022840b33da33
    * feat(rsc): Generate rsc modules
    
    Issue: https://linear.app/plasmic/issue/PLA-11492
    
    * chore: Improve comments and typings of codegen
    
    * fix: Remove invalid comma
    
    * feat: Include useDollarServerQueries in live preview
    
    * feat: Add minimal setup for server queries in canvas-rendering
    
    * fix: Support arguments in server queries codegen
    
    * chore: add tests for codegen server queries and params
    
    * feat: add support for server queries in loader
    
    * fix(codegen): custom function import should not repeat itself
    
    * feat(codegen): move  to  in the client component
    
    * feat(canvas): make server queries observable in the canvas
    
    * feat(server-queries): usePlasmicServerQuery in canvas rendering
    
    * [PLA-11801] Make studio cache for server queries (#625)
    
    * feat(server-queries): add studio cache for server queries
    
    * feat(server-queries): use the function registration as id for better caching
    
    * refactor(server-queries): add type for plasmic window internals
    
    * feat(rsc): Allow hostless functions to be used in loader rsc (#437)
    
    * feat(rsc): Allow hostless functions to be used in loader rsc
    
    * fix: Remove logs and add funcId
    
    * fix: Add missing isQuery in func expr registration
    
    * test(codegen): regenerate test snapshots
    
    * Multiple fixes (#666)
    
    * fix: Use canvasCtx to get function for execute
    
    * fix: Handle upgrade of custom functions
    
    * chore: Mark getServerQueriesData as unstable
    
    * fix: use usePlasmicServerQuery for useDollarServerQueries
    
    * fix: Handle args in project upgrade
    
    * fix: handle @plasmicapp/host/registerFunction in loader
    
    * fix: Minor fixes for dependency functions to execute
    
    * chore: add NODE_ENV in pm2
    
    * fix: Properly clone server queries
    
    * fix: Use valid js names for server queries code
    
    * refactor: Reuse loader snippet in rsc codegen
    
    * fix: Use proper signature and components
    
    * fix: Use hostless registry instead of canvasCtx
    
    * fix(codegen): minor fixes for RSC codegen
    
    ---------
    
    Co-authored-by: Felipe Mota <fmota@plasmic.app>
    GitOrigin-RevId: 4cde69b14eb820a80827a1f336396ae73ed05ee9
     - @plasmicapp/cli@0.1.336
     - @plasmicapp/loader-core@1.0.137
     - @plasmicapp/loader-edge@1.0.68
     - @plasmicapp/loader-fetcher@1.0.55
     - @plasmicapp/loader-gatsby@1.0.381
     - @plasmicapp/loader-nextjs@1.0.417
     - @plasmicapp/loader-react@1.0.378
     - @plasmicapp/loader-splits@1.0.63
     - @plasmicapp/loader-svelte@0.0.358
     - @plasmicapp/loader-vue@0.1.372
     - @plasmicapp/react-web@0.2.377
     - @plasmicapp/react-web-runtime@0.0.97
     - @plasmicapp/watcher@1.0.84
     - @plasmicpkgs/antd5@0.0.278
    
    GitOrigin-RevId: e390ac2d296d887f8ed34f29fd85f21a315c35da
    GitOrigin-RevId: c903cb478bcb127b4cd64763b5ec54e902b95ba0
    GitOrigin-RevId: 72663d6530d8de3b1244f4430749e4a3ee751a10
    GitOrigin-RevId: 533c76a1c6d415e79948db808bbf029860ae8df7
    sarahsga and others added 26 commits May 13, 2025 09:48
    Change-Id: I17ef6421f2e7dca206a83ea8f025fd995e8595a4
    GitOrigin-RevId: 71c3a833b6b4b646dd46cd82ffef13314c17ba60
    GitOrigin-RevId: 9ab2251c527fd47c4ec4b78c43683e29e4777762
     - @plasmicapp/data-sources@0.1.181
     - @plasmicapp/host@1.0.217
     - @plasmicapp/loader-gatsby@1.0.388
     - @plasmicapp/loader-nextjs@1.0.425
     - @plasmicapp/loader-react@1.0.385
     - @plasmicapp/react-web@0.2.385
     - @plasmicapp/react-web-runtime@0.0.105
     - @plasmicpkgs/airtable@0.0.231
     - @plasmicpkgs/antd@2.0.138
     - @plasmicpkgs/antd5@0.0.286
     - @plasmicpkgs/plasmic-chakra-ui@0.0.47
     - @plasmicpkgs/commerce@0.0.215
     - @plasmicpkgs/commerce-commercetools@0.0.164
     - @plasmicpkgs/commerce-local@0.0.215
     - @plasmicpkgs/commerce-saleor@0.0.179
     - @plasmicpkgs/commerce-shopify@0.0.223
     - @plasmicpkgs/commerce-swell@0.0.224
     - @plasmicpkgs/framer-motion@0.0.215
     - @plasmicpkgs/plasmic-keen-slider@0.0.61
     - @plasmicpkgs/lottie-react@0.0.208
     - @plasmicpkgs/plasmic-basic-components@0.0.242
     - @plasmicpkgs/plasmic-calendly@0.0.63
     - @plasmicpkgs/plasmic-cms@0.0.281
     - @plasmicpkgs/plasmic-content-stack@0.0.171
     - @plasmicpkgs/plasmic-contentful@0.0.159
     - @plasmicpkgs/plasmic-embed-css@0.1.202
     - @plasmicpkgs/plasmic-eventbrite@0.0.50
     - @plasmicpkgs/plasmic-giphy@0.0.49
     - @plasmicpkgs/plasmic-graphcms@0.0.188
     - @plasmicpkgs/plasmic-hubspot@0.0.61
     - @plasmicpkgs/plasmic-link-preview@1.0.114
     - @plasmicpkgs/plasmic-nav@0.0.187
     - @plasmicpkgs/plasmic-pigeon-maps@0.0.49
     - @plasmicpkgs/plasmic-query@0.0.236
     - @plasmicpkgs/plasmic-rich-components@1.0.213
     - @plasmicpkgs/plasmic-sanity-io@1.0.196
     - @plasmicpkgs/plasmic-soundcloud@0.0.61
     - @plasmicpkgs/plasmic-strapi@0.1.167
     - @plasmicpkgs/plasmic-tabs@0.0.58
     - @plasmicpkgs/plasmic-typeform@0.0.61
     - @plasmicpkgs/plasmic-wordpress@0.0.138
     - @plasmicpkgs/plasmic-wordpress-graphql@0.0.133
     - @plasmicpkgs/plasmic-yotpo@0.0.60
     - @plasmicpkgs/radix-ui@0.0.75
     - @plasmicpkgs/react-aria@0.0.136
     - @plasmicpkgs/react-audio-player@0.0.44
     - @plasmicpkgs/react-awesome-reveal@3.8.219
     - @plasmicpkgs/react-chartjs-2@1.0.127
     - @plasmicpkgs/react-parallax-tilt@0.0.217
     - @plasmicpkgs/react-quill@1.0.80
     - @plasmicpkgs/react-scroll-parallax@0.0.225
     - @plasmicpkgs/react-slick@0.0.238
     - @plasmicpkgs/react-twitter-widgets@0.0.215
     - @plasmicpkgs/react-youtube@7.13.221
     - @plasmicpkgs/rive@0.0.4
    
    GitOrigin-RevId: f9e1b14eafd98fbf686e03fabc6e99b5c32aae77
    GitOrigin-RevId: 6685edff424176afa9a7b401397bb7c72407c180
    …ot with styleSections: false (#1025)
    
    * fix(codegen): Type error in codegen for deeply nested code component root with styleSections: false
    
    Change-Id: Ifc4932e1e0644fd55d0949aa317a32d636e1a924
    
    * test: update snapshot test
    
    Change-Id: I1f86a15ea213985036e3ba35ad66bab8ca15e633
    GitOrigin-RevId: 6d88392dde7dc97423af4c9fd6ed0d5e27a07660
    Change-Id: I6f9fdcb957824febdb4ff9793bd3d65159881244
    GitOrigin-RevId: b21886f7d21a9f5ae5a8e61461c8222d33f67ca6
    …(#1012)
    
    * feat(emails): Add react-email code components + email testing script
    
    Change-Id: Ic4904c686c71a4ad4d693bb71574b2be6b158657
    
    * jason updates
    
    Change-Id: Icd6cdba1b5467f930801a47a81388e424db06746
    
    * refactor to server and rename things
    
    Change-Id: Ied747ec3fa1c11d286d870d2ca412adc46201afc
    
    * Add utilities for removing classes and basic email HTML verification
    
    Change-Id: I8ddb20153ff83a90a4b0cf80e9f9a07c96164d0c
    
    * fix: Remove obsolete email code
    
    Change-Id: I3285a8a831e02113af772b593a4c3a0106f66c5f
    
    * remove existing generated html in out/
    
    Change-Id: I5bdba38705a3a597203450d91aa4f02afd239a4c
    
    * fix(cc): improvements to react-email code components
    
    Change-Id: I9c56ed9a977267a003485a436849e247e2cb5253
    
    * fix CI
    
    Change-Id: I99aebf9e24fcacde8430772f901bb5398e0bf11a
    
    ---------
    
    Co-authored-by: Jason Long <j@jaslong.com>
    GitOrigin-RevId: 5033183886c216f75428b4436bc9ec7aac2a3d0d
    * Revert "fix(plasmic-basic-components): remove unused imports"
    
    This reverts commit f042be4dc808a087f45eaa068654f78f501d3f10.
    
    * Revert "fix(LoadingBoundary): don't render nothing on SSR"
    
    This reverts commit 7e9c3da319426f43996c5ea1970a9253127ccb73.
    
    * fix(loading-boundary): return fallback
    
    Change-Id: Ie3514d011b04276832087bac39d2b66794c8620b
    GitOrigin-RevId: 19333cfb5b37c46c13e4ad81b2652bfe47a9da80
     - @plasmicpkgs/plasmic-basic-components@0.0.243
    
    GitOrigin-RevId: 99abccd134bc5a53da9571390a72874b18aaa94c
    GitOrigin-RevId: a73aa8983ff5d5428bd821735430afb3f385180b
    GitOrigin-RevId: 2f1c68b9dd987f8569d83f24f212238d8686f7df
    Change-Id: I07bc2a4e8d24b9b08e08bb01565b7267fe79b869
    GitOrigin-RevId: 23e8533a0b542d75f669111a9df4dbdba15a4fbc
    Change-Id: Ie7530dcd792c9da986a5419438d781d8603f515e
    GitOrigin-RevId: 82c8b8f3d1cc13bf485897284488248c11771e2a
    GitOrigin-RevId: 91bc116f3d79798c2735c40828ba1bcdca990521
    Change-Id: I874fbdcd49cb341994161cf8ff429bfcd905371f
    GitOrigin-RevId: 759afca50b8808b0d1a2c899d6e0551a395646c3
    GitOrigin-RevId: cfa9b7c4a7a1deca4e83bb83ad10b19b5137bbce
    * refactor: custom relativeTime added to moment js for studio
    
    * chore: added tests for moment config
    
    * refactor: condensed moment-config tests
    
    GitOrigin-RevId: 6e3c39667d0d40e7ee1af94fdb0bbba8844704d1
    …tudio
    
    GitOrigin-RevId: eb3e70cb9dff135ddbe00bf7a76bf33469facb3b
    * feat(copilot): upgrade openai package version to latest
    
    * feat(copilot): add images support in ui copilot backend
    
    * chore(copilot): use OpenAI exported type for CreateChatCompletionRequest Options
    
    * chore(copilot): use OpenAI types for image content part
    
    * chore(copilot): minor code and type improvements
    
    * chore(copilot): remove unused type properties
    
    * chore(copilot): use data-urls utility functions
    
    * feat(copilot): use openai type for WholeChatCompletionResponse and update data references
    
    * feat(copilot): ensure copilot response data format as per client code
    
    * chore(copilot): simplify CopilotResponseData type
    
    * fix(copilot): openai sdk validation error in case of undefined api key
    
    * chore(copilot): fix type error in corpus-eval
    
    * fix(copilot): init OpenAI sdk inside function to avoid unncessary validation errors
    
    GitOrigin-RevId: 9d1e56b7e276afa269433ffb076b724f493fe471
    …ior (#994)
    
    * refactor: hide canvas marker like rest of the markers on canvas when zooming, moving, etc
    
    * refactor: added shouldHideUIOverlay in studioCtx and removed redundant checks
    
    * refactor: added param to include isResizeDragging in shouldHideUIOverlay
    
    GitOrigin-RevId: 73b2c10e955f59a858a83646162e4441343cf63b
    …expected. (#1046)
    
    * fix(user-mentions): add selection with insert text to mimic setRangeText
    
    * chore(user-mentions): remove unused argument onValueChange
    
    * chore(user-mentions): improve arg definition for insertText function
    
    GitOrigin-RevId: c44990315e39c4b71e17c2f45f8d45d269f86e47
    * feat(copilot): plasmic file changes for CopilotPromptImage component
    
    * feat(copilot): add image attachment backend integration in CopilotPromptDialog
    
    * chore(copilot): nitpick improve type definition using Pick utility
    
    * chore(copilot): sync list of copilot image types
    
    GitOrigin-RevId: 90d23022f099be56d3ac5c521b539e5790ed5142
    …nt of allowed type (#1055)
    
    Change-Id: I0baed30291041c523616a29a7686b705fe8fc0fc
    GitOrigin-RevId: 24774c6c8b530074ee67e3e0c50933e160bbada6
    GitOrigin-RevId: f8db6b80af422800e614807ce15c25431a0e164c
    * wip right click on erena switcher brings up context menu
    * fixed z index issue
    * removed the rename option from the areaSwitcher context menu
    * updated context menu to use the useContextMenu hook
    
    Change-Id: I924b59a5edde5fabfbb836cefe6816fa6d9a9d53
    GitOrigin-RevId: f874821dc593b54fe68f5f838c0ccb8de4c63641
    Often times, it's useful to perform multiple operations
    on the navigation dropdown. Previously, the dropdown closed
    for most context menu actions. Now, it only closes when using
    the find all references action.
    
    Refactored context menu to AreanContextMenu since it's now used
    in both NavigationRows and ArenaSwitcher.
    
    Change-Id: I532fce7389d655c5c47bf51af8d35763834d353f
    GitOrigin-RevId: 652c072f59a43ed92f47b580048a41e996785fe5
    @qodo-code-review
    Copy link

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Error Handling

    The HTML parser has several places where errors are silently caught or ignored. For example, line 46 has an empty catch block, and there are several places where potentially undefined values are accessed without proper checks.

    const styleSheetsContent = [...document.styleSheets].map((ss) => {
      let content = "";
      try {
        content = [...ss.cssRules]
          .map((cssRule) => {
            return cssRule.cssText;
          })
          .join("\n");
      } catch (e) {}
      return {
    Performance Concern

    The function traverseNodes recursively processes all DOM nodes which could lead to performance issues with large HTML documents. Consider implementing a more efficient traversal method or adding pagination/chunking for large documents.

    function traverseNodes(node: Node, callback: (node: Node) => void) {
      if (isElement(node)) {
        callback(node);
        // We don't want to traverse the children of a component
        if ((node as any).__wi_component) {
          return;
        }
        for (let i = 0; i < node.childNodes.length; i++) {
          traverseNodes(node.childNodes[i], callback);
        }
      }
    }
    Test Complexity

    The test file has grown significantly with many similar test cases. Consider refactoring to use parameterized tests or test factories to reduce duplication and improve maintainability.

      it("should notify user if thread is resolved and user have participated", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            await sudo.updateNotificationSettings(users[2].id, project.id, {
              notifyAbout: "none",
            }); // Do not notify at all
    
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 resolved his thread which should notify user1 because he has participated
            const commentThreadResolvedHistoryForUser0Comment =
              await updatedThreadStatus(
                userDbs[0](),
                user0comment.commentThreadId,
                true
              );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          await createNotification(
                            user0comment.commentThreadId,
                            users[1],
                            project,
                            commentThreadResolvedHistoryForUser0Comment.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history: commentThreadResolvedHistoryForUser0Comment,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify user if thread is resolved/unresolved multiple times and user have participated", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 resolved his thread which should notify user1 because he has participated
            const commentThreadResolvedHistoryForUser0Comment =
              await updatedThreadStatus(
                userDbs[0](),
                user0comment.commentThreadId,
                true
              );
            const commentThreadUnResolvedHistoryForUser0Comment =
              await updatedThreadStatus(
                userDbs[0](),
                user0comment.commentThreadId,
                false
              );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          await createNotification(
                            user0comment.commentThreadId,
                            users[1],
                            project,
                            commentThreadResolvedHistoryForUser0Comment.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history: commentThreadResolvedHistoryForUser0Comment,
                            },
                            sudo
                          ),
                          await createNotification(
                            user0comment.commentThreadId,
                            users[1],
                            project,
                            commentThreadUnResolvedHistoryForUser0Comment.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history:
                                commentThreadUnResolvedHistoryForUser0Comment,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify user if thread is resolved/unresolved if notification preference is mentions-and-replies", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            await sudo.updateNotificationSettings(users[1].id, project.id, {
              notifyAbout: "mentions-and-replies",
            });
    
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 resolved his thread which should notify user1 because he has participated
            const commentThreadResolvedHistoryForUser0Comment =
              await updatedThreadStatus(
                userDbs[0](),
                user0comment.commentThreadId,
                true
              );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          await createNotification(
                            user0comment.commentThreadId,
                            users[1],
                            project,
                            commentThreadResolvedHistoryForUser0Comment.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history: commentThreadResolvedHistoryForUser0Comment,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should not notify user if thread is resolved/unresolved by user himself", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 1 resolved his thread and user should not be notified
            const commentThreadResolvedHistoryForUser0Comment =
              await updatedThreadStatus(
                userDbs[1](),
                user1Comment.commentThreadId,
                true
              );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user0comment.commentThreadId,
              user1Comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify user if another user react to their comment", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 reacted to user 1 comment, user 1 should be notified
            const user0CommentReaction = await reactOnComment(
              userDbs[0](),
              user1Comment.id
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1Comment.commentThreadId,
                        [
                          await createNotification(
                            user1Comment.commentThreadId,
                            users[1],
                            project,
                            user0CommentReaction.createdAt,
                            {
                              type: "REACTION",
                              reaction: user0CommentReaction,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user0comment.commentThreadId,
              user1Comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should not be notified user if reaction is removed", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
            // user 0 reacted to user 1 comment, user 1
            const user0CommentReaction = await reactOnComment(
              userDbs[0](),
              user1Comment.id
            );
            // removed the reaction now user should not be notified for this reaction
            await removeReactionOnComment(userDbs[0](), user0CommentReaction.id);
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user0comment.commentThreadId,
              user1Comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify new reaction on old comment", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            const { notificationsByUser, recentCommentThreads, notifiedDate } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
    
            // Simulate sending notifications, after which the threads should be marked as notified
            // You can either mark them as notified in the system or simulate this in your mock logic
            await sudo.markCommentThreadsAsNotified(
              [user1Comment.commentThreadId, user0comment.commentThreadId],
              notifiedDate
            );
    
            // users are notified now we add a reaction on an old comment, it should only notify for reaction
    
            // user 0 reacted to user 1 comment, user 1
            const user0CommentReaction = await reactOnComment(
              userDbs[0](),
              user1Comment.id
            );
    
            const {
              notificationsByUser: notificationsByUser2,
              recentCommentThreads: recentCommentThreads2,
            } = await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification2: NotificationsByUser = new Map([
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1Comment.commentThreadId,
                        [
                          await createNotification(
                            user1Comment.commentThreadId,
                            users[1],
                            project,
                            user0CommentReaction.createdAt,
                            {
                              type: "REACTION",
                              reaction: user0CommentReaction,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
            expect(notificationsByUser2).toEqual(expectedNotification2);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads2).toEqual([user1Comment.commentThreadId]);
          }
        );
      });
    
      it("should notify reaction and comment combined if both are new", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 reacted to user 1 comment, user 1
            const user0CommentReaction = await reactOnComment(
              userDbs[0](),
              user1Comment.id
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // user 0 for user 1 reply, while user 1 will be notified for user 0 reaction on user 1 comment
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1Comment.commentThreadId,
                        [
                          await createNotification(
                            user1Comment.commentThreadId,
                            users[1],
                            project,
                            user0CommentReaction.createdAt,
                            {
                              type: "REACTION",
                              reaction: user0CommentReaction,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user0comment.commentThreadId,
              user1Comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify for resolving an old thread", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            const { notificationsByUser, recentCommentThreads, notifiedDate } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
    
            // Simulate sending notifications, after which the threads should be marked as notified
            // You can either mark them as notified in the system or simulate this in your mock logic
            await sudo.markCommentThreadsAsNotified(
              [user1Comment.commentThreadId, user0comment.commentThreadId],
              notifiedDate
            );
    
            // users are notified now we resolved an old thread, it should only notify for thread history
    
            // user 0 resolved their thread
            const user0CommentThreadHistory = await updatedThreadStatus(
              userDbs[0](),
              user0comment.commentThreadId,
              true
            );
    
            const {
              notificationsByUser: notificationsByUser2,
              recentCommentThreads: recentCommentThreads2,
            } = await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            // only user 1 has participated, user 0 is not notified for it's self thread history updated
            const expectedNotification2: NotificationsByUser = new Map([
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          await createNotification(
                            user0comment.commentThreadId,
                            users[1],
                            project,
                            user0CommentThreadHistory.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history: user0CommentThreadHistory,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
            expect(notificationsByUser2).toEqual(expectedNotification2);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads2).toEqual([user0comment.commentThreadId]);
          }
        );
      });
    
      it("should notify thread history and comment combined if both are new", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 resolved their thread
            const user0CommentThreadHistory = await updatedThreadStatus(
              userDbs[0](),
              user0comment.commentThreadId,
              true
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // user 0 and 2 will be notified for user 1 comment, while user 1 will be notified for user 0 reaction on user 1 comment
    
            // only user 1 has participated, user 1 will be notified for thread history and comment, user 0 is not notified for it's self thread history updated
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          await createNotification(
                            user0comment.commentThreadId,
                            users[1],
                            project,
                            user0CommentThreadHistory.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history: user0CommentThreadHistory,
                            },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify user if mentioned", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // user 2 is mentioned so user 2 should be notified
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await userDbs[1]().postCommentInThread(
              { projectId: project.id },
              {
                body: `@<${users[2].email}> should check`,
                threadId: user0comment.commentThreadId,
                id: uuid.v4() as CommentId,
              }
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            const expectedNotification = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
    
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[2].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          // user 2 is mentioned because user was mentioned
                          await createNotification(
                            user0comment.commentThreadId,
                            users[2],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            // Check if the notificationsByUser structure is correct
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should not notify if user mentioned self", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // user 1 mentioned self so user 1 should not be notified
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await userDbs[1]().postCommentInThread(
              { projectId: project.id },
              {
                body: `@<${users[1].email}> should check`,
                threadId: user0comment.commentThreadId,
                id: uuid.v4() as CommentId,
              }
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            const expectedNotification = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
    
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            // Check if the notificationsByUser structure is correct
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should notify user with 'mentions-and-replies'", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // Set user 2's preference to 'mentions-and-replies'
            await sudo.updateNotificationSettings(users[2].id, project.id, {
              notifyAbout: "mentions-and-replies",
            });
    
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // user 2 is mentioned so user 2 should be notified
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await userDbs[1]().postCommentInThread(
              { projectId: project.id },
              {
                body: `@<${users[2].email}> should check`,
                threadId: user0comment.commentThreadId,
                id: uuid.v4() as CommentId,
              }
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            const expectedNotification = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
    
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[2].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user0comment.commentThreadId,
                        [
                          // user 2 is mentioned because user was mentioned
                          await createNotification(
                            user0comment.commentThreadId,
                            users[2],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            // Check if the notificationsByUser structure is correct
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
    
      it("should not notify user if mentioned without preceding @", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // user 2 is mentioned but without preceding @ so user 2 should not be notified
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await userDbs[1]().postCommentInThread(
              { projectId: project.id },
              {
                body: `<${users[2].email}> should check`,
                threadId: user0comment.commentThreadId,
                id: uuid.v4() as CommentId,
              }
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            const expectedNotification = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
    
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            // Check if the notificationsByUser structure is correct
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
      it("should notify user for all recent notifications with 'all' notification preference", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // Set user 0's preference to 'all'
            await sudo.updateNotificationSettings(users[0].id, project.id, {
              notifyAbout: "all",
            });
    
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // user 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // user 2 reply to user 1 comment
            const user2Reply = await addComment(
              userDbs[2](),
              project.id,
              user1Comment.commentThreadId
            );
    
            // user 2 mentioned user 1
            const user2MentionUser1 = await userDbs[2]().postCommentInThread(
              { projectId: project.id },
              {
                body: `@<${users[1].email}> should check`,
                threadId: user0comment.commentThreadId,
                id: uuid.v4() as CommentId,
              }
            );
    
            // user 2 resolved a thread that user has participated
            const commentThreadUnResolvedHistoryForUser1Comment =
              await updatedThreadStatus(
                userDbs[2](),
                user1Comment.commentThreadId,
                false
              );
    
            // user 1 reacted to user 0 comment
            const user1Reaction = await reactOnComment(
              userDbs[1](),
              user0comment.id
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            const expectedNotification = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1Comment.commentThreadId,
                        [
                          await createNotification(
                            user1Comment.commentThreadId,
                            users[0],
                            project,
    
                            user1Comment.createdAt,
                            { type: "COMMENT", comment: user1Comment },
                            sudo
                          ),
                          await createNotification(
                            user2Reply.commentThreadId,
                            users[0],
                            project,
    
                            user2Reply.createdAt,
                            { type: "COMMENT", comment: user2Reply },
                            sudo
                          ),
                          await createNotification(
                            commentThreadUnResolvedHistoryForUser1Comment.commentThreadId,
                            users[0],
                            project,
    
                            commentThreadUnResolvedHistoryForUser1Comment.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history:
                                commentThreadUnResolvedHistoryForUser1Comment,
                            },
                            sudo
                          ),
                        ],
                      ],
                      [
                        user0comment.commentThreadId,
                        [
                          await createNotification(
                            user0comment.commentThreadId,
                            users[0],
                            project,
    
                            user2MentionUser1.createdAt,
                            {
                              type: "COMMENT",
                              comment: user2MentionUser1,
                            },
                            sudo
                          ),
                          await createNotification(
                            user0comment.commentThreadId,
                            users[0],
                            project,
    
                            user1Reaction.createdAt,
                            { type: "REACTION", reaction: user1Reaction },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user2Reply.commentThreadId,
                        [
                          await createNotification(
                            user2Reply.commentThreadId,
                            users[1],
                            project,
                            user2Reply.createdAt,
                            { type: "COMMENT", comment: user2Reply },
                            sudo
                          ),
                          await createNotification(
                            commentThreadUnResolvedHistoryForUser1Comment.commentThreadId,
                            users[1],
                            project,
    
                            commentThreadUnResolvedHistoryForUser1Comment.createdAt,
                            {
                              type: "THREAD_HISTORY",
                              history:
                                commentThreadUnResolvedHistoryForUser1Comment,
                            },
                            sudo
                          ),
                        ],
                      ],
                      [
                        user2MentionUser1.commentThreadId,
                        [
                          await createNotification(
                            user2MentionUser1.commentThreadId,
                            users[1],
                            project,
                            user2MentionUser1.createdAt,
                            { type: "COMMENT", comment: user2MentionUser1 },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user1Comment.commentThreadId,
              user0comment.commentThreadId,
            ]);
          }
        );
      });
      it("should not notify user if reacted to self comment", async () => {
        await withEndUserNotificationSetup(
          async ({ sudo, users, project, userDbs }) => {
            // user 0 comment
            const user0comment = await addComment(userDbs[0](), project.id);
    
            // User 1 comment
            const user1Comment = await addComment(userDbs[1](), project.id);
    
            // User 1 reply to user 0 comment
            const user1ReplyToUser0Comment = await addComment(
              userDbs[1](),
              project.id,
              user0comment.commentThreadId
            );
    
            // user 0 reacted to self comment, user 0 should not be notified
            const user0CommentReaction = await reactOnComment(
              userDbs[0](),
              user0comment.id
            );
    
            // user 0 reacted to user 1 comment, user 1 should be notified
            const user1CommentReaction = await reactOnComment(
              userDbs[0](),
              user1Comment.id
            );
    
            const { notificationsByUser, recentCommentThreads } =
              await processUnnotifiedCommentsNotifications(sudo);
    
            // Check if the notificationsByUser structure is correct
            // user 1 not be notified of self reaction
            const expectedNotification: NotificationsByUser = new Map([
              [
                users[0].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1ReplyToUser0Comment.commentThreadId,
                        [
                          await createNotification(
                            user1ReplyToUser0Comment.commentThreadId,
                            users[0],
                            project,
                            user1ReplyToUser0Comment.createdAt,
                            { type: "COMMENT", comment: user1ReplyToUser0Comment },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
              [
                users[1].id,
                new Map([
                  [
                    project.id,
                    new Map([
                      [
                        user1Comment.commentThreadId,
                        [
                          await createNotification(
                            user1Comment.commentThreadId,
                            users[1],
                            project,
                            user1CommentReaction.createdAt,
                            { type: "REACTION", reaction: user1CommentReaction },
                            sudo
                          ),
                        ],
                      ],
                    ]),
                  ],
                ]),
              ],
            ]);
    
            expect(notificationsByUser).toEqual(expectedNotification);
    
            // Check if the processed threads match the recentThreads
            expect(recentCommentThreads).toEqual([
              user0comment.commentThreadId,
              user1Comment.commentThreadId,
            ]);
          }
        );
      });
    });

    @qodo-code-review
    Copy link

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix CSS value parsing

    The style parsing logic fails when CSS values contain colons (e.g., in URLs or
    data URIs). The current implementation splits on the first colon, which breaks
    values like "background-image: url(http://example.com)".

    platform/wab/src/wab/client/web-importer/html-parser.ts [142-151]

     function addSelfStyleRule(_node: Node) {
       if (!isElement(_node)) {
         return;
       }
       const cssText = (_node as HTMLElement).style.cssText;
       if (!cssText) {
         return;
       }
     
       const styles = cssText.split(";").reduce((acc, style) => {
    -    const [key, value] = style.split(":");
    +    if (!style.trim()) {
    +      return acc;
    +    }
    +    const colonIndex = style.indexOf(":");
    +    if (colonIndex === -1) {
    +      console.log("Invalid style", style);
    +      return acc;
    +    }
    +    const key = style.substring(0, colonIndex).trim();
    +    const value = style.substring(colonIndex + 1).trim();
         if (!key || !value) {
           console.log("Invalid style", style);
           return acc;
         }
    -    acc[key.trim()] = value.trim();
    +    acc[key] = value;
         return acc;
       }, {} as Record<string, string>);

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 8

    __

    Why: This suggestion fixes a real parsing bug where CSS values containing colons (such as URLs) would be incorrectly split, leading to incorrect style extraction. The fix is accurate and important for correct style parsing.

    Medium
    Incomplete test assertion

    The test is only checking if user[1]'s notifications match the expected
    structure, but ignores user[0]'s notifications. This could hide bugs where
    user[0] receives incorrect notifications. Verify the entire notification
    structure against the expected value.

    platform/wab/src/wab/server/scripts/send-comments-notifications.spec.ts [562-564]

    -expect(notificationsByUser.get(users[1].id)).toEqual(
    -  expectedNotification.get(users[1].id)
    -);
    +expect(notificationsByUser).toEqual(expectedNotification);

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion improves the test by asserting the entire notificationsByUser object instead of just a single user's notifications, increasing confidence that the notification logic works for all users. This is a moderate but meaningful improvement to test coverage and correctness.

    Medium
    Insufficient test verification

    The test only checks the length of notifications but doesn't verify the actual
    content. This could allow incorrect notification content to pass the test. Use a
    more specific assertion to verify both the count and content of notifications.

    platform/wab/src/wab/server/scripts/send-comments-notifications.spec.ts [308-313]

     // user 1 will be notified about user 0 reply to their comment
     expect(
       notificationsByUser
         .get(users[1].id)
         ?.get(project.id)
    -    ?.get(user1Comment.commentThreadId)?.length
    -).toBe(1);
    +    ?.get(user1Comment.commentThreadId)
    +).toEqual([
    +  await createNotification(
    +    user1Comment.commentThreadId,
    +    users[1],
    +    project,
    +    user0Replied.createdAt,
    +    { type: "COMMENT", comment: user0Replied },
    +    sudo
    +  ),
    +]);

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion enhances the test by checking both the count and the actual content of the notifications, making the test more robust and less likely to miss subtle bugs. This is a moderate improvement in test quality and reliability.

    Medium
    Handle empty array case

    Add a check to prevent processing empty arrays of thread IDs. When an empty
    array is passed to the In operator in TypeORM, it can cause SQL errors in some
    database engines or unexpected behavior.

    platform/wab/src/wab/server/db/DbMgr.ts [9821-9829]

     async markCommentThreadsAsNotified(
       commentThreadIds: string[],
       notifiedDate: Date
     ): Promise<void> {
       this.checkSuperUser();
    +  if (commentThreadIds.length === 0) {
    +    return;
    +  }
       await this.commentThreads().update(
         { id: In(commentThreadIds) },
         { lastEmailedAt: notifiedDate }
       );
     }

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion to short-circuit when commentThreadIds is empty prevents unnecessary database operations and potential SQL errors, improving robustness. This is a minor but meaningful improvement for defensive programming.

    Medium
    Handle potential null values

    The function uses non-null assertion (!) when finding terms with specific units,
    but there's no guarantee the find() operation will return a non-null value even
    after checking with some(). This could lead to runtime errors if the regex
    pattern matches differently.

    platform/wab/src/wab/client/web-importer/html-parser.ts [200-215]

     function getFixedValue() {
       if (value.startsWith("calc(")) {
         const combination = value.slice(5, -1);
         // split all terms of mathematical combination
         const terms = combination.split(/([+\-*/])/);
         // get the likely biggest term
         if (terms.some((term) => term.includes("vh"))) {
    -      return terms.find((term) => term.includes("vh"))!.trim();
    +      const vhTerm = terms.find((term) => term.includes("vh"));
    +      return vhTerm ? vhTerm.trim() : terms[0].trim();
         }
         if (terms.some((term) => term.includes("vw"))) {
    -      return terms.find((term) => term.includes("vw"))!.trim();
    +      const vwTerm = terms.find((term) => term.includes("vw"));
    +      return vwTerm ? vwTerm.trim() : terms[0].trim();
         }
         if (terms.some((term) => term.includes("px"))) {
    -      return terms.find((term) => term.includes("px"))!.trim();
    +      const pxTerm = terms.find((term) => term.includes("px"));
    +      return pxTerm ? pxTerm.trim() : terms[0].trim();
         }
         return terms[0].trim();
       }

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion improves robustness by removing unsafe non-null assertions and providing a fallback, reducing the risk of runtime errors. While not critical, it is a solid defensive programming improvement.

    Medium
    Trim CSS values

    The function doesn't trim the input values, which can lead to unexpected
    behavior when handling CSS values with leading or trailing whitespace. This is
    particularly problematic when parsing style attributes from HTML elements.

    platform/wab/src/wab/client/web-importer/html-parser.ts [176-199]

     function fixCSSValue(key: string, value: string) {
       if (!value) {
         return {};
       }
    -
    -  if (key === "content") {
    +  
    +  const trimmedKey = key.trim();
    +  const trimmedValue = value.trim();
    +  
    +  if (trimmedKey === "content") {
         return {};
       }
    -
    +  
       function getFixedKey() {
    -    const val = camelCase(key);
    +    const val = camelCase(trimmedKey);
         if (val in translationTable) {
           return translationTable[val as keyof typeof translationTable];
         }
         return val;
       }
    -
    +  
       const fixedKey = getFixedKey();
    -
    +  
       if (ignoredStyles.has(fixedKey)) {
         return {};
       }

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 6

    __

    Why: Trimming whitespace from CSS keys and values can prevent subtle bugs when parsing inline styles, especially from user-generated or inconsistent HTML. This is a moderate improvement for robustness, though not critical for correctness.

    Low
    • More

    @willymwai willymwai merged commit df8233f into truehostcloud:master May 19, 2025
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.