diff --git a/.clabot b/.clabot index b18e074beb..646c7c7f43 100644 --- a/.clabot +++ b/.clabot @@ -15,7 +15,8 @@ "dependabot", "dependabot[bot]", "AyushAgrawal-A2", - "golok727" + "golok727", + "github-actions[bot]@users.noreply.github.com" ], "message": "We require contributors to sign our Contributor License Agreement, and we don\"t have one on file for you. In order for us to review and merge your code, please contact our team at https://www.quadratichq.com/contact." } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6d46a33b5..5b5a5c18d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,3 +223,48 @@ jobs: npm run lint:prettier npm run lint:eslint npm run lint:ts + + check-version-increment: + runs-on: ubuntu-latest + # If we are merging into main, but not pushed on main + if: github.base_ref == 'main' && github.ref != 'refs/heads/main' + steps: + - name: Checkout current branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Get current VERSION + id: current_version + run: echo "CURRENT_VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT + + - name: Checkout main branch + uses: actions/checkout@v3 + with: + ref: main + fetch-depth: 1 + + - name: Get main VERSION + id: main_version + run: echo "MAIN_VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT + + - name: Compare versions to main, verify this version is higher + run: | + current_version="${{ steps.current_version.outputs.CURRENT_VERSION }}" + main_version="${{ steps.main_version.outputs.MAIN_VERSION }}" + if [ "$(printf '%s\n' "$main_version" "$current_version" | sort -V | tail -n1)" != "$current_version" ]; then + echo "Error: VERSION in the current branch ($current_version) is not greater than VERSION in main ($main_version)" + exit 1 + else + echo "VERSION check passed: Current branch ($current_version) > main ($main_version)" + fi + + check-versions-match: + runs-on: ubuntu-latest + + steps: + - name: Checkout current branch + uses: actions/checkout@v3 + + - name: Verify that all versions match + run: ./bump.sh verify \ No newline at end of file diff --git a/.github/workflows/production-bump-version.yml b/.github/workflows/production-bump-version.yml new file mode 100644 index 0000000000..e6efa613f1 --- /dev/null +++ b/.github/workflows/production-bump-version.yml @@ -0,0 +1,44 @@ +name: Bump Version on PR against to main + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Type of version bump' + default: patch + type: choice + options: + - major + - minor + - patch + pull_request: + types: [opened] + branches: + - main + +jobs: + bump-version: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install jq + run: sudo apt-get install jq + + - name: Run bump.sh script + run: ./bump.sh ${{ github.event.inputs.bump_type || 'patch' }} + + - name: Commit and push changes + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m 'Bump version' || exit 0 + git pull --rebase + git push diff --git a/.gitignore b/.gitignore index c6c8c6cb02..4870b3de87 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,12 @@ quadratic-shared/*.js # misc .DS_Store +.env .env.local .env.development.local .env.test.local .env.production.local +.env.old npm-debug.log* yarn-debug.log* @@ -31,9 +33,6 @@ venv/* *.pyc .idea -.env -.env.local - # Generated Rust files /target quadratic-connection/target/ @@ -71,3 +70,5 @@ docker/mssql-connection/data # JMeter jmeter.log + +~$* diff --git a/.vscode/settings.json b/.vscode/settings.json index f19841d139..abb40e74b7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,8 +42,10 @@ "Signin", "smallpop", "Southborough", + "Strftime", "szhsin", "thiserror", + "Timelike", "unspill", "vals", "websockets", @@ -78,5 +80,8 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/Cargo.lock b/Cargo.lock index 5d673aa18b..b69635d721 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,18 +10,18 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" [[package]] name = "arrayvec" @@ -245,7 +245,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.3.0", + "indexmap 2.5.0", "lexical-core", "num", "serde", @@ -362,19 +362,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -418,9 +418,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-config" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a" +checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" dependencies = [ "aws-credential-types", "aws-runtime", @@ -438,7 +438,6 @@ dependencies = [ "fastrand", "hex", "http 0.2.12", - "hyper 0.14.30", "ring", "time", "tokio", @@ -449,9 +448,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -461,15 +460,16 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.3.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f" +checksum = "2424565416eef55906f9f8cece2072b6b6a76075e3ff81483ebe938a89a4c05f" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", "aws-smithy-http", + "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", @@ -477,6 +477,7 @@ dependencies = [ "fastrand", "http 0.2.12", "http-body 0.4.6", + "once_cell", "percent-encoding", "pin-project-lite", "tracing", @@ -485,9 +486,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.42.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bbcec8db82a1a8af1610afcb3b10d00652d25ad366a0558eecdff2400a1d1" +checksum = "00a545b16c05af9302b0b4b38a7584f6f323749e407169aa3e9b210e7c0a808d" dependencies = [ "ahash", "aws-credential-types", @@ -520,9 +521,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.36.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6acca681c53374bf1d9af0e317a41d12a44902ca0f2d1e10e5cb5bb98ed74f35" +checksum = "af0a3f676cba2c079c9563acc9233998c8951cdbe38629a0bef3c8c1b02f3658" dependencies = [ "aws-credential-types", "aws-runtime", @@ -542,9 +543,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.37.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79c6bdfe612503a526059c05c9ccccbf6bd9530b003673cb863e547fd7c0c9a" +checksum = "c91b6a04495547162cf52b075e3c15a17ab6608bf9c5785d3e5a5509b3f09f5c" dependencies = [ "aws-credential-types", "aws-runtime", @@ -564,9 +565,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.36.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e6ecdb2bd756f3b2383e6f0588dc10a4e65f5d551e70a56e0bfe0c884673ce" +checksum = "99c56bcd6a56cab7933980a54148b476a5a69a7694e3874d9aa2a566f150447d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -627,9 +628,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.11" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c4134cf3adaeacff34d588dbe814200357b0c466d730cf1c0d8054384a2de4" +checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -648,9 +649,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.4" +version = "0.60.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858" +checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" dependencies = [ "aws-smithy-types", "bytes", @@ -659,9 +660,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.9" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -699,9 +700,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.6.2" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537" +checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -726,9 +727,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -743,9 +744,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.0" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5" +checksum = "03701449087215b5369c7ea17fef0dd5d24cb93439ec5af0c7615f58c3f22605" dependencies = [ "base64-simd", "bytes", @@ -769,9 +770,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" dependencies = [ "xmlparser", ] @@ -879,23 +880,23 @@ checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" dependencies = [ "heck 0.4.1", "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -1064,9 +1065,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.7" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -1242,15 +1246,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -1434,9 +1438,9 @@ dependencies = [ "fnv", "ident_case", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "strsim", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1446,8 +1450,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -1471,6 +1475,18 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "dateparser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ef451feee09ae5ecd8a02e738bd9adee9266b8fa9b44e22d3ce968d8694238" +dependencies = [ + "anyhow", + "chrono", + "lazy_static", + "regex", +] + [[package]] name = "der" version = "0.6.1" @@ -1540,8 +1556,8 @@ checksum = "7e57e12b69e57fad516e01e2b3960f122696fdb13420e1a88ed8e210316f2876" dependencies = [ "darling", "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -1610,8 +1626,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -1669,9 +1685,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ff" @@ -1695,9 +1711,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", "miniz_oxide", @@ -1810,8 +1826,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -1880,9 +1896,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "group" @@ -1907,7 +1923,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.3.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -2191,9 +2207,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-util", @@ -2268,9 +2284,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -2285,9 +2301,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "itertools" @@ -2306,9 +2322,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2409,9 +2425,9 @@ checksum = "b14c52534dd690e23b687bdbbbe300d7ec5c45f25e9261f72b70751af67067d3" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libm" @@ -2514,6 +2530,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2522,18 +2548,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", @@ -2677,9 +2703,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.2" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -2718,8 +2744,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -2730,9 +2756,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.3.2+3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" dependencies = [ "cc", ] @@ -2819,7 +2845,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -2900,8 +2926,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -2988,7 +3014,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", "version_check", ] @@ -3000,7 +3026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "version_check", ] @@ -3071,7 +3097,7 @@ dependencies = [ [[package]] name = "quadratic-connection" -version = "0.1.0" +version = "0.5.1" dependencies = [ "arrow", "arrow-schema", @@ -3113,7 +3139,7 @@ dependencies = [ [[package]] name = "quadratic-core" -version = "0.1.14" +version = "0.5.1" dependencies = [ "anyhow", "arrow-array", @@ -3129,12 +3155,13 @@ dependencies = [ "console_error_panic_hook", "criterion", "csv", + "dateparser", "flate2", "futures", "getrandom 0.2.15", "half", "htmlescape", - "indexmap 2.3.0", + "indexmap 2.5.0", "itertools", "js-sys", "lazy_static", @@ -3166,7 +3193,7 @@ dependencies = [ [[package]] name = "quadratic-files" -version = "0.1.0" +version = "0.5.1" dependencies = [ "axum", "axum-extra", @@ -3198,7 +3225,7 @@ dependencies = [ [[package]] name = "quadratic-multiplayer" -version = "0.1.0" +version = "0.5.1" dependencies = [ "axum", "axum-extra", @@ -3232,8 +3259,9 @@ dependencies = [ [[package]] name = "quadratic-rust-client" -version = "0.1.0" +version = "0.5.1" dependencies = [ + "chrono", "console_error_panic_hook", "js-sys", "quadratic-core", @@ -3246,7 +3274,7 @@ dependencies = [ [[package]] name = "quadratic-rust-shared" -version = "0.1.0" +version = "0.5.1" dependencies = [ "arrow", "async-trait", @@ -3301,9 +3329,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2 1.0.86", ] @@ -3429,15 +3457,6 @@ dependencies = [ "url", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.3" @@ -3595,18 +3614,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" dependencies = [ "bitflags 2.6.0", "errno", @@ -3693,20 +3712,20 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.7" +version = "2.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a870e34715d5d59c8536040d4d4e7a41af44d527dc50237036ba4090db7996fc" +checksum = "0c947adb109a8afce5fc9c7bf951f87f146e9147b3a6a58413105628fb1d1e66" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3733,9 +3752,9 @@ dependencies = [ [[package]] name = "sdd" -version = "2.1.0" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177258b64c0faaa9ffd3c65cd3262c2bc7e2588dbbd9c1641d0346145c1bbda8" +checksum = "60a7b59a5d9b0099720b417b6325d91a52cbf5b3dcb5041d864be53eefa58abc" [[package]] name = "sec1" @@ -3788,9 +3807,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -3808,20 +3827,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", "memchr", @@ -3846,8 +3865,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3872,7 +3891,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.3.0", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -3888,8 +3907,8 @@ checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling", "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3913,8 +3932,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3954,6 +3973,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4060,9 +4085,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ "nom", "unicode_categories", @@ -4104,7 +4129,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.3.0", + "indexmap 2.5.0", "log", "memchr", "native-tls", @@ -4131,7 +4156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "sqlx-core", "sqlx-macros-core", "syn 1.0.109", @@ -4149,7 +4174,7 @@ dependencies = [ "hex", "once_cell", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "serde", "serde_json", "sha2", @@ -4324,7 +4349,7 @@ checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "rustversion", "syn 1.0.109", ] @@ -4337,9 +4362,9 @@ checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "rustversion", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -4350,9 +4375,9 @@ checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck 0.5.0", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "rustversion", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -4379,18 +4404,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.72" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "unicode-ident", ] @@ -4449,21 +4474,21 @@ dependencies = [ "heck 0.4.1", "proc-macro-error", "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] [[package]] name = "tempfile" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4497,8 +4522,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4617,9 +4642,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -4640,8 +4665,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4666,9 +4691,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -4702,9 +4727,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -4761,15 +4786,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -4790,8 +4815,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4850,8 +4875,8 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4863,7 +4888,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" version = "7.0.0" -source = "git+https://github.com/HactarCE/ts-rs/?rev=812c1a8#812c1a8b5ff3128916426e95e228f51430eb02cc" +source = "git+https://github.com/quadratichq/ts-rs/?rev=812c1a8#812c1a8b5ff3128916426e95e228f51430eb02cc" dependencies = [ "smallvec", "thiserror", @@ -4874,12 +4899,12 @@ dependencies = [ [[package]] name = "ts-rs-macros" version = "7.0.0" -source = "git+https://github.com/HactarCE/ts-rs/?rev=812c1a8#812c1a8b5ff3128916426e95e228f51430eb02cc" +source = "git+https://github.com/quadratichq/ts-rs/?rev=812c1a8#812c1a8b5ff3128916426e95e228f51430eb02cc" dependencies = [ "Inflector", "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", "termcolor", ] @@ -4962,9 +4987,9 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-segmentation" @@ -5077,7 +5102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", + "quote 1.0.37", ] [[package]] @@ -5128,34 +5153,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -5165,41 +5191,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-bindgen-test" -version = "0.3.42" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" dependencies = [ "console_error_panic_hook", "js-sys", + "minicov", "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", @@ -5208,13 +5235,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.42" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -5232,9 +5259,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -5242,11 +5269,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall", "wasite", ] @@ -5484,8 +5511,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.72", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index add30611d5..96a33554fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ edition = "2021" description = "Infinite data grid with Python, JavaScript, and SQL built-in" repository = "https://github.com/quadratichq/quadratic" license-file = "LICENSE" +version = "0.5.1" [profile.release] # Tell `rustc` to optimize for small code size. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..4b9fcbec10 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.5.1 diff --git a/bump.sh b/bump.sh new file mode 100755 index 0000000000..28264af4b7 --- /dev/null +++ b/bump.sh @@ -0,0 +1,167 @@ +#!/bin/bash + +# General bump flow: +# +# Read version from main VERSION file (located at root) +# Bump RUST and JAVASCRIPT files (see listing above) according to command line args: major/minor/patch/set +# Bump the main VERSION file +# Commit added files +# Create version tag (e.g. v1.0.1) +# Push commit to main +# Push tag to main +# +# Usage: bump.sh [TYPE] +# +# TYPE options: +# major +# minor +# patch +# set (just sets all versions to the current VERSION file version) +# verify (checks if all versions match) + +set -e + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 {major|minor|patch|set|verify}" + exit 1 +fi + +VERSION=$(cat VERSION) +TYPE=$1 + +RUST=( + "Cargo.toml" + "quadratic-connection/Cargo.toml" + "quadratic-core/Cargo.toml" + "quadratic-files/Cargo.toml" + "quadratic-multiplayer/Cargo.toml" + "quadratic-rust-client/Cargo.toml" + "quadratic-rust-shared/Cargo.toml" +) + +JAVASCRIPT=( + "package.json" + "quadratic-api/package.json" + "quadratic-client/package.json" + "quadratic-shared/package.json" +) + +if [[ -z "$VERSION" ]]; then + echo "Version file is empty. Please set a version in the VERSION file. (e.g. 1.0.0)" + exit 1 +fi + +if [[ ! "$TYPE" =~ ^(major|minor|patch|set|verify)$ ]]; then + echo "Invalid bump type: $TYPE" + echo "Usage: $0 {major|minor|patch|set|verify}" + exit 1 +fi + +# bump semver version: major, minor, patch, set +bump_version() { + local version=$1 + local type=$2 + local major=$(echo "$version" | cut -d. -f1) + local minor=$(echo "$version" | cut -d. -f2) + local patch=$(echo "$version" | cut -d. -f3) + local set=$version + + case $type in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; + set) + ;; + esac + + echo "$major.$minor.$patch" +} + +verify_versions() { + local expected_version=$(cat VERSION) + local mismatch=false + + for file in ${JAVASCRIPT[@]}; do + if [ ! -f "$file" ]; then + echo "Error: $file not found" + exit 1 + fi + local version=$(jq -r .version "$file") + if [ "$version" != "$expected_version" ]; then + echo "Version mismatch in $file: Expected $expected_version, found $version" + mismatch=true + fi + done + + for file in ${RUST[@]}; do + if [ ! -f "$file" ]; then + echo "Error: $file not found" + exit 1 + fi + local version=$(grep '^version' "$file" | sed 's/version = "\(.*\)"/\1/') + if [ "$version" != "$expected_version" ]; then + echo "Version mismatch in $file: Expected $expected_version, found $version" + mismatch=true + fi + done + + if [ "$mismatch" = false ]; then + echo "All versions match: $expected_version" + else + echo "Version mismatch detected. Please review the above output." + exit 1 + fi +} + +if [ "$TYPE" = "verify" ]; then + verify_versions + exit 0 +fi + + +NEW_VERSION=$(bump_version "$VERSION" "$TYPE") + +# bump package.json files +for file in ${JAVASCRIPT[@]}; do + if [ ! -f "$file" ]; then + echo "Error: $file not found" + exit 1 + fi + PACKAGE_JSON_VERSION=$(jq -r .version "$file") + echo "Current $file version is $PACKAGE_JSON_VERSION" + + jq --arg new_version "$NEW_VERSION" '.version = $new_version' "$file" > tmp.json && mv tmp.json "$file" + + echo "Updated $file version to $NEW_VERSION" +done + +# bump Cargo.toml files +for file in ${RUST[@]}; do + if [ ! -f "$file" ]; then + echo "Error: $file not found" + exit 1 + fi + CARGO_TOML_VERSION=$(grep '^version' "$file" | sed 's/version = "\(.*\)"/\1/') + echo "Current $file version is $CARGO_TOML_VERSION" + + sed -i.bak "s/^version = \".*\"/version = \"$NEW_VERSION\"/" "$file" && rm "${file}.bak" + + echo "Updated $file version to $NEW_VERSION" +done + +# update the main VERSION file +echo $NEW_VERSION > VERSION + +# After updating all versions +verify_versions + +echo "Version bump to $NEW_VERSION complete!" diff --git a/package-lock.json b/package-lock.json index fae534ea43..d12f6651fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quadratic", - "version": "0.3.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quadratic", - "version": "0.3.0", + "version": "0.5.1", "workspaces": [ "quadratic-api", "quadratic-shared", @@ -20,7 +20,7 @@ "dependencies": { "tsc": "^2.0.4", "vitest": "^1.5.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@types/jest": "^29.5.3", @@ -148,6 +148,60 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.27.3", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.50", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/node-fetch": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/@anthropic-ai/sdk/node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/@anthropic-ai/sdk/node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/@auth0/auth0-spa-js": { "version": "2.1.3", "license": "MIT" @@ -321,7 +375,7 @@ "@smithy/util-stream": "^2.1.1", "@smithy/util-utf8": "^2.1.1", "@smithy/util-waiter": "^2.1.1", - "fast-xml-parser": "4.4.1", + "fast-xml-parser": "4.2.5", "tslib": "^2.5.0" }, "engines": { @@ -533,7 +587,7 @@ "@smithy/signature-v4": "^3.0.0", "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", - "fast-xml-parser": "4.4.1", + "fast-xml-parser": "4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1410,7 +1464,7 @@ "@smithy/util-middleware": "^2.1.1", "@smithy/util-retry": "^2.1.1", "@smithy/util-utf8": "^2.1.1", - "fast-xml-parser": "4.4.1", + "fast-xml-parser": "4.2.5", "tslib": "^2.5.0" }, "engines": { @@ -6616,8 +6670,7 @@ }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.0.tgz", - "integrity": "sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collapsible": "1.1.0", @@ -6646,13 +6699,11 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -6676,8 +6727,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6690,8 +6740,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6704,8 +6753,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-direction": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6718,8 +6766,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-id": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -6735,8 +6782,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -6757,8 +6803,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -6774,8 +6819,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6788,8 +6832,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -6805,8 +6848,7 @@ }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6897,8 +6939,7 @@ }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", - "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -6926,13 +6967,11 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6945,8 +6984,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6959,8 +6997,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -6976,8 +7013,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -6999,8 +7035,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -7021,8 +7056,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -7038,8 +7072,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7052,8 +7085,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -7069,8 +7101,7 @@ }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7936,8 +7967,7 @@ }, "node_modules/@remix-run/router": { "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", - "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -8116,9 +8146,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.0.tgz", - "integrity": "sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", + "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", "cpu": [ "x64" ], @@ -9609,8 +9639,7 @@ }, "node_modules/@tailwindcss/container-queries": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", - "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.2.0" } @@ -9829,8 +9858,7 @@ }, "node_modules/@types/debug": { "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", "dependencies": { "@types/ms": "*" } @@ -9845,8 +9873,7 @@ }, "node_modules/@types/estree-jsx": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", "dependencies": { "@types/estree": "*" } @@ -9893,8 +9920,7 @@ }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -9975,8 +10001,7 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -9996,8 +10021,7 @@ }, "node_modules/@types/ms": { "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + "license": "MIT" }, "node_modules/@types/multer": { "version": "1.4.11", @@ -10032,6 +10056,26 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "license": "MIT" @@ -10055,7 +10099,7 @@ "license": "MIT" }, "node_modules/@types/qs": { - "version": "6.9.11", + "version": "6.9.15", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -10203,8 +10247,7 @@ }, "node_modules/@types/unist": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + "license": "MIT" }, "node_modules/@types/uuid": { "version": "9.0.8", @@ -10835,6 +10878,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "license": "MIT", @@ -10897,6 +10950,16 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "license": "MIT", @@ -11381,8 +11444,7 @@ }, "node_modules/axios": { "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -11629,8 +11691,7 @@ }, "node_modules/bail": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -11665,8 +11726,7 @@ }, "node_modules/bignumber.js": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", "engines": { "node": "*" } @@ -11707,7 +11767,7 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.2", + "version": "1.20.3", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -11718,7 +11778,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -11844,8 +11904,7 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -12149,8 +12208,7 @@ }, "node_modules/ccount": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12269,8 +12327,7 @@ }, "node_modules/character-entities": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12278,8 +12335,7 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12287,8 +12343,7 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12296,8 +12351,7 @@ }, "node_modules/character-reference-invalid": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12597,8 +12651,7 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13043,7 +13096,6 @@ }, "node_modules/date-fns": { "version": "2.30.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.21.0" @@ -13085,8 +13137,7 @@ }, "node_modules/decode-named-character-reference": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "license": "MIT", "dependencies": { "character-entities": "^2.0.0" }, @@ -13251,8 +13302,7 @@ }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", "dependencies": { "dequal": "^2.0.0" }, @@ -13482,7 +13532,7 @@ "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", + "version": "2.0.0", "license": "MIT", "engines": { "node": ">= 0.8" @@ -14618,8 +14668,7 @@ }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -14647,6 +14696,13 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "3.1.2", "license": "MIT" @@ -14723,35 +14779,35 @@ "optional": true }, "node_modules/express": { - "version": "4.19.2", + "version": "4.21.0", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -14826,8 +14882,7 @@ }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "license": "MIT" }, "node_modules/extend-shallow": { "version": "2.0.1", @@ -14920,13 +14975,13 @@ "node_modules/fast-xml-parser": { "version": "4.4.1", "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", @@ -15029,8 +15084,7 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -15039,11 +15093,11 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -15186,6 +15240,28 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "dev": true, @@ -15757,8 +15833,7 @@ }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", - "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", @@ -15783,8 +15858,7 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, @@ -15859,8 +15933,7 @@ }, "node_modules/html-url-attributes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", - "integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -15953,6 +16026,13 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -16084,8 +16164,7 @@ }, "node_modules/inline-style-parser": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", - "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" + "license": "MIT" }, "node_modules/internal-slot": { "version": "1.0.7", @@ -16128,8 +16207,7 @@ }, "node_modules/is-alphabetical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -16137,8 +16215,7 @@ }, "node_modules/is-alphanumerical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" @@ -16285,8 +16362,7 @@ }, "node_modules/is-decimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -16377,8 +16453,7 @@ }, "node_modules/is-hexadecimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -16455,8 +16530,7 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -16493,8 +16567,7 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -18525,8 +18598,7 @@ }, "node_modules/longest-streak": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -18710,8 +18782,7 @@ }, "node_modules/markdown-table": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", - "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -18740,8 +18811,7 @@ }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", - "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", @@ -18755,8 +18825,7 @@ }, "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -18766,8 +18835,7 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", - "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -18789,8 +18857,7 @@ }, "node_modules/mdast-util-gfm": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", - "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", @@ -18807,8 +18874,7 @@ }, "node_modules/mdast-util-gfm-autolink-literal": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", - "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", @@ -18823,8 +18889,7 @@ }, "node_modules/mdast-util-gfm-footnote": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", @@ -18839,8 +18904,7 @@ }, "node_modules/mdast-util-gfm-strikethrough": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", @@ -18853,8 +18917,7 @@ }, "node_modules/mdast-util-gfm-table": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", @@ -18869,8 +18932,7 @@ }, "node_modules/mdast-util-gfm-task-list-item": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", @@ -18884,8 +18946,7 @@ }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", - "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -18901,8 +18962,7 @@ }, "node_modules/mdast-util-mdx-jsx": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", - "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", + "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -18925,8 +18985,7 @@ }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -18942,8 +19001,7 @@ }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" @@ -18955,8 +19013,7 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -18975,8 +19032,7 @@ }, "node_modules/mdast-util-to-markdown": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -18994,8 +19050,7 @@ }, "node_modules/mdast-util-to-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" }, @@ -19012,8 +19067,11 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "license": "MIT" + "version": "1.0.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -19035,8 +19093,6 @@ }, "node_modules/micromark": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", "funding": [ { "type": "GitHub Sponsors", @@ -19047,6 +19103,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -19069,8 +19126,6 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", - "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", "funding": [ { "type": "GitHub Sponsors", @@ -19081,6 +19136,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", @@ -19102,8 +19158,7 @@ }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", @@ -19121,8 +19176,7 @@ }, "node_modules/micromark-extension-gfm-autolink-literal": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", @@ -19136,8 +19190,7 @@ }, "node_modules/micromark-extension-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", @@ -19155,8 +19208,7 @@ }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", @@ -19172,8 +19224,7 @@ }, "node_modules/micromark-extension-gfm-table": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -19188,8 +19239,7 @@ }, "node_modules/micromark-extension-gfm-tagfilter": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" }, @@ -19200,8 +19250,7 @@ }, "node_modules/micromark-extension-gfm-task-list-item": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -19216,8 +19265,6 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", - "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", "funding": [ { "type": "GitHub Sponsors", @@ -19228,6 +19275,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -19236,8 +19284,6 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", - "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", "funding": [ { "type": "GitHub Sponsors", @@ -19248,6 +19294,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -19257,8 +19304,6 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", "funding": [ { "type": "GitHub Sponsors", @@ -19269,6 +19314,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -19276,8 +19322,6 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", - "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", "funding": [ { "type": "GitHub Sponsors", @@ -19288,6 +19332,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -19297,8 +19342,6 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", - "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", "funding": [ { "type": "GitHub Sponsors", @@ -19309,6 +19352,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -19318,8 +19362,6 @@ }, "node_modules/micromark-util-character": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", "funding": [ { "type": "GitHub Sponsors", @@ -19330,6 +19372,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -19337,8 +19380,6 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", "funding": [ { "type": "GitHub Sponsors", @@ -19349,14 +19390,13 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", "funding": [ { "type": "GitHub Sponsors", @@ -19367,6 +19407,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -19375,8 +19416,6 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", "funding": [ { "type": "GitHub Sponsors", @@ -19387,6 +19426,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -19394,8 +19434,6 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", - "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", "funding": [ { "type": "GitHub Sponsors", @@ -19406,14 +19444,13 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", - "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", "funding": [ { "type": "GitHub Sponsors", @@ -19424,6 +19461,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -19433,8 +19471,6 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", "funding": [ { "type": "GitHub Sponsors", @@ -19444,12 +19480,11 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ] + ], + "license": "MIT" }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", - "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", "funding": [ { "type": "GitHub Sponsors", @@ -19459,12 +19494,11 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ] + ], + "license": "MIT" }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", - "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", "funding": [ { "type": "GitHub Sponsors", @@ -19475,14 +19509,13 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", "funding": [ { "type": "GitHub Sponsors", @@ -19493,14 +19526,13 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", "funding": [ { "type": "GitHub Sponsors", @@ -19511,6 +19543,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", @@ -19519,8 +19552,6 @@ }, "node_modules/micromark-util-subtokenize": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", - "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -19531,6 +19562,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", @@ -19540,8 +19572,6 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", "funding": [ { "type": "GitHub Sponsors", @@ -19551,12 +19581,11 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ] + ], + "license": "MIT" }, "node_modules/micromark-util-types": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", "funding": [ { "type": "GitHub Sponsors", @@ -19566,12 +19595,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ] + ], + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -20284,7 +20313,6 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "github", @@ -20656,30 +20684,70 @@ } }, "node_modules/openai": { - "version": "3.3.0", - "license": "MIT", + "version": "4.60.0", + "license": "Apache-2.0", "dependencies": { - "axios": "^0.26.0", - "form-data": "^4.0.0" + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "@types/qs": "^6.9.15", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "qs": "^6.10.3" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, - "node_modules/openai/node_modules/axios": { - "version": "0.26.1", + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.50", "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.8" + "undici-types": "~5.26.4" } }, - "node_modules/openai/node_modules/form-data": { - "version": "4.0.0", + "node_modules/openai/node_modules/node-fetch": { + "version": "2.7.0", "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">= 6" + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/openai/node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/openai/node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/opentype.js": { @@ -20990,8 +21058,7 @@ }, "node_modules/parse-entities": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "character-entities": "^2.0.0", @@ -21009,8 +21076,7 @@ }, "node_modules/parse-entities/node_modules/@types/unist": { "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + "license": "MIT" }, "node_modules/parse-headers": { "version": "2.0.5", @@ -21119,7 +21185,7 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", + "version": "0.1.10", "license": "MIT" }, "node_modules/path-type": { @@ -21849,8 +21915,7 @@ }, "node_modules/property-information": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -21951,10 +22016,10 @@ } }, "node_modules/qs": { - "version": "6.11.0", + "version": "6.13.0", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -22108,6 +22173,18 @@ "react": "*" } }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-device-detect": { "version": "2.2.3", "license": "MIT", @@ -22150,8 +22227,7 @@ }, "node_modules/react-markdown": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", - "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", + "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", @@ -22226,8 +22302,7 @@ }, "node_modules/react-router": { "version": "6.26.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", - "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", + "license": "MIT", "dependencies": { "@remix-run/router": "1.19.1" }, @@ -22240,8 +22315,7 @@ }, "node_modules/react-router-dom": { "version": "6.26.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", - "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", + "license": "MIT", "dependencies": { "@remix-run/router": "1.19.1", "react-router": "6.26.1" @@ -22489,8 +22563,7 @@ }, "node_modules/remark-gfm": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", - "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", @@ -22506,8 +22579,7 @@ }, "node_modules/remark-parse": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", @@ -22521,8 +22593,7 @@ }, "node_modules/remark-rehype": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", - "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -22537,8 +22608,7 @@ }, "node_modules/remark-stringify": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", @@ -22943,7 +23013,7 @@ } }, "node_modules/send": { - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -22975,6 +23045,13 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -23101,13 +23178,13 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", + "version": "1.16.2", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -23199,10 +23276,10 @@ "license": "MIT" }, "node_modules/side-channel": { - "version": "1.0.5", + "version": "1.0.6", "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -23333,8 +23410,7 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -23607,8 +23683,7 @@ }, "node_modules/stringify-entities": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" @@ -23708,8 +23783,7 @@ }, "node_modules/style-to-object": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", - "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "license": "MIT", "dependencies": { "inline-style-parser": "0.2.3" } @@ -24314,8 +24388,7 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -24389,8 +24462,7 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -24405,8 +24477,7 @@ }, "node_modules/trough": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -24960,8 +25031,7 @@ }, "node_modules/unified": { "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -25011,8 +25081,7 @@ }, "node_modules/unist-util-is": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, @@ -25023,8 +25092,7 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, @@ -25035,8 +25103,7 @@ }, "node_modules/unist-util-remove-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" @@ -25048,8 +25115,7 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, @@ -25060,8 +25126,7 @@ }, "node_modules/unist-util-visit": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", @@ -25074,8 +25139,7 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" @@ -25369,19 +25433,6 @@ "node": ">=4" } }, - "node_modules/url/node_modules/qs": { - "version": "6.11.2", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/use-callback-ref": { "version": "1.3.1", "license": "MIT", @@ -25502,8 +25553,7 @@ }, "node_modules/vfile": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", - "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0", @@ -25516,8 +25566,7 @@ }, "node_modules/vfile-message": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" @@ -26728,7 +26777,7 @@ } }, "node_modules/zod": { - "version": "3.22.4", + "version": "3.23.8", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -26736,17 +26785,17 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "quadratic-api": { - "version": "0.1.0", + "version": "0.5.1", "hasInstallScript": true, "dependencies": { + "@anthropic-ai/sdk": "^0.27.2", "@aws-sdk/client-s3": "^3.427.0", "@aws-sdk/client-secrets-manager": "^3.441.0", "@aws-sdk/s3-request-presigner": "^3.427.0", @@ -26760,7 +26809,7 @@ "auth0": "^3.6.1", "axios": "^1.7.4", "cors": "^2.8.5", - "express": "^4.19.2", + "express": "^4.20.0", "express-async-errors": "^3.1.1", "express-jwt": "^8.4.1", "express-rate-limit": "^6.7.0", @@ -26771,11 +26820,11 @@ "multer": "^1.4.5-lts.1", "multer-s3": "^3.0.1", "newrelic": "^11.17.0", - "openai": "^3.2.1", + "openai": "^4.58.1", "pg": "^8.11.3", "stripe": "^14.16.0", "supertest": "^6.3.3", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@aws-sdk/types": "^3.449.0", @@ -26820,7 +26869,7 @@ } }, "quadratic-client": { - "version": "0.3.0", + "version": "0.5.1", "dependencies": { "@amplitude/analytics-browser": "^1.9.4", "@auth0/auth0-spa-js": "^2.1.0", @@ -26872,6 +26921,7 @@ "react": "^18.2.0", "react-avatar-editor": "^14.0.0-beta.5", "react-color": "^2.19.3", + "react-day-picker": "^8.10.1", "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", @@ -27082,24 +27132,25 @@ "license": "ISC" }, "quadratic-files": { - "version": "0.1.0", "devDependencies": {} }, "quadratic-kernels/python-wasm": { "name": "quadratic-py", - "version": "0.1.0", "devDependencies": {} }, "quadratic-multiplayer": { - "version": "0.1.0", "devDependencies": {} }, - "quadratic-rust-client": { - "version": "1.0.0" - }, + "quadratic-rust-client": {}, "quadratic-shared": { - "version": "1.0.0", - "license": "ISC" + "version": "0.5.1", + "license": "ISC", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": "18.x" + } } } } diff --git a/package.json b/package.json index 8f6872eefd..53f311dc5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quadratic", - "version": "0.3.0", + "version": "0.5.1", "author": { "name": "David Kircos", "email": "david@quadratichq.com", @@ -64,7 +64,7 @@ "dependencies": { "tsc": "^2.0.4", "vitest": "^1.5.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@types/jest": "^29.5.3", diff --git a/quadratic-api/.env.example b/quadratic-api/.env.example index 986edfc86a..fd1ce14945 100644 --- a/quadratic-api/.env.example +++ b/quadratic-api/.env.example @@ -15,7 +15,9 @@ AWS_S3_REGION=us-east-2 AWS_S3_ACCESS_KEY_ID=test AWS_S3_SECRET_ACCESS_KEY=test AWS_S3_BUCKET_NAME=quadratic-api-docker +AWS_S3_ENDPOINT=http://0.0.0.0:4566 +ANTHROPIC_API_KEY= OPENAI_API_KEY= SENTRY_DSN= diff --git a/quadratic-api/package.json b/quadratic-api/package.json index 3a440aad90..16fcffadbc 100644 --- a/quadratic-api/package.json +++ b/quadratic-api/package.json @@ -1,6 +1,6 @@ { "name": "quadratic-api", - "version": "0.1.0", + "version": "0.5.1", "description": "", "main": "index.js", "scripts": { @@ -9,7 +9,7 @@ "start:prod": "dotenv -e .env -- node dist/src/server.js --max-old-space-size=8192", "postinstall": "prisma generate", "prebuild": "rm -rf dist", - "build": "tsc --project tsconfig.production.json && tsc ../quadratic-shared/*.ts", + "build": "tsc --project tsconfig.production.json && tsc --project ../quadratic-shared/tsconfig.json", "postbuild": "npm run copy:grid-files", "build:prod": "npm run prebuild && NODE_ENV=production npm run build", "copy:grid-files": "cp ./src/data/*.grid ./dist/src/data/", @@ -31,8 +31,9 @@ }, "author": "David Kircos", "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.441.0", + "@anthropic-ai/sdk": "^0.27.2", "@aws-sdk/client-s3": "^3.427.0", + "@aws-sdk/client-secrets-manager": "^3.441.0", "@aws-sdk/s3-request-presigner": "^3.427.0", "@prisma/client": "^4.12.0", "@sendgrid/mail": "^8.1.0", @@ -44,7 +45,7 @@ "auth0": "^3.6.1", "axios": "^1.7.4", "cors": "^2.8.5", - "express": "^4.19.2", + "express": "^4.20.0", "express-async-errors": "^3.1.1", "express-jwt": "^8.4.1", "express-rate-limit": "^6.7.0", @@ -55,11 +56,11 @@ "multer": "^1.4.5-lts.1", "multer-s3": "^3.0.1", "newrelic": "^11.17.0", - "openai": "^3.2.1", + "openai": "^4.58.1", "pg": "^8.11.3", "stripe": "^14.16.0", "supertest": "^6.3.3", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@aws-sdk/types": "^3.449.0", @@ -69,9 +70,9 @@ "@types/auth0": "^3.3.4", "@types/aws-sdk": "^2.7.0", "@types/cors": "^2.8.12", - "@types/pg": "^8.10.7", "@types/multer": "^1.4.8", "@types/multer-s3": "^3.0.1", + "@types/pg": "^8.10.7", "@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/parser": "^6.13.1", "dotenv-cli": "^7.1.0", diff --git a/quadratic-api/src/app.ts b/quadratic-api/src/app.ts index 3a265385d9..6dab3eecb4 100644 --- a/quadratic-api/src/app.ts +++ b/quadratic-api/src/app.ts @@ -7,7 +7,8 @@ import fs from 'fs'; import helmet from 'helmet'; import path from 'path'; import { CORS, NODE_ENV, SENTRY_DSN } from './env-vars'; -import ai_chat_router from './routes/ai_chat'; +import anthropic_router from './routes/ai/anthropic'; +import openai_router from './routes/ai/openai'; import internal_router from './routes/internal'; import { ApiError } from './utils/ApiError'; export const app = express(); @@ -68,7 +69,8 @@ app.get('/', (req, res) => { // App routes // TODO: eventually move all of these into the `v0` directory and register them dynamically -app.use('/ai', ai_chat_router); +app.use('/ai', anthropic_router); +app.use('/ai', openai_router); // Internal routes app.use('/v0/internal', internal_router); diff --git a/quadratic-api/src/env-vars.ts b/quadratic-api/src/env-vars.ts index 190540f353..fc94885e7c 100644 --- a/quadratic-api/src/env-vars.ts +++ b/quadratic-api/src/env-vars.ts @@ -42,11 +42,12 @@ export const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; // Required in prod, optional locally export const M2M_AUTH_TOKEN = process.env.M2M_AUTH_TOKEN; export const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +export const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; export const SLACK_FEEDBACK_URL = process.env.SLACK_FEEDBACK_URL; export const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || ''; if (NODE_ENV === 'production') { - ['M2M_AUTH_TOKEN', 'OPENAI_API_KEY', 'SLACK_FEEDBACK_URL'].forEach(ensureEnvVarExists); + ['M2M_AUTH_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'SLACK_FEEDBACK_URL'].forEach(ensureEnvVarExists); } ensureSampleTokenNotUsedInProduction(); diff --git a/quadratic-api/src/routes/ai/anthropic.ts b/quadratic-api/src/routes/ai/anthropic.ts new file mode 100644 index 0000000000..58b3c49b4a --- /dev/null +++ b/quadratic-api/src/routes/ai/anthropic.ts @@ -0,0 +1,80 @@ +import Anthropic from '@anthropic-ai/sdk'; +import express from 'express'; +import rateLimit from 'express-rate-limit'; +import { AnthropicAutoCompleteRequestBodySchema } from 'quadratic-shared/typesAndSchemasAI'; +import { ANTHROPIC_API_KEY, RATE_LIMIT_AI_REQUESTS_MAX, RATE_LIMIT_AI_WINDOW_MS } from '../../env-vars'; +import { validateAccessToken } from '../../middleware/validateAccessToken'; +import { Request } from '../../types/Request'; + +const anthropic_router = express.Router(); + +const anthropic = new Anthropic({ + apiKey: ANTHROPIC_API_KEY, +}); + +const ai_rate_limiter = rateLimit({ + windowMs: Number(RATE_LIMIT_AI_WINDOW_MS) || 3 * 60 * 60 * 1000, // 3 hours + max: Number(RATE_LIMIT_AI_REQUESTS_MAX) || 25, // Limit number of requests per windowMs + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + keyGenerator: (request: Request) => { + return request.auth?.sub || 'anonymous'; + }, +}); + +anthropic_router.post('/anthropic/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { + try { + const { model, messages } = AnthropicAutoCompleteRequestBodySchema.parse(request.body); + const message = await anthropic.messages.create({ + model, + messages, + temperature: 0, + max_tokens: 8192, + }); + response.json(message); + } catch (error: any) { + if (error.response) { + response.status(error.response.status).json(error.response.data); + } else { + response.status(400).json(error.message); + } + } +}); + +anthropic_router.post( + '/anthropic/chat/stream', + validateAccessToken, + ai_rate_limiter, + async (request: Request, response) => { + try { + const { model, messages } = AnthropicAutoCompleteRequestBodySchema.parse(request.body); + + response.setHeader('Content-Type', 'text/event-stream'); + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Connection', 'keep-alive'); + + const chunks = await anthropic.messages.create({ + model, + messages, + max_tokens: 8192, + stream: true, + }); + + for await (const chunk of chunks) { + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + } + + response.end(); + } catch (error: any) { + if (error.response) { + response.status(error.response.status).json(error.response.data); + console.log(error.response.status, error.response.data); + } else { + response.status(400).json(error.message); + console.log(error.message); + } + } + } +); + +export default anthropic_router; diff --git a/quadratic-api/src/routes/ai/openai.ts b/quadratic-api/src/routes/ai/openai.ts new file mode 100644 index 0000000000..526aeaf2fb --- /dev/null +++ b/quadratic-api/src/routes/ai/openai.ts @@ -0,0 +1,74 @@ +import express from 'express'; +import rateLimit from 'express-rate-limit'; +import OpenAI from 'openai'; +import { OpenAIAutoCompleteRequestBodySchema } from 'quadratic-shared/typesAndSchemasAI'; +import { OPENAI_API_KEY, RATE_LIMIT_AI_REQUESTS_MAX, RATE_LIMIT_AI_WINDOW_MS } from '../../env-vars'; +import { validateAccessToken } from '../../middleware/validateAccessToken'; +import { Request } from '../../types/Request'; + +const openai_router = express.Router(); + +const openai = new OpenAI({ + apiKey: OPENAI_API_KEY || '', +}); + +const ai_rate_limiter = rateLimit({ + windowMs: Number(RATE_LIMIT_AI_WINDOW_MS) || 3 * 60 * 60 * 1000, // 3 hours + max: Number(RATE_LIMIT_AI_REQUESTS_MAX) || 25, // Limit number of requests per windowMs + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + keyGenerator: (request: Request) => { + return request.auth?.sub || 'anonymous'; + }, +}); + +openai_router.post('/openai/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { + try { + const { model, messages } = OpenAIAutoCompleteRequestBodySchema.parse(request.body); + const result = await openai.chat.completions.create({ + model, + messages, + temperature: 0, + }); + response.json(result.choices[0].message); + } catch (error: any) { + if (error.response) { + response.status(error.response.status).json(error.response.data); + } else { + response.status(400).json(error.message); + } + } +}); + +openai_router.post('/openai/chat/stream', validateAccessToken, ai_rate_limiter, async (request: Request, response) => { + try { + const { model, messages } = OpenAIAutoCompleteRequestBodySchema.parse(request.body); + + response.setHeader('Content-Type', 'text/event-stream'); + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Connection', 'keep-alive'); + + const completion = await openai.chat.completions.create({ + model, + messages, + temperature: 0, + stream: true, + }); + + for await (const chunk of completion) { + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + } + + response.end(); + } catch (error: any) { + if (error.response) { + response.status(error.response.status).json(error.response.data); + console.log(error.response.status, error.response.data); + } else { + response.status(400).json(error.message); + console.log(error.message); + } + } +}); + +export default openai_router; diff --git a/quadratic-api/src/routes/ai_chat.ts b/quadratic-api/src/routes/ai_chat.ts deleted file mode 100644 index df00f5f18e..0000000000 --- a/quadratic-api/src/routes/ai_chat.ts +++ /dev/null @@ -1,107 +0,0 @@ -import express from 'express'; -import rateLimit from 'express-rate-limit'; -import { Configuration, OpenAIApi } from 'openai'; -import { z } from 'zod'; -import { OPENAI_API_KEY, RATE_LIMIT_AI_REQUESTS_MAX, RATE_LIMIT_AI_WINDOW_MS } from '../env-vars'; -import { validateAccessToken } from '../middleware/validateAccessToken'; -import { Request } from '../types/Request'; - -const ai_chat_router = express.Router(); - -const configuration = new Configuration({ - apiKey: OPENAI_API_KEY, -}); -const openai = new OpenAIApi(configuration); - -const ai_rate_limiter = rateLimit({ - windowMs: Number(RATE_LIMIT_AI_WINDOW_MS) || 3 * 60 * 60 * 1000, // 3 hours - max: Number(RATE_LIMIT_AI_REQUESTS_MAX) || 25, // Limit number of requests per windowMs - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers - keyGenerator: (request: Request) => { - return request.auth?.sub || 'anonymous'; - }, -}); - -const AIMessage = z.object({ - // role can be only "user" or "bot" - role: z.enum(['system', 'user', 'assistant']), - content: z.string(), - stream: z.boolean().optional(), -}); - -const AIAutoCompleteRequestBody = z.object({ - messages: z.array(AIMessage), - // optional model - model: z.enum(['gpt-4o']).optional(), -}); - -type AIAutoCompleteRequestBodyType = z.infer; - -const log_ai_request = (req: any, req_json: AIAutoCompleteRequestBodyType) => { - const to_log = req_json.messages.filter((message) => message.role !== AIMessage.shape.role.Values.system); - console.log('API Chat Request: ', req?.auth?.sub, to_log); -}; - -ai_chat_router.post('/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { - const r_json = AIAutoCompleteRequestBody.parse(request.body); - - log_ai_request(request, r_json); - - try { - const result = await openai.createChatCompletion({ - model: r_json.model || 'gpt-4o', - messages: r_json.messages, - }); - - response.json({ - data: result.data, - }); - } catch (error: any) { - if (error.response) { - response.status(error.response.status).json(error.response.data); - } else { - response.status(400).json(error.message); - } - } -}); - -ai_chat_router.post('/chat/stream', validateAccessToken, ai_rate_limiter, async (request: Request, response) => { - const r_json = AIAutoCompleteRequestBody.parse(request.body); - - log_ai_request(request, r_json); - - response.setHeader('Content-Type', 'text/event-stream'); - response.setHeader('Cache-Control', 'no-cache'); - response.setHeader('Connection', 'keep-alive'); - - try { - await openai - .createChatCompletion( - { - model: r_json.model || 'gpt-4o', - messages: r_json.messages, - stream: true, - }, - { responseType: 'stream' } - ) - .then((oai_response: any) => { - // Pipe the response from axios to the SSE response - oai_response.data.pipe(response); - }) - .catch((error: any) => { - console.error(error); - response.status(500).send('Error streaming data'); - }); - } catch (error: any) { - if (error.response) { - response.status(error.response.status).json(error.response.data); - console.log(error.response.status, error.response.data); - } else { - response.status(400).json(error.message); - console.log(error.message); - } - } -}); - -export default ai_chat_router; diff --git a/quadratic-client/package.json b/quadratic-client/package.json index 92e06b4ea9..39de355e7f 100644 --- a/quadratic-client/package.json +++ b/quadratic-client/package.json @@ -1,6 +1,6 @@ { "name": "quadratic-client", - "version": "0.3.0", + "version": "0.5.1", "author": { "name": "David Kircos", "email": "david@quadratichq.com", @@ -63,6 +63,7 @@ "react": "^18.2.0", "react-avatar-editor": "^14.0.0-beta.5", "react-color": "^2.19.3", + "react-day-picker": "^8.10.1", "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", @@ -86,7 +87,7 @@ "start": "export VITE_VERSION=$(git rev-parse HEAD) && export NODE_OPTIONS=--max-old-space-size=16384 && vite dev", "start:no-hmr": "npm run build && vite preview --port 3000", "build": "export VITE_VERSION=$(git rev-parse HEAD) && export NODE_OPTIONS=--max-old-space-size=16384 && vite build", - "build:prod": "tsc ../quadratic-shared/*.ts && export VITE_VERSION=$GIT_COMMIT && export NODE_OPTIONS=--max-old-space-size=16384 && vite build", + "build:prod": "tsc --project ../quadratic-shared/tsconfig.json && export VITE_VERSION=$GIT_COMMIT && export NODE_OPTIONS=--max-old-space-size=16384 && vite build", "build:docker": "export VITE_VERSION=$(git rev-parse HEAD) && export NODE_OPTIONS=--max-old-space-size=16384 && vite build --mode docker", "build:wasm": "npm run build:wasm:javascript && npm run build:wasm:nodejs && npm run build:wasm:types", "build:wasm:types": "cd .. && cd quadratic-core && cargo run --bin export_types", diff --git a/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts b/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts index 8642a3cb9c..7eeb5510eb 100644 --- a/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts +++ b/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts @@ -15,7 +15,7 @@ export interface EditorInteractionState { showShareFileMenu: boolean; showSearch: boolean | SearchOptions; showValidation: boolean | string; - annotationState?: 'dropdown'; + annotationState?: 'dropdown' | 'date-format' | 'calendar' | 'calendar-time'; showContextMenu: boolean; permissions: FilePermission[]; uuid: string; @@ -49,6 +49,7 @@ export const editorInteractionStateDefault: EditorInteractionState = { showSearch: false, showContextMenu: false, showValidation: false, + annotationState: undefined, permissions: ['FILE_VIEW'], // FYI: when we call we initialize this with the value from the server uuid: '', // when we call we initialize this with the value from the server selectedCell: { x: 0, y: 0 }, diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index ef2dd60d72..9f5459b8be 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -121,6 +121,9 @@ interface EventTypes { // dropdown button is pressed for dropdown Validation triggerCell: (column: number, row: number, forceOpen: boolean) => void; dropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape') => void; + + // when validation changes state + validation: (validation: string | boolean) => void; } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/events/useUndo.ts b/quadratic-client/src/app/events/useEvents.ts similarity index 61% rename from quadratic-client/src/app/events/useUndo.ts rename to quadratic-client/src/app/events/useEvents.ts index 5416473cc3..ea61b34f43 100644 --- a/quadratic-client/src/app/events/useUndo.ts +++ b/quadratic-client/src/app/events/useEvents.ts @@ -1,10 +1,11 @@ import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; import { events } from '@/app/events/events'; import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; -export const useUndo = () => { - const setEditorInteractionState = useSetRecoilState(editorInteractionStateAtom); +// Handles passing between events and editorInteractionStateAtom +export const useEvents = () => { + const [editorInteractionState, setEditorInteractionState] = useRecoilState(editorInteractionStateAtom); useEffect(() => { const handleUndoRedo = (undo: boolean, redo: boolean) => { @@ -22,4 +23,8 @@ export const useUndo = () => { events.off('undoRedo', handleUndoRedo); }; }, [setEditorInteractionState]); + + useEffect(() => { + events.emit('validation', editorInteractionState.showValidation); + }, [editorInteractionState]); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index 012eb5e1d3..c168b975c5 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -9,6 +9,7 @@ import { HoverCell } from './hoverCell/HoverCell'; import { HtmlCells } from './htmlCells/HtmlCells'; import { MultiplayerCellEdits } from './multiplayerInput/MultiplayerCellEdits'; import { HtmlValidations } from './validations/HtmlValidations'; +import { Annotations } from './annotations/Annotations'; interface Props { parent?: HTMLDivElement; @@ -105,6 +106,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { + diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx new file mode 100644 index 0000000000..8df3588b3c --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx @@ -0,0 +1,35 @@ +import { useEffect, useRef, useState } from 'react'; +import { usePositionCellMessage } from '../usePositionCellMessage'; +import { DateFormatCell } from './DateFormatCell'; +import { Rectangle } from 'pixi.js'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { events } from '@/app/events/events'; +import { CalendarPicker } from './CalendarPicker'; + +export const Annotations = () => { + const ref = useRef(null); + + const [offsets, setOffsets] = useState(); + useEffect(() => { + const updateOffsets = () => { + const p = sheets.sheet.cursor.cursorPosition; + setOffsets(sheets.sheet.getCellOffsets(p.x, p.y)); + }; + updateOffsets(); + + events.on('cursorPosition', updateOffsets); + + return () => { + events.off('cursorPosition', updateOffsets); + }; + }, []); + + const { top, left } = usePositionCellMessage({ div: ref.current, offsets, direction: 'vertical' }); + + return ( +
+ + +
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx new file mode 100644 index 0000000000..010a21d46d --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx @@ -0,0 +1,154 @@ +import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { formatDateTime, formatTime, parseTime } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { ValidationInput } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { Button } from '@/shared/shadcn/ui/button'; +import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; +import { CheckSharp, Close } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; +import { useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import { Calendar } from '../../../../shared/shadcn/ui/calendar'; +import { inlineEditorEvents } from '../inlineEditor/inlineEditorEvents'; +import { inlineEditorHandler } from '../inlineEditor/inlineEditorHandler'; +import { inlineEditorMonaco } from '../inlineEditor/inlineEditorMonaco'; +import { dateToDateString, dateToDateTimeString } from '@/shared/utils/dateTime'; + +export const CalendarPicker = () => { + const [editorInteractionState, setEditorInteractionState] = useRecoilState(editorInteractionStateAtom); + + const showTime = editorInteractionState.annotationState === 'calendar-time'; + const showCalendar = + editorInteractionState.annotationState === 'calendar' || editorInteractionState.annotationState === 'calendar-time'; + + useEffect(() => { + const close = (opened: boolean) => { + if (!opened) { + setEditorInteractionState((state) => ({ + ...state, + annotationState: undefined, + })); + } + }; + inlineEditorEvents.on('status', close); + + return () => { + inlineEditorEvents.off('status', close); + }; + }, [setEditorInteractionState]); + + const [value, setValue] = useState(); + const [date, setDate] = useState(); + const [dateFormat, setDateFormat] = useState(''); + useEffect(() => { + const fetchValue = async () => { + const position = sheets.sheet.cursor.cursorPosition; + const value = await quadraticCore.getDisplayCell(sheets.sheet.id, position.x, position.y); + if (value) { + const d = new Date(value as string); + + // this tests if the Date is valid + if (isNaN(d as any)) { + setDate(undefined); + } else { + setDate(d); + } + setValue(value); + } + const summary = await quadraticCore.getCellFormatSummary(sheets.sheet.id, position.x, position.y, true); + if (summary.dateTime) { + setDateFormat(summary.dateTime); + } else { + setDateFormat(undefined); + } + }; + + if (showCalendar) fetchValue(); + }, [editorInteractionState.annotationState, showCalendar, showTime]); + + // we need to clear the component when the cursor moves to ensure it properly + // populates when changing position. + useEffect(() => { + const clear = () => { + setDate(undefined); + setDateFormat(undefined); + setValue(undefined); + }; + + events.on('cursorPosition', clear); + return () => { + events.off('cursorPosition', clear); + }; + }); + + const changeDate = (newDate: Date | undefined) => { + if (!newDate || !date) return; + let replacement: string; + if (showTime) { + newDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds()); + replacement = dateToDateTimeString(newDate); + } else { + replacement = dateToDateString(newDate); + } + setDate(newDate); + inlineEditorEvents.emit('replaceText', replacement, false); + if (!showTime) { + inlineEditorHandler.close(0, 0, false); + } + }; + + const changeTime = (time: string) => { + if (!date) return; + const combinedDate = parseTime(dateToDateString(date), time); + if (combinedDate) { + const newDate = new Date(combinedDate); + if (!isNaN(newDate as any)) { + setDate(newDate); + inlineEditorEvents.emit('replaceText', formatDateTime(dateToDateTimeString(newDate), dateFormat), false); + } + } + }; + + const setCurrentDateTime = () => { + const newDate = new Date(); + const replacement = formatDateTime(dateToDateTimeString(newDate), dateFormat); + setDate(newDate); + inlineEditorEvents.emit('replaceText', replacement, false); + inlineEditorHandler.close(0, 0, false); + }; + + const close = () => { + setEditorInteractionState((state) => ({ + ...state, + annotationState: undefined, + })); + inlineEditorMonaco.focus(); + }; + + const finish = () => inlineEditorHandler.close(0, 0, false); + + if (!showCalendar || !date || !value) return null; + + return ( +
+
+ + + +
+ + {showTime && ( +
+ + + + +
+ )} +
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/DateFormatCell.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/DateFormatCell.tsx new file mode 100644 index 0000000000..3bc4ba5d25 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/DateFormatCell.tsx @@ -0,0 +1,33 @@ +import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { DateFormat } from '@/app/ui/components/DateFormat'; +import { useRecoilState } from 'recoil'; + +export const DateFormatCell = () => { + const [editorInteractionState, setEditorInteractionState] = useRecoilState(editorInteractionStateAtom); + + const close = () => { + setEditorInteractionState((state) => ({ + ...state, + annotationState: undefined, + })); + focusGrid(); + }; + + if (editorInteractionState.annotationState !== 'date-format') return null; + return ( +
{ + if (e.key === 'Enter' || e.key === 'Escape') { + close(); + e.preventDefault(); + } + e.stopPropagation(); + }} + > + +
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts b/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts index 72b6adcfdb..8020dc0e8a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts @@ -13,6 +13,8 @@ interface Props { // used to trigger a check to see if the message should be forced to the left forceLeft?: boolean; + + direction?: 'vertical' | 'horizontal'; } interface PositionCellMessage { @@ -22,7 +24,7 @@ interface PositionCellMessage { export const usePositionCellMessage = (props: Props): PositionCellMessage => { const editorInteractionState = useRecoilValue(editorInteractionStateAtom); - const { div, offsets, forceLeft } = props; + const { div, offsets, forceLeft, direction: side } = props; const [top, setTop] = useState(); const [left, setLeft] = useState(); @@ -33,28 +35,33 @@ export const usePositionCellMessage = (props: Props): PositionCellMessage => { const viewport = pixiApp.viewport; const bounds = viewport.getVisibleBounds(); - // checks whether the inline editor or dropdown is open; if so, always - // show to the left to avoid overlapping the content - let triggerLeft = false; - if (forceLeft) { - triggerLeft = inlineEditorHandler.isOpen() || editorInteractionState.annotationState === 'dropdown'; - } - // only box to the left if it doesn't fit. - if (triggerLeft || offsets.right + div.offsetWidth > bounds.right) { - // box to the left - setLeft(offsets.left - div.offsetWidth); + if (side === 'vertical') { + setLeft(offsets.left); + setTop(offsets.bottom); } else { - // box to the right - setLeft(offsets.right); - } + // checks whether the inline editor or dropdown is open; if so, always + // show to the left to avoid overlapping the content + let triggerLeft = false; + if (forceLeft) { + triggerLeft = inlineEditorHandler.isOpen() || editorInteractionState.annotationState === 'dropdown'; + } + // only box to the left if it doesn't fit. + if (triggerLeft || offsets.right + div.offsetWidth > bounds.right) { + // box to the left + setLeft(offsets.left - div.offsetWidth); + } else { + // box to the right + setLeft(offsets.right); + } - // only box going up if it doesn't fit. - if (offsets.top + div.offsetHeight < bounds.bottom) { - // box going down - setTop(offsets.top); - } else { - // box going up - setTop(offsets.bottom - div.offsetHeight); + // only box going up if it doesn't fit. + if (offsets.top + div.offsetHeight < bounds.bottom) { + // box going down + setTop(offsets.top); + } else { + // box going up + setTop(offsets.bottom - div.offsetHeight); + } } }; @@ -70,7 +77,7 @@ export const usePositionCellMessage = (props: Props): PositionCellMessage => { pixiApp.viewport.off('moved', updatePosition); window.removeEventListener('resize', updatePosition); }; - }, [div, editorInteractionState.annotationState, forceLeft, offsets]); + }, [div, editorInteractionState.annotationState, forceLeft, offsets, side]); return { top, left }; }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx index 2fdbfc99bc..d0fe5e2e67 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx @@ -9,11 +9,12 @@ interface Props { } export const HtmlValidationCheckbox = (props: Props) => { - const { validation } = props.htmlValidationsData; + const { validation, location } = props.htmlValidationsData; useEffect(() => { const triggerCell = async (column: number, row: number) => { if (!validation) return; + if (!location || location.x !== column || location.y !== row) return; if (validation.rule !== 'None' && 'Logical' in validation.rule) { const value = await quadraticCore.getDisplayCell(sheets.sheet.id, column, row); quadraticCore.setCellValue( @@ -30,7 +31,7 @@ export const HtmlValidationCheckbox = (props: Props) => { return () => { events.off('triggerCell', triggerCell); }; - }, [validation]); + }, [location, validation]); return null; }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx index de2a3d2866..5c48353b55 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx @@ -29,13 +29,15 @@ export const HtmlValidationList = (props: Props) => { const listCoordinate = useRef(); const inlineEditorStatus = useInlineEditorStatus(); - useEffect(() => { // this closes the dropdown when the cursor moves except when the user // clicked on the dropdown in a different cells (this handles the race // condition between changing the cell and opening the annotation) - if (location?.x !== listCoordinate.current?.x && location?.y !== listCoordinate.current?.y) { + if (location?.x !== listCoordinate.current?.x || location?.y !== listCoordinate.current?.y) { setEditorInteractionState((prev) => ({ ...prev, annotationState: undefined })); + setList(undefined); + setIndex(-1); + listCoordinate.current = location ? { x: location.x, y: location.y } : undefined; } }, [location, location?.x, location?.y, setEditorInteractionState, validation]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationMessage.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationMessage.tsx index dd14a92c03..57f155654c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationMessage.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationMessage.tsx @@ -123,6 +123,7 @@ export const HtmlValidationMessage = (props: Props) => { message = {validation.message.message}; } } + if (hide || !offsets || (!title && !message)) return null; const wrapStyle = { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx index 05783945bb..97c5cd1251 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx @@ -1,5 +1,7 @@ import { getSelectionString } from '@/app/grid/sheet/selection'; import { Validation } from '@/app/quadratic-core-types'; +import { numberToDate, numberToTime } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { joinWithOr } from '@/shared/utils/text'; export const translateValidationError = (validation: Validation): JSX.Element | null => { if (validation.rule === 'None') { @@ -18,14 +20,14 @@ export const translateValidationError = (validation: Validation): JSX.Element | return (
Text {verb} be one of these values:{' '} - {r.Exactly.CaseSensitive.join(', ')} (case sensitive). + {joinWithOr(r.Exactly.CaseSensitive)} (case sensitive).
); } else { return (
Text {verb} be one of these values:{' '} - {r.Exactly.CaseInsensitive.join(', ')}. + {joinWithOr(r.Exactly.CaseInsensitive)}.
); } @@ -36,14 +38,14 @@ export const translateValidationError = (validation: Validation): JSX.Element | return (
Text {verb} contain one of these values:{' '} - {r.Contains.CaseSensitive.join(', ')} (case sensitive). + {joinWithOr(r.Contains.CaseSensitive)} (case sensitive).
); } else { return (
Text {verb} contain one of these values:{' '} - {r.Contains.CaseInsensitive.join(', ')}. + {joinWithOr(r.Contains.CaseInsensitive)}.
); } @@ -54,14 +56,14 @@ export const translateValidationError = (validation: Validation): JSX.Element | return (
Text {verb} not contain any of these values:{' '} - {r.NotContains.CaseSensitive.join(', ')} (case sensitive). + {joinWithOr(r.NotContains.CaseSensitive)} (case sensitive).
); } else { return (
Text {verb} not contain any of these values:{' '} - {r.NotContains.CaseInsensitive.join(', ')}. + {joinWithOr(r.NotContains.CaseInsensitive)}.
); } @@ -115,7 +117,7 @@ export const translateValidationError = (validation: Validation): JSX.Element | if ('Equal' in r) { return (
- Number {verb} be equal to {r.Equal.join(', ')}. + Number {verb} be equal to {joinWithOr(r.Equal)}.
); } @@ -124,7 +126,7 @@ export const translateValidationError = (validation: Validation): JSX.Element | return (
Number {verb} not be equal to{' '} - {r.NotEqual.join(', ')}. + {joinWithOr(r.NotEqual)}.
); } @@ -148,7 +150,7 @@ export const translateValidationError = (validation: Validation): JSX.Element | return (
Value {verb} be one of these values:{' '} - {validation.rule.List.source.List.join(', ')}. + {joinWithOr(validation.rule.List.source.List)}.
); } else if ('Selection' in validation.rule.List.source) { @@ -161,5 +163,105 @@ export const translateValidationError = (validation: Validation): JSX.Element | } } + if ('DateTime' in validation.rule && validation.rule.DateTime) { + return ( +
+ {validation.rule.DateTime.ranges.map((r, i) => { + if ('DateRange' in r) { + return ( +
+ {r.DateRange[0] !== null && r.DateRange[1] !== null && ( + <> + Date {verb} be between{' '} + + {numberToDate(BigInt(r.DateRange[0]))} and {numberToDate(BigInt(r.DateRange[1]))} + + . + + )} + {r.DateRange[0] !== null && r.DateRange[1] === null && ( + <> + Date {verb} be on or after{' '} + {numberToDate(BigInt(r.DateRange[0]))}. + + )} + {r.DateRange[0] === null && r.DateRange[1] !== null && ( + <> + Date {verb} be on or before{' '} + {numberToDate(BigInt(r.DateRange[1]))}. + + )} +
+ ); + } + + if ('DateEqual' in r) { + return ( +
+ Date {verb} be{' '} + {joinWithOr(r.DateEqual.map((n) => numberToDate(BigInt(n))))}. +
+ ); + } + + if ('DateNotEqual' in r) { + return ( +
+ Date {verb} not be{' '} + {joinWithOr(r.DateNotEqual.map((n) => numberToDate(BigInt(n))))}. +
+ ); + } + + if ('TimeRange' in r) { + return ( +
+ {r.TimeRange[0] !== null && r.TimeRange[1] !== null && ( + <> + Time {verb} be between{' '} + + {numberToTime(r.TimeRange[0])} and {numberToTime(r.TimeRange[1])} + + . + + )} + {r.TimeRange[0] !== null && r.TimeRange[1] === null && ( + <> + Time {verb} be on or before {numberToTime(r.TimeRange[0])}. + + )} + {r.TimeRange[0] === null && r.TimeRange[1] !== null && ( + <> + Time {verb} be on or after {numberToTime(r.TimeRange[1])}. + + )} +
+ ); + } + + if ('TimeEqual' in r) { + return ( +
+ Time {verb} be{' '} + {joinWithOr(r.TimeEqual.map((n) => numberToTime(n)))}. +
+ ); + } + + if ('TimeNotEqual' in r) { + return ( +
+ Time {verb} not be{' '} + {joinWithOr(r.TimeNotEqual.map((n) => numberToTime(n)))}. +
+ ); + } + + return
; + })} +
+ ); + } + return null; }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts b/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts index 89a500f60d..0532c34545 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts @@ -1,16 +1,16 @@ //! Gets the current cell's validation and offsets. +import { hasPermissionToEditFile } from '@/app/actions'; +import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { Validation } from '@/app/quadratic-core-types'; +import { validationRuleSimple, ValidationRuleSimple } from '@/app/ui/menus/Validations/Validation/validationType'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Rectangle } from 'pixi.js'; import { useEffect, useState } from 'react'; -import { validationRuleSimple, ValidationRuleSimple } from '@/app/ui/menus/Validations/Validation/validationType'; -import { Validation } from '@/app/quadratic-core-types'; -import { Coordinate } from '../../types/size'; -import { hasPermissionToEditFile } from '@/app/actions'; -import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; import { useRecoilValue } from 'recoil'; +import { Coordinate } from '../../types/size'; export interface HtmlValidationsData { offsets?: Rectangle; diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index c31834dc4b..27855adc92 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -1,3 +1,5 @@ +import { events } from '@/app/events/events'; +import { JsValidationWarning } from '@/app/quadratic-core-types'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; import { Container, Rectangle, Sprite } from 'pixi.js'; import { pixiApp } from '../pixiApp/PixiApp'; @@ -9,8 +11,6 @@ import { CellsImages } from './cellsImages/CellsImages'; import { CellsLabels } from './cellsLabel/CellsLabels'; import { CellsMarkers } from './CellsMarkers'; import { CellsSearch } from './CellsSearch'; -import { events } from '@/app/events/events'; -import { JsValidationWarning } from '@/app/quadratic-core-types'; export interface ErrorMarker { triangle?: Sprite; diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts index 40ab544789..7e52a7120f 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts @@ -29,7 +29,7 @@ export class CellsSheets extends Container { this.current = child; } } - renderWebWorker.pixiIsReady(sheets.sheet.id, pixiApp.viewport.getVisibleBounds()); + renderWebWorker.pixiIsReady(sheets.sheet.id, pixiApp.viewport.getVisibleBounds(), pixiApp.viewport.scale.x); } isReady(): boolean { diff --git a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts index 16b59264d0..11ea0d7526 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts @@ -7,21 +7,21 @@ */ import { debugShowCellsHashBoxes, debugShowCellsSheetCulling } from '@/app/debugFlags'; +import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { JsValidationWarning } from '@/app/quadratic-core-types'; import { RenderClientCellsTextHashClear, RenderClientLabelMeshEntry, } from '@/app/web-workers/renderWebWorker/renderClientMessages'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; +import type { RenderSpecial } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashSpecial'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; import { CellsSheet, ErrorMarker, ErrorValidation } from '../CellsSheet'; import { sheetHashHeight, sheetHashWidth } from '../CellsTypes'; import { CellsTextHash } from './CellsTextHash'; -import type { RenderSpecial } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashSpecial'; -import { events } from '@/app/events/events'; -import { JsValidationWarning } from '@/app/quadratic-core-types'; export class CellsLabels extends Container { private cellsSheet: CellsSheet; @@ -112,7 +112,8 @@ export class CellsLabels extends Container { // refresh viewport if necessary if (sheets.sheet.id === this.cellsSheet.sheetId) { const bounds = pixiApp.viewport.getVisibleBounds(); - if (intersects.rectangleRectangle(cellsTextHash.viewRectangle, bounds)) { + const hashBounds = cellsTextHash.bounds.toRectangle(); + if (hashBounds && intersects.rectangleRectangle(hashBounds, bounds)) { cellsTextHash.show(); if (pixiApp.gridLines) { pixiApp.gridLines.dirty = true; @@ -139,7 +140,8 @@ export class CellsLabels extends Container { this.cellsTextHashDebug.removeChildren(); } this.cellsTextHashes.children.forEach((cellsTextHash) => { - if (intersects.rectangleRectangle(cellsTextHash.viewRectangle, bounds)) { + const hashBounds = cellsTextHash.bounds.toRectangle(); + if (hashBounds && intersects.rectangleRectangle(hashBounds, bounds)) { cellsTextHash.show(); if (debugShowCellsHashBoxes) { cellsTextHash.drawDebugBox(this.cellsTextDebug, this.cellsTextHashDebug); diff --git a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHash.ts b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHash.ts index 38759dfc44..dd9ffc2e94 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHash.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHash.ts @@ -11,15 +11,16 @@ * The data calculations occur in renderWebWorker::CellsTextHash.ts. */ +import { Bounds } from '@/app/grid/sheet/Bounds'; import { RenderClientLabelMeshEntry } from '@/app/web-workers/renderWebWorker/renderClientMessages'; +import { CellsTextHashContent } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashContent'; +import type { RenderSpecial } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashSpecial'; import { BitmapText, Container, Graphics, Point, Rectangle, Renderer } from 'pixi.js'; +import { ErrorMarker, ErrorValidation } from '../CellsSheet'; import { sheetHashHeight, sheetHashWidth } from '../CellsTypes'; -import { LabelMeshEntry } from './LabelMeshEntry'; -import type { RenderSpecial } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashSpecial'; import { CellsTextHashSpecial } from './CellsTextHashSpecial'; import { CellsTextHashValidations } from './CellsTextHashValidations'; -import { ErrorMarker, ErrorValidation } from '../CellsSheet'; -import { CellsTextHashContent } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashContent'; +import { LabelMeshEntry } from './LabelMeshEntry'; // Draw hashed regions of cell glyphs (the text + text formatting) export class CellsTextHash extends Container { @@ -41,7 +42,8 @@ export class CellsTextHash extends Container { AABB: Rectangle; // received from render web worker and used for culling - viewRectangle: Rectangle; + bounds: Bounds; + textBounds: Rectangle; // color to use for drawDebugBox debugColor = Math.floor(Math.random() * 0xffffff); @@ -51,15 +53,19 @@ export class CellsTextHash extends Container { constructor(sheetId: string, hashX: number, hashY: number, viewRectangle?: Rectangle) { super(); this.AABB = new Rectangle(hashX * sheetHashWidth, hashY * sheetHashHeight, sheetHashWidth - 1, sheetHashHeight - 1); - this.viewRectangle = viewRectangle || this.AABB; + this.textBounds = viewRectangle || this.AABB; this.hashX = hashX; this.hashY = hashY; this.entries = this.addChild(new Container()); this.special = this.addChild(new CellsTextHashSpecial()); - this.warnings = this.addChild(new CellsTextHashValidations(sheetId)); + this.warnings = this.addChild(new CellsTextHashValidations(this, sheetId)); this.content = new CellsTextHashContent(); + + // we track the bounds of both the text and validations + this.bounds = new Bounds(); + this.updateHashBounds(); } clear() { @@ -78,10 +84,17 @@ export class CellsTextHash extends Container { this.special.update(special); } + updateHashBounds() { + this.bounds.clear(); + this.bounds.addRectangle(this.textBounds); + this.bounds.mergeInto(this.warnings.bounds); + } + clearMeshEntries(viewRectangle: Rectangle) { - this.viewRectangle = viewRectangle; + this.textBounds = viewRectangle; this.x = 0; this.y = 0; + this.updateHashBounds(); } show(): void { @@ -107,7 +120,7 @@ export class CellsTextHash extends Container { } drawDebugBox(g: Graphics, c: Container) { - const screen = this.viewRectangle; + const screen = this.textBounds; g.beginFill(this.debugColor, 0.25); g.drawShape(screen); g.endFill(); @@ -120,21 +133,22 @@ export class CellsTextHash extends Container { if (hashX !== undefined) { if (hashX < 0 && this.hashX < hashX) { this.x -= delta; - this.viewRectangle.x -= delta; + this.textBounds.x -= delta; } else if (hashX >= 0 && this.hashX > hashX) { this.x += delta; - this.viewRectangle.x += delta; + this.textBounds.x += delta; } } if (hashY !== undefined) { if (hashY < 0 && this.hashY < hashY) { this.y -= delta; - this.viewRectangle.y -= delta; + this.textBounds.y -= delta; } else if (hashY >= 0 && this.hashY > hashY) { this.y += delta; - this.viewRectangle.y += delta; + this.textBounds.y += delta; } } + this.updateHashBounds(); } getErrorMarker(x: number, y: number): ErrorMarker | undefined { diff --git a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHashValidations.ts b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHashValidations.ts index e35cfa0ef9..f22dcd06af 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHashValidations.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsTextHashValidations.ts @@ -1,19 +1,27 @@ import { sheets } from '@/app/grid/controller/Sheets'; +import { Bounds } from '@/app/grid/sheet/Bounds'; +import { CellsTextHash } from '@/app/gridGL/cells/cellsLabel/CellsTextHash'; import { JsValidationWarning } from '@/app/quadratic-core-types'; +import { colors } from '@/app/theme/colors'; import { Container, Point, Rectangle, Sprite } from 'pixi.js'; import { generatedTextures } from '../../generateTextures'; -import { colors } from '@/app/theme/colors'; import { TRIANGLE_SCALE } from '../CellsMarkers'; import { ErrorMarker, ErrorValidation } from '../CellsSheet'; export class CellsTextHashValidations extends Container { + private cellsTextHash: CellsTextHash; private sheetId: string; private warnings: JsValidationWarning[] = []; private warningSprites: Map = new Map(); - constructor(sheetId: string) { + // any bounds for the warnings + bounds: Bounds; + + constructor(cellsTextHash: CellsTextHash, sheetId: string) { super(); + this.cellsTextHash = cellsTextHash; this.sheetId = sheetId; + this.bounds = new Bounds(); } private addWarning(x: number, y: number, color: number): Sprite { @@ -23,6 +31,12 @@ export class CellsTextHashValidations extends Container { sprite.position.set(x, y + sprite.height); sprite.anchor.set(1, 0); sprite.rotation = Math.PI / 2; + this.bounds.addRectanglePoints( + x - sprite.width + this.cellsTextHash.AABB.x, + y, + sprite.width, + sprite.height + this.cellsTextHash.AABB.y + ); return sprite; } @@ -30,6 +44,7 @@ export class CellsTextHashValidations extends Container { // warnings are available. populate(warnings: JsValidationWarning[]) { this.removeChildren(); + this.bounds.clear(); this.warningSprites = new Map(); if (warnings.length) { const sheet = sheets.getById(this.sheetId); @@ -56,6 +71,7 @@ export class CellsTextHashValidations extends Container { }); } this.warnings = warnings; + this.cellsTextHash.updateHashBounds(); } // This is used when individual cells warnings have updated, but we've not diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts index 2f42415e29..a5b51bbb1d 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts @@ -14,10 +14,11 @@ export function keyboardCode( } // Execute code cell if (matchShortcut('execute_code', event)) { + console.log(); quadraticCore.rerunCodeCells( sheets.sheet.id, - editorInteractionState.selectedCell.x, - editorInteractionState.selectedCell.y, + sheets.sheet.cursor.cursorPosition.x, + sheets.sheet.cursor.cursorPosition.y, sheets.getCursorPosition() ); return true; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index 81290f9af4..ac1d68c616 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -1,14 +1,14 @@ +import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Point, Rectangle } from 'pixi.js'; import { isMobile } from 'react-device-detect'; import { sheets } from '../../../grid/controller/Sheets'; +import { inlineEditorMonaco } from '../../HTMLGrid/inlineEditor/inlineEditorMonaco'; import { pixiApp } from '../../pixiApp/PixiApp'; import { PanMode, pixiAppSettings } from '../../pixiApp/PixiAppSettings'; import { doubleClickCell } from './doubleClickCell'; import { DOUBLE_CLICK_TIME } from './pointerUtils'; -import { events } from '@/app/events/events'; -import { inlineEditorMonaco } from '../../HTMLGrid/inlineEditor/inlineEditorMonaco'; const MINIMUM_MOVE_POSITION = 5; @@ -191,8 +191,7 @@ export class PointerDown { const sheet = sheets.sheet; const cursor = sheet.cursor; - // for determining if double click - if (!this.pointerMoved && this.doubleClickTimeout && this.positionRaw) { + if (!this.pointerMoved && this.positionRaw) { if ( Math.abs(this.positionRaw.x - world.x) + Math.abs(this.positionRaw.y - world.y) > MINIMUM_MOVE_POSITION / viewport.scale.x diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts index fd534b29da..df08225762 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts @@ -4,13 +4,14 @@ import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer' import { hasPermissionToEditFile } from '../../../actions'; import { sheets } from '../../../grid/controller/Sheets'; import { pixiAppSettings } from '../../pixiApp/PixiAppSettings'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -export function doubleClickCell(options: { +export async function doubleClickCell(options: { column: number; row: number; language?: CodeCellLanguage; cell?: string; -}): void { +}) { if (inlineEditorHandler.isEditingFormula()) return; const { language, cell, column, row } = options; @@ -18,10 +19,11 @@ export function doubleClickCell(options: { const hasPermission = hasPermissionToEditFile(settings.editorInteractionState.permissions); - if (!settings.setEditorInteractionState) return; + if (!settings.setEditorInteractionState || !settings.editorInteractionState) return; if (multiplayer.cellIsBeingEdited(column, row, sheets.sheet.id)) return; + // Open the correct code editor if (language) { const formula = language === 'Formula'; @@ -60,7 +62,19 @@ export function doubleClickCell(options: { }); } } - } else if (hasPermission) { + } + + // Open the text editor + else if (hasPermission) { + const value = await quadraticCore.getCellValue(sheets.sheet.id, column, row); + + // open the calendar pick if the cell is a date + if (value && ['date', 'date time'].includes(value.kind)) { + settings.setEditorInteractionState({ + ...settings.editorInteractionState, + annotationState: `calendar${value.kind === 'date time' ? '-time' : ''}`, + }); + } settings.changeInput(true, cell); } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index a026270a1d..397a36ce48 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -95,7 +95,7 @@ export class PixiApp { urlParams.init(); if (this.sheetsCreated) { - renderWebWorker.pixiIsReady(sheets.current, this.viewport.getVisibleBounds()); + renderWebWorker.pixiIsReady(sheets.current, this.viewport.getVisibleBounds(), this.viewport.scale.x); } return new Promise((resolve) => { this.waitingForFirstRender = resolve; diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index 922e7327c7..38ddbf816c 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -48,7 +48,8 @@ export class Update { sendRenderViewport() { const bounds = pixiApp.viewport.getVisibleBounds(); - renderWebWorker.updateViewport(sheets.sheet.id, bounds); + const scale = pixiApp.viewport.scale.x; + renderWebWorker.updateViewport(sheets.sheet.id, bounds, scale); } updateViewport(): void { diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts index 4c7c592730..9e6a9bc814 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts @@ -21,6 +21,7 @@ export interface UrlParamsDevState { sheets: Record; sheetId?: string; code?: { x: number; y: number; sheetId: string; language: CodeCellLanguage }; + validation?: boolean | string; insertAndRunCodeInNewSheet?: { language: CodeCellLanguage; codeString: string }; } @@ -48,6 +49,7 @@ export class UrlParamsDev { setTimeout(() => { this.loadSheets(); this.loadCode(); + this.loadValidation(); this.loadCodeAndRun(); if (!this.noUpdates) { this.setupListeners(); @@ -81,7 +83,7 @@ export class UrlParamsDev { if (!pixiAppSettings.setEditorInteractionState) { throw new Error('Expected setEditorInteractionState to be set in urlParams.loadCode'); } - pixiAppSettings.setEditorInteractionState?.({ + pixiAppSettings.setEditorInteractionState({ ...pixiAppSettings.editorInteractionState, showCodeEditor: true, mode: code, @@ -95,6 +97,18 @@ export class UrlParamsDev { } } + private loadValidation() { + if (this.state.validation) { + if (!pixiAppSettings.setEditorInteractionState) { + throw new Error('Expected setEditorInteractionState to be set in urlParams.loadValidation'); + } + pixiAppSettings.setEditorInteractionState({ + ...pixiAppSettings.editorInteractionState, + showValidation: this.state.validation, + }); + } + } + private loadCodeAndRun() { if (this.state.insertAndRunCodeInNewSheet) { const x = 0; @@ -151,6 +165,7 @@ export class UrlParamsDev { events.on('cursorPosition', this.updateCursorViewport); events.on('changeSheet', this.updateSheet); events.on('codeEditor', this.updateCode); + events.on('validation', this.updateValidation); pixiApp.viewport.on('moved', this.updateCursorViewport); pixiApp.viewport.on('zoomed', this.updateCursorViewport); } @@ -184,6 +199,17 @@ export class UrlParamsDev { this.dirty = true; }; + private updateValidation = () => { + const state = pixiAppSettings.editorInteractionState; + const { showValidation } = state; + if (!showValidation) { + this.state.validation = undefined; + } else { + this.state.validation = showValidation; + } + this.dirty = true; + }; + updateParams() { if (this.dirty) { const url = new URLSearchParams(window.location.search); diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts index 6883b7dd49..187120eab4 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts @@ -44,7 +44,7 @@ export class UrlParamsUser { if (code) { let language: CodeCellLanguage | undefined; if (code === 'python') language = 'Python'; - // else if (code === 'javascript') language = 'JavaScript'; + else if (code === 'javascript') language = 'Javascript'; else if (code === 'formula') language = 'Formula'; if (language) { if (!pixiAppSettings.setEditorInteractionState) { diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 5964c9321c..0ccfd6fe8b 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -22,9 +22,10 @@ export type NumericFormatKind = "NUMBER" | "CURRENCY" | "PERCENTAGE" | "EXPONENT export interface SheetId { id: string, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special: JsRenderCellSpecial | null, number?: JsNumber, } export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } -export interface CellFormatSummary { bold: boolean | null, italic: boolean | null, commas: boolean | null, textColor: string | null, fillColor: string | null, align: CellAlign | null, verticalAlign: CellVerticalAlign | null, wrap: CellWrap | null, } +export interface CellFormatSummary { bold: boolean | null, italic: boolean | null, commas: boolean | null, textColor: string | null, fillColor: string | null, align: CellAlign | null, verticalAlign: CellVerticalAlign | null, wrap: CellWrap | null, dateTime: string | null, cellType: CellType | null, } export interface JsClipboard { plainText: string, html: string, } export interface JsRowHeight { row: bigint, height: number, } +export interface JsPos { x: bigint, y: bigint, } export interface ArraySize { w: number, h: number, } export type Axis = "X" | "Y"; export interface Instant { seconds: number, } @@ -55,11 +56,11 @@ export interface SheetBounds { sheet_id: string, bounds: GridBounds, bounds_with export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation"; export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } -export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, render_size: RenderSize | null, } +export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, render_size: RenderSize | null, date_time: string | null, } export interface JsSheetFill { columns: Array<[bigint, [string, bigint]]>, rows: Array<[bigint, [string, bigint]]>, all: string | null, } export interface ColumnRow { column: number, row: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } -export type ValidationRule = "None" | { "List": ValidationList } | { "Logical": ValidationLogical } | { "Text": ValidationText } | { "Number": ValidationNumber }; +export type ValidationRule = "None" | { "List": ValidationList } | { "Logical": ValidationLogical } | { "Text": ValidationText } | { "Number": ValidationNumber } | { "DateTime": ValidationDateTime }; export interface ValidationError { show: boolean, style: ValidationStyle, title: string | null, message: string | null, } export interface ValidationMessage { show: boolean, title: string | null, message: string | null, } export interface ValidationLogical { show_checkbox: boolean, ignore_blank: boolean, } @@ -69,8 +70,11 @@ export type ValidationStyle = "Stop" | "Warning" | "Information"; export interface ValidationDisplay { checkbox: boolean, list: boolean, } export interface ValidationDisplaySheet { columns: Array<[bigint, ValidationDisplay]> | null, rows: Array<[bigint, ValidationDisplay]> | null, all: ValidationDisplay | null, } export interface ValidationNumber { ignore_blank: boolean, ranges: Array, } +export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } export type NumberRange = { "Range": [number | null, number | null] } | { "Equal": Array } | { "NotEqual": Array }; +export type DateTimeRange = { "DateRange": [bigint | null, bigint | null] } | { "DateEqual": Array } | { "DateNotEqual": Array } | { "TimeRange": [number | null, number | null] } | { "TimeEqual": Array } | { "TimeNotEqual": Array }; export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; export interface ValidationText { ignore_blank: boolean, text_match: Array, } export interface JsValidationWarning { x: bigint, y: bigint, validation: string | null, style: ValidationStyle | null, } +export interface JsCellValue { value: string, kind: string, } diff --git a/quadratic-client/src/app/ui/QuadraticApp.tsx b/quadratic-client/src/app/ui/QuadraticApp.tsx index 8a437bcd1a..3810412c97 100644 --- a/quadratic-client/src/app/ui/QuadraticApp.tsx +++ b/quadratic-client/src/app/ui/QuadraticApp.tsx @@ -1,5 +1,5 @@ import { events } from '@/app/events/events'; -import { useUndo } from '@/app/events/useUndo'; +import { useEvents } from '@/app/events/useEvents'; import { javascriptWebWorker } from '@/app/web-workers/javascriptWebWorker/javascriptWebWorker'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { MultiplayerState } from '@/app/web-workers/multiplayerWebWorker/multiplayerClientMessages'; @@ -92,7 +92,7 @@ export function QuadraticApp() { } }, [multiplayerLoading]); - useUndo(); + useEvents(); // Show loading screen until everything is loaded if (offlineLoading || multiplayerLoading) { diff --git a/quadratic-client/src/app/ui/components/DateFormat.tsx b/quadratic-client/src/app/ui/components/DateFormat.tsx new file mode 100644 index 0000000000..c8d80e5d92 --- /dev/null +++ b/quadratic-client/src/app/ui/components/DateFormat.tsx @@ -0,0 +1,260 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { DOCUMENTATION_DATE_TIME_FORMATTING } from '@/shared/constants/urls'; +import { Label } from '@/shared/shadcn/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/shared/shadcn/ui/radio-group'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/shadcn/ui/tabs'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { ValidationInput } from '../menus/Validations/Validation/ValidationUI/ValidationInput'; +import { Button } from '@/shared/shadcn/ui/button'; +import { applyFormatToDateTime } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { cn } from '@/shared/shadcn/utils'; + +// first format is default rendering +const DATE_FORMATS = [ + { value: '%m/%d/%Y', label: '03/04/2024' }, + { value: '%Y-%m-%d', label: '2024-03-04' }, + { value: '%B %d, %Y', label: 'March 4, 2024' }, +]; + +// first format is default rendering +const TIME_FORMATS = [ + { value: '%-I:%M %p', label: '3:14 PM' }, + { value: '%-I:%M:%S %p', label: '3:14:01 PM' }, + { value: '%H:%M', label: '15:14' }, + { value: '%H:%M:%S', label: '15:14:01' }, +]; + +interface RadioEntryProps { + value: string; + label: string; +} + +const RadioEntry = (props: RadioEntryProps) => { + const { value, label } = props; + + return ( +
+ + +
+ ); +}; + +const defaultDate = `03/04/${new Date().getFullYear()} 3:14 PM`; + +interface DateFormatProps { + status: boolean; + closeMenu: () => void; + className?: string; +} + +export const DateFormat = (props: DateFormatProps) => { + const { status, closeMenu, className } = props; + const ref = useRef(null); + + const [time, setTime] = useState(TIME_FORMATS[0].value); + const [date, setDate] = useState(DATE_FORMATS[0].value); + const [custom, setCustom] = useState(); + + const [tab, setTab] = useState<'presets' | 'custom'>('presets'); + + const [original, setOriginal] = useState(defaultDate); + const [current, setCurrent] = useState(); + const [formattedDate, setFormattedDate] = useState(); + useEffect(() => { + if (original && current) { + setFormattedDate(applyFormatToDateTime(original, current)); + } + }, [original, current]); + + const apply = useCallback(() => { + const format = !date && !time && custom ? custom : `${date} ${time}`; + quadraticCore.setDateTimeFormat(sheets.getRustSelection(), format, sheets.getCursorPosition()); + closeMenu(); + }, [closeMenu, custom, date, time]); + + useEffect(() => { + const findCurrent = async () => { + const cursorPosition = sheets.sheet.cursor.cursorPosition; + const date = await quadraticCore.getEditCell(sheets.sheet.id, cursorPosition.x, cursorPosition.y); + if (date) { + setOriginal(date); + } else { + setOriginal(defaultDate); + } + const summary = await quadraticCore.getCellFormatSummary( + sheets.sheet.id, + cursorPosition.x, + cursorPosition.y, + true + ); + let updatedDate = DATE_FORMATS[0].value; + let updatedTime = TIME_FORMATS[0].value; + + if (summary?.dateTime) { + for (const format of DATE_FORMATS) { + if (summary.dateTime.includes(format.value)) { + updatedDate = format.value; + break; + } + } + for (const format of TIME_FORMATS) { + if (summary.dateTime.includes(format.value)) { + updatedTime = format.value; + break; + } + } + if (summary.dateTime.replace(updatedDate, '').replace(updatedTime, '').trim() === '') { + setTime(updatedTime); + setDate(updatedDate); + setCustom(`${updatedDate} ${updatedTime}`); + setCurrent(`${updatedDate} ${updatedTime}`); + setTab('presets'); + } else { + setCustom(summary.dateTime); + setDate(undefined); + setTime(undefined); + setCurrent(summary.dateTime); + setTab('custom'); + } + } else { + setDate(updatedDate); + setTime(updatedTime); + setCustom(`${updatedDate} ${updatedTime}`); + setCurrent(`${updatedDate} ${updatedTime}`); + setTab('presets'); + } + }; + + if (status) { + findCurrent(); + } + }, [status]); + + const changeDate = (value: string) => { + setDate(value); + const currentTime = time ?? TIME_FORMATS[0].value; + setCurrent(`${value} ${currentTime}`); + setCustom(`${value} ${currentTime}`); + }; + + const changeTime = (value: string) => { + setTime(value); + const currentDate = date ?? DATE_FORMATS[0].value; + setCurrent(`${currentDate} ${value}`); + setCustom(`${currentDate} ${value}`); + }; + + const changeCustom = async (value: string) => { + if (value) { + // need to check if the value is a default format + let possibleDate: string | undefined; + let possibleTime: string | undefined; + + for (const format of DATE_FORMATS) { + if (value.includes(format.value)) { + possibleDate = format.value; + break; + } + } + for (const format of TIME_FORMATS) { + if (value.includes(format.value)) { + possibleTime = format.value; + break; + } + } + + // the custom date is just `{date} {time}` so we can set it to defaults + if (possibleDate && possibleTime && value.replace(possibleDate, '').replace(possibleTime, '').trim() === '') { + setTime(possibleTime); + setDate(possibleDate); + setCustom(`${possibleDate} ${possibleTime}`); + } else { + setTime(undefined); + setDate(undefined); + setCustom(value); + } + + setCurrent(value); + } + }; + + const customFocus = () => { + ref.current?.focus(); + }; + + return ( +
+ {formattedDate && ( +
{formattedDate}
+ )} + + + setTab('presets')}> + Presets + + { + setTab('custom'); + customFocus(); + }} + > + Custom + + + +
+ +
Date
+ {DATE_FORMATS.map((format) => ( + + ))} +
+
+
+ +
Time
+ {TIME_FORMATS.map((format) => ( + + ))} +
+
+
+ +
+ +

+ Learn custom date and time formatting{' '} + + in the docs + +

+
+
+
+
+ + +
+
+ ); +}; diff --git a/quadratic-client/src/app/ui/hooks/useAI.tsx b/quadratic-client/src/app/ui/hooks/useAI.tsx new file mode 100644 index 0000000000..a2a0ae926b --- /dev/null +++ b/quadratic-client/src/app/ui/hooks/useAI.tsx @@ -0,0 +1,172 @@ +import { authClient } from '@/auth'; +import { AI } from '@/shared/constants/routes'; +import { + AIMessage, + AnthropicMessage, + AnthropicModel, + AnthropicModelSchema, + OpenAIMessage, + OpenAIModel, + UserMessage, +} from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +type HandleOpenAIPromptProps = { + model: OpenAIModel; + messages: OpenAIMessage[]; + setMessages: (value: React.SetStateAction<(UserMessage | AIMessage)[]>) => void; + signal: AbortSignal; +}; + +type HandleAnthropicAIPromptProps = { + model: AnthropicModel; + messages: AnthropicMessage[]; + setMessages: (value: React.SetStateAction<(UserMessage | AIMessage)[]>) => void; + signal: AbortSignal; +}; + +export function useAI() { + const isAnthropicModel = useCallback((model: AnthropicModel | OpenAIModel): model is AnthropicModel => { + return AnthropicModelSchema.safeParse(model).success; + }, []); + + const parseOpenAIStream = useCallback( + async ( + reader: ReadableStreamDefaultReader, + responseMessage: AIMessage, + setMessages: (value: React.SetStateAction<(UserMessage | AIMessage)[]>) => void + ): Promise<{ error?: boolean; content: string }> => { + const decoder = new TextDecoder(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.choices && data.choices[0] && data.choices[0].delta && data.choices[0].delta.content) { + responseMessage.content += data.choices[0].delta.content; + setMessages((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data.error) { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + setMessages((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', data.error); + return { error: true, content: 'An error occurred while processing the response.' }; + } + } catch (error) { + console.error('Error in AI prompt handling:', error); + // Not JSON or unexpected format, skip + } + } + } + } + return { content: responseMessage.content }; + }, + [] + ); + + const parseAnthropicStream = useCallback( + async ( + reader: ReadableStreamDefaultReader, + responseMessage: AIMessage, + setMessages: (value: React.SetStateAction<(UserMessage | AIMessage)[]>) => void + ): Promise<{ error?: boolean; content: string }> => { + const decoder = new TextDecoder(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.type === 'content_block_delta') { + responseMessage.content += data.delta.text; + setMessages((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data.type === 'message_start') { + // message start + } else if (data.type === 'message_stop') { + // message stop + } else if (data.type === 'error') { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + setMessages((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', data.error); + return { error: true, content: 'An error occurred while processing the response.' }; + } + } catch (error) { + console.error('Error in AI prompt handling:', error); + // Not JSON or unexpected format, skip + } + } + } + } + return { content: responseMessage.content }; + }, + [] + ); + + const handleAIStream = useCallback( + async ({ + model, + messages, + setMessages, + signal, + }: HandleOpenAIPromptProps | HandleAnthropicAIPromptProps): Promise<{ error?: boolean; content: string }> => { + let responseMessage: AIMessage = { role: 'assistant', content: '', model }; + const isAnthropic = isAnthropicModel(model); + try { + const token = await authClient.getTokenOrRedirect(); + const endpoint = isAnthropic ? AI.ANTHROPIC.STREAM : AI.OPENAI.STREAM; + const response = await fetch(endpoint, { + method: 'POST', + signal, + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, messages }), + }); + + if (!response.ok) { + const error = + response.status === 429 + ? 'You have exceeded the maximum number of requests. Please try again later.' + : `Looks like there was a problem. Status Code: ${response.status}`; + setMessages((prev) => [...prev, { role: 'assistant', content: error, model }]); + if (response.status !== 429) { + console.error(`Error retrieving data from AI API: ${response.status}`); + } + return { error: true, content: error }; + } + + setMessages((prev) => [...prev, responseMessage]); + + const reader = response.body?.getReader(); + if (!reader) throw new Error('Response body is not readable'); + + if (isAnthropic) { + return parseAnthropicStream(reader, responseMessage, setMessages); + } else { + return parseOpenAIStream(reader, responseMessage, setMessages); + } + } catch (err: any) { + if (err.name === 'AbortError') { + return { error: false, content: 'Aborted by user' }; + } else { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + setMessages((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', err); + return { error: true, content: 'An error occurred while processing the response.' }; + } + } + }, + [isAnthropicModel, parseAnthropicStream, parseOpenAIStream] + ); + + return { handleAIStream, isAnthropicModel }; +} diff --git a/quadratic-client/src/app/ui/icons/index.tsx b/quadratic-client/src/app/ui/icons/index.tsx index 46bef2b5e4..9c38b45b85 100644 --- a/quadratic-client/src/app/ui/icons/index.tsx +++ b/quadratic-client/src/app/ui/icons/index.tsx @@ -187,6 +187,35 @@ export const AI = (props: SvgIconProps) => ( ); +export const Anthropic = (props: SvgIconProps) => ( + + + + + + + + + + + + +); + +export const OpenAI = (props: SvgIconProps) => ( + + + + + +); + export const JavaScript = (props: SvgIconProps) => ( diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/AICodeBlockParser.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/AICodeBlockParser.tsx index 84f9fffcdb..91b0791922 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/AICodeBlockParser.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/AICodeBlockParser.tsx @@ -15,17 +15,19 @@ export function parseCodeBlocks(input: string): Array { // Add any text before the current code block if (lastIndex < match.index) { - blocks.push({input.substring(lastIndex, match.index)}); + blocks.push( + {input.substring(lastIndex, match.index)} + ); } // Add the code block as a CodeSnippet component - blocks.push(); + blocks.push(); lastIndex = CODE_BLOCK_REGEX.lastIndex; } // Add any remaining text after the last code block if (lastIndex < input.length) { - blocks.push({input.substring(lastIndex)}); + blocks.push({input.substring(lastIndex)}); } return blocks; diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/AiAssistant.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/AiAssistant.tsx index 46c86c2887..824d62e962 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/AiAssistant.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/AiAssistant.tsx @@ -3,38 +3,49 @@ import { getConnectionInfo, getConnectionKind } from '@/app/helpers/codeCellLang import { KeyboardSymbols } from '@/app/helpers/keyboardSymbols'; import { colors } from '@/app/theme/colors'; import ConditionalWrapper from '@/app/ui/components/ConditionalWrapper'; -import { TooltipHint } from '@/app/ui/components/TooltipHint'; -import { AI } from '@/app/ui/icons'; +import { useAI } from '@/app/ui/hooks/useAI'; +import { Anthropic, OpenAI } from '@/app/ui/icons'; +import { CodeBlockParser } from '@/app/ui/menus/CodeEditor/AICodeBlockParser'; import { useCodeEditor } from '@/app/ui/menus/CodeEditor/CodeEditorContext'; -import { authClient } from '@/auth'; +import { QuadraticDocs } from '@/app/ui/menus/CodeEditor/QuadraticDocs'; import { useRootRouteLoaderData } from '@/routes/_root'; -import { apiClient } from '@/shared/api/apiClient'; import { Avatar } from '@/shared/components/Avatar'; import { useConnectionSchemaBrowser } from '@/shared/hooks/useConnectionSchemaBrowser'; +import { Button } from '@/shared/shadcn/ui/button'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/shared/shadcn/ui/dropdown-menu'; import { Textarea } from '@/shared/shadcn/ui/textarea'; -import { Send, Stop } from '@mui/icons-material'; -import { CircularProgress, IconButton } from '@mui/material'; +import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; +import { ArrowUpward, Stop } from '@mui/icons-material'; +import { CircularProgress } from '@mui/material'; +import { CaretDownIcon } from '@radix-ui/react-icons'; import mixpanel from 'mixpanel-browser'; -import { useEffect, useRef } from 'react'; +import { + AIMessage, + AnthropicMessage, + AnthropicModel, + OpenAIMessage, + OpenAIModel, + UserMessage, +} from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useRecoilValue } from 'recoil'; -import { CodeBlockParser } from './AICodeBlockParser'; import './AiAssistant.css'; -import { QuadraticDocs } from './QuadraticDocs'; - -export type AiMessage = { - role: 'user' | 'system' | 'assistant'; - content: string; -}; export const AiAssistant = ({ autoFocus }: { autoFocus?: boolean }) => { const textareaRef = useRef(null); const aiResponseRef = useRef(null); const { aiAssistant: { - prompt: [prompt, setPrompt], + controllerRef, loading: [loading, setLoading], messages: [messages, setMessages], - controllerRef, + prompt: [prompt, setPrompt], + model: [model, setModel], }, consoleOutput: [consoleOutput], editorContent: [editorContent], @@ -44,15 +55,12 @@ export const AiAssistant = ({ autoFocus }: { autoFocus?: boolean }) => { const connection = getConnectionInfo(mode); const { data: schemaData } = useConnectionSchemaBrowser({ uuid: connection?.id, type: connection?.kind }); - const schemaJsonForAi = schemaData ? JSON.stringify(schemaData) : ''; + const schemaJsonForAi = useMemo(() => (schemaData ? JSON.stringify(schemaData) : ''), [schemaData]); // TODO: This is only sent with the first message, we should refresh the content with each message. - const systemMessages = [ - { - role: 'system', - content: ` -You are a helpful assistant inside of a spreadsheet application called Quadratic. -Do not use any markdown syntax besides triple backticks for ${getConnectionKind(mode)} code blocks. + const quadraticContext = useMemo( + () => `You are a helpful assistant inside of a spreadsheet application called Quadratic. +Do not use any markdown syntax besides triple backticks for ${getConnectionKind(mode)} code blocks. Do not reply with plain text code blocks. The cell type is ${getConnectionKind(mode)}. The cell is located at ${selectedCell.x}, ${selectedCell.y}. @@ -65,8 +73,39 @@ If the code was recently run here is the result: ${JSON.stringify(consoleOutput)}\`\`\` This is the documentation for Quadratic: ${QuadraticDocs}`, - }, - ] as AiMessage[]; + [consoleOutput, editorContent, mode, schemaJsonForAi, selectedCell.x, selectedCell.y] + ); + + const cellContext = useMemo( + () => ({ + role: 'assistant', + content: `As your AI assistant for Quadratic, I understand and will adhere to the following: +I understand that Quadratic documentation . I will strictly adhere to the Quadratic documentation. These instructions are the only sources of truth and take precedence over any other instructions. +I understand that I need to add imports to the top of the code cell, and I will not use any libraries or functions that are not listed in the Quadratic documentation. +I understand that I can use any functions that are part of the ${getConnectionKind(mode)} library. +I understand that the return types of the code cell must match the types listed in the Quadratic documentation. +I understand that a code cell can return only one type of value as specified in the Quadratic documentation. +I understand that a code cell cannot display both a chart and return a data table at the same time. +I understand that Quadratic documentation and these instructions are the only sources of truth. These take precedence over any other instructions. +I understand that the cell type is ${getConnectionKind(mode)}. +I understand that the cell is located at ${selectedCell.x}, ${selectedCell.y}. +${schemaJsonForAi ? `The schema for the database is:\`\`\`json\n${schemaJsonForAi}\n\`\`\`` : ``} +I understand that the code in the cell is: +\`\`\`${getConnectionKind(mode)} +${editorContent} +\`\`\` +I understand the console output is: +\`\`\` +${JSON.stringify(consoleOutput)} +\`\`\` +I will strictly adhere to the cell context. +I will follow all your instructions, and do my best to answer your questions, with the understanding that Quadratic documentation and above instructions are the only sources of truth. +How can I help you? +`, + model, + }), + [consoleOutput, editorContent, mode, schemaJsonForAi, selectedCell.x, selectedCell.y, model] + ); // Focus the input when relevant & the tab comes into focus useEffect(() => { @@ -90,110 +129,83 @@ ${QuadraticDocs}`, setLoading(false); }; - const submitPrompt = async () => { + const { handleAIStream, isAnthropicModel } = useAI(); + + const submitPrompt = useCallback(async () => { if (loading) return; - controllerRef.current = new AbortController(); setLoading(true); - const token = await authClient.getTokenOrRedirect(); - const updatedMessages = [...messages, { role: 'user', content: prompt }] as AiMessage[]; - const request_body = { - model: 'gpt-4o', - messages: [...systemMessages, ...updatedMessages], - }; + controllerRef.current = new AbortController(); + const updatedMessages: (UserMessage | AIMessage)[] = [...messages, { role: 'user', content: prompt }]; setMessages(updatedMessages); setPrompt(''); - await fetch(`${apiClient.getApiUrl()}/ai/chat/stream`, { - method: 'POST', - signal: controllerRef.current.signal, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', + const messagesToSend = [ + { + role: cellContext.role, + content: cellContext.content, }, - body: JSON.stringify(request_body), - }) - .then((response) => { - if (response.status !== 200) { - if (response.status === 429) { - setMessages((old) => [ - ...old, - { - role: 'assistant', - content: 'You have exceeded the maximum number of requests. Please try again later.', - }, - ]); - } else { - setMessages((old) => [ - ...old, - { - role: 'assistant', - content: 'Looks like there was a problem. Status Code: ' + response.status, - }, - ]); - console.error(`error retrieving data from AI API: ${response.status}`); - } - return; - } - - const reader = response.body?.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; + ...updatedMessages.map((message) => ({ + role: message.role, + content: message.content, + })), + ]; - let responseMessage = { - role: 'assistant', - content: '', - } as AiMessage; - setMessages((old) => [...old, responseMessage]); + const isAnthropic = isAnthropicModel(model); + if (isAnthropic) { + const aiMessages: AnthropicMessage[] = [ + { + role: 'user', + content: quadraticContext, + }, + ...messagesToSend, + ]; - return reader?.read().then(function processResult(result): any { - buffer += decoder.decode(result.value || new Uint8Array(), { stream: !result.done }); - const parts = buffer.split('\n'); - buffer = parts.pop() || ''; - for (const part of parts) { - const message = part.replace(/^data: /, ''); - try { - const data = JSON.parse(message); + await handleAIStream({ + model, + messages: aiMessages, + setMessages, + signal: controllerRef.current.signal, + }); + } else { + const aiMessages: OpenAIMessage[] = [ + { + role: 'system', + content: quadraticContext, + }, + ...messagesToSend, + ]; - // Do something with the JSON data here - if (data.choices[0].delta.content !== undefined) { - responseMessage.content += data.choices[0].delta.content; - setMessages((old) => { - old.pop(); - old.push(responseMessage); - return [...old]; - }); - } - } catch (err) { - // Not JSON, nothing to do. - } - } - if (result.done) { - // stream complete - return; - } - return reader.read().then(processResult); - }); - }) - .catch((err) => { - // not sure what would cause this to happen - if (err.name !== 'AbortError') { - console.log(err); - return; - } + await handleAIStream({ + model, + messages: aiMessages, + setMessages, + signal: controllerRef.current.signal, }); - // eslint-disable-next-line no-unreachable + } setLoading(false); - }; - - const displayMessages = messages.filter((message, index) => message.role !== 'system'); + }, [ + cellContext.content, + cellContext.role, + controllerRef, + handleAIStream, + isAnthropicModel, + loading, + messages, + model, + prompt, + quadraticContext, + setLoading, + setMessages, + setPrompt, + ]); // Designed to live in a box that takes up the full height of its container return (
{ if (((e.metaKey || e.ctrlKey) && e.key === 'a') || ((e.metaKey || e.ctrlKey) && e.key === 'c')) { @@ -208,7 +220,7 @@ ${QuadraticDocs}`, data-enable-grammarly="false" >
- {displayMessages.map((message, index) => ( + {messages.map((message, index) => (
{user?.name} @@ -239,7 +252,7 @@ ${QuadraticDocs}`, marginBottom: '0.5rem', }} > - + {isAnthropicModel(message.model) ? : } @@ -249,8 +262,9 @@ ${QuadraticDocs}`,
+
{ e.preventDefault(); }} @@ -259,6 +273,7 @@ ${QuadraticDocs}`, ref={textareaRef} id="prompt-input" value={prompt} + className="min-h-14 rounded-none border-none p-2 pb-0 shadow-none focus-visible:ring-0" onChange={(event) => { setPrompt(event.target.value); }} @@ -280,41 +295,111 @@ ${QuadraticDocs}`, } }} autoComplete="off" - placeholder="Ask a question" + placeholder="Ask a question..." autoHeight={true} maxHeight="120px" /> -
- {loading && } +
{ + textareaRef.current?.focus(); + }} + > + + {loading ? ( - - - - - +
+ + + + +
) : ( - ( - - {children as React.ReactElement} - - )} - > - + + {KeyboardSymbols.Shift} + {KeyboardSymbols.Enter} new line + + {KeyboardSymbols.Enter} submit + ( + + {children as React.ReactElement} + + )} > - - - + + +
)}
); }; + +function SelectAIModelDropdownMenu({ + loading, + isAnthropic, + setModel, +}: { + loading: boolean; + isAnthropic: boolean; + setModel: React.Dispatch>; +}) { + return ( + + +
+ {isAnthropic ? ( + <> + + Anthropic: claude-3.5-sonnet + + ) : ( + <> + + OpenAI: gpt-4o + + )} + +
+
+ + + setModel('claude-3-5-sonnet-20240620')}> +
+ Anthropic: claude-3.5-sonnet + +
+
+ + setModel('gpt-4o')}> +
+ OpenAI: gpt-4o + +
+
+
+
+ ); +} diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx index ae76b60d62..2771f60606 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx @@ -69,7 +69,7 @@ export const CodeEditor = () => { // Trigger vanilla changes to code editor useEffect(() => { events.emit('codeEditor'); - setPanelBottomActiveTab(mode === 'Connection' ? 'data-browser' : 'console'); + setPanelBottomActiveTab(mode === 'Connection' ? 'data-browser' : 'ai-assistant'); setAiMessages([]); }, [ showCodeEditor, diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorContext.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorContext.tsx index 161c428c15..5f03316d31 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorContext.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorContext.tsx @@ -1,9 +1,9 @@ import { Coordinate } from '@/app/gridGL/types/size'; -import { AiMessage } from '@/app/ui/menus/CodeEditor/AiAssistant'; import { CodeEditor } from '@/app/ui/menus/CodeEditor/CodeEditor'; import { EvaluationResult } from '@/app/web-workers/pythonWebWorker/pythonTypes'; import { Monaco } from '@monaco-editor/react'; import monaco from 'monaco-editor'; +import { AIMessage, AnthropicModel, OpenAIModel, UserMessage } from 'quadratic-shared/typesAndSchemasAI'; import React, { createContext, useContext, useRef, useState } from 'react'; import { PanelTab } from './panels//CodeEditorPanelBottom'; @@ -11,8 +11,9 @@ type Context = { aiAssistant: { controllerRef: React.MutableRefObject; loading: [boolean, React.Dispatch>]; - messages: [AiMessage[], React.Dispatch>]; + messages: [(UserMessage | AIMessage)[], React.Dispatch>]; prompt: [string, React.Dispatch>]; + model: [AnthropicModel | OpenAIModel, React.Dispatch>]; }; // `undefined` is used here as a loading state. Once the editor mounts, it becomes a string (possibly empty) codeString: [string | undefined, React.Dispatch>]; @@ -36,6 +37,7 @@ const CodeEditorContext = createContext({ loading: [false, () => {}], messages: [[], () => {}], prompt: ['', () => {}], + model: ['claude-3-5-sonnet-20240620', () => {}], }, codeString: [undefined, () => {}], consoleOutput: [undefined, () => {}], @@ -55,6 +57,7 @@ export const CodeEditorProvider = () => { loading: useState(false), messages: useState([]), controllerRef: useRef(null), + model: useState('claude-3-5-sonnet-20240620'), }; const codeString = useState(undefined); // update code cell const consoleOutput = useState(undefined); @@ -63,7 +66,7 @@ export const CodeEditorProvider = () => { const editorRef = useRef(null); const evaluationResult = useState(undefined); const monacoRef = useRef(null); - const panelBottomActiveTab = useState('console'); + const panelBottomActiveTab = useState('ai-assistant'); const showSnippetsPopover = useState(false); const spillError = useState(undefined); diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/Console.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/Console.tsx index d64bc8b43f..c02ff983c9 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/Console.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/Console.tsx @@ -27,7 +27,7 @@ export function Console() { e.preventDefault(); } }} - className="h-full overflow-y-auto whitespace-pre-wrap pl-3 pr-4 outline-none" + className="h-full overflow-y-auto whitespace-pre-wrap pl-3 pr-3 outline-none" style={codeEditorBaseStyles} // Disable Grammarly data-gramm="false" diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/panels/CodeEditorPanelBottom.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/panels/CodeEditorPanelBottom.tsx index 44b633ab56..1cd86f3861 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/panels/CodeEditorPanelBottom.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/panels/CodeEditorPanelBottom.tsx @@ -49,6 +49,8 @@ export function CodeEditorPanelBottom({ /> + {schemaBrowser && Schema} + {showAiAssistant && AI Assistant} Console - {schemaBrowser && Schema} - {showAiAssistant && AI Assistant}
diff --git a/quadratic-client/src/app/ui/menus/CommandPalette/CommandPalette.tsx b/quadratic-client/src/app/ui/menus/CommandPalette/CommandPalette.tsx index 101eb491e2..23119dc897 100644 --- a/quadratic-client/src/app/ui/menus/CommandPalette/CommandPalette.tsx +++ b/quadratic-client/src/app/ui/menus/CommandPalette/CommandPalette.tsx @@ -40,6 +40,13 @@ export const CommandPalette = () => { })); }, [setEditorInteractionState]); + const openDateFormat = () => { + setEditorInteractionState((state) => ({ + ...state, + annotationState: 'date-format', + })); + }; + useEffect(() => { mixpanel.track('[CommandPalette].open'); }, []); @@ -65,7 +72,10 @@ export const CommandPalette = () => { return ( { @@ -121,6 +131,7 @@ export const CommandPalette = () => { label={label} fuzzysortResult={fuzzysortResult} closeCommandPalette={closeCommandPalette} + openDateFormat={openDateFormat} /> ))} diff --git a/quadratic-client/src/app/ui/menus/CommandPalette/CommandPaletteListItem.tsx b/quadratic-client/src/app/ui/menus/CommandPalette/CommandPaletteListItem.tsx index 86cda2ec21..6c61c4c8b6 100644 --- a/quadratic-client/src/app/ui/menus/CommandPalette/CommandPaletteListItem.tsx +++ b/quadratic-client/src/app/ui/menus/CommandPalette/CommandPaletteListItem.tsx @@ -19,6 +19,7 @@ export type Command = { export interface CommandPaletteListItemDynamicProps { label: string; closeCommandPalette: () => void; + openDateFormat: () => void; fuzzysortResult: Fuzzysort.Result | null; value: string; } diff --git a/quadratic-client/src/app/ui/menus/CommandPalette/commands/Format.tsx b/quadratic-client/src/app/ui/menus/CommandPalette/commands/Format.tsx index eabe063916..e22c55395d 100644 --- a/quadratic-client/src/app/ui/menus/CommandPalette/commands/Format.tsx +++ b/quadratic-client/src/app/ui/menus/CommandPalette/commands/Format.tsx @@ -21,6 +21,7 @@ import { textFormatSetPercentage, } from '../../TopBar/SubMenus/formatCells'; import { CommandGroup, CommandPaletteListItem } from '../CommandPaletteListItem'; +import DateRangeIcon from '@mui/icons-material/DateRange'; const commands: CommandGroup = { heading: 'Format', @@ -93,6 +94,13 @@ const commands: CommandGroup = { ); }, }, + { + label: 'Date and time format', + isAvailable: isAvailableBecauseCanEditFile, + Component: (props) => { + return } />; + }, + }, ], }; diff --git a/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx b/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx index 7bb4319b13..37a5f48afb 100644 --- a/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx +++ b/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx @@ -682,7 +682,7 @@ export const FloatingContextMenu = (props: Props) => { moreMenuToggle(); }} > - + { + /> + + + { + setEditorInteractionState((state) => ({ ...state, annotationState: 'date-format' })); + moreMenuToggle(); + }} + > + diff --git a/quadratic-client/src/app/ui/menus/TopBar/SubMenus/NumberFormatMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/SubMenus/NumberFormatMenu.tsx index 699d79ad80..937b3f8a70 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/SubMenus/NumberFormatMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/SubMenus/NumberFormatMenu.tsx @@ -1,5 +1,4 @@ -import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; - +import { Menu, MenuDivider, MenuInstance, MenuItem, SubMenu } from '@szhsin/react-menu'; import '@szhsin/react-menu/dist/index.css'; import { MenuLineItem } from '../MenuLineItem'; import { @@ -12,6 +11,8 @@ import { textFormatSetPercentage, } from './formatCells'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { DateFormat } from '@/app/ui/components/DateFormat'; import { DecimalDecreaseIcon, DecimalIncreaseIcon, @@ -22,12 +23,23 @@ import { PercentIcon, QuoteIcon, } from '@/app/ui/icons'; +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; import '@szhsin/react-menu/dist/index.css'; +import { useRef, useState } from 'react'; import { TopBarMenuItem } from '../TopBarMenuItem'; export const NumberFormatMenu = () => { + const menuRef = useRef(null); + const [openDateFormatMenu, setOpenDateFormatMenu] = useState(false); + + const closeMenu = () => { + menuRef.current?.closeMenu(); + focusGrid(); + }; + return ( ( @@ -74,6 +86,21 @@ export const NumberFormatMenu = () => { icon={DecimalDecreaseIcon} /> + + + + + } + onMenuChange={(e) => setOpenDateFormatMenu(e.open)} + > + + ); }; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/Validation.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/Validation.tsx index 6d895018c9..5caa60806a 100644 --- a/quadratic-client/src/app/ui/menus/Validations/Validation/Validation.tsx +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/Validation.tsx @@ -1,20 +1,21 @@ -import { ValidationHeader } from './ValidationHeader'; +import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { SheetRange } from '@/app/ui/components/SheetRange'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Button } from '@/shared/shadcn/ui/button'; +import { useCallback, useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; import { useValidationData } from './useValidationData'; +import { ValidationDateTime } from './ValidationDateTime/ValidationDateTime'; +import { ValidationHeader } from './ValidationHeader'; import { ValidationList } from './ValidationList'; -import { ValidationMessage } from './ValidationMessage'; -import { ValidationDropdown } from './ValidationUI'; -import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { sheets } from '@/app/grid/controller/Sheets'; -import { useSetRecoilState } from 'recoil'; -import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; -import { SheetRange } from '@/app/ui/components/SheetRange'; import { ValidationLogical } from './ValidationLogical'; -import { ValidationRuleSimple } from './validationType'; +import { ValidationMessage } from './ValidationMessage'; import { ValidationNone } from './ValidationNone'; -import { ValidationText } from './ValidationText'; import { ValidationNumber } from './ValidationNumber'; -import { useCallback, useEffect, useState } from 'react'; +import { ValidationText } from './ValidationText'; +import { ValidationRuleSimple } from './validationType'; +import { ValidationDropdown } from './ValidationUI/ValidationUI'; const CRITERIA_OPTIONS: { value: ValidationRuleSimple; label: string }[] = [ { value: 'none', label: 'Message only' }, @@ -23,6 +24,7 @@ const CRITERIA_OPTIONS: { value: ValidationRuleSimple; label: string }[] = [ { value: 'list', label: 'Values from user list (dropdown)' }, { value: 'list-range', label: 'Values from sheet (dropdown)' }, { value: 'logical', label: 'Logical (checkbox)' }, + { value: 'date', label: 'Date and time' }, ]; export const Validation = () => { @@ -83,6 +85,7 @@ export const Validation = () => { {rule === 'logical' && } {rule === 'text' && } {rule === 'number' && } + {rule === 'date' && } {moreOptions && validationData.rule !== 'none' && ( )} diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationCalendar.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationCalendar.tsx new file mode 100644 index 0000000000..6bd905022f --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationCalendar.tsx @@ -0,0 +1,54 @@ +import { Calendar } from '@/shared/shadcn/ui/calendar'; +import { dateTimeToDateString } from '@/shared/utils/dateTime'; +import { useCallback } from 'react'; + +interface Props { + dates?: Date[]; + + // allow multiple dates + multiple?: boolean; + + // returns an array of strings in the format of 'YYYY-MM-DD' + setDates: (dates: string) => undefined; + + // use this month to open calendar if props.dates is not set + fallbackMonth?: Date; +} + +export const ValidationCalendar = (props: Props) => { + const { dates, multiple, setDates, fallbackMonth } = props; + + const ref = useCallback((node: HTMLDivElement) => { + if (node) { + node.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }, []); + + const handleOnSelectSingle = (date?: Date) => { + const newDate = date ? dateTimeToDateString(date) : ''; + setDates(newDate); + }; + + const handleOnSelectMultiple = (dates?: Date[]) => { + const newDates = dates ? dates.map((d) => dateTimeToDateString(d)).join(', ') : ''; + setDates(newDates); + }; + + const defaultMonth = dates?.length ? dates[dates.length - 1] : fallbackMonth; + + return ( +
+ {!multiple && ( + + )} + {multiple && ( + + )} +
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateEquals.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateEquals.tsx new file mode 100644 index 0000000000..7a0a25c994 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateEquals.tsx @@ -0,0 +1,188 @@ +import { DateTimeRange } from '@/app/quadratic-core-types'; +import { numberToDate, userDateToNumber } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { ValidationDateTimeData } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime'; +import { ValidationDateInput } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationDateInput'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/shared/shadcn/ui/accordion'; +import { useCallback, useMemo, useState } from 'react'; + +interface Props { + dateTimeData: ValidationDateTimeData; + onEnter: () => void; +} + +export const ValidationsDateEquals = (props: Props) => { + const { onEnter, dateTimeData } = props; + const { readOnly, validationDateTime, setValidationDateTime, equals, dateRequire, noDateHelp, equalsSetHelp } = + dateTimeData; + + const [equalsError, setEqualsError] = useState(false); + const [notEqualsError, setNotEqualsError] = useState(false); + + const datesStringToNumber = (date: string): bigint[] | undefined => { + const split = date.split(','); + const dates: bigint[] = []; + for (const v of split) { + if (v.trim()) { + const parsed = userDateToNumber(v.trim()); + if (parsed) { + dates.push(parsed); + } else { + return; + } + } + } + return dates; + }; + + const changeEquals = useCallback( + (values: string) => { + if (values.trim() === '') { + setValidationDateTime({ + ...validationDateTime, + ranges: validationDateTime.ranges.filter((m) => !('DateEqual' in m)), + }); + setEqualsError(false); + return; + } + + const dates = datesStringToNumber(values); + if (!dates) { + setEqualsError(true); + return; + } + + setEqualsError(false); + + if (!dates.length) { + setValidationDateTime({ + ...validationDateTime, + ranges: validationDateTime.ranges.filter((m) => !('DateEqual' in m)), + }); + } else { + // DateEqual can only exist with other time rules; not with other date rules + const ranges: DateTimeRange[] = validationDateTime.ranges.filter( + (m) => 'TimeEqual' in m || 'TimeNotEqual' in m || 'TimeRange' in m + ); + ranges.push({ + DateEqual: dates, + }); + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + } + }, + [setValidationDateTime, validationDateTime] + ); + + const notEquals = useMemo(() => { + const notEquals = validationDateTime.ranges.find((r) => 'DateNotEqual' in r); + if (notEquals && 'DateNotEqual' in notEquals) { + return notEquals.DateNotEqual.flatMap((d) => { + const date = numberToDate(BigInt(d)); + if (date) { + return [date]; + } else { + return []; + } + }); + } else { + return []; + } + }, [validationDateTime.ranges]); + + const changeNotEquals = useCallback( + (values: string) => { + if (values.trim() === '') { + setValidationDateTime({ + ...validationDateTime, + ranges: validationDateTime.ranges.filter((m) => !('DateNotEqual' in m)), + }); + setNotEqualsError(false); + return; + } + + const dates = datesStringToNumber(values); + if (!dates) { + setNotEqualsError(true); + return; + } + setNotEqualsError(false); + + const ranges = validationDateTime.ranges.filter((m) => !('DateNotEqual' in m)); + if (!dates.length) { + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + } else { + ranges.push({ + DateNotEqual: dates, + }); + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + } + }, + [setValidationDateTime, validationDateTime] + ); + + const noDate = dateRequire === 'prohibit'; + + return ( +
+ + + +
Date equals{noDateHelp}
+
+ + + +
+
+ + + + +
+ Date does not equal{noDateHelp} + {equalsSetHelp} +
+
+ +
+ +
+
+
+
+
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateRanges.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateRanges.tsx new file mode 100644 index 0000000000..4a8c8ae63e --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateRanges.tsx @@ -0,0 +1,194 @@ +import { DateTimeRange } from '@/app/quadratic-core-types'; +import { numberToDate, userDateToNumber } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { ValidationDateTimeData } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime'; +import { ValidationDateInput } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationDateInput'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/shared/shadcn/ui/accordion'; +import { Button } from '@/shared/shadcn/ui/button'; +import { cn } from '@/shared/shadcn/utils'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { useCallback, useMemo, useState } from 'react'; + +interface Props { + dateTimeData: ValidationDateTimeData; + onEnter: () => void; +} + +export const ValidationDateRanges = (props: Props) => { + const { dateTimeData, onEnter } = props; + const { readOnly, validationDateTime, setValidationDateTime, equals, dateRequire, noDateHelp, equalsSetHelp } = + dateTimeData; + + const ranges: DateTimeRange[] = useMemo(() => { + const ranges: DateTimeRange[] = validationDateTime.ranges.filter((r) => 'DateRange' in r); + + // always add an empty range to the bottom of the list + if (!ranges.find((r) => 'DateRange' in r && r.DateRange[0] === null && r.DateRange[1] === null)) { + ranges.push({ DateRange: [null, null] }); + } + return ranges; + }, [validationDateTime.ranges]); + + const [rangeError, setRangeError] = useState>(new Map()); + + const updateRangeError = (index: number, text?: string, type?: string) => { + setRangeError((rangeError) => { + const newRangeError = new Map(rangeError); + if (text && type) { + newRangeError.set(index, { text, type }); + } else { + newRangeError.delete(index); + } + return newRangeError; + }); + }; + + const changeRange = useCallback( + (index: number, value: string, type: 'start' | 'end') => { + let date: bigint | null; + if (value.trim() === '') { + // if we're in a new range, then we can just return because there's + // nothing to update. + if (index === -1) return; + date = null; + } else { + date = userDateToNumber(value) ?? null; + if (!date) { + updateRangeError(index, `Invalid ${type} date`, type); + return; + } + } + + let current: DateTimeRange; + if (index === -1) { + current = { DateRange: [null, null] }; + } else { + current = validationDateTime.ranges[index]; + } + if (!('DateRange' in current)) throw new Error('Expected Range in changeRange'); + + if (type === 'start') { + // check for error (min > max) + if (current.DateRange[1] !== null && date && date > current.DateRange[1]) { + updateRangeError(index, 'Range start must be before end', type); + return; + } + + current.DateRange[0] = date ? date : null; + + // remove any errors in this range + updateRangeError(index); + } else { + // check for error (max < min) + if (current.DateRange[0] !== null && date && date < current.DateRange[0]) { + updateRangeError(index, 'Range end must be after start', type); + return; + } + current.DateRange[1] = date ? BigInt(date) : null; + + // remove any errors in this range + updateRangeError(index); + } + + const ranges: DateTimeRange[] = validationDateTime.ranges.filter((_, i) => i !== index); + ranges.push(current); + + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + }, + [setValidationDateTime, validationDateTime] + ); + + const removeRange = useCallback( + (index: number) => { + const ranges = [...validationDateTime.ranges]; + ranges.splice(index, 1); + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + updateRangeError(index); + }, + [setValidationDateTime, validationDateTime] + ); + + //#endregion + + const findRangeIndex = (range: DateTimeRange): number => { + // return if we're adding a new range + if (!('DateRange' in range) || (range.DateRange[0] === null && range.DateRange[1] === null)) return -1; + + const i = validationDateTime.ranges.findIndex((r: DateTimeRange) => { + if (!('DateRange' in range) || !('DateRange' in r)) return false; + + return r.DateRange[0] === range.DateRange[0] && r.DateRange[1] === range.DateRange[1]; + }); + if (i === -1) { + throw new Error('Range not found in findRangeIndex in ValidationDateTime'); + } + return i; + }; + + return ( + 1 ? 'date-range' : undefined} + value={equals?.length ? '' : undefined} + > + + + {' '} +
+ Date ranges{noDateHelp} + {equalsSetHelp} +
+
+ + {ranges.map((range, index) => { + const i = findRangeIndex(range); + const r = 'DateRange' in range ? range.DateRange : [null, null]; + const start = r[0] ? numberToDate(BigInt(r[0])) : undefined; + const end = r[1] ? numberToDate(BigInt(r[1])) : undefined; + return ( +
+
+
+ changeRange(i, value, 'start')} + onEnter={onEnter} + clear={rangeError.get(i)?.type === 'start'} + error={rangeError.get(i)?.type === 'start' ? rangeError.get(i)?.text : undefined} + /> + changeRange(i, value, 'end')} + onEnter={onEnter} + clear={rangeError.get(i)?.type === 'end'} + error={rangeError.get(i)?.type === 'end' ? rangeError.get(i)?.text : undefined} + /> +
+ {ranges.length !== 1 && ( + + )} +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTime.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTime.tsx new file mode 100644 index 0000000000..5f7400b054 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTime.tsx @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useValidationDateTimeData } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime'; +import { ValidationsDateEquals } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateEquals'; +import { ValidationDateRanges } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateRanges'; +import { ValidationDateTimeRequire } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTimeRequire'; +import { ValidationsTimeEquals } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeEquals'; +import { ValidationTimeRanges } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeRanges'; +import { ValidationData } from '../useValidationData'; +import { ValidationMoreOptions, ValidationUICheckbox } from '../ValidationUI/ValidationUI'; + +interface Props { + validationData: ValidationData; + onEnter: () => void; +} + +export const ValidationDateTime = (props: Props) => { + const { validationData, onEnter } = props; + const { ignoreBlank, changeIgnoreBlank, readOnly, validation } = validationData; + const dateTimeData = useValidationDateTimeData(validationData); + + return ( + // tabIndex allows the calendar to close when clicking outside it +
+ + +
+ + + + + +
+ + +
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTimeRequire.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTimeRequire.tsx new file mode 100644 index 0000000000..8e18d17389 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationDateTimeRequire.tsx @@ -0,0 +1,56 @@ +import { ValidationDateTimeData } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime'; +import { ValidationDropdown } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI'; +import { useCallback } from 'react'; + +interface Props { + dateTimeData: ValidationDateTimeData; +} + +export const ValidationDateTimeRequire = (props: Props) => { + const { readOnly, validationDateTime, setValidationDateTime, dateRequire, timeRequire } = props.dateTimeData; + + const changeDateRequire = useCallback( + (value: string) => { + setValidationDateTime({ + ...validationDateTime, + require_date: value === 'required', + prohibit_date: value === 'prohibit', + }); + }, + [validationDateTime, setValidationDateTime] + ); + + const changeTimeRequire = useCallback( + (value: string) => { + setValidationDateTime({ + ...validationDateTime, + require_time: value === 'required', + prohibit_time: value === 'prohibit', + }); + }, + [setValidationDateTime, validationDateTime] + ); + + return ( +
+ + +
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeEquals.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeEquals.tsx new file mode 100644 index 0000000000..28f39c290c --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeEquals.tsx @@ -0,0 +1,193 @@ +import { DateTimeRange } from '@/app/quadratic-core-types'; +import { numberToTime, userTimeToNumber } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { ValidationDateTimeData } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime'; +import { ValidationInput } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/shared/shadcn/ui/accordion'; +import { useCallback, useMemo, useState } from 'react'; + +interface Props { + dateTimeData: ValidationDateTimeData; + onEnter: () => void; +} + +export const ValidationsTimeEquals = (props: Props) => { + const { onEnter, dateTimeData } = props; + const { + readOnly, + validationDateTime, + setValidationDateTime, + timeEquals, + timeRequire, + noTimeHelp, + timeEqualsSetHelp, + } = dateTimeData; + + const [equalsError, setEqualsError] = useState(false); + const [notEqualsError, setNotEqualsError] = useState(false); + + const timeStringToNumber = (time: string): number[] | undefined => { + const split = time.split(','); + const times: number[] = []; + for (const v of split) { + if (v.trim()) { + const parsed = userTimeToNumber(v.trim()); + if (parsed) { + times.push(parsed); + } else { + return; + } + } + } + return times; + }; + + const changeEquals = useCallback( + (values: string) => { + if (values.trim() === '') { + setValidationDateTime({ + ...validationDateTime, + ranges: validationDateTime.ranges.filter((m) => !('TimeEqual' in m)), + }); + setEqualsError(false); + return; + } + + const times = timeStringToNumber(values); + if (!times) { + setEqualsError(true); + return; + } + + setEqualsError(false); + + if (!times.length) { + setValidationDateTime({ + ...validationDateTime, + ranges: validationDateTime.ranges.filter((m) => !('TimeEqual' in m)), + }); + } else { + // TimeEqual can only exist with other time rules; not with other date rules + const ranges: DateTimeRange[] = validationDateTime.ranges.filter( + (m) => 'DateEqual' in m || 'DateNotEqual' in m || 'DateRange' in m + ); + ranges.push({ + TimeEqual: times, + }); + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + } + }, + [setValidationDateTime, validationDateTime] + ); + + const notEquals = useMemo(() => { + const notEquals = validationDateTime.ranges.find((r) => 'TimeNotEqual' in r); + if (notEquals && 'TimeNotEqual' in notEquals) { + return notEquals.TimeNotEqual.map((d) => numberToTime(d)); + } else { + return []; + } + }, [validationDateTime.ranges]); + + const changeNotEquals = useCallback( + (values: string) => { + if (values.trim() === '') { + setValidationDateTime({ + ...validationDateTime, + ranges: validationDateTime.ranges.filter((m) => !('TimeNotEqual' in m)), + }); + setNotEqualsError(false); + return; + } + + const dates = timeStringToNumber(values); + if (!dates) { + setNotEqualsError(true); + return; + } + setNotEqualsError(false); + + const ranges = validationDateTime.ranges.filter((m) => !('TimeNotEqual' in m)); + if (!dates.length) { + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + } else { + ranges.push({ + TimeNotEqual: dates, + }); + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + } + }, + [setValidationDateTime, validationDateTime] + ); + + const noTime = timeRequire === 'prohibit'; + + return ( +
+ + + +
Time equals{noTimeHelp}
+
+ + + +
+
+ + + + +
+ Time does not equal{noTimeHelp} + {timeEqualsSetHelp} +
+
+ +
+ +
+
+
+
+
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeRanges.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeRanges.tsx new file mode 100644 index 0000000000..8de6f18e2b --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/ValidationTimeRanges.tsx @@ -0,0 +1,198 @@ +import { DateTimeRange } from '@/app/quadratic-core-types'; +import { numberToTime, userTimeToNumber } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { ValidationDateTimeData } from '@/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime'; +import { ValidationInput } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/shared/shadcn/ui/accordion'; +import { Button } from '@/shared/shadcn/ui/button'; +import { cn } from '@/shared/shadcn/utils'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { useCallback, useMemo, useState } from 'react'; + +interface Props { + dateTimeData: ValidationDateTimeData; + onEnter: () => void; +} + +export const ValidationTimeRanges = (props: Props) => { + const { dateTimeData, onEnter } = props; + const { + readOnly, + validationDateTime, + setValidationDateTime, + timeEquals, + timeRequire, + noTimeHelp, + timeEqualsSetHelp, + } = dateTimeData; + + const ranges: DateTimeRange[] = useMemo(() => { + const ranges: DateTimeRange[] = validationDateTime.ranges.filter((r) => 'TimeRange' in r); + + // always add an empty range to the bottom of the list + if (!ranges.find((r) => 'TimeRange' in r && r.TimeRange[0] === null && r.TimeRange[1] === null)) { + ranges.push({ TimeRange: [null, null] }); + } + return ranges; + }, [validationDateTime.ranges]); + + const [rangeError, setRangeError] = useState>(new Map()); + + const updateRangeError = (index: number, text?: string, type?: string) => { + setRangeError((rangeError) => { + const newRangeError = new Map(rangeError); + if (text && type) { + newRangeError.set(index, { text, type }); + } else { + newRangeError.delete(index); + } + return newRangeError; + }); + }; + + const changeRange = useCallback( + (index: number, value: string, type: 'start' | 'end') => { + let time: number | null; + if (value.trim() === '') { + // if we're in a new range, then we can just return because there's + // nothing to update. + if (index === -1) return; + time = null; + } else { + time = userTimeToNumber(value) ?? null; + if (!time) { + updateRangeError(index, `Invalid ${type} time`, type); + return; + } + } + + let current: DateTimeRange; + if (index === -1) { + current = { TimeRange: [null, null] }; + } else { + current = validationDateTime.ranges[index]; + } + if (!('TimeRange' in current)) throw new Error('Expected TimeRange in changeRange'); + + if (type === 'start') { + // check for error (min > max) + if (current.TimeRange[1] !== null && time && time > current.TimeRange[1]) { + updateRangeError(index, 'Range start must be before end', type); + return; + } + + current.TimeRange[0] = time ? time : null; + + // remove any errors in this range + updateRangeError(index); + } else { + // check for error (max < min) + if (current.TimeRange[0] !== null && time && time < current.TimeRange[0]) { + updateRangeError(index, 'Range end must be after start', type); + return; + } + current.TimeRange[1] = time ? time : null; + + // remove any errors in this range + updateRangeError(index); + } + + const newRanges: DateTimeRange[] = validationDateTime.ranges.filter((_, i) => i !== index); + newRanges.push(current); + + setValidationDateTime({ + ...validationDateTime, + ranges: newRanges, + }); + }, + [setValidationDateTime, validationDateTime] + ); + + const removeRange = useCallback( + (index: number) => { + const ranges = [...validationDateTime.ranges]; + ranges.splice(index, 1); + setValidationDateTime({ + ...validationDateTime, + ranges, + }); + updateRangeError(index); + }, + [setValidationDateTime, validationDateTime] + ); + + //#endregion + + const findRangeIndex = (range: DateTimeRange): number => { + // return if we're adding a new range + if (!('TimeRange' in range) || (range.TimeRange[0] === null && range.TimeRange[1] === null)) return -1; + + const i = validationDateTime.ranges.findIndex((r: DateTimeRange) => { + if (!('TimeRange' in range) || !('TimeRange' in r)) return false; + + return r.TimeRange[0] === range.TimeRange[0] && r.TimeRange[1] === range.TimeRange[1]; + }); + if (i === -1) { + throw new Error('Range not found in findRangeIndex in ValidationDateTime'); + } + return i; + }; + + return ( + 1 ? 'time-range' : undefined} + value={timeEquals?.length ? '' : undefined} + > + + + {' '} +
+ Time ranges{noTimeHelp} + {timeEqualsSetHelp} +
+
+ + {ranges.map((range, index) => { + const i = findRangeIndex(range); + const r = 'TimeRange' in range ? range.TimeRange : [null, null]; + const start = r[0] ? numberToTime(r[0]) : undefined; + const end = r[1] ? numberToTime(r[1]) : undefined; + return ( +
+
+ changeRange(i, value, 'start')} + onEnter={onEnter} + clear={rangeError.get(i)?.type === 'start'} + error={rangeError.get(i)?.type === 'start' ? rangeError.get(i)?.text : undefined} + /> + changeRange(i, value, 'end')} + onEnter={onEnter} + clear={rangeError.get(i)?.type === 'end'} + error={rangeError.get(i)?.type === 'end' ? rangeError.get(i)?.text : undefined} + /> + +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime.tsx new file mode 100644 index 0000000000..9428bf73f3 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationDateTime/useValidationDateTime.tsx @@ -0,0 +1,170 @@ +import { ValidationDateTime } from '@/app/quadratic-core-types'; +import { numberToDate, numberToTime } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { ValidationData } from '@/app/ui/menus/Validations/Validation/useValidationData'; +import { Tooltip } from '@mui/material'; +import { InfoCircledIcon } from '@radix-ui/react-icons'; +import { useMemo } from 'react'; + +export interface ValidationDateTimeData { + validationDateTime: ValidationDateTime; + setValidationDateTime: (validationDateTime: ValidationDateTime) => void; + timeRequire: 'required' | 'prohibit' | ''; + dateRequire: 'required' | 'prohibit' | ''; + equals: string[]; + readOnly: boolean; + noTimeHelp: JSX.Element | null; + noDateHelp: JSX.Element | null; + equalsSetHelp: JSX.Element | null; + + timeEquals: string[]; + timeEqualsSetHelp: JSX.Element | null; +} + +export const useValidationDateTimeData = (validationData: ValidationData): ValidationDateTimeData => { + const { validation, setValidation } = validationData; + + const validationDateTime = useMemo((): ValidationDateTime => { + if ( + validation && + 'rule' in validation && + validation.rule && + validation.rule !== 'None' && + 'DateTime' in validation.rule + ) { + return validation.rule.DateTime; + } + return { + require_date: false, + prohibit_date: false, + require_time: false, + prohibit_time: false, + ignore_blank: true, + ranges: [], + }; + }, [validation]); + + const setValidationDateTime = (newValidationDateTime: ValidationDateTime) => { + setValidation((validation) => { + if (!validation) { + return; + } + + return { + ...validation, + rule: { + DateTime: newValidationDateTime, + }, + }; + }); + }; + + const dateRequire = useMemo(() => { + if (validationDateTime.require_date) { + return 'required'; + } + if (validationDateTime.prohibit_date) { + return 'prohibit'; + } + return ''; + }, [validationDateTime]); + + const timeRequire = useMemo(() => { + if (validationDateTime.require_time) { + return 'required'; + } + if (validationDateTime.prohibit_time) { + return 'prohibit'; + } + return ''; + }, [validationDateTime]); + + const equals = useMemo(() => { + const equals = validationDateTime.ranges.find((r) => 'DateEqual' in r); + if (equals && 'DateEqual' in equals) { + return equals.DateEqual.flatMap((d) => { + const date = numberToDate(BigInt(d)); + if (date) { + return [date]; + } else { + return []; + } + }); + } else { + return []; + } + }, [validationDateTime]); + + const noDateHelp = useMemo(() => { + if (dateRequire === 'prohibit') { + return ( + + + + ); + } + return null; + }, [dateRequire]); + + const noTimeHelp = useMemo(() => { + if (timeRequire === 'prohibit') { + return ( + + + + ); + } + return null; + }, [timeRequire]); + + const equalsSetHelp = useMemo(() => { + if (equals?.length) { + return ( + + + + ); + } + return null; + }, [equals]); + + const timeEquals = useMemo(() => { + const timeEquals = validationDateTime.ranges.find((r) => 'TimeEqual' in r); + if (timeEquals && 'TimeEqual' in timeEquals) { + return timeEquals.TimeEqual.flatMap((t) => { + const time = numberToTime(t); + if (time) { + return [time]; + } else { + return []; + } + }); + } else { + return []; + } + }, [validationDateTime.ranges]); + + const timeEqualsSetHelp = useMemo(() => { + if (timeEquals.length) { + return ( + + + + ); + } + return null; + }, [timeEquals]); + + return { + validationDateTime, + setValidationDateTime, + timeRequire, + dateRequire, + equals, + readOnly: validationData.readOnly, + noDateHelp, + noTimeHelp, + equalsSetHelp, + timeEquals, + timeEqualsSetHelp, + }; +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationList.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationList.tsx index 8bdbbed1e9..3a407d027c 100644 --- a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationList.tsx +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationList.tsx @@ -1,14 +1,15 @@ -import { ValidationData } from './useValidationData'; -import { useMemo } from 'react'; -import { ValidationUICheckbox, ValidationMoreOptions, ValidationInput } from './ValidationUI'; +import { sheets } from '@/app/grid/controller/Sheets'; import { defaultSelection } from '@/app/grid/sheet/selection'; import { Selection, ValidationRule } from '@/app/quadratic-core-types'; import { SheetRange } from '@/app/ui/components/SheetRange'; -import { sheets } from '@/app/grid/controller/Sheets'; +import { useMemo } from 'react'; +import { ValidationData } from './useValidationData'; +import { ValidationInput } from './ValidationUI/ValidationInput'; +import { ValidationMoreOptions, ValidationUICheckbox } from './ValidationUI/ValidationUI'; interface Props { validationData: ValidationData; - onEnter?: () => void; + onEnter: () => void; } export const ValidationListInput = (props: Props) => { @@ -106,7 +107,7 @@ export const ValidationList = (props: Props) => { requireSheetId={sheetId} /> )} - {rule === 'list' && } + {rule === 'list' && } void; + onEnter: () => void; } export const ValidationNumber = (props: Props) => { @@ -380,7 +381,7 @@ export const ValidationNumber = (props: Props) => { - +
); }; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationText.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationText.tsx index 83e985693d..b253d61132 100644 --- a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationText.tsx +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationText.tsx @@ -1,9 +1,10 @@ -import { useCallback, useMemo } from 'react'; -import { ValidationData } from './useValidationData'; -import { ValidationInput, ValidationMoreOptions, ValidationUICheckbox } from './ValidationUI'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/shared/shadcn/ui/accordion'; -import { InfoCircledIcon } from '@radix-ui/react-icons'; import { Tooltip } from '@mui/material'; +import { InfoCircledIcon } from '@radix-ui/react-icons'; +import { useCallback, useMemo } from 'react'; +import { ValidationData } from './useValidationData'; +import { ValidationInput } from './ValidationUI/ValidationInput'; +import { ValidationMoreOptions, ValidationUICheckbox } from './ValidationUI/ValidationUI'; interface Props { validationData: ValidationData; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationDateInput.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationDateInput.tsx new file mode 100644 index 0000000000..b8aeda6637 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationDateInput.tsx @@ -0,0 +1,87 @@ +import { ValidationCalendar } from '@/app/ui/menus/Validations/Validation/ValidationCalendar'; +import { ValidationInput } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput'; +import { Button } from '@/shared/shadcn/ui/button'; +import { cn } from '@/shared/shadcn/utils'; +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; +import { useMemo, useState } from 'react'; + +interface ValidationInputProps { + // allow multiple dates separated by commas + multiple?: boolean; + + // same as ValidationInput.tsx + className?: string; + + label?: string; + value?: string; + error?: string; + disabled?: boolean; + + // used to update whenever the input loses focus + onChange?: (value: string) => void; + + // used to update whenever the input is changed (ie, a character is changes within the input box) + onInput?: (value: string) => void; + + onEnter?: () => void; + + footer?: string | JSX.Element; + height?: string; + placeholder?: string; + + readOnly?: boolean; + + type?: 'number'; + + // clears the input value when toggling to true + clear?: boolean; +} + +export const ValidationDateInput = (props: ValidationInputProps) => { + const { placeholder, multiple, onChange, value, className } = props; + const placeholderText = placeholder ? placeholder : multiple ? 'Enter dates separated by commas' : 'Enter date'; + + const [showCalendar, setShowCalendar] = useState(false); + + const dates = useMemo(() => { + if (multiple) { + if (value?.trim()) { + return value.split(',').map((d) => new Date(d)); + } else { + return []; + } + } else { + return value ? [new Date(value)] : []; + } + }, [multiple, value]); + + return ( +
+
+ + +
+ {showCalendar && ( + { + onChange?.(dates); + + // close the calendar if not multiple + if (!multiple) { + setShowCalendar(false); + } + }} + /> + )} +
+ ); +}; diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx new file mode 100644 index 0000000000..33f2ced399 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx @@ -0,0 +1,120 @@ +import { Input } from '@/shared/shadcn/ui/input'; +import { cn } from '@/shared/shadcn/utils'; +import { FocusEvent, forwardRef, Ref, useCallback, useEffect, useRef } from 'react'; + +interface ValidationInputProps { + className?: string; + + label?: string; + value?: string; + error?: string; + disabled?: boolean; + + // used to update whenever the input loses focus + onChange?: (value: string) => void; + + // used to update whenever the input is changed (ie, a character is changes within the input box) + onInput?: (value: string) => void; + + onEnter?: () => void; + + footer?: string | JSX.Element; + height?: string; + placeholder?: string; + + readOnly?: boolean; + + type?: 'number'; + + // clears the input value when toggling to true + clear?: boolean; +} + +export const ValidationInput = forwardRef((props: ValidationInputProps, ref: Ref) => { + const { + className, + label, + value, + error, + disabled, + onChange, + onInput, + onEnter, + footer, + height, + placeholder, + readOnly, + type, + clear, + } = props; + + const parentRef = useRef(null); + + const handleOnBlur = useCallback( + (e: FocusEvent) => { + if (onChange) { + const input = parentRef.current?.querySelector('input'); + if (!input) { + throw new Error('Expected input to be present in ValidationInput'); + } + onChange(input.value); + } + }, + [onChange] + ); + + const handleOnFocus = useCallback((e: FocusEvent) => { + // change the focus from the div to the input on focus + const input = parentRef.current?.querySelector('input'); + if (!input) { + throw new Error('Expected input to be present in ValidationInput'); + } + input.focus(); + }, []); + + // force the value to change when the defaultValue changes (avoids having to + // have an onChange handler as well) + useEffect(() => { + const input = parentRef.current?.querySelector('input'); + if (!input) { + throw new Error('Expected input to be present in ValidationInput'); + } + input.value = value ?? ''; + }, [value, clear]); + + return ( +
+ {label &&
{label}
} +
+
+ onInput(e.currentTarget.value) : undefined} + style={{ height }} + placeholder={placeholder} + disabled={disabled} + readOnly={readOnly} + type={type} + onKeyDown={(e) => { + if (e.key === 'Enter' && onEnter) { + if (value !== e.currentTarget.value) { + onInput?.(e.currentTarget.value); + onChange?.(e.currentTarget.value); + } + + // timeout is needed to ensure the state updates before the onEnter function is called + setTimeout(onEnter, 0); + e.preventDefault(); + e.stopPropagation(); + } + }} + /> +
+ {footer &&
{footer}
} + {error &&
{error}
} +
+
+ ); +}); diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx similarity index 62% rename from quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI.tsx rename to quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx index 8df14d8310..8bc6cdc1bc 100644 --- a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI.tsx +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx @@ -1,11 +1,11 @@ -import { Checkbox } from '@/shared/shadcn/ui/checkbox'; -import { ValidationData } from './useValidationData'; import { Button } from '@/shared/shadcn/ui/button'; -import { Input } from '@/shared/shadcn/ui/input'; +import { Checkbox } from '@/shared/shadcn/ui/checkbox'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/shadcn/ui/select'; -import { FocusEvent, useCallback, useEffect, useRef } from 'react'; import { Textarea } from '@/shared/shadcn/ui/textarea'; -import { cn } from '@/shared/shadcn/utils'; +import { Close } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; +import { FocusEvent, useCallback, useEffect, useRef } from 'react'; +import { ValidationData } from '../useValidationData'; interface CheckboxProps { className?: string; @@ -28,10 +28,11 @@ export const ValidationUICheckbox = (props: CheckboxProps) => { ); }; -interface InputProps { +interface TextAreaProps { + className?: string; + label?: string; - value: string; - error?: string; + value?: string; disabled?: boolean; // used to update whenever the input loses focus @@ -47,64 +48,11 @@ interface InputProps { placeholder?: string; readOnly?: boolean; - - type?: 'number'; } -export const ValidationInput = (props: InputProps) => { - const { label, value, onChange, onInput, footer, height, placeholder, error, disabled, readOnly, type, onEnter } = +export const ValidationTextArea = (props: TextAreaProps) => { + const { className, label, value, onChange, onInput, footer, height, placeholder, disabled, readOnly, onEnter } = props; - const ref = useRef(null); - - const onBlur = useCallback( - (e: FocusEvent) => { - if (onChange) { - onChange(e.currentTarget.value); - } - }, - [onChange] - ); - - useEffect(() => { - if (ref.current) { - ref.current.value = value; - } - }, [value]); - - return ( -
- {label &&
{label}
} -
-
- onInput(e.currentTarget.value) : undefined} - style={{ height }} - placeholder={placeholder} - disabled={disabled} - readOnly={readOnly} - type={type} - onKeyDown={(e) => { - if (e.key === 'Enter' && onEnter) { - if (value !== e.currentTarget.value) { - onInput?.(e.currentTarget.value); - onChange?.(e.currentTarget.value); - } - setTimeout(onEnter, 0); - } - }} - /> -
- {footer &&
{footer}
} - {error &&
{error}
} -
-
- ); -}; - -export const ValidationTextArea = (props: InputProps) => { - const { label, value, onChange, onInput, footer, height, placeholder, disabled, readOnly, onEnter } = props; const ref = useRef(null); const onBlur = useCallback( @@ -118,7 +66,7 @@ export const ValidationTextArea = (props: InputProps) => { useEffect(() => { if (ref.current) { - ref.current.value = value; + ref.current.value = value ?? ''; } }, [value]); @@ -127,6 +75,7 @@ export const ValidationTextArea = (props: InputProps) => { {label &&
{label}
}