mirror of
https://github.com/fastapi/full-stack-fastapi-template.git
synced 2026-11-03 12:29:42 +00:00
Compare commits
329 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4022a9502 | |||
| 686f786b13 | |||
| 1321241572 | |||
| 5a7c5b1a10 | |||
| a6e5812021 | |||
| a8863840dc | |||
| 153a295767 | |||
| d3a14929c3 | |||
| 4e678c3925 | |||
| ac9eddd9cf | |||
| 5521b23141 | |||
| 84e4d7491b | |||
| e4d3771e01 | |||
| 901a6d0508 | |||
| 8a86ce29de | |||
| fdc0acb941 | |||
| e430446bac | |||
| 00a85c1b2c | |||
| c04e324207 | |||
| fd39ace1bf | |||
| 1df9cfde7e | |||
| 996f22129b | |||
| 4b708d1187 | |||
| 7450503938 | |||
| e6e3063106 | |||
| 3318f26df1 | |||
| 2fcdf03fcb | |||
| 272c4c2dc6 | |||
| 69743fa3ca | |||
| 429e7e8379 | |||
| 2476ee7f36 | |||
| 83acb33908 | |||
| 8c2532a5c3 | |||
| 61b7cd673a | |||
| 6106b898e3 | |||
| 0c228d554b | |||
| d27647ffe0 | |||
| 6c08a05f48 | |||
| b9ca4d3ab2 | |||
| 34b09af4f3 | |||
| f1c3145e87 | |||
| 73e94a6680 | |||
| 725ea4b302 | |||
| 54a797b9bf | |||
| bf91b30e98 | |||
| 91ce13e8f2 | |||
| 9fab11a9f8 | |||
| 59d9d82716 | |||
| 838a974069 | |||
| 1d95d01fc7 | |||
| 758f7507e8 | |||
| 057840589b | |||
| 53ed9a5e22 | |||
| 12859acccf | |||
| 282962851d | |||
| 1834c107ab | |||
| fe886d0d6f | |||
| 10ee759780 | |||
| a742011bec | |||
| e3be7f8959 | |||
| af76b6d075 | |||
| 40261268f8 | |||
| 2002447e06 | |||
| 6704482e5a | |||
| e4cf037d38 | |||
| 116f2bbf77 | |||
| c788f943ef | |||
| fa9c79ead4 | |||
| c2fdc6680f | |||
| 493d4ed13a | |||
| 22892d7507 | |||
| 9272d369e6 | |||
| 2604dcbaef | |||
| c3f5427c4a | |||
| bf1243ef85 | |||
| 8f35b76384 | |||
| 98a515fb07 | |||
| 0f488061a6 | |||
| a1b68b17de | |||
| 66b92191c3 | |||
| 20799b39e0 | |||
| 7eaae4d2aa | |||
| 80fb16c0a9 | |||
| 1d36577d79 | |||
| efad48a2fb | |||
| ad9375169e | |||
| 0278f13186 | |||
| 75ac042c22 | |||
| 73f37aae51 | |||
| c1e3b2668b | |||
| 06cd9aa762 | |||
| 2bf8d79119 | |||
| b50dbc36ea | |||
| 8d0e240af4 | |||
| 740b1f84fd | |||
| 8a584cafb4 | |||
| c34911cb86 | |||
| 77c6633b07 | |||
| 53723696f3 | |||
| 0f37fc60fa | |||
| a4b99df319 | |||
| bec7e0ee42 | |||
| 483d9b5966 | |||
| adeb872199 | |||
| 83efa4f28d | |||
| ea6b168f19 | |||
| 37c961e051 | |||
| 176476bcad | |||
| de79cc2626 | |||
| 45d94ee6f8 | |||
| 66e7f918e2 | |||
| d573e0595d | |||
| 42572c56f6 | |||
| 963bd8a77d | |||
| 9b1d095aee | |||
| 77b524df60 | |||
| c612641e92 | |||
| fd74a4de94 | |||
| 5882a2952b | |||
| 978e1f8151 | |||
| 3c643e3a21 | |||
| 3f913b786b | |||
| 74b0399eb4 | |||
| 55221e457c | |||
| b3561c43a0 | |||
| 9cfa77e265 | |||
| a0c6e96b21 | |||
| f745ebf87f | |||
| 8d49d7e3f7 | |||
| 68f6a33d8e | |||
| 2b5d798344 | |||
| 2e107b4d0a | |||
| 8694881478 | |||
| 64fbc8e5d8 | |||
| bac4d8f5a2 | |||
| c6e99db70f | |||
| 0c22075d52 | |||
| c9c9d6c364 | |||
| 6e75aa3710 | |||
| 632f1ac4b3 | |||
| 1a2baee6ad | |||
| 62fa5d8993 | |||
| 89ed10be98 | |||
| 46e904fd61 | |||
| 0162fd36a0 | |||
| d65a69e89a | |||
| 7ffcd2c357 | |||
| 0a68dd19a1 | |||
| e0f969a692 | |||
| 598b74bda6 | |||
| 40e99d2113 | |||
| e5e7308f95 | |||
| e7d18f11c1 | |||
| 1c5d2de404 | |||
| 748490a8bc | |||
| 8366155c77 | |||
| 77992a5d38 | |||
| 984cc5847e | |||
| 0b473f5b6f | |||
| 46e86d4d4d | |||
| dceea46002 | |||
| da58e61dcc | |||
| 8f267e9b60 | |||
| e178566027 | |||
| 028caf1421 | |||
| fed081f586 | |||
| 83f8c00bc6 | |||
| 3f6219634d | |||
| 137a133b42 | |||
| 88635c61d7 | |||
| a8e6c5d025 | |||
| 4758dda2f9 | |||
| 9df6335bab | |||
| 54ff123ebb | |||
| 8febe03244 | |||
| 62becd2482 | |||
| 4ff9b341e1 | |||
| a0a5e39c07 | |||
| db104eb455 | |||
| 3bdb6dd8d2 | |||
| 4d3096cc77 | |||
| f47138448f | |||
| 417f1da8c5 | |||
| f5032c2209 | |||
| e843c15863 | |||
| 2e95aea2bf | |||
| 1b9ba20e13 | |||
| 9ec0244de0 | |||
| 2ea21b08b5 | |||
| 3c8d2f1111 | |||
| aa00321ca3 | |||
| 12203cc205 | |||
| 6eb43ec7bd | |||
| b11c2a3ef5 | |||
| d5b456cc2a | |||
| 5fee1fdc6a | |||
| 943eaeb337 | |||
| f5e66ea69a | |||
| f813161912 | |||
| 61174f1806 | |||
| cce6f884d3 | |||
| 4db05d6abe | |||
| 6e0881d284 | |||
| 0d1da3e09d | |||
| 4434677497 | |||
| e5e6ca5158 | |||
| 2906473557 | |||
| d05e93d396 | |||
| 67be5fa7a5 | |||
| 6dbc9aa7b0 | |||
| 83ad72e193 | |||
| d5eca12ed1 | |||
| 6f778fdcc3 | |||
| 68535c6eda | |||
| 7b5c348cef | |||
| 36ddc1a190 | |||
| 5f1ba46404 | |||
| d8bdcb2ef2 | |||
| 780e6691a0 | |||
| 3f2005a427 | |||
| 12a5fce8b2 | |||
| 92a44c6e70 | |||
| eb0b2c2f45 | |||
| 5da9b867c4 | |||
| 404a712063 | |||
| f84f57884f | |||
| 162029d230 | |||
| 2198748628 | |||
| fce1c2a5cf | |||
| 32dc098c24 | |||
| 96e42344e7 | |||
| c678c2e5f0 | |||
| 981f821e23 | |||
| a650663d8a | |||
| 47afef8720 | |||
| 918847682a | |||
| e4bfdfa856 | |||
| 948e7c92c6 | |||
| 51454b223e | |||
| b6c4e98922 | |||
| 53dd6ea4b2 | |||
| fe028c5ff9 | |||
| e3f1f69d15 | |||
| b0e228049d | |||
| af654fae06 | |||
| 0e33d6bc28 | |||
| f6a284fd6e | |||
| 96d2869c21 | |||
| ea32978f38 | |||
| 26acc1a6c4 | |||
| c1cfd729ee | |||
| 45d7b83b5f | |||
| f8dba14cdf | |||
| 8af907c763 | |||
| 0c51c5f66c | |||
| 6453dea7ff | |||
| 882f951e8a | |||
| c539db8c91 | |||
| 7c9815b167 | |||
| 283c9de1e9 | |||
| 74860873a0 | |||
| 2f5ccf5676 | |||
| cd53ad3fb6 | |||
| ac60caeaaa | |||
| 1503b5cd18 | |||
| e4387f4832 | |||
| ffacbad7ce | |||
| 6c9b1fa2ce | |||
| 531ecb75f7 | |||
| 3b4561848a | |||
| 88bd7fe68b | |||
| 453ccf5ca4 | |||
| 8028b3c8e3 | |||
| 277c1c2493 | |||
| be40eb6741 | |||
| 11099310f8 | |||
| 0dd78c5d84 | |||
| 0328e80eb8 | |||
| da01b69304 | |||
| 3a81472ff8 | |||
| dd414058f0 | |||
| c4aa1fbbb4 | |||
| bd615fc655 | |||
| cd3ecd4c04 | |||
| 2ff7bc813c | |||
| 0e74243a71 | |||
| 43934f1ecb | |||
| c71a101985 | |||
| cd7162372a | |||
| 7db5099493 | |||
| a7723743b4 | |||
| 02cce07150 | |||
| 7965f02a21 | |||
| 9f769cf63f | |||
| 4d95e382f0 | |||
| 1cca3804d3 | |||
| a855046127 | |||
| f0d68e872c | |||
| 92e758e035 | |||
| 37b03e37a5 | |||
| 08956affa8 | |||
| 5dde6a41da | |||
| 84f133ca2e | |||
| e51fe626ad | |||
| a4eebf0c0b | |||
| b9c9737741 | |||
| 1024bd7829 | |||
| 335e62fd54 | |||
| 893f5ff3ce | |||
| 399bd5b4e1 | |||
| e0a0bc4848 | |||
| 75159910d5 | |||
| 043409d6a5 | |||
| de5ab25785 | |||
| 084da0c71d | |||
| ee49a985d9 | |||
| d85f58fe2c | |||
| d1df85e809 | |||
| 83c65459d3 | |||
| 9f2c3d24a2 | |||
| 54ca043e27 | |||
| 521e6e4eda | |||
| 05bebffba1 | |||
| 6efd3ba9a0 | |||
| c067292fec | |||
| b64e374d76 | |||
| d18cf4b6be | |||
| e46b8d9421 | |||
| ccbdb30a87 |
+34
-4
@@ -7,10 +7,40 @@ updates:
|
|||||||
interval: daily
|
interval: daily
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: ⬆
|
prefix: ⬆
|
||||||
# Python
|
labels: [dependencies, internal]
|
||||||
- package-ecosystem: pip
|
# Python uv
|
||||||
directory: /
|
- package-ecosystem: uv
|
||||||
|
directory: /backend
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: ⬆
|
prefix: ⬆
|
||||||
|
labels: [dependencies, internal]
|
||||||
|
# npm
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /frontend
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
commit-message:
|
||||||
|
prefix: ⬆
|
||||||
|
labels: [dependencies, internal]
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "@hey-api/openapi-ts"
|
||||||
|
# Docker
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directories:
|
||||||
|
- /backend
|
||||||
|
- /frontend
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
commit-message:
|
||||||
|
prefix: ⬆
|
||||||
|
labels: [dependencies, internal]
|
||||||
|
# Docker Compose
|
||||||
|
- package-ecosystem: docker-compose
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
commit-message:
|
||||||
|
prefix: ⬆
|
||||||
|
labels: [dependencies, internal]
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ jobs:
|
|||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build
|
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build
|
||||||
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d
|
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ jobs:
|
|||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build
|
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build
|
||||||
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d
|
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: "Conflict detector"
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request_target:
|
||||||
|
types: [synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if PRs have merge conflicts
|
||||||
|
uses: eps1lon/actions-label-merge-conflict@v3
|
||||||
|
with:
|
||||||
|
dirtyLabel: "conflicts"
|
||||||
|
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
commentOnDirty: "This pull request has a merge conflict that needs to be resolved."
|
||||||
@@ -13,21 +13,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# For PRs from forks
|
# For PRs from forks
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
# For PRs from the same repo
|
# For PRs from the same repo
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
|
if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
token: ${{ secrets.FULL_STACK_FASTAPI_TEMPLATE_REPO_TOKEN }}
|
token: ${{ secrets.FULL_STACK_FASTAPI_TEMPLATE_REPO_TOKEN }}
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: "0.4.15"
|
version: "0.4.15"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -39,7 +39,6 @@ jobs:
|
|||||||
- run: uv run bash scripts/generate-client.sh
|
- run: uv run bash scripts/generate-client.sh
|
||||||
env:
|
env:
|
||||||
VIRTUAL_ENV: backend/.venv
|
VIRTUAL_ENV: backend/.venv
|
||||||
ENVIRONMENT: production
|
|
||||||
SECRET_KEY: just-for-generating-client
|
SECRET_KEY: just-for-generating-client
|
||||||
POSTGRES_PASSWORD: just-for-generating-client
|
POSTGRES_PASSWORD: just-for-generating-client
|
||||||
FIRST_SUPERUSER_PASSWORD: just-for-generating-client
|
FIRST_SUPERUSER_PASSWORD: just-for-generating-client
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
run: echo "$GITHUB_CONTEXT"
|
run: echo "$GITHUB_CONTEXT"
|
||||||
- uses: tiangolo/issue-manager@0.5.1
|
- uses: tiangolo/issue-manager@0.6.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
config: >
|
config: >
|
||||||
@@ -38,7 +38,11 @@ jobs:
|
|||||||
},
|
},
|
||||||
"waiting": {
|
"waiting": {
|
||||||
"delay": 2628000,
|
"delay": 2628000,
|
||||||
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR."
|
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.",
|
||||||
|
"reminder": {
|
||||||
|
"before": "P3D",
|
||||||
|
"message": "Heads-up: this will be closed in 3 days unless there's new activity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"invalid": {
|
"invalid": {
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v5
|
- uses: actions/labeler@v6
|
||||||
if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
|
if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
|
||||||
- run: echo "Done adding labels"
|
- run: echo "Done adding labels"
|
||||||
# Run this after labeler applied labels
|
# Run this after labeler applied labels
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
run: echo "$GITHUB_CONTEXT"
|
run: echo "$GITHUB_CONTEXT"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
# To allow latest-changes to commit to the main branch
|
# To allow latest-changes to commit to the main branch
|
||||||
token: ${{ secrets.LATEST_CHANGES }}
|
token: ${{ secrets.LATEST_CHANGES }}
|
||||||
- uses: tiangolo/latest-changes@0.3.2
|
- uses: tiangolo/latest-changes@0.4.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
latest_changes_file: ./release-notes.md
|
latest_changes_file: ./release-notes.md
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: "0.4.15"
|
version: "0.4.15"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
changed: ${{ steps.filter.outputs.changed }}
|
changed: ${{ steps.filter.outputs.changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
# For pull requests it's not necessary to checkout the code but for the main branch it is
|
# For pull requests it's not necessary to checkout the code but for the main branch it is
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
@@ -47,11 +47,11 @@ jobs:
|
|||||||
shardTotal: [4]
|
shardTotal: [4]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
- name: Setup tmate session
|
- name: Setup tmate session
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
limit-access-to-actor: true
|
limit-access-to-actor: true
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: "0.4.15"
|
version: "0.4.15"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
path: frontend/blob-report
|
path: frontend/blob-report
|
||||||
@@ -93,15 +93,15 @@ jobs:
|
|||||||
if: ${{ !cancelled() && needs.changes.outputs.changed == 'true' }}
|
if: ${{ !cancelled() && needs.changes.outputs.changed == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: frontend/all-blob-reports
|
path: frontend/all-blob-reports
|
||||||
pattern: blob-report-*
|
pattern: blob-report-*
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
- name: Upload HTML report
|
- name: Upload HTML report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
name: html-report--attempt-${{ github.run_attempt }}
|
||||||
path: frontend/playwright-report
|
path: frontend/playwright-report
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ jobs:
|
|||||||
statuses: write
|
statuses: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- run: pip install smokeshow
|
- run: pip install smokeshow
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: coverage-html
|
name: coverage-html
|
||||||
path: backend/htmlcov
|
path: backend/htmlcov
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: "0.4.15"
|
version: "0.4.15"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
working-directory: backend
|
working-directory: backend
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- name: Store coverage files
|
- name: Store coverage files
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: coverage-html
|
name: coverage-html
|
||||||
path: backend/htmlcov
|
path: backend/htmlcov
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- run: docker compose build
|
- run: docker compose build
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- run: docker compose up -d --wait backend frontend adminer
|
- run: docker compose up -d --wait backend frontend adminer
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- --fix
|
- --fix
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: local-biome-check
|
||||||
|
name: biome check
|
||||||
|
entry: bash -c 'cd frontend && npm run lint'
|
||||||
|
language: system
|
||||||
|
types: [text]
|
||||||
|
files: ^frontend/
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
|
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
# Full Stack FastAPI Template
|
# Full Stack FastAPI Template
|
||||||
|
|
||||||
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3ATest" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test/badge.svg" alt="Test"></a>
|
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3A%22Test+Docker+Compose%22" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test%20Docker%20Compose/badge.svg" alt="Test Docker Compose"></a>
|
||||||
|
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3A%22Test+Backend%22" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test%20Backend/badge.svg" alt="Test Backend"></a>
|
||||||
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/full-stack-fastapi-template" target="_blank"><img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/full-stack-fastapi-template.svg" alt="Coverage"></a>
|
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/full-stack-fastapi-template" target="_blank"><img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/full-stack-fastapi-template.svg" alt="Coverage"></a>
|
||||||
|
|
||||||
## Technology Stack and Features
|
## Technology Stack and Features
|
||||||
|
|
||||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
|
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
|
||||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
|
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
|
||||||
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
||||||
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
||||||
- 🚀 [React](https://react.dev) for the frontend.
|
- 🚀 [React](https://react.dev) for the frontend.
|
||||||
- 💃 Using TypeScript, hooks, Vite, and other parts of a modern frontend stack.
|
- 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack.
|
||||||
- 🎨 [Chakra UI](https://chakra-ui.com) for the frontend components.
|
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components.
|
||||||
- 🤖 An automatically generated frontend client.
|
- 🤖 An automatically generated frontend client.
|
||||||
- 🧪 [Playwright](https://playwright.dev) for End-to-End testing.
|
- 🧪 [Playwright](https://playwright.dev) for End-to-End testing.
|
||||||
- 🦇 Dark mode support.
|
- 🦇 Dark mode support.
|
||||||
- 🐋 [Docker Compose](https://www.docker.com) for development and production.
|
- 🐋 [Docker Compose](https://www.docker.com) for development and production.
|
||||||
- 🔒 Secure password hashing by default.
|
- 🔒 Secure password hashing by default.
|
||||||
- 🔑 JWT (JSON Web Token) authentication.
|
- 🔑 JWT (JSON Web Token) authentication.
|
||||||
- 📫 Email based password recovery.
|
- 📫 Email based password recovery.
|
||||||
|
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
|
||||||
- ✅ Tests with [Pytest](https://pytest.org).
|
- ✅ Tests with [Pytest](https://pytest.org).
|
||||||
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
||||||
- 🚢 Deployment instructions using Docker Compose, including how to set up a frontend Traefik proxy to handle automatic HTTPS certificates.
|
- 🚢 Deployment instructions using Docker Compose, including how to set up a frontend Traefik proxy to handle automatic HTTPS certificates.
|
||||||
@@ -32,18 +34,10 @@
|
|||||||
|
|
||||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||||
|
|
||||||
### Dashboard - Create User
|
|
||||||
|
|
||||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
|
||||||
|
|
||||||
### Dashboard - Items
|
### Dashboard - Items
|
||||||
|
|
||||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||||
|
|
||||||
### Dashboard - User Settings
|
|
||||||
|
|
||||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
|
||||||
|
|
||||||
### Dashboard - Dark Mode
|
### Dashboard - Dark Mode
|
||||||
|
|
||||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ COPY ./scripts /app/scripts
|
|||||||
COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/
|
COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/
|
||||||
|
|
||||||
COPY ./app /app/app
|
COPY ./app /app/app
|
||||||
|
COPY ./tests /app/tests
|
||||||
|
|
||||||
# Sync the project
|
# Sync the project
|
||||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||||
|
|||||||
+1
-1
@@ -97,7 +97,7 @@ To test the backend run:
|
|||||||
$ bash ./scripts/test.sh
|
$ bash ./scripts/test.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The tests run with Pytest, modify and add tests to `./backend/app/tests/`.
|
The tests run with Pytest, modify and add tests to `./backend/tests/`.
|
||||||
|
|
||||||
If you use GitHub Actions the tests will run automatically.
|
If you use GitHub Actions the tests will run automatically.
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ from pydantic import (
|
|||||||
computed_field,
|
computed_field,
|
||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
from pydantic_core import MultiHostUrl
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
def parse_cors(v: Any) -> list[str] | str:
|
def parse_cors(v: Any) -> list[str] | str:
|
||||||
if isinstance(v, str) and not v.startswith("["):
|
if isinstance(v, str) and not v.startswith("["):
|
||||||
return [i.strip() for i in v.split(",")]
|
return [i.strip() for i in v.split(",") if i.strip()]
|
||||||
elif isinstance(v, list | str):
|
elif isinstance(v, list | str):
|
||||||
return v
|
return v
|
||||||
raise ValueError(v)
|
raise ValueError(v)
|
||||||
@@ -60,7 +59,7 @@ class Settings(BaseSettings):
|
|||||||
@computed_field # type: ignore[prop-decorator]
|
@computed_field # type: ignore[prop-decorator]
|
||||||
@property
|
@property
|
||||||
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
|
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
|
||||||
return MultiHostUrl.build(
|
return PostgresDsn.build(
|
||||||
scheme="postgresql+psycopg",
|
scheme="postgresql+psycopg",
|
||||||
username=self.POSTGRES_USER,
|
username=self.POSTGRES_USER,
|
||||||
password=self.POSTGRES_PASSWORD,
|
password=self.POSTGRES_PASSWORD,
|
||||||
@@ -76,7 +75,7 @@ class Settings(BaseSettings):
|
|||||||
SMTP_USER: str | None = None
|
SMTP_USER: str | None = None
|
||||||
SMTP_PASSWORD: str | None = None
|
SMTP_PASSWORD: str | None = None
|
||||||
EMAILS_FROM_EMAIL: EmailStr | None = None
|
EMAILS_FROM_EMAIL: EmailStr | None = None
|
||||||
EMAILS_FROM_NAME: EmailStr | None = None
|
EMAILS_FROM_NAME: str | None = None
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _set_default_emails_from(self) -> Self:
|
def _set_default_emails_from(self) -> Self:
|
||||||
|
|||||||
@@ -14,19 +14,19 @@ class UserBase(SQLModel):
|
|||||||
|
|
||||||
# Properties to receive via API on creation
|
# Properties to receive via API on creation
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
password: str = Field(min_length=8, max_length=40)
|
password: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
class UserRegister(SQLModel):
|
class UserRegister(SQLModel):
|
||||||
email: EmailStr = Field(max_length=255)
|
email: EmailStr = Field(max_length=255)
|
||||||
password: str = Field(min_length=8, max_length=40)
|
password: str = Field(min_length=8, max_length=128)
|
||||||
full_name: str | None = Field(default=None, max_length=255)
|
full_name: str | None = Field(default=None, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
# Properties to receive via API on update, all are optional
|
# Properties to receive via API on update, all are optional
|
||||||
class UserUpdate(UserBase):
|
class UserUpdate(UserBase):
|
||||||
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
|
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
|
||||||
password: str | None = Field(default=None, min_length=8, max_length=40)
|
password: str | None = Field(default=None, min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateMe(SQLModel):
|
class UserUpdateMe(SQLModel):
|
||||||
@@ -35,8 +35,8 @@ class UserUpdateMe(SQLModel):
|
|||||||
|
|
||||||
|
|
||||||
class UpdatePassword(SQLModel):
|
class UpdatePassword(SQLModel):
|
||||||
current_password: str = Field(min_length=8, max_length=40)
|
current_password: str = Field(min_length=8, max_length=128)
|
||||||
new_password: str = Field(min_length=8, max_length=40)
|
new_password: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
# Database model, database table inferred from class name
|
# Database model, database table inferred from class name
|
||||||
@@ -75,7 +75,6 @@ class ItemUpdate(ItemBase):
|
|||||||
# Database model, database table inferred from class name
|
# Database model, database table inferred from class name
|
||||||
class Item(ItemBase, table=True):
|
class Item(ItemBase, table=True):
|
||||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
title: str = Field(max_length=255)
|
|
||||||
owner_id: uuid.UUID = Field(
|
owner_id: uuid.UUID = Field(
|
||||||
foreign_key="user.id", nullable=False, ondelete="CASCADE"
|
foreign_key="user.id", nullable=False, ondelete="CASCADE"
|
||||||
)
|
)
|
||||||
@@ -111,4 +110,4 @@ class TokenPayload(SQLModel):
|
|||||||
|
|
||||||
class NewPassword(SQLModel):
|
class NewPassword(SQLModel):
|
||||||
token: str
|
token: str
|
||||||
new_password: str = Field(min_length=8, max_length=40)
|
new_password: str = Field(min_length=8, max_length=128)
|
||||||
|
|||||||
+13
-1
@@ -17,7 +17,7 @@ dependencies = [
|
|||||||
"psycopg[binary]<4.0.0,>=3.1.13",
|
"psycopg[binary]<4.0.0,>=3.1.13",
|
||||||
"sqlmodel<1.0.0,>=0.0.21",
|
"sqlmodel<1.0.0,>=0.0.21",
|
||||||
# Pin bcrypt until passlib supports the latest
|
# Pin bcrypt until passlib supports the latest
|
||||||
"bcrypt==4.0.1",
|
"bcrypt==4.3.0",
|
||||||
"pydantic-settings<3.0.0,>=2.2.1",
|
"pydantic-settings<3.0.0,>=2.2.1",
|
||||||
"sentry-sdk[fastapi]<2.0.0,>=1.40.6",
|
"sentry-sdk[fastapi]<2.0.0,>=1.40.6",
|
||||||
"pyjwt<3.0.0,>=2.8.0",
|
"pyjwt<3.0.0,>=2.8.0",
|
||||||
@@ -55,6 +55,7 @@ select = [
|
|||||||
"C4", # flake8-comprehensions
|
"C4", # flake8-comprehensions
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"ARG001", # unused arguments in functions
|
"ARG001", # unused arguments in functions
|
||||||
|
"T201", # print statements are not allowed
|
||||||
]
|
]
|
||||||
ignore = [
|
ignore = [
|
||||||
"E501", # line too long, handled by black
|
"E501", # line too long, handled by black
|
||||||
@@ -66,3 +67,14 @@ ignore = [
|
|||||||
[tool.ruff.lint.pyupgrade]
|
[tool.ruff.lint.pyupgrade]
|
||||||
# Preserve types, even if a file imports `from __future__ import annotations`.
|
# Preserve types, even if a file imports `from __future__ import annotations`.
|
||||||
keep-runtime-typing = true
|
keep-runtime-typing = true
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["app"]
|
||||||
|
dynamic_context = "test_function"
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
show_missing = true
|
||||||
|
sort = "-Cover"
|
||||||
|
|
||||||
|
[tool.coverage.html]
|
||||||
|
show_contexts = true
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
set -e
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
coverage run --source=app -m pytest
|
coverage run -m pytest tests/
|
||||||
coverage report --show-missing
|
coverage report
|
||||||
coverage html --title "${@-coverage}"
|
coverage html --title "${@-coverage}"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from fastapi.testclient import TestClient
|
|||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.tests.utils.item import create_random_item
|
from tests.utils.item import create_random_item
|
||||||
|
|
||||||
|
|
||||||
def test_create_item(
|
def test_create_item(
|
||||||
@@ -7,9 +7,9 @@ from app.core.config import settings
|
|||||||
from app.core.security import verify_password
|
from app.core.security import verify_password
|
||||||
from app.crud import create_user
|
from app.crud import create_user
|
||||||
from app.models import UserCreate
|
from app.models import UserCreate
|
||||||
from app.tests.utils.user import user_authentication_headers
|
|
||||||
from app.tests.utils.utils import random_email, random_lower_string
|
|
||||||
from app.utils import generate_password_reset_token
|
from app.utils import generate_password_reset_token
|
||||||
|
from tests.utils.user import user_authentication_headers
|
||||||
|
from tests.utils.utils import random_email, random_lower_string
|
||||||
|
|
||||||
|
|
||||||
def test_get_access_token(client: TestClient) -> None:
|
def test_get_access_token(client: TestClient) -> None:
|
||||||
@@ -8,7 +8,7 @@ from app import crud
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.security import verify_password
|
from app.core.security import verify_password
|
||||||
from app.models import User, UserCreate
|
from app.models import User, UserCreate
|
||||||
from app.tests.utils.utils import random_email, random_lower_string
|
from tests.utils.utils import random_email, random_lower_string
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_superuser_me(
|
def test_get_users_superuser_me(
|
||||||
@@ -8,8 +8,8 @@ from app.core.config import settings
|
|||||||
from app.core.db import engine, init_db
|
from app.core.db import engine, init_db
|
||||||
from app.main import app
|
from app.main import app
|
||||||
from app.models import Item, User
|
from app.models import Item, User
|
||||||
from app.tests.utils.user import authentication_token_from_email
|
from tests.utils.user import authentication_token_from_email
|
||||||
from app.tests.utils.utils import get_superuser_token_headers
|
from tests.utils.utils import get_superuser_token_headers
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
@@ -4,7 +4,7 @@ from sqlmodel import Session
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.core.security import verify_password
|
from app.core.security import verify_password
|
||||||
from app.models import User, UserCreate, UserUpdate
|
from app.models import User, UserCreate, UserUpdate
|
||||||
from app.tests.utils.utils import random_email, random_lower_string
|
from tests.utils.utils import random_email, random_lower_string
|
||||||
|
|
||||||
|
|
||||||
def test_create_user(db: Session) -> None:
|
def test_create_user(db: Session) -> None:
|
||||||
@@ -2,8 +2,8 @@ from sqlmodel import Session
|
|||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
from app.models import Item, ItemCreate
|
from app.models import Item, ItemCreate
|
||||||
from app.tests.utils.user import create_random_user
|
from tests.utils.user import create_random_user
|
||||||
from app.tests.utils.utils import random_lower_string
|
from tests.utils.utils import random_lower_string
|
||||||
|
|
||||||
|
|
||||||
def create_random_item(db: Session) -> Item:
|
def create_random_item(db: Session) -> Item:
|
||||||
@@ -4,7 +4,7 @@ from sqlmodel import Session
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.models import User, UserCreate, UserUpdate
|
from app.models import User, UserCreate, UserUpdate
|
||||||
from app.tests.utils.utils import random_email, random_lower_string
|
from tests.utils.utils import random_email, random_lower_string
|
||||||
|
|
||||||
|
|
||||||
def user_authentication_headers(
|
def user_authentication_headers(
|
||||||
Generated
+856
-751
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -12,7 +12,7 @@ But you have to configure a couple things first. 🤓
|
|||||||
|
|
||||||
* Have a remote server ready and available.
|
* Have a remote server ready and available.
|
||||||
* Configure the DNS records of your domain to point to the IP of the server you just created.
|
* Configure the DNS records of your domain to point to the IP of the server you just created.
|
||||||
* Configure a wildcard subdomain for your domain, so that you can have multiple subdomains for different services, e.g. `*.fastapi-project.example.com`. This will be useful for accessing different components, like `dashboard.fastapi-project.example.com`, `api.fastapi-project.example.com`, `traefik.fastapi-project.example.com`, `adminer.fastapi-project.example.com`, etc. And also for `staging`, like `dashboard.staging.fastapi-project.example.com`, `adminer.staging..fastapi-project.example.com`, etc.
|
* Configure a wildcard subdomain for your domain, so that you can have multiple subdomains for different services, e.g. `*.fastapi-project.example.com`. This will be useful for accessing different components, like `dashboard.fastapi-project.example.com`, `api.fastapi-project.example.com`, `traefik.fastapi-project.example.com`, `adminer.fastapi-project.example.com`, etc. And also for `staging`, like `dashboard.staging.fastapi-project.example.com`, `adminer.staging.fastapi-project.example.com`, etc.
|
||||||
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
||||||
|
|
||||||
## Public Traefik
|
## Public Traefik
|
||||||
|
|||||||
+31
-19
@@ -10,15 +10,15 @@ docker compose watch
|
|||||||
|
|
||||||
* Now you can open your browser and interact with these URLs:
|
* Now you can open your browser and interact with these URLs:
|
||||||
|
|
||||||
Frontend, built with Docker, with routes handled based on the path: http://localhost:5173
|
Frontend, built with Docker, with routes handled based on the path: <http://localhost:5173>
|
||||||
|
|
||||||
Backend, JSON based web API based on OpenAPI: http://localhost:8000
|
Backend, JSON based web API based on OpenAPI: <http://localhost:8000>
|
||||||
|
|
||||||
Automatic interactive documentation with Swagger UI (from the OpenAPI backend): http://localhost:8000/docs
|
Automatic interactive documentation with Swagger UI (from the OpenAPI backend): <http://localhost:8000/docs>
|
||||||
|
|
||||||
Adminer, database web administration: http://localhost:8080
|
Adminer, database web administration: <http://localhost:8080>
|
||||||
|
|
||||||
Traefik UI, to see how the routes are being handled by the proxy: http://localhost:8090
|
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
|
||||||
|
|
||||||
**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it.
|
**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it.
|
||||||
|
|
||||||
@@ -34,6 +34,18 @@ To check the logs of a specific service, add the name of the service, e.g.:
|
|||||||
docker compose logs backend
|
docker compose logs backend
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Mailcatcher
|
||||||
|
|
||||||
|
Mailcatcher is a simple SMTP server that catches all emails sent by the backend during local development. Instead of sending real emails, they are captured and displayed in a web interface.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
* Testing email functionality during development
|
||||||
|
* Verifying email content and formatting
|
||||||
|
* Debugging email-related functionality without sending real emails
|
||||||
|
|
||||||
|
The backend is automatically configured to use Mailcatcher when running with Docker Compose locally (SMTP on port 1025). All captured emails can be viewed at <http://localhost:1080>.
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
The Docker Compose files are configured so that each of the services is available in a different port in `localhost`.
|
The Docker Compose files are configured so that each of the services is available in a different port in `localhost`.
|
||||||
@@ -174,34 +186,34 @@ The production or staging URLs would use these same paths, but with your own dom
|
|||||||
|
|
||||||
Development URLs, for local development.
|
Development URLs, for local development.
|
||||||
|
|
||||||
Frontend: http://localhost:5173
|
Frontend: <http://localhost:5173>
|
||||||
|
|
||||||
Backend: http://localhost:8000
|
Backend: <http://localhost:8000>
|
||||||
|
|
||||||
Automatic Interactive Docs (Swagger UI): http://localhost:8000/docs
|
Automatic Interactive Docs (Swagger UI): <http://localhost:8000/docs>
|
||||||
|
|
||||||
Automatic Alternative Docs (ReDoc): http://localhost:8000/redoc
|
Automatic Alternative Docs (ReDoc): <http://localhost:8000/redoc>
|
||||||
|
|
||||||
Adminer: http://localhost:8080
|
Adminer: <http://localhost:8080>
|
||||||
|
|
||||||
Traefik UI: http://localhost:8090
|
Traefik UI: <http://localhost:8090>
|
||||||
|
|
||||||
MailCatcher: http://localhost:1080
|
MailCatcher: <http://localhost:1080>
|
||||||
|
|
||||||
### Development URLs with `localhost.tiangolo.com` Configured
|
### Development URLs with `localhost.tiangolo.com` Configured
|
||||||
|
|
||||||
Development URLs, for local development.
|
Development URLs, for local development.
|
||||||
|
|
||||||
Frontend: http://dashboard.localhost.tiangolo.com
|
Frontend: <http://dashboard.localhost.tiangolo.com>
|
||||||
|
|
||||||
Backend: http://api.localhost.tiangolo.com
|
Backend: <http://api.localhost.tiangolo.com>
|
||||||
|
|
||||||
Automatic Interactive Docs (Swagger UI): http://api.localhost.tiangolo.com/docs
|
Automatic Interactive Docs (Swagger UI): <http://api.localhost.tiangolo.com/docs>
|
||||||
|
|
||||||
Automatic Alternative Docs (ReDoc): http://api.localhost.tiangolo.com/redoc
|
Automatic Alternative Docs (ReDoc): <http://api.localhost.tiangolo.com/redoc>
|
||||||
|
|
||||||
Adminer: http://localhost.tiangolo.com:8080
|
Adminer: <http://localhost.tiangolo.com:8080>
|
||||||
|
|
||||||
Traefik UI: http://localhost.tiangolo.com:8090
|
Traefik UI: <http://localhost.tiangolo.com:8090>
|
||||||
|
|
||||||
MailCatcher: http://localhost.tiangolo.com:1080
|
MailCatcher: <http://localhost.tiangolo.com:1080>
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:12
|
image: postgres:17
|
||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
20
|
24
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Stage 0, "build-stage", based on Node.js, to build and compile the frontend
|
# Stage 0, "build-stage", based on Node.js, to build and compile the frontend
|
||||||
FROM node:20 AS build-stage
|
FROM node:24 AS build-stage
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20
|
FROM mcr.microsoft.com/playwright:v1.57.0-noble
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -6,8 +6,6 @@ COPY package*.json /app/
|
|||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
RUN npx -y playwright install --with-deps
|
|
||||||
|
|
||||||
COPY ./ /app/
|
COPY ./ /app/
|
||||||
|
|
||||||
ARG VITE_API_URL=${VITE_API_URL}
|
ARG VITE_API_URL=${VITE_API_URL}
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,6 @@
|
|||||||
# FastAPI Project - Frontend
|
# FastAPI Project - Frontend
|
||||||
|
|
||||||
The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Chakra UI](https://chakra-ui.com/).
|
The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Tailwind CSS](https://tailwindcss.com/).
|
||||||
|
|
||||||
## Frontend development
|
## Frontend development
|
||||||
|
|
||||||
@@ -121,7 +121,6 @@ The frontend code is structured as follows:
|
|||||||
* `frontend/src/components` - The different components of the frontend.
|
* `frontend/src/components` - The different components of the frontend.
|
||||||
* `frontend/src/hooks` - Custom hooks.
|
* `frontend/src/hooks` - Custom hooks.
|
||||||
* `frontend/src/routes` - The different routes of the frontend which include the pages.
|
* `frontend/src/routes` - The different routes of the frontend which include the pages.
|
||||||
* `theme.tsx` - The Chakra UI custom theme.
|
|
||||||
|
|
||||||
## End-to-End Testing with Playwright
|
## End-to-End Testing with Playwright
|
||||||
|
|
||||||
|
|||||||
+20
-10
@@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.6.1/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
||||||
"organizeImports": {
|
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"files": {
|
"files": {
|
||||||
"ignore": [
|
"includes": [
|
||||||
"node_modules",
|
"**",
|
||||||
"src/routeTree.gen.ts",
|
"!**/dist/**/*",
|
||||||
"playwright.config.ts",
|
"!**/node_modules/**/*",
|
||||||
"playwright-report"
|
"!**/src/routeTree.gen.ts",
|
||||||
|
"!**/src/client/**/*",
|
||||||
|
"!**/src/components/ui/**/*",
|
||||||
|
"!**/playwright-report",
|
||||||
|
"!**/playwright.config.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
@@ -20,7 +22,10 @@
|
|||||||
"noArrayIndexKey": "off"
|
"noArrayIndexKey": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noNonNullAssertion": "off"
|
"noNonNullAssertion": "off",
|
||||||
|
"noParameterAssign": "error",
|
||||||
|
"useSelfClosingElements": "error",
|
||||||
|
"noUselessElse": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -32,5 +37,10 @@
|
|||||||
"quoteStyle": "double",
|
"quoteStyle": "double",
|
||||||
"semicolons": "asNeeded"
|
"semicolons": "asNeeded"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import { defineConfig } from "@hey-api/openapi-ts"
|
import { defineConfig } from "@hey-api/openapi-ts"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
client: "legacy/axios",
|
|
||||||
input: "./openapi.json",
|
input: "./openapi.json",
|
||||||
output: "./src/client",
|
output: "./src/client",
|
||||||
// exportSchemas: true,
|
|
||||||
plugins: [
|
plugins: [
|
||||||
|
"legacy/axios",
|
||||||
{
|
{
|
||||||
name: "@hey-api/sdk",
|
name: "@hey-api/sdk",
|
||||||
// NOTE: this doesn't allow tree-shaking
|
// NOTE: this doesn't allow tree-shaking
|
||||||
asClass: true,
|
asClass: true,
|
||||||
operationId: true,
|
operationId: true,
|
||||||
|
classNameBuilder: "{{name}}Service",
|
||||||
methodNameBuilder: (operation) => {
|
methodNameBuilder: (operation) => {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
let name: string = operation.name
|
let name: string = operation.name
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const service: string = operation.service
|
const service: string = operation.service
|
||||||
|
|
||||||
if (service && name.toLowerCase().startsWith(service.toLowerCase())) {
|
if (service && name.toLowerCase().startsWith(service.toLowerCase())) {
|
||||||
@@ -24,5 +25,9 @@ export default defineConfig({
|
|||||||
return name.charAt(0).toLowerCase() + name.slice(1)
|
return name.charAt(0).toLowerCase() + name.slice(1)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "@hey-api/schemas",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Generated
+6743
-3707
File diff suppressed because it is too large
Load Diff
+48
-26
@@ -6,37 +6,59 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -p tsconfig.build.json && vite build",
|
"build": "tsc -p tsconfig.build.json && vite build",
|
||||||
"lint": "biome check --apply-unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
|
"lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-client": "openapi-ts"
|
"generate-client": "openapi-ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^3.8.0",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@tanstack/react-query": "^5.28.14",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@tanstack/react-query-devtools": "^5.28.14",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@tanstack/react-router": "1.19.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"axios": "1.7.4",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"form-data": "4.0.0",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"next-themes": "^0.4.4",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"react": "^18.2.0",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"react-dom": "^18.2.0",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"react-error-boundary": "^4.0.13",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"react-hook-form": "7.49.3",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"react-icons": "^5.4.0"
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
"@tanstack/react-query": "^5.90.12",
|
||||||
|
"@tanstack/react-query-devtools": "^5.91.1",
|
||||||
|
"@tanstack/react-router": "^1.131.50",
|
||||||
|
"@tanstack/react-router-devtools": "^1.139.12",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"axios": "1.13.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"form-data": "4.0.5",
|
||||||
|
"lucide-react": "^0.556.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.2.1",
|
||||||
|
"react-error-boundary": "^6.0.0",
|
||||||
|
"react-hook-form": "^7.68.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.6.1",
|
"@biomejs/biome": "^2.3.8",
|
||||||
"@hey-api/openapi-ts": "^0.57.0",
|
"@hey-api/openapi-ts": "0.73.0",
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "1.57.0",
|
||||||
"@tanstack/router-devtools": "1.19.1",
|
"@tanstack/router-devtools": "^1.140.0",
|
||||||
"@tanstack/router-vite-plugin": "1.19.0",
|
"@tanstack/router-plugin": "^1.140.0",
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^17.2.3",
|
||||||
"typescript": "^5.2.2",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite": "^5.4.14"
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
viewBox="0 0 132.29167 132.29166"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="icon-white-nomargin-transparent.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
inkscape:export-filename="icon-teal-nomargin-transparent-500.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1720"
|
||||||
|
inkscape:window-height="1371"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.73657628"
|
||||||
|
inkscape:cx="448.01877"
|
||||||
|
inkscape:cy="-91.640203"
|
||||||
|
inkscape:window-x="1720"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="g2149">
|
||||||
|
<g
|
||||||
|
id="g2141">
|
||||||
|
<g
|
||||||
|
id="g1">
|
||||||
|
<path
|
||||||
|
id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.28098;stop-color:#000000"
|
||||||
|
d="M 66.145833 0 A 66.145836 65.931923 0 0 0 0 65.931893 A 66.145836 65.931923 0 0 0 66.145833 131.86379 A 66.145836 65.931923 0 0 0 132.29167 65.931893 A 66.145836 65.931923 0 0 0 66.145833 0 z M 61.581771 29.853992 L 103.20042 29.853992 L 61.410205 59.222742 L 89.976937 59.222742 L 29.091248 102.00979 L 42.315763 72.641044 L 48.358289 59.222742 L 61.581771 29.853992 z " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
y="-49.422424"
|
||||||
|
x="-51.908718"
|
||||||
|
height="162.82199"
|
||||||
|
width="451.52316"
|
||||||
|
id="rect824"
|
||||||
|
style="opacity:0.98;fill:none;fill-opacity:1;stroke-width:0.311037" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
viewBox="0 0 132.29167 132.29166"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="icon-teal-nomargin-transparent.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
inkscape:export-filename="icon-teal-nomargin-192.png"
|
||||||
|
inkscape:export-xdpi="36.863998"
|
||||||
|
inkscape:export-ydpi="36.863998"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1720"
|
||||||
|
inkscape:window-height="1371"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.73657628"
|
||||||
|
inkscape:cx="448.01877"
|
||||||
|
inkscape:cy="-91.640203"
|
||||||
|
inkscape:window-x="1720"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="g2149">
|
||||||
|
<g
|
||||||
|
id="g2141">
|
||||||
|
<g
|
||||||
|
id="g1">
|
||||||
|
<path
|
||||||
|
id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7"
|
||||||
|
style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.28098;stop-color:#000000"
|
||||||
|
d="M 66.145833 0 A 66.145836 65.931923 0 0 0 0 65.931893 A 66.145836 65.931923 0 0 0 66.145833 131.86379 A 66.145836 65.931923 0 0 0 132.29167 65.931893 A 66.145836 65.931923 0 0 0 66.145833 0 z M 61.581771 29.853992 L 103.20042 29.853992 L 61.410205 59.222742 L 89.976937 59.222742 L 29.091248 102.00979 L 42.315763 72.641044 L 48.358289 59.222742 L 61.581771 29.853992 z " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
y="-49.422424"
|
||||||
|
x="-51.908718"
|
||||||
|
height="162.82199"
|
||||||
|
width="451.52316"
|
||||||
|
id="rect824"
|
||||||
|
style="opacity:0.98;fill:none;fill-opacity:1;stroke-width:0.311037" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="341.26324mm"
|
||||||
|
height="63.977489mm"
|
||||||
|
viewBox="0 0 341.26324 63.977485"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="logo-white-nomargin-vector.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
inkscape:export-filename="logo-white-margin-vector.png"
|
||||||
|
inkscape:export-xdpi="57.604134"
|
||||||
|
inkscape:export-ydpi="57.604134"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1720"
|
||||||
|
inkscape:window-height="1371"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.73657628"
|
||||||
|
inkscape:cx="644.8755"
|
||||||
|
inkscape:cy="95.713101"
|
||||||
|
inkscape:window-x="1720"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g2141"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="g2149"
|
||||||
|
transform="translate(2.5752783e-4,1.1668595e-4)">
|
||||||
|
<g
|
||||||
|
id="g2141">
|
||||||
|
<g
|
||||||
|
id="g1">
|
||||||
|
<path
|
||||||
|
id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.136325;stop-color:#000000"
|
||||||
|
d="M 32.092357,-1.166849e-4 A 32.092354,31.988569 0 0 0 -2.5752734e-4,31.988628 32.092354,31.988569 0 0 0 32.092357,63.977374 32.092354,31.988569 0 0 0 64.184455,31.988628 32.092354,31.988569 0 0 0 32.092357,-1.166849e-4 Z M 29.878022,14.484271 H 50.070588 L 29.794823,28.73353 H 43.654442 L 14.114126,49.49247 20.530789,35.243727 23.462393,28.73353 Z" />
|
||||||
|
<path
|
||||||
|
style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#ffffff;stroke-width:1.99288"
|
||||||
|
d="M 89.762163,59.410606 V 4.1680399 H 121.88735 V 9.5089518 H 95.979941 V 28.082571 h 22.957949 v 5.340912 H 95.979941 v 25.987123 z m 51.017587,0.876867 q -4.46405,0 -7.97152,-1.275442 -3.50746,-1.275442 -5.50034,-4.145185 -1.99288,-2.869744 -1.99288,-7.572935 0,-4.543761 2.23203,-7.33379 2.23202,-2.869743 6.13806,-4.145185 3.98576,-1.275442 8.92809,-1.275442 2.23203,0 4.70319,0.398576 2.47117,0.398575 3.10889,0.717436 v -2.391453 q 0,-2.710314 -0.71743,-5.181482 -0.63772,-2.550883 -2.71032,-4.145186 -2.07259,-1.594302 -6.29749,-1.594302 -4.38433,0 -6.69607,0.637721 -2.31174,0.637721 -3.42775,1.036297 l -0.79715,-5.101767 q 1.43487,-0.637721 4.38433,-1.195727 2.94946,-0.558005 6.93522,-0.558005 5.65977,0 8.92809,1.992877 3.34803,1.992878 4.7829,5.500342 1.51459,3.42775 1.51459,7.891796 v 25.907408 q -1.67402,0.398576 -5.97863,1.116012 -4.30462,0.717436 -9.56581,0.717436 z m 0.87686,-5.101767 q 2.79003,0 5.02205,-0.15943 2.23203,-0.239146 3.74661,-0.558006 V 40.597842 q -0.79715,-0.398575 -2.71031,-0.797151 -1.91316,-0.398576 -4.94234,-0.398576 -2.55088,0 -5.18148,0.558006 -2.6306,0.558006 -4.46404,2.232023 -1.75374,1.674017 -1.75374,5.022052 0,4.464045 2.79003,6.217778 2.79003,1.753732 7.49322,1.753732 z m 36.4298,5.181482 q -5.34092,0 -8.37009,-0.956582 -2.94946,-0.876866 -3.98575,-1.355156 l 1.43487,-5.261197 q 0.87686,0.31886 3.58718,1.355157 2.71031,1.036296 7.33379,1.036296 4.38433,0 7.09464,-1.355157 2.71031,-1.355157 2.71031,-4.703191 0,-2.152308 -0.95658,-3.427749 -0.95658,-1.355157 -3.10889,-2.471169 -2.07259,-1.116011 -5.73948,-2.550883 -3.10889,-1.275442 -5.73949,-2.710313 -2.6306,-1.434872 -4.30462,-3.666895 -1.5943,-2.232023 -1.5943,-5.739488 0,-3.427749 1.75373,-5.978632 1.75374,-2.550884 4.94234,-3.985755 3.26832,-1.434872 7.73237,-1.434872 4.14518,0 7.01492,0.717436 2.86975,0.717436 4.06547,1.275441 l -1.35515,5.181482 q -1.0363,-0.558006 -3.34804,-1.275442 -2.31173,-0.797151 -6.53663,-0.797151 -3.34804,0 -5.81921,1.434872 -2.47117,1.355157 -2.47117,4.384331 0,2.152308 1.0363,3.507464 1.0363,1.275442 3.10889,2.311738 2.07259,1.036297 5.10177,2.232023 3.42775,1.355157 6.13806,2.869744 2.79003,1.434872 4.46405,3.74661 1.67401,2.311738 1.67401,6.138063 0,3.74661 -1.91316,6.377208 -1.91316,2.550883 -5.50034,3.826325 -3.50747,1.275442 -8.4498,1.275442 z m 39.21967,-0.07972 q -5.2612,0 -8.29037,-1.833448 -3.02917,-1.833447 -4.30461,-5.500342 -1.19573,-3.666895 -1.19573,-9.167237 V 6.1609175 L 209.494,5.1246211 V 18.118183 h 16.18217 v 5.022051 H 209.494 v 21.124503 q 0,4.38433 0.95658,6.696068 1.0363,2.311738 2.86975,3.188605 1.91316,0.797151 4.46404,0.797151 3.02918,0 5.02205,-0.717436 2.0726,-0.717436 3.26832,-1.275442 l 1.27544,4.862621 q -1.19572,0.717436 -3.98575,1.594302 -2.71031,0.876867 -6.05835,0.876867 z m 11.95723,-0.876867 q 4.30462,-11.55869 7.8918,-21.044787 3.58718,-9.486097 7.01493,-17.776468 3.50746,-8.370086 7.4135,-16.4213111 h 5.58006 q 2.86974,6.0583481 5.50034,12.2761261 2.71032,6.138063 5.34091,12.754416 2.6306,6.616354 5.42063,14.109574 2.86975,7.413504 6.05835,16.10245 h -6.77578 q -1.43488,-3.985755 -2.79003,-7.572934 -1.35516,-3.666895 -2.55089,-7.17436 h -26.30598 q -1.27544,3.507465 -2.6306,7.17436 -1.35516,3.587179 -2.71031,7.572934 z M 242.8946,39.402115 h 22.63909 q -1.51459,-3.985755 -2.94946,-7.732365 -1.43487,-3.746609 -2.86975,-7.254074 -1.35515,-3.507464 -2.79002,-6.775784 -1.35516,-3.348034 -2.79003,-6.536638 -1.35516,3.188604 -2.79003,6.536638 -1.35516,3.26832 -2.79003,6.775784 -1.35516,3.507465 -2.79003,7.254074 -1.43487,3.74661 -2.86974,7.732365 z m 44.481,20.008491 V 5.2043362 q 3.02917,-0.797151 6.93521,-1.1160114 3.98576,-0.3985755 7.33379,-0.3985755 11.71812,0 17.53732,4.5437609 5.89892,4.4640458 5.89892,12.7544168 0,6.297493 -2.94946,10.123818 -2.86974,3.826325 -8.37008,5.580057 -5.42063,1.674017 -13.07328,1.674017 h -7.09465 v 21.044787 z m 6.21777,-26.385699 h 6.53664 q 5.65978,0 9.80496,-0.956581 4.14519,-1.036296 6.37721,-3.666895 2.31174,-2.630598 2.31174,-7.493219 0,-4.703192 -2.39146,-7.254075 -2.39145,-2.550883 -6.21777,-3.587179 -3.82633,-1.0362968 -8.13094,-1.0362968 -2.79003,0 -4.86263,0.2391453 -1.99287,0.1594302 -3.42775,0.3188604 z M 335.0452,59.410606 V 4.1680399 h 6.21778 V 59.410606 Z"
|
||||||
|
id="text979-3"
|
||||||
|
aria-label="FastAPI" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
y="-49.422306"
|
||||||
|
x="-51.908459"
|
||||||
|
height="162.82199"
|
||||||
|
width="451.52316"
|
||||||
|
id="rect824"
|
||||||
|
style="opacity:0.98;fill:none;fill-opacity:1;stroke-width:0.311037" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -1,15 +1,43 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg
|
<svg
|
||||||
id="svg8"
|
width="341.26297mm"
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 346.52395 63.977134"
|
|
||||||
height="63.977139mm"
|
height="63.977139mm"
|
||||||
width="346.52396mm"
|
viewBox="0 0 341.26297 63.977134"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="logo-teal-nomargin-vector.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1720"
|
||||||
|
inkscape:window-height="1371"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.73657628"
|
||||||
|
inkscape:cx="448.01877"
|
||||||
|
inkscape:cy="-91.640203"
|
||||||
|
inkscape:window-x="1720"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm" />
|
||||||
<defs
|
<defs
|
||||||
id="defs2" />
|
id="defs2" />
|
||||||
<metadata
|
<metadata
|
||||||
@@ -28,24 +56,36 @@
|
|||||||
<g
|
<g
|
||||||
id="g2141">
|
id="g2141">
|
||||||
<g
|
<g
|
||||||
id="g2106"
|
id="g1">
|
||||||
transform="matrix(0.96564264,0,0,0.96251987,-899.3295,194.86874)">
|
<g
|
||||||
<circle
|
id="g2106"
|
||||||
style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000"
|
transform="matrix(0.96564264,0,0,0.96251987,-899.3295,194.86874)">
|
||||||
id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7"
|
<circle
|
||||||
cx="964.56165"
|
style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000"
|
||||||
cy="-169.22266"
|
id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7"
|
||||||
r="33.234192" />
|
cx="964.56165"
|
||||||
|
cy="-169.22266"
|
||||||
|
r="33.234192"
|
||||||
|
inkscape:export-xdpi="1543.8315"
|
||||||
|
inkscape:export-ydpi="1543.8315" />
|
||||||
|
<path
|
||||||
|
id="rect1249-6-3-4-4-3-6-6-1-2"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000"
|
||||||
|
d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" />
|
||||||
|
</g>
|
||||||
<path
|
<path
|
||||||
id="rect1249-6-3-4-4-3-6-6-1-2"
|
style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#009688;stroke-width:1.99288"
|
||||||
style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000"
|
d="M 89.762163,59.410606 V 4.1680399 H 121.88735 V 9.5089518 H 95.979941 V 28.082571 h 22.957949 v 5.340912 H 95.979941 v 25.987123 z m 51.017587,0.876867 q -4.46405,0 -7.97152,-1.275442 -3.50746,-1.275442 -5.50034,-4.145185 -1.99288,-2.869744 -1.99288,-7.572935 0,-4.543761 2.23203,-7.33379 2.23202,-2.869743 6.13806,-4.145185 3.98576,-1.275442 8.92809,-1.275442 2.23203,0 4.70319,0.398576 2.47117,0.398575 3.10889,0.717436 v -2.391453 q 0,-2.710314 -0.71743,-5.181482 -0.63772,-2.550883 -2.71032,-4.145186 -2.07259,-1.594302 -6.29749,-1.594302 -4.38433,0 -6.69607,0.637721 -2.31174,0.637721 -3.42775,1.036297 l -0.79715,-5.101767 q 1.43487,-0.637721 4.38433,-1.195727 2.94946,-0.558005 6.93522,-0.558005 5.65977,0 8.92809,1.992877 3.34803,1.992878 4.7829,5.500342 1.51459,3.42775 1.51459,7.891796 v 25.907408 q -1.67402,0.398576 -5.97863,1.116012 -4.30462,0.717436 -9.56581,0.717436 z m 0.87686,-5.101767 q 2.79003,0 5.02205,-0.15943 2.23203,-0.239146 3.74661,-0.558006 V 40.597842 q -0.79715,-0.398575 -2.71031,-0.797151 -1.91316,-0.398576 -4.94234,-0.398576 -2.55088,0 -5.18148,0.558006 -2.6306,0.558006 -4.46404,2.232023 -1.75374,1.674017 -1.75374,5.022052 0,4.464045 2.79003,6.217778 2.79003,1.753732 7.49322,1.753732 z m 36.4298,5.181482 q -5.34092,0 -8.37009,-0.956582 -2.94946,-0.876866 -3.98575,-1.355156 l 1.43487,-5.261197 q 0.87686,0.31886 3.58718,1.355157 2.71031,1.036296 7.33379,1.036296 4.38433,0 7.09464,-1.355157 2.71031,-1.355157 2.71031,-4.703191 0,-2.152308 -0.95658,-3.427749 -0.95658,-1.355157 -3.10889,-2.471169 -2.07259,-1.116011 -5.73948,-2.550883 -3.10889,-1.275442 -5.73949,-2.710313 -2.6306,-1.434872 -4.30462,-3.666895 -1.5943,-2.232023 -1.5943,-5.739488 0,-3.427749 1.75373,-5.978632 1.75374,-2.550884 4.94234,-3.985755 3.26832,-1.434872 7.73237,-1.434872 4.14518,0 7.01492,0.717436 2.86975,0.717436 4.06547,1.275441 l -1.35515,5.181482 q -1.0363,-0.558006 -3.34804,-1.275442 -2.31173,-0.797151 -6.53663,-0.797151 -3.34804,0 -5.81921,1.434872 -2.47117,1.355157 -2.47117,4.384331 0,2.152308 1.0363,3.507464 1.0363,1.275442 3.10889,2.311738 2.07259,1.036297 5.10177,2.232023 3.42775,1.355157 6.13806,2.869744 2.79003,1.434872 4.46405,3.74661 1.67401,2.311738 1.67401,6.138063 0,3.74661 -1.91316,6.377208 -1.91316,2.550883 -5.50034,3.826325 -3.50747,1.275442 -8.4498,1.275442 z m 39.21967,-0.07972 q -5.2612,0 -8.29037,-1.833448 -3.02917,-1.833447 -4.30461,-5.500342 -1.19573,-3.666895 -1.19573,-9.167237 V 6.1609175 L 209.494,5.1246211 V 18.118183 h 16.18217 v 5.022051 H 209.494 v 21.124503 q 0,4.38433 0.95658,6.696068 1.0363,2.311738 2.86975,3.188605 1.91316,0.797151 4.46404,0.797151 3.02918,0 5.02205,-0.717436 2.0726,-0.717436 3.26832,-1.275442 l 1.27544,4.862621 q -1.19572,0.717436 -3.98575,1.594302 -2.71031,0.876867 -6.05835,0.876867 z m 11.95723,-0.876867 q 4.30462,-11.55869 7.8918,-21.044787 3.58718,-9.486097 7.01493,-17.776468 3.50746,-8.370086 7.4135,-16.4213111 h 5.58006 q 2.86974,6.0583481 5.50034,12.2761261 2.71032,6.138063 5.34091,12.754416 2.6306,6.616354 5.42063,14.109574 2.86975,7.413504 6.05835,16.10245 h -6.77578 q -1.43488,-3.985755 -2.79003,-7.572934 -1.35516,-3.666895 -2.55089,-7.17436 h -26.30598 q -1.27544,3.507465 -2.6306,7.17436 -1.35516,3.587179 -2.71031,7.572934 z M 242.8946,39.402115 h 22.63909 q -1.51459,-3.985755 -2.94946,-7.732365 -1.43487,-3.746609 -2.86975,-7.254074 -1.35515,-3.507464 -2.79002,-6.775784 -1.35516,-3.348034 -2.79003,-6.536638 -1.35516,3.188604 -2.79003,6.536638 -1.35516,3.26832 -2.79003,6.775784 -1.35516,3.507465 -2.79003,7.254074 -1.43487,3.74661 -2.86974,7.732365 z m 44.481,20.008491 V 5.2043362 q 3.02917,-0.797151 6.93521,-1.1160114 3.98576,-0.3985755 7.33379,-0.3985755 11.71812,0 17.53732,4.5437609 5.89892,4.4640458 5.89892,12.7544168 0,6.297493 -2.94946,10.123818 -2.86974,3.826325 -8.37008,5.580057 -5.42063,1.674017 -13.07328,1.674017 h -7.09465 v 21.044787 z m 6.21777,-26.385699 h 6.53664 q 5.65978,0 9.80496,-0.956581 4.14519,-1.036296 6.37721,-3.666895 2.31174,-2.630598 2.31174,-7.493219 0,-4.703192 -2.39146,-7.254075 -2.39145,-2.550883 -6.21777,-3.587179 -3.82633,-1.0362968 -8.13094,-1.0362968 -2.79003,0 -4.86263,0.2391453 -1.99287,0.1594302 -3.42775,0.3188604 z M 335.0452,59.410606 V 4.1680399 h 6.21778 V 59.410606 Z"
|
||||||
d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" />
|
id="text979-3"
|
||||||
|
aria-label="FastAPI" />
|
||||||
</g>
|
</g>
|
||||||
<path
|
|
||||||
style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#009688;stroke-width:1.99288"
|
|
||||||
d="M 89.523017,59.410606 V 4.1680399 H 122.84393 V 10.784393 H 97.255382 V 27.44485 h 22.718808 v 6.536638 H 97.255382 v 25.429118 z m 52.292963,-5.340912 q 2.6306,0 4.62348,-0.07972 2.07259,-0.15943 3.42774,-0.47829 V 41.155848 q -0.79715,-0.398576 -2.63059,-0.637721 -1.75374,-0.31886 -4.30462,-0.31886 -1.67402,0 -3.58718,0.239145 -1.83345,0.239145 -3.42775,1.036296 -1.51459,0.717436 -2.55088,2.072593 -1.0363,1.275442 -1.0363,3.427749 0,3.985755 2.55089,5.580058 2.55088,1.514586 6.93521,1.514586 z m -0.63772,-37.147238 q 4.46404,0 7.49322,1.195727 3.10889,1.116011 4.94233,3.268319 1.91317,2.072593 2.71032,5.022052 0.79715,2.869743 0.79715,6.377208 V 58.69317 q -0.95658,0.159431 -2.71031,0.478291 -1.67402,0.239145 -3.82633,0.478291 -2.15231,0.239145 -4.70319,0.398575 -2.47117,0.239146 -4.94234,0.239146 -3.50746,0 -6.45692,-0.717436 -2.94946,-0.717436 -5.10177,-2.232023 -2.1523,-1.594302 -3.34803,-4.145186 -1.19573,-2.550883 -1.19573,-6.138063 0,-3.427749 1.35516,-5.898917 1.43487,-2.471168 3.82632,-3.985755 2.39146,-1.514587 5.58006,-2.232023 3.18861,-0.717436 6.69607,-0.717436 1.11601,0 2.31174,0.15943 1.19572,0.07972 2.23202,0.31886 1.11601,0.159431 1.91316,0.318861 0.79715,0.15943 1.11601,0.239145 v -2.072593 q 0,-1.833447 -0.39857,-3.587179 -0.39858,-1.833448 -1.43487,-3.188604 -1.0363,-1.434872 -2.86975,-2.232023 -1.75373,-0.876866 -4.62347,-0.876866 -3.6669,0 -6.45693,0.558005 -2.71031,0.478291 -4.06547,1.036297 l -0.87686,-6.138063 q 1.43487,-0.637721 4.7829,-1.195727 3.34804,-0.637721 7.25408,-0.637721 z m 37.86462,37.147238 q 4.54377,0 6.69607,-1.195726 2.23203,-1.195727 2.23203,-3.826325 0,-2.710314 -2.15231,-4.304616 -2.15231,-1.594302 -7.09465,-3.587179 -2.39145,-0.956581 -4.62347,-1.913163 -2.15231,-1.036296 -3.74661,-2.391453 -1.5943,-1.355157 -2.55088,-3.268319 -0.95659,-1.913163 -0.95659,-4.703191 0,-5.500342 4.06547,-8.688946 4.06547,-3.26832 11.0804,-3.26832 1.75374,0 3.50747,0.239146 1.75373,0.15943 3.26832,0.47829 1.51458,0.239146 2.6306,0.558006 1.19572,0.31886 1.83344,0.558006 l -1.35515,6.377208 q -1.19573,-0.637721 -3.74661,-1.275442 -2.55089,-0.717436 -6.13807,-0.717436 -3.10889,0 -5.42062,1.275442 -2.31174,1.195727 -2.31174,3.826325 0,1.355157 0.47829,2.391453 0.55801,1.036296 1.5943,1.913163 1.11601,0.797151 2.71031,1.514587 1.59431,0.717436 3.82633,1.514587 2.94946,1.116011 5.2612,2.232022 2.31173,1.036297 3.90604,2.471169 1.67401,1.434871 2.55088,3.507464 0.87687,1.992878 0.87687,4.942337 0,5.739487 -4.30462,8.688946 -4.2249,2.949459 -12.1167,2.949459 -5.50034,0 -8.60923,-0.956582 -3.10889,-0.876866 -4.2249,-1.355156 l 1.35516,-6.377209 q 1.27544,0.478291 4.06547,1.434872 2.79003,0.956581 7.4135,0.956581 z m 32.84256,-36.110941 h 15.70387 v 6.217778 h -15.70387 v 19.131625 q 0,3.108889 0.47829,5.181481 0.47829,1.992878 1.43487,3.188604 0.95658,1.116012 2.39145,1.594302 1.43487,0.478291 3.34804,0.478291 3.34803,0 5.34091,-0.717436 2.07259,-0.797151 2.86974,-1.116011 l 1.43487,6.138063 q -1.11601,0.558005 -3.90604,1.355156 -2.79003,0.876867 -6.37721,0.876867 -4.2249,0 -7.01492,-1.036297 -2.71032,-1.116011 -4.38434,-3.268319 -1.67401,-2.152308 -2.39145,-5.261197 -0.63772,-3.188604 -0.63772,-7.333789 V 6.4000628 l 7.41351,-1.2754417 z m 62.49652,41.451853 q -1.35516,-3.587179 -2.55088,-7.014929 -1.19573,-3.507464 -2.47117,-7.094644 h -25.03054 l -5.02205,14.109573 h -8.05123 q 3.18861,-8.768661 5.97863,-16.182166 2.79003,-7.493219 5.42063,-14.189288 2.71031,-6.696069 5.34091,-12.754416 2.6306,-6.138063 5.50034,-12.1166961 h 7.09465 q 2.86974,5.9786331 5.50034,12.1166961 2.6306,6.058347 5.2612,12.754416 2.71031,6.696069 5.50034,14.189288 2.79003,7.413505 5.97863,16.182166 z m -7.25407,-20.486781 q -2.55089,-6.935214 -5.10177,-13.392137 -2.47117,-6.536639 -5.18148,-12.515272 -2.79003,5.978633 -5.34091,12.515272 -2.47117,6.456923 -4.94234,13.392137 z M 304.99242,3.6100342 q 11.6384,0 17.85618,4.4640458 6.29749,4.384331 6.29749,13.152992 0,4.782906 -1.75373,8.210656 -1.67402,3.348034 -4.94234,5.500342 -3.1886,2.072592 -7.81208,3.029174 -4.62347,0.956581 -10.44268,0.956581 h -6.13806 v 20.486781 h -7.73236 V 4.9651909 q 3.26832,-0.797151 7.25407,-1.0362963 4.06547,-0.3188604 7.41351,-0.3188604 z m 0.63772,6.7757838 q -4.94234,0 -7.57294,0.239145 v 21.682508 h 5.8192 q 3.98576,0 7.17436,-0.47829 3.18861,-0.558006 5.34092,-1.753733 2.23202,-1.275441 3.42774,-3.427749 1.19573,-2.152308 1.19573,-5.500342 0,-3.188604 -1.27544,-5.261197 -1.19573,-2.072593 -3.34803,-3.268319 -2.0726,-1.275442 -4.86263,-1.753732 -2.79002,-0.478291 -5.89891,-0.478291 z M 338.7916,4.1680399 h 7.73237 V 59.410606 h -7.73237 z"
|
|
||||||
id="text979"
|
|
||||||
aria-label="FastAPI" />
|
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
<rect
|
||||||
|
y="-49.422424"
|
||||||
|
x="-51.908718"
|
||||||
|
height="162.82199"
|
||||||
|
width="451.52316"
|
||||||
|
id="rect824"
|
||||||
|
style="opacity:0.98;fill:none;fill-opacity:1;stroke-width:0.311037" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 7.1 KiB |
@@ -1,25 +1,21 @@
|
|||||||
import type { ApiRequestOptions } from "./ApiRequestOptions"
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
import type { ApiResult } from "./ApiResult"
|
import type { ApiResult } from './ApiResult';
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
public readonly url: string
|
public readonly url: string;
|
||||||
public readonly status: number
|
public readonly status: number;
|
||||||
public readonly statusText: string
|
public readonly statusText: string;
|
||||||
public readonly body: unknown
|
public readonly body: unknown;
|
||||||
public readonly request: ApiRequestOptions
|
public readonly request: ApiRequestOptions;
|
||||||
|
|
||||||
constructor(
|
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||||
request: ApiRequestOptions,
|
super(message);
|
||||||
response: ApiResult,
|
|
||||||
message: string,
|
|
||||||
) {
|
|
||||||
super(message)
|
|
||||||
|
|
||||||
this.name = "ApiError"
|
this.name = 'ApiError';
|
||||||
this.url = response.url
|
this.url = response.url;
|
||||||
this.status = response.status
|
this.status = response.status;
|
||||||
this.statusText = response.statusText
|
this.statusText = response.statusText;
|
||||||
this.body = response.body
|
this.body = response.body;
|
||||||
this.request = request
|
this.request = request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
export type ApiRequestOptions<T = unknown> = {
|
export type ApiRequestOptions<T = unknown> = {
|
||||||
readonly body?: any
|
readonly body?: any;
|
||||||
readonly cookies?: Record<string, unknown>
|
readonly cookies?: Record<string, unknown>;
|
||||||
readonly errors?: Record<number | string, string>
|
readonly errors?: Record<number | string, string>;
|
||||||
readonly formData?: Record<string, unknown> | any[] | Blob | File
|
readonly formData?: Record<string, unknown> | any[] | Blob | File;
|
||||||
readonly headers?: Record<string, unknown>
|
readonly headers?: Record<string, unknown>;
|
||||||
readonly mediaType?: string
|
readonly mediaType?: string;
|
||||||
readonly method:
|
readonly method:
|
||||||
| "DELETE"
|
| 'DELETE'
|
||||||
| "GET"
|
| 'GET'
|
||||||
| "HEAD"
|
| 'HEAD'
|
||||||
| "OPTIONS"
|
| 'OPTIONS'
|
||||||
| "PATCH"
|
| 'PATCH'
|
||||||
| "POST"
|
| 'POST'
|
||||||
| "PUT"
|
| 'PUT';
|
||||||
readonly path?: Record<string, unknown>
|
readonly path?: Record<string, unknown>;
|
||||||
readonly query?: Record<string, unknown>
|
readonly query?: Record<string, unknown>;
|
||||||
readonly responseHeader?: string
|
readonly responseHeader?: string;
|
||||||
readonly responseTransformer?: (data: unknown) => Promise<T>
|
readonly responseTransformer?: (data: unknown) => Promise<T>;
|
||||||
readonly url: string
|
readonly url: string;
|
||||||
}
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export type ApiResult<TData = any> = {
|
export type ApiResult<TData = any> = {
|
||||||
readonly body: TData
|
readonly body: TData;
|
||||||
readonly ok: boolean
|
readonly ok: boolean;
|
||||||
readonly status: number
|
readonly status: number;
|
||||||
readonly statusText: string
|
readonly statusText: string;
|
||||||
readonly url: string
|
readonly url: string;
|
||||||
}
|
};
|
||||||
@@ -1,126 +1,126 @@
|
|||||||
export class CancelError extends Error {
|
export class CancelError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message)
|
super(message);
|
||||||
this.name = "CancelError"
|
this.name = 'CancelError';
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isCancelled(): boolean {
|
public get isCancelled(): boolean {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnCancel {
|
export interface OnCancel {
|
||||||
readonly isResolved: boolean
|
readonly isResolved: boolean;
|
||||||
readonly isRejected: boolean
|
readonly isRejected: boolean;
|
||||||
readonly isCancelled: boolean
|
readonly isCancelled: boolean;
|
||||||
|
|
||||||
(cancelHandler: () => void): void
|
(cancelHandler: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CancelablePromise<T> implements Promise<T> {
|
export class CancelablePromise<T> implements Promise<T> {
|
||||||
private _isResolved: boolean
|
private _isResolved: boolean;
|
||||||
private _isRejected: boolean
|
private _isRejected: boolean;
|
||||||
private _isCancelled: boolean
|
private _isCancelled: boolean;
|
||||||
readonly cancelHandlers: (() => void)[]
|
readonly cancelHandlers: (() => void)[];
|
||||||
readonly promise: Promise<T>
|
readonly promise: Promise<T>;
|
||||||
private _resolve?: (value: T | PromiseLike<T>) => void
|
private _resolve?: (value: T | PromiseLike<T>) => void;
|
||||||
private _reject?: (reason?: unknown) => void
|
private _reject?: (reason?: unknown) => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
executor: (
|
executor: (
|
||||||
resolve: (value: T | PromiseLike<T>) => void,
|
resolve: (value: T | PromiseLike<T>) => void,
|
||||||
reject: (reason?: unknown) => void,
|
reject: (reason?: unknown) => void,
|
||||||
onCancel: OnCancel,
|
onCancel: OnCancel
|
||||||
) => void,
|
) => void
|
||||||
) {
|
) {
|
||||||
this._isResolved = false
|
this._isResolved = false;
|
||||||
this._isRejected = false
|
this._isRejected = false;
|
||||||
this._isCancelled = false
|
this._isCancelled = false;
|
||||||
this.cancelHandlers = []
|
this.cancelHandlers = [];
|
||||||
this.promise = new Promise<T>((resolve, reject) => {
|
this.promise = new Promise<T>((resolve, reject) => {
|
||||||
this._resolve = resolve
|
this._resolve = resolve;
|
||||||
this._reject = reject
|
this._reject = reject;
|
||||||
|
|
||||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this._isResolved = true
|
this._isResolved = true;
|
||||||
if (this._resolve) this._resolve(value)
|
if (this._resolve) this._resolve(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onReject = (reason?: unknown): void => {
|
const onReject = (reason?: unknown): void => {
|
||||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this._isRejected = true
|
this._isRejected = true;
|
||||||
if (this._reject) this._reject(reason)
|
if (this._reject) this._reject(reason);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onCancel = (cancelHandler: () => void): void => {
|
const onCancel = (cancelHandler: () => void): void => {
|
||||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.cancelHandlers.push(cancelHandler)
|
this.cancelHandlers.push(cancelHandler);
|
||||||
}
|
};
|
||||||
|
|
||||||
Object.defineProperty(onCancel, "isResolved", {
|
Object.defineProperty(onCancel, 'isResolved', {
|
||||||
get: (): boolean => this._isResolved,
|
get: (): boolean => this._isResolved,
|
||||||
})
|
});
|
||||||
|
|
||||||
Object.defineProperty(onCancel, "isRejected", {
|
Object.defineProperty(onCancel, 'isRejected', {
|
||||||
get: (): boolean => this._isRejected,
|
get: (): boolean => this._isRejected,
|
||||||
})
|
});
|
||||||
|
|
||||||
Object.defineProperty(onCancel, "isCancelled", {
|
Object.defineProperty(onCancel, 'isCancelled', {
|
||||||
get: (): boolean => this._isCancelled,
|
get: (): boolean => this._isCancelled,
|
||||||
})
|
});
|
||||||
|
|
||||||
return executor(onResolve, onReject, onCancel as OnCancel)
|
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
get [Symbol.toStringTag]() {
|
||||||
return "Cancellable Promise"
|
return "Cancellable Promise";
|
||||||
}
|
}
|
||||||
|
|
||||||
public then<TResult1 = T, TResult2 = never>(
|
public then<TResult1 = T, TResult2 = never>(
|
||||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||||
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
|
||||||
): Promise<TResult1 | TResult2> {
|
): Promise<TResult1 | TResult2> {
|
||||||
return this.promise.then(onFulfilled, onRejected)
|
return this.promise.then(onFulfilled, onRejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
public catch<TResult = never>(
|
public catch<TResult = never>(
|
||||||
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
|
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
|
||||||
): Promise<T | TResult> {
|
): Promise<T | TResult> {
|
||||||
return this.promise.catch(onRejected)
|
return this.promise.catch(onRejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||||
return this.promise.finally(onFinally)
|
return this.promise.finally(onFinally);
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancel(): void {
|
public cancel(): void {
|
||||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this._isCancelled = true
|
this._isCancelled = true;
|
||||||
if (this.cancelHandlers.length) {
|
if (this.cancelHandlers.length) {
|
||||||
try {
|
try {
|
||||||
for (const cancelHandler of this.cancelHandlers) {
|
for (const cancelHandler of this.cancelHandlers) {
|
||||||
cancelHandler()
|
cancelHandler();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Cancellation threw an error", error)
|
console.warn('Cancellation threw an error', error);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.cancelHandlers.length = 0
|
this.cancelHandlers.length = 0;
|
||||||
if (this._reject) this._reject(new CancelError("Request aborted"))
|
if (this._reject) this._reject(new CancelError('Request aborted'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isCancelled(): boolean {
|
public get isCancelled(): boolean {
|
||||||
return this._isCancelled
|
return this._isCancelled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,57 @@
|
|||||||
import type { AxiosRequestConfig, AxiosResponse } from "axios"
|
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import type { ApiRequestOptions } from "./ApiRequestOptions"
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
|
|
||||||
type Headers = Record<string, string>
|
type Headers = Record<string, string>;
|
||||||
type Middleware<T> = (value: T) => T | Promise<T>
|
type Middleware<T> = (value: T) => T | Promise<T>;
|
||||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>
|
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||||
|
|
||||||
export class Interceptors<T> {
|
export class Interceptors<T> {
|
||||||
_fns: Middleware<T>[]
|
_fns: Middleware<T>[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._fns = []
|
this._fns = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
eject(fn: Middleware<T>): void {
|
eject(fn: Middleware<T>): void {
|
||||||
const index = this._fns.indexOf(fn)
|
const index = this._fns.indexOf(fn);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]
|
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use(fn: Middleware<T>): void {
|
use(fn: Middleware<T>): void {
|
||||||
this._fns = [...this._fns, fn]
|
this._fns = [...this._fns, fn];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpenAPIConfig = {
|
export type OpenAPIConfig = {
|
||||||
BASE: string
|
BASE: string;
|
||||||
CREDENTIALS: "include" | "omit" | "same-origin"
|
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||||
ENCODE_PATH?: ((path: string) => string) | undefined
|
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||||
HEADERS?: Headers | Resolver<Headers> | undefined
|
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||||
PASSWORD?: string | Resolver<string> | undefined
|
PASSWORD?: string | Resolver<string> | undefined;
|
||||||
TOKEN?: string | Resolver<string> | undefined
|
TOKEN?: string | Resolver<string> | undefined;
|
||||||
USERNAME?: string | Resolver<string> | undefined
|
USERNAME?: string | Resolver<string> | undefined;
|
||||||
VERSION: string
|
VERSION: string;
|
||||||
WITH_CREDENTIALS: boolean
|
WITH_CREDENTIALS: boolean;
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: Interceptors<AxiosRequestConfig>
|
request: Interceptors<AxiosRequestConfig>;
|
||||||
response: Interceptors<AxiosResponse>
|
response: Interceptors<AxiosResponse>;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const OpenAPI: OpenAPIConfig = {
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
BASE: "",
|
BASE: '',
|
||||||
CREDENTIALS: "include",
|
CREDENTIALS: 'include',
|
||||||
ENCODE_PATH: undefined,
|
ENCODE_PATH: undefined,
|
||||||
HEADERS: undefined,
|
HEADERS: undefined,
|
||||||
PASSWORD: undefined,
|
PASSWORD: undefined,
|
||||||
TOKEN: undefined,
|
TOKEN: undefined,
|
||||||
USERNAME: undefined,
|
USERNAME: undefined,
|
||||||
VERSION: "0.1.0",
|
VERSION: '0.1.0',
|
||||||
WITH_CREDENTIALS: false,
|
WITH_CREDENTIALS: false,
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: new Interceptors(),
|
request: new Interceptors(),
|
||||||
response: new Interceptors(),
|
response: new Interceptors(),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
+279
-319
@@ -1,325 +1,301 @@
|
|||||||
import axios from "axios"
|
import axios from 'axios';
|
||||||
import type {
|
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
|
||||||
AxiosError,
|
|
||||||
AxiosRequestConfig,
|
|
||||||
AxiosResponse,
|
|
||||||
AxiosInstance,
|
|
||||||
} from "axios"
|
|
||||||
|
|
||||||
import { ApiError } from "./ApiError"
|
import { ApiError } from './ApiError';
|
||||||
import type { ApiRequestOptions } from "./ApiRequestOptions"
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
import type { ApiResult } from "./ApiResult"
|
import type { ApiResult } from './ApiResult';
|
||||||
import { CancelablePromise } from "./CancelablePromise"
|
import { CancelablePromise } from './CancelablePromise';
|
||||||
import type { OnCancel } from "./CancelablePromise"
|
import type { OnCancel } from './CancelablePromise';
|
||||||
import type { OpenAPIConfig } from "./OpenAPI"
|
import type { OpenAPIConfig } from './OpenAPI';
|
||||||
|
|
||||||
export const isString = (value: unknown): value is string => {
|
export const isString = (value: unknown): value is string => {
|
||||||
return typeof value === "string"
|
return typeof value === 'string';
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isStringWithValue = (value: unknown): value is string => {
|
export const isStringWithValue = (value: unknown): value is string => {
|
||||||
return isString(value) && value !== ""
|
return isString(value) && value !== '';
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isBlob = (value: any): value is Blob => {
|
export const isBlob = (value: any): value is Blob => {
|
||||||
return value instanceof Blob
|
return value instanceof Blob;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isFormData = (value: unknown): value is FormData => {
|
export const isFormData = (value: unknown): value is FormData => {
|
||||||
return value instanceof FormData
|
return value instanceof FormData;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isSuccess = (status: number): boolean => {
|
export const isSuccess = (status: number): boolean => {
|
||||||
return status >= 200 && status < 300
|
return status >= 200 && status < 300;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const base64 = (str: string): string => {
|
export const base64 = (str: string): string => {
|
||||||
try {
|
try {
|
||||||
return btoa(str)
|
return btoa(str);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return Buffer.from(str).toString("base64")
|
return Buffer.from(str).toString('base64');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getQueryString = (params: Record<string, unknown>): string => {
|
export const getQueryString = (params: Record<string, unknown>): string => {
|
||||||
const qs: string[] = []
|
const qs: string[] = [];
|
||||||
|
|
||||||
const append = (key: string, value: unknown) => {
|
const append = (key: string, value: unknown) => {
|
||||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const encodePair = (key: string, value: unknown) => {
|
const encodePair = (key: string, value: unknown) => {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
append(key, value.toISOString())
|
append(key, value.toISOString());
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
value.forEach((v) => encodePair(key, v))
|
value.forEach(v => encodePair(key, v));
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === 'object') {
|
||||||
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v))
|
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
|
||||||
} else {
|
} else {
|
||||||
append(key, value)
|
append(key, value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Object.entries(params).forEach(([key, value]) => encodePair(key, value))
|
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
|
||||||
|
|
||||||
return qs.length ? `?${qs.join("&")}` : ""
|
return qs.length ? `?${qs.join('&')}` : '';
|
||||||
}
|
};
|
||||||
|
|
||||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||||
const encoder = config.ENCODE_PATH || encodeURI
|
const encoder = config.ENCODE_PATH || encodeURI;
|
||||||
|
|
||||||
const path = options.url
|
const path = options.url
|
||||||
.replace("{api-version}", config.VERSION)
|
.replace('{api-version}', config.VERSION)
|
||||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||||
if (options.path?.hasOwnProperty(group)) {
|
if (options.path?.hasOwnProperty(group)) {
|
||||||
return encoder(String(options.path[group]))
|
return encoder(String(options.path[group]));
|
||||||
}
|
}
|
||||||
return substring
|
return substring;
|
||||||
})
|
});
|
||||||
|
|
||||||
const url = config.BASE + path
|
const url = config.BASE + path;
|
||||||
return options.query ? url + getQueryString(options.query) : url
|
return options.query ? url + getQueryString(options.query) : url;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getFormData = (
|
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||||
options: ApiRequestOptions,
|
if (options.formData) {
|
||||||
): FormData | undefined => {
|
const formData = new FormData();
|
||||||
if (options.formData) {
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
const process = (key: string, value: unknown) => {
|
const process = (key: string, value: unknown) => {
|
||||||
if (isString(value) || isBlob(value)) {
|
if (isString(value) || isBlob(value)) {
|
||||||
formData.append(key, value)
|
formData.append(key, value);
|
||||||
} else {
|
} else {
|
||||||
formData.append(key, JSON.stringify(value))
|
formData.append(key, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Object.entries(options.formData)
|
Object.entries(options.formData)
|
||||||
.filter(([, value]) => value !== undefined && value !== null)
|
.filter(([, value]) => value !== undefined && value !== null)
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((v) => process(key, v))
|
value.forEach(v => process(key, v));
|
||||||
} else {
|
} else {
|
||||||
process(key, value)
|
process(key, value);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return formData
|
return formData;
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>
|
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||||
|
|
||||||
export const resolve = async <T>(
|
export const resolve = async <T>(options: ApiRequestOptions<T>, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||||
options: ApiRequestOptions<T>,
|
if (typeof resolver === 'function') {
|
||||||
resolver?: T | Resolver<T>,
|
return (resolver as Resolver<T>)(options);
|
||||||
): Promise<T | undefined> => {
|
}
|
||||||
if (typeof resolver === "function") {
|
return resolver;
|
||||||
return (resolver as Resolver<T>)(options)
|
};
|
||||||
}
|
|
||||||
return resolver
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getHeaders = async <T>(
|
export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>): Promise<Record<string, string>> => {
|
||||||
config: OpenAPIConfig,
|
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||||
options: ApiRequestOptions<T>,
|
// @ts-ignore
|
||||||
): Promise<Record<string, string>> => {
|
resolve(options, config.TOKEN),
|
||||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
// @ts-ignore
|
||||||
// @ts-ignore
|
resolve(options, config.USERNAME),
|
||||||
resolve(options, config.TOKEN),
|
// @ts-ignore
|
||||||
// @ts-ignore
|
resolve(options, config.PASSWORD),
|
||||||
resolve(options, config.USERNAME),
|
// @ts-ignore
|
||||||
// @ts-ignore
|
resolve(options, config.HEADERS),
|
||||||
resolve(options, config.PASSWORD),
|
]);
|
||||||
// @ts-ignore
|
|
||||||
resolve(options, config.HEADERS),
|
|
||||||
])
|
|
||||||
|
|
||||||
const headers = Object.entries({
|
const headers = Object.entries({
|
||||||
Accept: "application/json",
|
Accept: 'application/json',
|
||||||
...additionalHeaders,
|
...additionalHeaders,
|
||||||
...options.headers,
|
...options.headers,
|
||||||
})
|
})
|
||||||
.filter(([, value]) => value !== undefined && value !== null)
|
.filter(([, value]) => value !== undefined && value !== null)
|
||||||
.reduce(
|
.reduce((headers, [key, value]) => ({
|
||||||
(headers, [key, value]) => ({
|
...headers,
|
||||||
...headers,
|
[key]: String(value),
|
||||||
[key]: String(value),
|
}), {} as Record<string, string>);
|
||||||
}),
|
|
||||||
{} as Record<string, string>,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isStringWithValue(token)) {
|
if (isStringWithValue(token)) {
|
||||||
headers["Authorization"] = `Bearer ${token}`
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||||
const credentials = base64(`${username}:${password}`)
|
const credentials = base64(`${username}:${password}`);
|
||||||
headers["Authorization"] = `Basic ${credentials}`
|
headers['Authorization'] = `Basic ${credentials}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.body !== undefined) {
|
if (options.body !== undefined) {
|
||||||
if (options.mediaType) {
|
if (options.mediaType) {
|
||||||
headers["Content-Type"] = options.mediaType
|
headers['Content-Type'] = options.mediaType;
|
||||||
} else if (isBlob(options.body)) {
|
} else if (isBlob(options.body)) {
|
||||||
headers["Content-Type"] = options.body.type || "application/octet-stream"
|
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||||
} else if (isString(options.body)) {
|
} else if (isString(options.body)) {
|
||||||
headers["Content-Type"] = "text/plain"
|
headers['Content-Type'] = 'text/plain';
|
||||||
} else if (!isFormData(options.body)) {
|
} else if (!isFormData(options.body)) {
|
||||||
headers["Content-Type"] = "application/json"
|
headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
} else if (options.formData !== undefined) {
|
} else if (options.formData !== undefined) {
|
||||||
if (options.mediaType) {
|
if (options.mediaType) {
|
||||||
headers["Content-Type"] = options.mediaType
|
headers['Content-Type'] = options.mediaType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
return headers;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getRequestBody = (options: ApiRequestOptions): unknown => {
|
export const getRequestBody = (options: ApiRequestOptions): unknown => {
|
||||||
if (options.body) {
|
if (options.body) {
|
||||||
return options.body
|
return options.body;
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const sendRequest = async <T>(
|
export const sendRequest = async <T>(
|
||||||
config: OpenAPIConfig,
|
config: OpenAPIConfig,
|
||||||
options: ApiRequestOptions<T>,
|
options: ApiRequestOptions<T>,
|
||||||
url: string,
|
url: string,
|
||||||
body: unknown,
|
body: unknown,
|
||||||
formData: FormData | undefined,
|
formData: FormData | undefined,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
onCancel: OnCancel,
|
onCancel: OnCancel,
|
||||||
axiosClient: AxiosInstance,
|
axiosClient: AxiosInstance
|
||||||
): Promise<AxiosResponse<T>> => {
|
): Promise<AxiosResponse<T>> => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController();
|
||||||
|
|
||||||
let requestConfig: AxiosRequestConfig = {
|
let requestConfig: AxiosRequestConfig = {
|
||||||
data: body ?? formData,
|
data: body ?? formData,
|
||||||
headers,
|
headers,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
url,
|
url,
|
||||||
withCredentials: config.WITH_CREDENTIALS,
|
withCredentials: config.WITH_CREDENTIALS,
|
||||||
}
|
};
|
||||||
|
|
||||||
onCancel(() => controller.abort())
|
onCancel(() => controller.abort());
|
||||||
|
|
||||||
for (const fn of config.interceptors.request._fns) {
|
for (const fn of config.interceptors.request._fns) {
|
||||||
requestConfig = await fn(requestConfig)
|
requestConfig = await fn(requestConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await axiosClient.request(requestConfig)
|
return await axiosClient.request(requestConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError<T>
|
const axiosError = error as AxiosError<T>;
|
||||||
if (axiosError.response) {
|
if (axiosError.response) {
|
||||||
return axiosError.response
|
return axiosError.response;
|
||||||
}
|
}
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getResponseHeader = (
|
export const getResponseHeader = (response: AxiosResponse<unknown>, responseHeader?: string): string | undefined => {
|
||||||
response: AxiosResponse<unknown>,
|
if (responseHeader) {
|
||||||
responseHeader?: string,
|
const content = response.headers[responseHeader];
|
||||||
): string | undefined => {
|
if (isString(content)) {
|
||||||
if (responseHeader) {
|
return content;
|
||||||
const content = response.headers[responseHeader]
|
}
|
||||||
if (isString(content)) {
|
}
|
||||||
return content
|
return undefined;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
|
export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
|
||||||
if (response.status !== 204) {
|
if (response.status !== 204) {
|
||||||
return response.data
|
return response.data;
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const catchErrorCodes = (
|
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||||
options: ApiRequestOptions,
|
const errors: Record<number, string> = {
|
||||||
result: ApiResult,
|
400: 'Bad Request',
|
||||||
): void => {
|
401: 'Unauthorized',
|
||||||
const errors: Record<number, string> = {
|
402: 'Payment Required',
|
||||||
400: "Bad Request",
|
403: 'Forbidden',
|
||||||
401: "Unauthorized",
|
404: 'Not Found',
|
||||||
402: "Payment Required",
|
405: 'Method Not Allowed',
|
||||||
403: "Forbidden",
|
406: 'Not Acceptable',
|
||||||
404: "Not Found",
|
407: 'Proxy Authentication Required',
|
||||||
405: "Method Not Allowed",
|
408: 'Request Timeout',
|
||||||
406: "Not Acceptable",
|
409: 'Conflict',
|
||||||
407: "Proxy Authentication Required",
|
410: 'Gone',
|
||||||
408: "Request Timeout",
|
411: 'Length Required',
|
||||||
409: "Conflict",
|
412: 'Precondition Failed',
|
||||||
410: "Gone",
|
413: 'Payload Too Large',
|
||||||
411: "Length Required",
|
414: 'URI Too Long',
|
||||||
412: "Precondition Failed",
|
415: 'Unsupported Media Type',
|
||||||
413: "Payload Too Large",
|
416: 'Range Not Satisfiable',
|
||||||
414: "URI Too Long",
|
417: 'Expectation Failed',
|
||||||
415: "Unsupported Media Type",
|
418: 'Im a teapot',
|
||||||
416: "Range Not Satisfiable",
|
421: 'Misdirected Request',
|
||||||
417: "Expectation Failed",
|
422: 'Unprocessable Content',
|
||||||
418: "Im a teapot",
|
423: 'Locked',
|
||||||
421: "Misdirected Request",
|
424: 'Failed Dependency',
|
||||||
422: "Unprocessable Content",
|
425: 'Too Early',
|
||||||
423: "Locked",
|
426: 'Upgrade Required',
|
||||||
424: "Failed Dependency",
|
428: 'Precondition Required',
|
||||||
425: "Too Early",
|
429: 'Too Many Requests',
|
||||||
426: "Upgrade Required",
|
431: 'Request Header Fields Too Large',
|
||||||
428: "Precondition Required",
|
451: 'Unavailable For Legal Reasons',
|
||||||
429: "Too Many Requests",
|
500: 'Internal Server Error',
|
||||||
431: "Request Header Fields Too Large",
|
501: 'Not Implemented',
|
||||||
451: "Unavailable For Legal Reasons",
|
502: 'Bad Gateway',
|
||||||
500: "Internal Server Error",
|
503: 'Service Unavailable',
|
||||||
501: "Not Implemented",
|
504: 'Gateway Timeout',
|
||||||
502: "Bad Gateway",
|
505: 'HTTP Version Not Supported',
|
||||||
503: "Service Unavailable",
|
506: 'Variant Also Negotiates',
|
||||||
504: "Gateway Timeout",
|
507: 'Insufficient Storage',
|
||||||
505: "HTTP Version Not Supported",
|
508: 'Loop Detected',
|
||||||
506: "Variant Also Negotiates",
|
510: 'Not Extended',
|
||||||
507: "Insufficient Storage",
|
511: 'Network Authentication Required',
|
||||||
508: "Loop Detected",
|
...options.errors,
|
||||||
510: "Not Extended",
|
}
|
||||||
511: "Network Authentication Required",
|
|
||||||
...options.errors,
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = errors[result.status]
|
const error = errors[result.status];
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new ApiError(options, result, error)
|
throw new ApiError(options, result, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
const errorStatus = result.status ?? "unknown"
|
const errorStatus = result.status ?? 'unknown';
|
||||||
const errorStatusText = result.statusText ?? "unknown"
|
const errorStatusText = result.statusText ?? 'unknown';
|
||||||
const errorBody = (() => {
|
const errorBody = (() => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(result.body, null, 2)
|
return JSON.stringify(result.body, null, 2);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
})()
|
})();
|
||||||
|
|
||||||
throw new ApiError(
|
throw new ApiError(options, result,
|
||||||
options,
|
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||||
result,
|
);
|
||||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
|
}
|
||||||
)
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request method
|
* Request method
|
||||||
@@ -329,59 +305,43 @@ export const catchErrorCodes = (
|
|||||||
* @returns CancelablePromise<T>
|
* @returns CancelablePromise<T>
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
export const request = <T>(
|
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
|
||||||
config: OpenAPIConfig,
|
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||||
options: ApiRequestOptions<T>,
|
try {
|
||||||
axiosClient: AxiosInstance = axios,
|
const url = getUrl(config, options);
|
||||||
): CancelablePromise<T> => {
|
const formData = getFormData(options);
|
||||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
const body = getRequestBody(options);
|
||||||
try {
|
const headers = await getHeaders(config, options);
|
||||||
const url = getUrl(config, options)
|
|
||||||
const formData = getFormData(options)
|
|
||||||
const body = getRequestBody(options)
|
|
||||||
const headers = await getHeaders(config, options)
|
|
||||||
|
|
||||||
if (!onCancel.isCancelled) {
|
if (!onCancel.isCancelled) {
|
||||||
let response = await sendRequest<T>(
|
let response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
|
||||||
config,
|
|
||||||
options,
|
|
||||||
url,
|
|
||||||
body,
|
|
||||||
formData,
|
|
||||||
headers,
|
|
||||||
onCancel,
|
|
||||||
axiosClient,
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const fn of config.interceptors.response._fns) {
|
for (const fn of config.interceptors.response._fns) {
|
||||||
response = await fn(response)
|
response = await fn(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody = getResponseBody(response)
|
const responseBody = getResponseBody(response);
|
||||||
const responseHeader = getResponseHeader(
|
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||||
response,
|
|
||||||
options.responseHeader,
|
|
||||||
)
|
|
||||||
|
|
||||||
let transformedBody = responseBody
|
let transformedBody = responseBody;
|
||||||
if (options.responseTransformer && isSuccess(response.status)) {
|
if (options.responseTransformer && isSuccess(response.status)) {
|
||||||
transformedBody = await options.responseTransformer(responseBody)
|
transformedBody = await options.responseTransformer(responseBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: ApiResult = {
|
const result: ApiResult = {
|
||||||
url,
|
url,
|
||||||
ok: isSuccess(response.status),
|
ok: isSuccess(response.status),
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
body: responseHeader ?? transformedBody,
|
body: responseHeader ?? transformedBody,
|
||||||
}
|
};
|
||||||
|
|
||||||
catchErrorCodes(options, result)
|
catchErrorCodes(options, result);
|
||||||
|
|
||||||
resolve(result.body)
|
resolve(result.body);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error)
|
reject(error);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
export { ApiError } from "./core/ApiError"
|
export { ApiError } from './core/ApiError';
|
||||||
export { CancelablePromise, CancelError } from "./core/CancelablePromise"
|
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||||
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI"
|
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
|
||||||
export * from "./sdk.gen"
|
export * from './sdk.gen';
|
||||||
export * from "./types.gen"
|
export * from './types.gen';
|
||||||
+456
-431
@@ -1,501 +1,526 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
export const Body_login_login_access_tokenSchema = {
|
export const Body_login_login_access_tokenSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
grant_type: {
|
grant_type: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
type: "string",
|
type: 'string',
|
||||||
pattern: "password",
|
pattern: 'password'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Grant Type'
|
||||||
},
|
},
|
||||||
{
|
username: {
|
||||||
type: "null",
|
type: 'string',
|
||||||
|
title: 'Username'
|
||||||
},
|
},
|
||||||
],
|
password: {
|
||||||
title: "Grant Type",
|
type: 'string',
|
||||||
},
|
title: 'Password'
|
||||||
username: {
|
|
||||||
type: "string",
|
|
||||||
title: "Username",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: "string",
|
|
||||||
title: "Password",
|
|
||||||
},
|
|
||||||
scope: {
|
|
||||||
type: "string",
|
|
||||||
title: "Scope",
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
client_id: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
},
|
},
|
||||||
{
|
scope: {
|
||||||
type: "null",
|
type: 'string',
|
||||||
|
title: 'Scope',
|
||||||
|
default: ''
|
||||||
},
|
},
|
||||||
],
|
client_id: {
|
||||||
title: "Client Id",
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Client Id'
|
||||||
|
},
|
||||||
|
client_secret: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Client Secret'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
client_secret: {
|
type: 'object',
|
||||||
anyOf: [
|
required: ['username', 'password'],
|
||||||
{
|
title: 'Body_login-login_access_token'
|
||||||
type: "string",
|
} as const;
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "null",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: "Client Secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["username", "password"],
|
|
||||||
title: "Body_login-login_access_token",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const HTTPValidationErrorSchema = {
|
export const HTTPValidationErrorSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
detail: {
|
detail: {
|
||||||
items: {
|
items: {
|
||||||
$ref: "#/components/schemas/ValidationError",
|
'$ref': '#/components/schemas/ValidationError'
|
||||||
},
|
},
|
||||||
type: "array",
|
type: 'array',
|
||||||
title: "Detail",
|
title: 'Detail'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
type: 'object',
|
||||||
type: "object",
|
title: 'HTTPValidationError'
|
||||||
title: "HTTPValidationError",
|
} as const;
|
||||||
} as const
|
|
||||||
|
|
||||||
export const ItemCreateSchema = {
|
export const ItemCreateSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
title: "Title",
|
title: 'Title'
|
||||||
},
|
|
||||||
description: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
},
|
||||||
{
|
description: {
|
||||||
type: "null",
|
anyOf: [
|
||||||
},
|
{
|
||||||
],
|
type: 'string',
|
||||||
title: "Description",
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Description'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
type: 'object',
|
||||||
type: "object",
|
required: ['title'],
|
||||||
required: ["title"],
|
title: 'ItemCreate'
|
||||||
title: "ItemCreate",
|
} as const;
|
||||||
} as const
|
|
||||||
|
|
||||||
export const ItemPublicSchema = {
|
export const ItemPublicSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
title: "Title",
|
title: 'Title'
|
||||||
},
|
|
||||||
description: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
},
|
||||||
{
|
description: {
|
||||||
type: "null",
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Description'
|
||||||
},
|
},
|
||||||
],
|
id: {
|
||||||
title: "Description",
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
title: 'Id'
|
||||||
|
},
|
||||||
|
owner_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
title: 'Owner Id'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
id: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['title', 'id', 'owner_id'],
|
||||||
format: "uuid",
|
title: 'ItemPublic'
|
||||||
title: "Id",
|
} as const;
|
||||||
},
|
|
||||||
owner_id: {
|
|
||||||
type: "string",
|
|
||||||
format: "uuid",
|
|
||||||
title: "Owner Id",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["title", "id", "owner_id"],
|
|
||||||
title: "ItemPublic",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const ItemUpdateSchema = {
|
export const ItemUpdateSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
minLength: 1,
|
minLength: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Title'
|
||||||
},
|
},
|
||||||
{
|
description: {
|
||||||
type: "null",
|
anyOf: [
|
||||||
},
|
{
|
||||||
],
|
type: 'string',
|
||||||
title: "Title",
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Description'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: {
|
type: 'object',
|
||||||
anyOf: [
|
title: 'ItemUpdate'
|
||||||
{
|
} as const;
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "null",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: "Description",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
title: "ItemUpdate",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const ItemsPublicSchema = {
|
export const ItemsPublicSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
data: {
|
data: {
|
||||||
items: {
|
items: {
|
||||||
$ref: "#/components/schemas/ItemPublic",
|
'$ref': '#/components/schemas/ItemPublic'
|
||||||
},
|
},
|
||||||
type: "array",
|
type: 'array',
|
||||||
title: "Data",
|
title: 'Data'
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Count'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
count: {
|
type: 'object',
|
||||||
type: "integer",
|
required: ['data', 'count'],
|
||||||
title: "Count",
|
title: 'ItemsPublic'
|
||||||
},
|
} as const;
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["data", "count"],
|
|
||||||
title: "ItemsPublic",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const MessageSchema = {
|
export const MessageSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
message: {
|
message: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
title: "Message",
|
title: 'Message'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
type: 'object',
|
||||||
type: "object",
|
required: ['message'],
|
||||||
required: ["message"],
|
title: 'Message'
|
||||||
title: "Message",
|
} as const;
|
||||||
} as const
|
|
||||||
|
|
||||||
export const NewPasswordSchema = {
|
export const NewPasswordSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
token: {
|
token: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
title: "Token",
|
title: 'Token'
|
||||||
|
},
|
||||||
|
new_password: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 128,
|
||||||
|
minLength: 8,
|
||||||
|
title: 'New Password'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
new_password: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['token', 'new_password'],
|
||||||
maxLength: 40,
|
title: 'NewPassword'
|
||||||
minLength: 8,
|
} as const;
|
||||||
title: "New Password",
|
|
||||||
|
export const PrivateUserCreateSchema = {
|
||||||
|
properties: {
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Email'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Password'
|
||||||
|
},
|
||||||
|
full_name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Full Name'
|
||||||
|
},
|
||||||
|
is_verified: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'Is Verified',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
type: 'object',
|
||||||
type: "object",
|
required: ['email', 'password', 'full_name'],
|
||||||
required: ["token", "new_password"],
|
title: 'PrivateUserCreate'
|
||||||
title: "NewPassword",
|
} as const;
|
||||||
} as const
|
|
||||||
|
|
||||||
export const TokenSchema = {
|
export const TokenSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
access_token: {
|
access_token: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
title: "Access Token",
|
title: 'Access Token'
|
||||||
|
},
|
||||||
|
token_type: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Token Type',
|
||||||
|
default: 'bearer'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
token_type: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['access_token'],
|
||||||
title: "Token Type",
|
title: 'Token'
|
||||||
default: "bearer",
|
} as const;
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["access_token"],
|
|
||||||
title: "Token",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UpdatePasswordSchema = {
|
export const UpdatePasswordSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
current_password: {
|
current_password: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 40,
|
maxLength: 128,
|
||||||
minLength: 8,
|
minLength: 8,
|
||||||
title: "Current Password",
|
title: 'Current Password'
|
||||||
|
},
|
||||||
|
new_password: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 128,
|
||||||
|
minLength: 8,
|
||||||
|
title: 'New Password'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
new_password: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['current_password', 'new_password'],
|
||||||
maxLength: 40,
|
title: 'UpdatePassword'
|
||||||
minLength: 8,
|
} as const;
|
||||||
title: "New Password",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["current_password", "new_password"],
|
|
||||||
title: "UpdatePassword",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UserCreateSchema = {
|
export const UserCreateSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
email: {
|
email: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
format: "email",
|
format: 'email',
|
||||||
title: "Email",
|
title: 'Email'
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: "boolean",
|
|
||||||
title: "Is Active",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
is_superuser: {
|
|
||||||
type: "boolean",
|
|
||||||
title: "Is Superuser",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
full_name: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
},
|
||||||
{
|
is_active: {
|
||||||
type: "null",
|
type: 'boolean',
|
||||||
|
title: 'Is Active',
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
],
|
is_superuser: {
|
||||||
title: "Full Name",
|
type: 'boolean',
|
||||||
|
title: 'Is Superuser',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
full_name: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Full Name'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 128,
|
||||||
|
minLength: 8,
|
||||||
|
title: 'Password'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
password: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['email', 'password'],
|
||||||
maxLength: 40,
|
title: 'UserCreate'
|
||||||
minLength: 8,
|
} as const;
|
||||||
title: "Password",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["email", "password"],
|
|
||||||
title: "UserCreate",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UserPublicSchema = {
|
export const UserPublicSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
email: {
|
email: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
format: "email",
|
format: 'email',
|
||||||
title: "Email",
|
title: 'Email'
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: "boolean",
|
|
||||||
title: "Is Active",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
is_superuser: {
|
|
||||||
type: "boolean",
|
|
||||||
title: "Is Superuser",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
full_name: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
},
|
||||||
{
|
is_active: {
|
||||||
type: "null",
|
type: 'boolean',
|
||||||
|
title: 'Is Active',
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
],
|
is_superuser: {
|
||||||
title: "Full Name",
|
type: 'boolean',
|
||||||
|
title: 'Is Superuser',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
full_name: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Full Name'
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
title: 'Id'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
id: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['email', 'id'],
|
||||||
format: "uuid",
|
title: 'UserPublic'
|
||||||
title: "Id",
|
} as const;
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["email", "id"],
|
|
||||||
title: "UserPublic",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UserRegisterSchema = {
|
export const UserRegisterSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
email: {
|
email: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
format: "email",
|
format: 'email',
|
||||||
title: "Email",
|
title: 'Email'
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: "string",
|
|
||||||
maxLength: 40,
|
|
||||||
minLength: 8,
|
|
||||||
title: "Password",
|
|
||||||
},
|
|
||||||
full_name: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
},
|
||||||
{
|
password: {
|
||||||
type: "null",
|
type: 'string',
|
||||||
|
maxLength: 128,
|
||||||
|
minLength: 8,
|
||||||
|
title: 'Password'
|
||||||
},
|
},
|
||||||
],
|
full_name: {
|
||||||
title: "Full Name",
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Full Name'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
type: 'object',
|
||||||
type: "object",
|
required: ['email', 'password'],
|
||||||
required: ["email", "password"],
|
title: 'UserRegister'
|
||||||
title: "UserRegister",
|
} as const;
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UserUpdateSchema = {
|
export const UserUpdateSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
email: {
|
email: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
format: "email",
|
format: 'email'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Email'
|
||||||
},
|
},
|
||||||
{
|
is_active: {
|
||||||
type: "null",
|
type: 'boolean',
|
||||||
|
title: 'Is Active',
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
],
|
is_superuser: {
|
||||||
title: "Email",
|
type: 'boolean',
|
||||||
|
title: 'Is Superuser',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
full_name: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Full Name'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 128,
|
||||||
|
minLength: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Password'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
is_active: {
|
type: 'object',
|
||||||
type: "boolean",
|
title: 'UserUpdate'
|
||||||
title: "Is Active",
|
} as const;
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
is_superuser: {
|
|
||||||
type: "boolean",
|
|
||||||
title: "Is Superuser",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
full_name: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "null",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: "Full Name",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
type: "string",
|
|
||||||
maxLength: 40,
|
|
||||||
minLength: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "null",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: "Password",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
title: "UserUpdate",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UserUpdateMeSchema = {
|
export const UserUpdateMeSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
full_name: {
|
full_name: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
type: "string",
|
type: 'string',
|
||||||
maxLength: 255,
|
maxLength: 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Full Name'
|
||||||
},
|
},
|
||||||
{
|
email: {
|
||||||
type: "null",
|
anyOf: [
|
||||||
},
|
{
|
||||||
],
|
type: 'string',
|
||||||
title: "Full Name",
|
maxLength: 255,
|
||||||
|
format: 'email'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Email'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
email: {
|
type: 'object',
|
||||||
anyOf: [
|
title: 'UserUpdateMe'
|
||||||
{
|
} as const;
|
||||||
type: "string",
|
|
||||||
maxLength: 255,
|
|
||||||
format: "email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "null",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: "Email",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
title: "UserUpdateMe",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const UsersPublicSchema = {
|
export const UsersPublicSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
data: {
|
data: {
|
||||||
items: {
|
items: {
|
||||||
$ref: "#/components/schemas/UserPublic",
|
'$ref': '#/components/schemas/UserPublic'
|
||||||
},
|
},
|
||||||
type: "array",
|
type: 'array',
|
||||||
title: "Data",
|
title: 'Data'
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Count'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
count: {
|
type: 'object',
|
||||||
type: "integer",
|
required: ['data', 'count'],
|
||||||
title: "Count",
|
title: 'UsersPublic'
|
||||||
},
|
} as const;
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["data", "count"],
|
|
||||||
title: "UsersPublic",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const ValidationErrorSchema = {
|
export const ValidationErrorSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
loc: {
|
loc: {
|
||||||
items: {
|
items: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
type: "string",
|
type: 'string'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "integer",
|
type: 'integer'
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
type: "array",
|
type: 'array',
|
||||||
title: "Location",
|
title: 'Location'
|
||||||
|
},
|
||||||
|
msg: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Message'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Error Type'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
msg: {
|
type: 'object',
|
||||||
type: "string",
|
required: ['loc', 'msg', 'type'],
|
||||||
title: "Message",
|
title: 'ValidationError'
|
||||||
},
|
} as const;
|
||||||
type: {
|
|
||||||
type: "string",
|
|
||||||
title: "Error Type",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "object",
|
|
||||||
required: ["loc", "msg", "type"],
|
|
||||||
title: "ValidationError",
|
|
||||||
} as const
|
|
||||||
+454
-509
@@ -1,523 +1,468 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { CancelablePromise } from "./core/CancelablePromise"
|
import type { CancelablePromise } from './core/CancelablePromise';
|
||||||
import { OpenAPI } from "./core/OpenAPI"
|
import { OpenAPI } from './core/OpenAPI';
|
||||||
import { request as __request } from "./core/request"
|
import { request as __request } from './core/request';
|
||||||
import type {
|
import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
|
||||||
ItemsReadItemsData,
|
|
||||||
ItemsReadItemsResponse,
|
|
||||||
ItemsCreateItemData,
|
|
||||||
ItemsCreateItemResponse,
|
|
||||||
ItemsReadItemData,
|
|
||||||
ItemsReadItemResponse,
|
|
||||||
ItemsUpdateItemData,
|
|
||||||
ItemsUpdateItemResponse,
|
|
||||||
ItemsDeleteItemData,
|
|
||||||
ItemsDeleteItemResponse,
|
|
||||||
LoginLoginAccessTokenData,
|
|
||||||
LoginLoginAccessTokenResponse,
|
|
||||||
LoginTestTokenResponse,
|
|
||||||
LoginRecoverPasswordData,
|
|
||||||
LoginRecoverPasswordResponse,
|
|
||||||
LoginResetPasswordData,
|
|
||||||
LoginResetPasswordResponse,
|
|
||||||
LoginRecoverPasswordHtmlContentData,
|
|
||||||
LoginRecoverPasswordHtmlContentResponse,
|
|
||||||
UsersReadUsersData,
|
|
||||||
UsersReadUsersResponse,
|
|
||||||
UsersCreateUserData,
|
|
||||||
UsersCreateUserResponse,
|
|
||||||
UsersReadUserMeResponse,
|
|
||||||
UsersDeleteUserMeResponse,
|
|
||||||
UsersUpdateUserMeData,
|
|
||||||
UsersUpdateUserMeResponse,
|
|
||||||
UsersUpdatePasswordMeData,
|
|
||||||
UsersUpdatePasswordMeResponse,
|
|
||||||
UsersRegisterUserData,
|
|
||||||
UsersRegisterUserResponse,
|
|
||||||
UsersReadUserByIdData,
|
|
||||||
UsersReadUserByIdResponse,
|
|
||||||
UsersUpdateUserData,
|
|
||||||
UsersUpdateUserResponse,
|
|
||||||
UsersDeleteUserData,
|
|
||||||
UsersDeleteUserResponse,
|
|
||||||
UtilsTestEmailData,
|
|
||||||
UtilsTestEmailResponse,
|
|
||||||
UtilsHealthCheckResponse,
|
|
||||||
} from "./types.gen"
|
|
||||||
|
|
||||||
export class ItemsService {
|
export class ItemsService {
|
||||||
/**
|
/**
|
||||||
* Read Items
|
* Read Items
|
||||||
* Retrieve items.
|
* Retrieve items.
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.skip
|
* @param data.skip
|
||||||
* @param data.limit
|
* @param data.limit
|
||||||
* @returns ItemsPublic Successful Response
|
* @returns ItemsPublic Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static readItems(
|
public static readItems(data: ItemsReadItemsData = {}): CancelablePromise<ItemsReadItemsResponse> {
|
||||||
data: ItemsReadItemsData = {},
|
return __request(OpenAPI, {
|
||||||
): CancelablePromise<ItemsReadItemsResponse> {
|
method: 'GET',
|
||||||
return __request(OpenAPI, {
|
url: '/api/v1/items/',
|
||||||
method: "GET",
|
query: {
|
||||||
url: "/api/v1/items/",
|
skip: data.skip,
|
||||||
query: {
|
limit: data.limit
|
||||||
skip: data.skip,
|
},
|
||||||
limit: data.limit,
|
errors: {
|
||||||
},
|
422: 'Validation Error'
|
||||||
errors: {
|
}
|
||||||
422: "Validation Error",
|
});
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
/**
|
||||||
|
* Create Item
|
||||||
/**
|
* Create new item.
|
||||||
* Create Item
|
* @param data The data for the request.
|
||||||
* Create new item.
|
* @param data.requestBody
|
||||||
* @param data The data for the request.
|
* @returns ItemPublic Successful Response
|
||||||
* @param data.requestBody
|
* @throws ApiError
|
||||||
* @returns ItemPublic Successful Response
|
*/
|
||||||
* @throws ApiError
|
public static createItem(data: ItemsCreateItemData): CancelablePromise<ItemsCreateItemResponse> {
|
||||||
*/
|
return __request(OpenAPI, {
|
||||||
public static createItem(
|
method: 'POST',
|
||||||
data: ItemsCreateItemData,
|
url: '/api/v1/items/',
|
||||||
): CancelablePromise<ItemsCreateItemResponse> {
|
body: data.requestBody,
|
||||||
return __request(OpenAPI, {
|
mediaType: 'application/json',
|
||||||
method: "POST",
|
errors: {
|
||||||
url: "/api/v1/items/",
|
422: 'Validation Error'
|
||||||
body: data.requestBody,
|
}
|
||||||
mediaType: "application/json",
|
});
|
||||||
errors: {
|
}
|
||||||
422: "Validation Error",
|
|
||||||
},
|
/**
|
||||||
})
|
* Read Item
|
||||||
}
|
* Get item by ID.
|
||||||
|
* @param data The data for the request.
|
||||||
/**
|
* @param data.id
|
||||||
* Read Item
|
* @returns ItemPublic Successful Response
|
||||||
* Get item by ID.
|
* @throws ApiError
|
||||||
* @param data The data for the request.
|
*/
|
||||||
* @param data.id
|
public static readItem(data: ItemsReadItemData): CancelablePromise<ItemsReadItemResponse> {
|
||||||
* @returns ItemPublic Successful Response
|
return __request(OpenAPI, {
|
||||||
* @throws ApiError
|
method: 'GET',
|
||||||
*/
|
url: '/api/v1/items/{id}',
|
||||||
public static readItem(
|
path: {
|
||||||
data: ItemsReadItemData,
|
id: data.id
|
||||||
): CancelablePromise<ItemsReadItemResponse> {
|
},
|
||||||
return __request(OpenAPI, {
|
errors: {
|
||||||
method: "GET",
|
422: 'Validation Error'
|
||||||
url: "/api/v1/items/{id}",
|
}
|
||||||
path: {
|
});
|
||||||
id: data.id,
|
}
|
||||||
},
|
|
||||||
errors: {
|
/**
|
||||||
422: "Validation Error",
|
* Update Item
|
||||||
},
|
* Update an item.
|
||||||
})
|
* @param data The data for the request.
|
||||||
}
|
* @param data.id
|
||||||
|
* @param data.requestBody
|
||||||
/**
|
* @returns ItemPublic Successful Response
|
||||||
* Update Item
|
* @throws ApiError
|
||||||
* Update an item.
|
*/
|
||||||
* @param data The data for the request.
|
public static updateItem(data: ItemsUpdateItemData): CancelablePromise<ItemsUpdateItemResponse> {
|
||||||
* @param data.id
|
return __request(OpenAPI, {
|
||||||
* @param data.requestBody
|
method: 'PUT',
|
||||||
* @returns ItemPublic Successful Response
|
url: '/api/v1/items/{id}',
|
||||||
* @throws ApiError
|
path: {
|
||||||
*/
|
id: data.id
|
||||||
public static updateItem(
|
},
|
||||||
data: ItemsUpdateItemData,
|
body: data.requestBody,
|
||||||
): CancelablePromise<ItemsUpdateItemResponse> {
|
mediaType: 'application/json',
|
||||||
return __request(OpenAPI, {
|
errors: {
|
||||||
method: "PUT",
|
422: 'Validation Error'
|
||||||
url: "/api/v1/items/{id}",
|
}
|
||||||
path: {
|
});
|
||||||
id: data.id,
|
}
|
||||||
},
|
|
||||||
body: data.requestBody,
|
/**
|
||||||
mediaType: "application/json",
|
* Delete Item
|
||||||
errors: {
|
* Delete an item.
|
||||||
422: "Validation Error",
|
* @param data The data for the request.
|
||||||
},
|
* @param data.id
|
||||||
})
|
* @returns Message Successful Response
|
||||||
}
|
* @throws ApiError
|
||||||
|
*/
|
||||||
/**
|
public static deleteItem(data: ItemsDeleteItemData): CancelablePromise<ItemsDeleteItemResponse> {
|
||||||
* Delete Item
|
return __request(OpenAPI, {
|
||||||
* Delete an item.
|
method: 'DELETE',
|
||||||
* @param data The data for the request.
|
url: '/api/v1/items/{id}',
|
||||||
* @param data.id
|
path: {
|
||||||
* @returns Message Successful Response
|
id: data.id
|
||||||
* @throws ApiError
|
},
|
||||||
*/
|
errors: {
|
||||||
public static deleteItem(
|
422: 'Validation Error'
|
||||||
data: ItemsDeleteItemData,
|
}
|
||||||
): CancelablePromise<ItemsDeleteItemResponse> {
|
});
|
||||||
return __request(OpenAPI, {
|
}
|
||||||
method: "DELETE",
|
|
||||||
url: "/api/v1/items/{id}",
|
|
||||||
path: {
|
|
||||||
id: data.id,
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
422: "Validation Error",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoginService {
|
export class LoginService {
|
||||||
/**
|
/**
|
||||||
* Login Access Token
|
* Login Access Token
|
||||||
* OAuth2 compatible token login, get an access token for future requests
|
* OAuth2 compatible token login, get an access token for future requests
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.formData
|
* @param data.formData
|
||||||
* @returns Token Successful Response
|
* @returns Token Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static loginAccessToken(
|
public static loginAccessToken(data: LoginLoginAccessTokenData): CancelablePromise<LoginLoginAccessTokenResponse> {
|
||||||
data: LoginLoginAccessTokenData,
|
return __request(OpenAPI, {
|
||||||
): CancelablePromise<LoginLoginAccessTokenResponse> {
|
method: 'POST',
|
||||||
return __request(OpenAPI, {
|
url: '/api/v1/login/access-token',
|
||||||
method: "POST",
|
formData: data.formData,
|
||||||
url: "/api/v1/login/access-token",
|
mediaType: 'application/x-www-form-urlencoded',
|
||||||
formData: data.formData,
|
errors: {
|
||||||
mediaType: "application/x-www-form-urlencoded",
|
422: 'Validation Error'
|
||||||
errors: {
|
}
|
||||||
422: "Validation Error",
|
});
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
/**
|
||||||
|
* Test Token
|
||||||
|
* Test access token
|
||||||
|
* @returns UserPublic Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static testToken(): CancelablePromise<LoginTestTokenResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/login/test-token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recover Password
|
||||||
|
* Password Recovery
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.email
|
||||||
|
* @returns Message Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static recoverPassword(data: LoginRecoverPasswordData): CancelablePromise<LoginRecoverPasswordResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/password-recovery/{email}',
|
||||||
|
path: {
|
||||||
|
email: data.email
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset Password
|
||||||
|
* Reset password
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.requestBody
|
||||||
|
* @returns Message Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static resetPassword(data: LoginResetPasswordData): CancelablePromise<LoginResetPasswordResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/reset-password/',
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recover Password Html Content
|
||||||
|
* HTML Content for Password Recovery
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.email
|
||||||
|
* @returns string Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static recoverPasswordHtmlContent(data: LoginRecoverPasswordHtmlContentData): CancelablePromise<LoginRecoverPasswordHtmlContentResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/password-recovery-html-content/{email}',
|
||||||
|
path: {
|
||||||
|
email: data.email
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
export class PrivateService {
|
||||||
* Test Token
|
/**
|
||||||
* Test access token
|
* Create User
|
||||||
* @returns UserPublic Successful Response
|
* Create a new user.
|
||||||
* @throws ApiError
|
* @param data The data for the request.
|
||||||
*/
|
* @param data.requestBody
|
||||||
public static testToken(): CancelablePromise<LoginTestTokenResponse> {
|
* @returns UserPublic Successful Response
|
||||||
return __request(OpenAPI, {
|
* @throws ApiError
|
||||||
method: "POST",
|
*/
|
||||||
url: "/api/v1/login/test-token",
|
public static createUser(data: PrivateCreateUserData): CancelablePromise<PrivateCreateUserResponse> {
|
||||||
})
|
return __request(OpenAPI, {
|
||||||
}
|
method: 'POST',
|
||||||
|
url: '/api/v1/private/users/',
|
||||||
/**
|
body: data.requestBody,
|
||||||
* Recover Password
|
mediaType: 'application/json',
|
||||||
* Password Recovery
|
errors: {
|
||||||
* @param data The data for the request.
|
422: 'Validation Error'
|
||||||
* @param data.email
|
}
|
||||||
* @returns Message Successful Response
|
});
|
||||||
* @throws ApiError
|
}
|
||||||
*/
|
|
||||||
public static recoverPassword(
|
|
||||||
data: LoginRecoverPasswordData,
|
|
||||||
): CancelablePromise<LoginRecoverPasswordResponse> {
|
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: "POST",
|
|
||||||
url: "/api/v1/password-recovery/{email}",
|
|
||||||
path: {
|
|
||||||
email: data.email,
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
422: "Validation Error",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset Password
|
|
||||||
* Reset password
|
|
||||||
* @param data The data for the request.
|
|
||||||
* @param data.requestBody
|
|
||||||
* @returns Message Successful Response
|
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public static resetPassword(
|
|
||||||
data: LoginResetPasswordData,
|
|
||||||
): CancelablePromise<LoginResetPasswordResponse> {
|
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: "POST",
|
|
||||||
url: "/api/v1/reset-password/",
|
|
||||||
body: data.requestBody,
|
|
||||||
mediaType: "application/json",
|
|
||||||
errors: {
|
|
||||||
422: "Validation Error",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recover Password Html Content
|
|
||||||
* HTML Content for Password Recovery
|
|
||||||
* @param data The data for the request.
|
|
||||||
* @param data.email
|
|
||||||
* @returns string Successful Response
|
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public static recoverPasswordHtmlContent(
|
|
||||||
data: LoginRecoverPasswordHtmlContentData,
|
|
||||||
): CancelablePromise<LoginRecoverPasswordHtmlContentResponse> {
|
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: "POST",
|
|
||||||
url: "/api/v1/password-recovery-html-content/{email}",
|
|
||||||
path: {
|
|
||||||
email: data.email,
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
422: "Validation Error",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
/**
|
/**
|
||||||
* Read Users
|
* Read Users
|
||||||
* Retrieve users.
|
* Retrieve users.
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.skip
|
* @param data.skip
|
||||||
* @param data.limit
|
* @param data.limit
|
||||||
* @returns UsersPublic Successful Response
|
* @returns UsersPublic Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static readUsers(
|
public static readUsers(data: UsersReadUsersData = {}): CancelablePromise<UsersReadUsersResponse> {
|
||||||
data: UsersReadUsersData = {},
|
return __request(OpenAPI, {
|
||||||
): CancelablePromise<UsersReadUsersResponse> {
|
method: 'GET',
|
||||||
return __request(OpenAPI, {
|
url: '/api/v1/users/',
|
||||||
method: "GET",
|
query: {
|
||||||
url: "/api/v1/users/",
|
skip: data.skip,
|
||||||
query: {
|
limit: data.limit
|
||||||
skip: data.skip,
|
},
|
||||||
limit: data.limit,
|
errors: {
|
||||||
},
|
422: 'Validation Error'
|
||||||
errors: {
|
}
|
||||||
422: "Validation Error",
|
});
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
/**
|
||||||
|
* Create User
|
||||||
/**
|
* Create new user.
|
||||||
* Create User
|
* @param data The data for the request.
|
||||||
* Create new user.
|
* @param data.requestBody
|
||||||
* @param data The data for the request.
|
* @returns UserPublic Successful Response
|
||||||
* @param data.requestBody
|
* @throws ApiError
|
||||||
* @returns UserPublic Successful Response
|
*/
|
||||||
* @throws ApiError
|
public static createUser(data: UsersCreateUserData): CancelablePromise<UsersCreateUserResponse> {
|
||||||
*/
|
return __request(OpenAPI, {
|
||||||
public static createUser(
|
method: 'POST',
|
||||||
data: UsersCreateUserData,
|
url: '/api/v1/users/',
|
||||||
): CancelablePromise<UsersCreateUserResponse> {
|
body: data.requestBody,
|
||||||
return __request(OpenAPI, {
|
mediaType: 'application/json',
|
||||||
method: "POST",
|
errors: {
|
||||||
url: "/api/v1/users/",
|
422: 'Validation Error'
|
||||||
body: data.requestBody,
|
}
|
||||||
mediaType: "application/json",
|
});
|
||||||
errors: {
|
}
|
||||||
422: "Validation Error",
|
|
||||||
},
|
/**
|
||||||
})
|
* Read User Me
|
||||||
}
|
* Get current user.
|
||||||
|
* @returns UserPublic Successful Response
|
||||||
/**
|
* @throws ApiError
|
||||||
* Read User Me
|
*/
|
||||||
* Get current user.
|
public static readUserMe(): CancelablePromise<UsersReadUserMeResponse> {
|
||||||
* @returns UserPublic Successful Response
|
return __request(OpenAPI, {
|
||||||
* @throws ApiError
|
method: 'GET',
|
||||||
*/
|
url: '/api/v1/users/me'
|
||||||
public static readUserMe(): CancelablePromise<UsersReadUserMeResponse> {
|
});
|
||||||
return __request(OpenAPI, {
|
}
|
||||||
method: "GET",
|
|
||||||
url: "/api/v1/users/me",
|
/**
|
||||||
})
|
* Delete User Me
|
||||||
}
|
* Delete own user.
|
||||||
|
* @returns Message Successful Response
|
||||||
/**
|
* @throws ApiError
|
||||||
* Delete User Me
|
*/
|
||||||
* Delete own user.
|
public static deleteUserMe(): CancelablePromise<UsersDeleteUserMeResponse> {
|
||||||
* @returns Message Successful Response
|
return __request(OpenAPI, {
|
||||||
* @throws ApiError
|
method: 'DELETE',
|
||||||
*/
|
url: '/api/v1/users/me'
|
||||||
public static deleteUserMe(): CancelablePromise<UsersDeleteUserMeResponse> {
|
});
|
||||||
return __request(OpenAPI, {
|
}
|
||||||
method: "DELETE",
|
|
||||||
url: "/api/v1/users/me",
|
/**
|
||||||
})
|
* Update User Me
|
||||||
}
|
* Update own user.
|
||||||
|
* @param data The data for the request.
|
||||||
/**
|
* @param data.requestBody
|
||||||
* Update User Me
|
* @returns UserPublic Successful Response
|
||||||
* Update own user.
|
* @throws ApiError
|
||||||
* @param data The data for the request.
|
*/
|
||||||
* @param data.requestBody
|
public static updateUserMe(data: UsersUpdateUserMeData): CancelablePromise<UsersUpdateUserMeResponse> {
|
||||||
* @returns UserPublic Successful Response
|
return __request(OpenAPI, {
|
||||||
* @throws ApiError
|
method: 'PATCH',
|
||||||
*/
|
url: '/api/v1/users/me',
|
||||||
public static updateUserMe(
|
body: data.requestBody,
|
||||||
data: UsersUpdateUserMeData,
|
mediaType: 'application/json',
|
||||||
): CancelablePromise<UsersUpdateUserMeResponse> {
|
errors: {
|
||||||
return __request(OpenAPI, {
|
422: 'Validation Error'
|
||||||
method: "PATCH",
|
}
|
||||||
url: "/api/v1/users/me",
|
});
|
||||||
body: data.requestBody,
|
}
|
||||||
mediaType: "application/json",
|
|
||||||
errors: {
|
/**
|
||||||
422: "Validation Error",
|
* Update Password Me
|
||||||
},
|
* Update own password.
|
||||||
})
|
* @param data The data for the request.
|
||||||
}
|
* @param data.requestBody
|
||||||
|
* @returns Message Successful Response
|
||||||
/**
|
* @throws ApiError
|
||||||
* Update Password Me
|
*/
|
||||||
* Update own password.
|
public static updatePasswordMe(data: UsersUpdatePasswordMeData): CancelablePromise<UsersUpdatePasswordMeResponse> {
|
||||||
* @param data The data for the request.
|
return __request(OpenAPI, {
|
||||||
* @param data.requestBody
|
method: 'PATCH',
|
||||||
* @returns Message Successful Response
|
url: '/api/v1/users/me/password',
|
||||||
* @throws ApiError
|
body: data.requestBody,
|
||||||
*/
|
mediaType: 'application/json',
|
||||||
public static updatePasswordMe(
|
errors: {
|
||||||
data: UsersUpdatePasswordMeData,
|
422: 'Validation Error'
|
||||||
): CancelablePromise<UsersUpdatePasswordMeResponse> {
|
}
|
||||||
return __request(OpenAPI, {
|
});
|
||||||
method: "PATCH",
|
}
|
||||||
url: "/api/v1/users/me/password",
|
|
||||||
body: data.requestBody,
|
/**
|
||||||
mediaType: "application/json",
|
* Register User
|
||||||
errors: {
|
* Create new user without the need to be logged in.
|
||||||
422: "Validation Error",
|
* @param data The data for the request.
|
||||||
},
|
* @param data.requestBody
|
||||||
})
|
* @returns UserPublic Successful Response
|
||||||
}
|
* @throws ApiError
|
||||||
|
*/
|
||||||
/**
|
public static registerUser(data: UsersRegisterUserData): CancelablePromise<UsersRegisterUserResponse> {
|
||||||
* Register User
|
return __request(OpenAPI, {
|
||||||
* Create new user without the need to be logged in.
|
method: 'POST',
|
||||||
* @param data The data for the request.
|
url: '/api/v1/users/signup',
|
||||||
* @param data.requestBody
|
body: data.requestBody,
|
||||||
* @returns UserPublic Successful Response
|
mediaType: 'application/json',
|
||||||
* @throws ApiError
|
errors: {
|
||||||
*/
|
422: 'Validation Error'
|
||||||
public static registerUser(
|
}
|
||||||
data: UsersRegisterUserData,
|
});
|
||||||
): CancelablePromise<UsersRegisterUserResponse> {
|
}
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: "POST",
|
/**
|
||||||
url: "/api/v1/users/signup",
|
* Read User By Id
|
||||||
body: data.requestBody,
|
* Get a specific user by id.
|
||||||
mediaType: "application/json",
|
* @param data The data for the request.
|
||||||
errors: {
|
* @param data.userId
|
||||||
422: "Validation Error",
|
* @returns UserPublic Successful Response
|
||||||
},
|
* @throws ApiError
|
||||||
})
|
*/
|
||||||
}
|
public static readUserById(data: UsersReadUserByIdData): CancelablePromise<UsersReadUserByIdResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
/**
|
method: 'GET',
|
||||||
* Read User By Id
|
url: '/api/v1/users/{user_id}',
|
||||||
* Get a specific user by id.
|
path: {
|
||||||
* @param data The data for the request.
|
user_id: data.userId
|
||||||
* @param data.userId
|
},
|
||||||
* @returns UserPublic Successful Response
|
errors: {
|
||||||
* @throws ApiError
|
422: 'Validation Error'
|
||||||
*/
|
}
|
||||||
public static readUserById(
|
});
|
||||||
data: UsersReadUserByIdData,
|
}
|
||||||
): CancelablePromise<UsersReadUserByIdResponse> {
|
|
||||||
return __request(OpenAPI, {
|
/**
|
||||||
method: "GET",
|
* Update User
|
||||||
url: "/api/v1/users/{user_id}",
|
* Update a user.
|
||||||
path: {
|
* @param data The data for the request.
|
||||||
user_id: data.userId,
|
* @param data.userId
|
||||||
},
|
* @param data.requestBody
|
||||||
errors: {
|
* @returns UserPublic Successful Response
|
||||||
422: "Validation Error",
|
* @throws ApiError
|
||||||
},
|
*/
|
||||||
})
|
public static updateUser(data: UsersUpdateUserData): CancelablePromise<UsersUpdateUserResponse> {
|
||||||
}
|
return __request(OpenAPI, {
|
||||||
|
method: 'PATCH',
|
||||||
/**
|
url: '/api/v1/users/{user_id}',
|
||||||
* Update User
|
path: {
|
||||||
* Update a user.
|
user_id: data.userId
|
||||||
* @param data The data for the request.
|
},
|
||||||
* @param data.userId
|
body: data.requestBody,
|
||||||
* @param data.requestBody
|
mediaType: 'application/json',
|
||||||
* @returns UserPublic Successful Response
|
errors: {
|
||||||
* @throws ApiError
|
422: 'Validation Error'
|
||||||
*/
|
}
|
||||||
public static updateUser(
|
});
|
||||||
data: UsersUpdateUserData,
|
}
|
||||||
): CancelablePromise<UsersUpdateUserResponse> {
|
|
||||||
return __request(OpenAPI, {
|
/**
|
||||||
method: "PATCH",
|
* Delete User
|
||||||
url: "/api/v1/users/{user_id}",
|
* Delete a user.
|
||||||
path: {
|
* @param data The data for the request.
|
||||||
user_id: data.userId,
|
* @param data.userId
|
||||||
},
|
* @returns Message Successful Response
|
||||||
body: data.requestBody,
|
* @throws ApiError
|
||||||
mediaType: "application/json",
|
*/
|
||||||
errors: {
|
public static deleteUser(data: UsersDeleteUserData): CancelablePromise<UsersDeleteUserResponse> {
|
||||||
422: "Validation Error",
|
return __request(OpenAPI, {
|
||||||
},
|
method: 'DELETE',
|
||||||
})
|
url: '/api/v1/users/{user_id}',
|
||||||
}
|
path: {
|
||||||
|
user_id: data.userId
|
||||||
/**
|
},
|
||||||
* Delete User
|
errors: {
|
||||||
* Delete a user.
|
422: 'Validation Error'
|
||||||
* @param data The data for the request.
|
}
|
||||||
* @param data.userId
|
});
|
||||||
* @returns Message Successful Response
|
}
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public static deleteUser(
|
|
||||||
data: UsersDeleteUserData,
|
|
||||||
): CancelablePromise<UsersDeleteUserResponse> {
|
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: "DELETE",
|
|
||||||
url: "/api/v1/users/{user_id}",
|
|
||||||
path: {
|
|
||||||
user_id: data.userId,
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
422: "Validation Error",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UtilsService {
|
export class UtilsService {
|
||||||
/**
|
/**
|
||||||
* Test Email
|
* Test Email
|
||||||
* Test emails.
|
* Test emails.
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.emailTo
|
* @param data.emailTo
|
||||||
* @returns Message Successful Response
|
* @returns Message Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static testEmail(
|
public static testEmail(data: UtilsTestEmailData): CancelablePromise<UtilsTestEmailResponse> {
|
||||||
data: UtilsTestEmailData,
|
return __request(OpenAPI, {
|
||||||
): CancelablePromise<UtilsTestEmailResponse> {
|
method: 'POST',
|
||||||
return __request(OpenAPI, {
|
url: '/api/v1/utils/test-email/',
|
||||||
method: "POST",
|
query: {
|
||||||
url: "/api/v1/utils/test-email/",
|
email_to: data.emailTo
|
||||||
query: {
|
},
|
||||||
email_to: data.emailTo,
|
errors: {
|
||||||
},
|
422: 'Validation Error'
|
||||||
errors: {
|
}
|
||||||
422: "Validation Error",
|
});
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
/**
|
||||||
|
* Health Check
|
||||||
/**
|
* @returns boolean Successful Response
|
||||||
* Health Check
|
* @throws ApiError
|
||||||
* @returns boolean Successful Response
|
*/
|
||||||
* @throws ApiError
|
public static healthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
|
||||||
*/
|
return __request(OpenAPI, {
|
||||||
public static healthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
|
method: 'GET',
|
||||||
return __request(OpenAPI, {
|
url: '/api/v1/utils/health-check/'
|
||||||
method: "GET",
|
});
|
||||||
url: "/api/v1/utils/health-check/",
|
}
|
||||||
})
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
+141
-128
@@ -1,221 +1,234 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
export type Body_login_login_access_token = {
|
export type Body_login_login_access_token = {
|
||||||
grant_type?: string | null
|
grant_type?: (string | null);
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
scope?: string
|
scope?: string;
|
||||||
client_id?: string | null
|
client_id?: (string | null);
|
||||||
client_secret?: string | null
|
client_secret?: (string | null);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type HTTPValidationError = {
|
export type HTTPValidationError = {
|
||||||
detail?: Array<ValidationError>
|
detail?: Array<ValidationError>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemCreate = {
|
export type ItemCreate = {
|
||||||
title: string
|
title: string;
|
||||||
description?: string | null
|
description?: (string | null);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemPublic = {
|
export type ItemPublic = {
|
||||||
title: string
|
title: string;
|
||||||
description?: string | null
|
description?: (string | null);
|
||||||
id: string
|
id: string;
|
||||||
owner_id: string
|
owner_id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsPublic = {
|
export type ItemsPublic = {
|
||||||
data: Array<ItemPublic>
|
data: Array<ItemPublic>;
|
||||||
count: number
|
count: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemUpdate = {
|
export type ItemUpdate = {
|
||||||
title?: string | null
|
title?: (string | null);
|
||||||
description?: string | null
|
description?: (string | null);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
message: string
|
message: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NewPassword = {
|
export type NewPassword = {
|
||||||
token: string
|
token: string;
|
||||||
new_password: string
|
new_password: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type PrivateUserCreate = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
full_name: string;
|
||||||
|
is_verified?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type Token = {
|
export type Token = {
|
||||||
access_token: string
|
access_token: string;
|
||||||
token_type?: string
|
token_type?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UpdatePassword = {
|
export type UpdatePassword = {
|
||||||
current_password: string
|
current_password: string;
|
||||||
new_password: string
|
new_password: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UserCreate = {
|
export type UserCreate = {
|
||||||
email: string
|
email: string;
|
||||||
is_active?: boolean
|
is_active?: boolean;
|
||||||
is_superuser?: boolean
|
is_superuser?: boolean;
|
||||||
full_name?: string | null
|
full_name?: (string | null);
|
||||||
password: string
|
password: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UserPublic = {
|
export type UserPublic = {
|
||||||
email: string
|
email: string;
|
||||||
is_active?: boolean
|
is_active?: boolean;
|
||||||
is_superuser?: boolean
|
is_superuser?: boolean;
|
||||||
full_name?: string | null
|
full_name?: (string | null);
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UserRegister = {
|
export type UserRegister = {
|
||||||
email: string
|
email: string;
|
||||||
password: string
|
password: string;
|
||||||
full_name?: string | null
|
full_name?: (string | null);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersPublic = {
|
export type UsersPublic = {
|
||||||
data: Array<UserPublic>
|
data: Array<UserPublic>;
|
||||||
count: number
|
count: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UserUpdate = {
|
export type UserUpdate = {
|
||||||
email?: string | null
|
email?: (string | null);
|
||||||
is_active?: boolean
|
is_active?: boolean;
|
||||||
is_superuser?: boolean
|
is_superuser?: boolean;
|
||||||
full_name?: string | null
|
full_name?: (string | null);
|
||||||
password?: string | null
|
password?: (string | null);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UserUpdateMe = {
|
export type UserUpdateMe = {
|
||||||
full_name?: string | null
|
full_name?: (string | null);
|
||||||
email?: string | null
|
email?: (string | null);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ValidationError = {
|
export type ValidationError = {
|
||||||
loc: Array<string | number>
|
loc: Array<(string | number)>;
|
||||||
msg: string
|
msg: string;
|
||||||
type: string
|
type: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsReadItemsData = {
|
export type ItemsReadItemsData = {
|
||||||
limit?: number
|
limit?: number;
|
||||||
skip?: number
|
skip?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsReadItemsResponse = ItemsPublic
|
export type ItemsReadItemsResponse = (ItemsPublic);
|
||||||
|
|
||||||
export type ItemsCreateItemData = {
|
export type ItemsCreateItemData = {
|
||||||
requestBody: ItemCreate
|
requestBody: ItemCreate;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsCreateItemResponse = ItemPublic
|
export type ItemsCreateItemResponse = (ItemPublic);
|
||||||
|
|
||||||
export type ItemsReadItemData = {
|
export type ItemsReadItemData = {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsReadItemResponse = ItemPublic
|
export type ItemsReadItemResponse = (ItemPublic);
|
||||||
|
|
||||||
export type ItemsUpdateItemData = {
|
export type ItemsUpdateItemData = {
|
||||||
id: string
|
id: string;
|
||||||
requestBody: ItemUpdate
|
requestBody: ItemUpdate;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsUpdateItemResponse = ItemPublic
|
export type ItemsUpdateItemResponse = (ItemPublic);
|
||||||
|
|
||||||
export type ItemsDeleteItemData = {
|
export type ItemsDeleteItemData = {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ItemsDeleteItemResponse = Message
|
export type ItemsDeleteItemResponse = (Message);
|
||||||
|
|
||||||
export type LoginLoginAccessTokenData = {
|
export type LoginLoginAccessTokenData = {
|
||||||
formData: Body_login_login_access_token
|
formData: Body_login_login_access_token;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type LoginLoginAccessTokenResponse = Token
|
export type LoginLoginAccessTokenResponse = (Token);
|
||||||
|
|
||||||
export type LoginTestTokenResponse = UserPublic
|
export type LoginTestTokenResponse = (UserPublic);
|
||||||
|
|
||||||
export type LoginRecoverPasswordData = {
|
export type LoginRecoverPasswordData = {
|
||||||
email: string
|
email: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type LoginRecoverPasswordResponse = Message
|
export type LoginRecoverPasswordResponse = (Message);
|
||||||
|
|
||||||
export type LoginResetPasswordData = {
|
export type LoginResetPasswordData = {
|
||||||
requestBody: NewPassword
|
requestBody: NewPassword;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type LoginResetPasswordResponse = Message
|
export type LoginResetPasswordResponse = (Message);
|
||||||
|
|
||||||
export type LoginRecoverPasswordHtmlContentData = {
|
export type LoginRecoverPasswordHtmlContentData = {
|
||||||
email: string
|
email: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type LoginRecoverPasswordHtmlContentResponse = string
|
export type LoginRecoverPasswordHtmlContentResponse = (string);
|
||||||
|
|
||||||
|
export type PrivateCreateUserData = {
|
||||||
|
requestBody: PrivateUserCreate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrivateCreateUserResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersReadUsersData = {
|
export type UsersReadUsersData = {
|
||||||
limit?: number
|
limit?: number;
|
||||||
skip?: number
|
skip?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersReadUsersResponse = UsersPublic
|
export type UsersReadUsersResponse = (UsersPublic);
|
||||||
|
|
||||||
export type UsersCreateUserData = {
|
export type UsersCreateUserData = {
|
||||||
requestBody: UserCreate
|
requestBody: UserCreate;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersCreateUserResponse = UserPublic
|
export type UsersCreateUserResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersReadUserMeResponse = UserPublic
|
export type UsersReadUserMeResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersDeleteUserMeResponse = Message
|
export type UsersDeleteUserMeResponse = (Message);
|
||||||
|
|
||||||
export type UsersUpdateUserMeData = {
|
export type UsersUpdateUserMeData = {
|
||||||
requestBody: UserUpdateMe
|
requestBody: UserUpdateMe;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersUpdateUserMeResponse = UserPublic
|
export type UsersUpdateUserMeResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersUpdatePasswordMeData = {
|
export type UsersUpdatePasswordMeData = {
|
||||||
requestBody: UpdatePassword
|
requestBody: UpdatePassword;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersUpdatePasswordMeResponse = Message
|
export type UsersUpdatePasswordMeResponse = (Message);
|
||||||
|
|
||||||
export type UsersRegisterUserData = {
|
export type UsersRegisterUserData = {
|
||||||
requestBody: UserRegister
|
requestBody: UserRegister;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersRegisterUserResponse = UserPublic
|
export type UsersRegisterUserResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersReadUserByIdData = {
|
export type UsersReadUserByIdData = {
|
||||||
userId: string
|
userId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersReadUserByIdResponse = UserPublic
|
export type UsersReadUserByIdResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersUpdateUserData = {
|
export type UsersUpdateUserData = {
|
||||||
requestBody: UserUpdate
|
requestBody: UserUpdate;
|
||||||
userId: string
|
userId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersUpdateUserResponse = UserPublic
|
export type UsersUpdateUserResponse = (UserPublic);
|
||||||
|
|
||||||
export type UsersDeleteUserData = {
|
export type UsersDeleteUserData = {
|
||||||
userId: string
|
userId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UsersDeleteUserResponse = Message
|
export type UsersDeleteUserResponse = (Message);
|
||||||
|
|
||||||
export type UtilsTestEmailData = {
|
export type UtilsTestEmailData = {
|
||||||
emailTo: string
|
emailTo: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UtilsTestEmailResponse = Message
|
export type UtilsTestEmailResponse = (Message);
|
||||||
|
|
||||||
export type UtilsHealthCheckResponse = boolean
|
export type UtilsHealthCheckResponse = (boolean);
|
||||||
@@ -1,49 +1,64 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Controller, type SubmitHandler, useForm } from "react-hook-form"
|
import { Plus } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
import { type UserCreate, UsersService } from "@/client"
|
import { type UserCreate, UsersService } from "@/client"
|
||||||
import type { ApiError } from "@/client/core/ApiError"
|
import { Button } from "@/components/ui/button"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { emailPattern, handleError } from "@/utils"
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Dialog,
|
||||||
DialogActionTrigger,
|
DialogClose,
|
||||||
DialogTitle,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { FaPlus } from "react-icons/fa"
|
|
||||||
import { Checkbox } from "../ui/checkbox"
|
|
||||||
import {
|
|
||||||
DialogBody,
|
|
||||||
DialogCloseTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogRoot,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "../ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Field } from "../ui/field"
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
interface UserCreateForm extends UserCreate {
|
const formSchema = z
|
||||||
confirm_password: string
|
.object({
|
||||||
}
|
email: z.email({ message: "Invalid email address" }),
|
||||||
|
full_name: z.string().optional(),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Password is required" })
|
||||||
|
.min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
confirm_password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Please confirm your password" }),
|
||||||
|
is_superuser: z.boolean(),
|
||||||
|
is_active: z.boolean(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirm_password, {
|
||||||
|
message: "The passwords don't match",
|
||||||
|
path: ["confirm_password"],
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|
||||||
const AddUser = () => {
|
const AddUser = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
|
||||||
control,
|
const form = useForm<FormData>({
|
||||||
register,
|
resolver: zodResolver(formSchema),
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
getValues,
|
|
||||||
formState: { errors, isValid, isSubmitting },
|
|
||||||
} = useForm<UserCreateForm>({
|
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -60,170 +75,163 @@ const AddUser = () => {
|
|||||||
mutationFn: (data: UserCreate) =>
|
mutationFn: (data: UserCreate) =>
|
||||||
UsersService.createUser({ requestBody: data }),
|
UsersService.createUser({ requestBody: data }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("User created successfully.")
|
showSuccessToast("User created successfully")
|
||||||
reset()
|
form.reset()
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
},
|
},
|
||||||
onError: (err: ApiError) => {
|
onError: handleError.bind(showErrorToast),
|
||||||
handleError(err)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["users"] })
|
queryClient.invalidateQueries({ queryKey: ["users"] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<UserCreateForm> = (data) => {
|
const onSubmit = (data: FormData) => {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
size={{ base: "xs", md: "md" }}
|
|
||||||
placement="center"
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button value="add-user" my={4}>
|
<Button className="my-4">
|
||||||
<FaPlus fontSize="16px" />
|
<Plus className="mr-2" />
|
||||||
Add User
|
Add User
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="sm:max-w-md">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>Add User</DialogTitle>
|
||||||
<DialogTitle>Add User</DialogTitle>
|
<DialogDescription>
|
||||||
</DialogHeader>
|
Fill in the form below to add a new user to the system.
|
||||||
<DialogBody>
|
</DialogDescription>
|
||||||
<Text mb={4}>
|
</DialogHeader>
|
||||||
Fill in the form below to add a new user to the system.
|
<Form {...form}>
|
||||||
</Text>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<VStack gap={4}>
|
<div className="grid gap-4 py-4">
|
||||||
<Field
|
<FormField
|
||||||
required
|
control={form.control}
|
||||||
invalid={!!errors.email}
|
name="email"
|
||||||
errorText={errors.email?.message}
|
render={({ field }) => (
|
||||||
label="Email"
|
<FormItem>
|
||||||
>
|
<FormLabel>
|
||||||
<Input
|
Email <span className="text-destructive">*</span>
|
||||||
id="email"
|
</FormLabel>
|
||||||
{...register("email", {
|
<FormControl>
|
||||||
required: "Email is required",
|
<Input
|
||||||
pattern: emailPattern,
|
placeholder="Email"
|
||||||
})}
|
type="email"
|
||||||
placeholder="Email"
|
{...field}
|
||||||
type="email"
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
invalid={!!errors.full_name}
|
control={form.control}
|
||||||
errorText={errors.full_name?.message}
|
name="full_name"
|
||||||
label="Full Name"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
<Input
|
<FormLabel>Full Name</FormLabel>
|
||||||
id="name"
|
<FormControl>
|
||||||
{...register("full_name")}
|
<Input placeholder="Full name" type="text" {...field} />
|
||||||
placeholder="Full name"
|
</FormControl>
|
||||||
type="text"
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</Field>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
required
|
control={form.control}
|
||||||
invalid={!!errors.password}
|
name="password"
|
||||||
errorText={errors.password?.message}
|
render={({ field }) => (
|
||||||
label="Set Password"
|
<FormItem>
|
||||||
>
|
<FormLabel>
|
||||||
<Input
|
Set Password <span className="text-destructive">*</span>
|
||||||
id="password"
|
</FormLabel>
|
||||||
{...register("password", {
|
<FormControl>
|
||||||
required: "Password is required",
|
<Input
|
||||||
minLength: {
|
placeholder="Password"
|
||||||
value: 8,
|
type="password"
|
||||||
message: "Password must be at least 8 characters",
|
{...field}
|
||||||
},
|
required
|
||||||
})}
|
/>
|
||||||
placeholder="Password"
|
</FormControl>
|
||||||
type="password"
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</Field>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
required
|
control={form.control}
|
||||||
invalid={!!errors.confirm_password}
|
name="confirm_password"
|
||||||
errorText={errors.confirm_password?.message}
|
render={({ field }) => (
|
||||||
label="Confirm Password"
|
<FormItem>
|
||||||
>
|
<FormLabel>
|
||||||
<Input
|
Confirm Password{" "}
|
||||||
id="confirm_password"
|
<span className="text-destructive">*</span>
|
||||||
{...register("confirm_password", {
|
</FormLabel>
|
||||||
required: "Please confirm your password",
|
<FormControl>
|
||||||
validate: (value) =>
|
<Input
|
||||||
value === getValues().password ||
|
placeholder="Password"
|
||||||
"The passwords do not match",
|
type="password"
|
||||||
})}
|
{...field}
|
||||||
placeholder="Password"
|
required
|
||||||
type="password"
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</Field>
|
<FormMessage />
|
||||||
</VStack>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Flex mt={4} direction="column" gap={4}>
|
<FormField
|
||||||
<Controller
|
control={form.control}
|
||||||
control={control}
|
|
||||||
name="is_superuser"
|
name="is_superuser"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Field disabled={field.disabled} colorPalette="teal">
|
<FormItem className="flex items-center gap-3 space-y-0">
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Checkbox
|
||||||
onCheckedChange={({ checked }) => field.onChange(checked)}
|
checked={field.value}
|
||||||
>
|
onCheckedChange={field.onChange}
|
||||||
Is superuser?
|
/>
|
||||||
</Checkbox>
|
</FormControl>
|
||||||
</Field>
|
<FormLabel className="font-normal">Is superuser?</FormLabel>
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={control}
|
<FormField
|
||||||
|
control={form.control}
|
||||||
name="is_active"
|
name="is_active"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Field disabled={field.disabled} colorPalette="teal">
|
<FormItem className="flex items-center gap-3 space-y-0">
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Checkbox
|
||||||
onCheckedChange={({ checked }) => field.onChange(checked)}
|
checked={field.value}
|
||||||
>
|
onCheckedChange={field.onChange}
|
||||||
Is active?
|
/>
|
||||||
</Checkbox>
|
</FormControl>
|
||||||
</Field>
|
<FormLabel className="font-normal">Is active?</FormLabel>
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</div>
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter gap={2}>
|
<DialogFooter>
|
||||||
<DialogActionTrigger asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
variant="subtle"
|
Cancel
|
||||||
colorPalette="gray"
|
</Button>
|
||||||
disabled={isSubmitting}
|
</DialogClose>
|
||||||
>
|
<LoadingButton type="submit" loading={mutation.isPending}>
|
||||||
Cancel
|
Save
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</DialogActionTrigger>
|
</DialogFooter>
|
||||||
<Button
|
</form>
|
||||||
variant="solid"
|
</Form>
|
||||||
type="submit"
|
|
||||||
disabled={!isValid}
|
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
<DialogCloseTrigger />
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,34 @@
|
|||||||
import { Button, DialogTitle, Text } from "@chakra-ui/react"
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Trash2 } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { FiTrash2 } from "react-icons/fi"
|
|
||||||
|
|
||||||
import { UsersService } from "@/client"
|
import { UsersService } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
DialogActionTrigger,
|
Dialog,
|
||||||
DialogBody,
|
DialogClose,
|
||||||
DialogCloseTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogRoot,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
const DeleteUser = ({ id }: { id: string }) => {
|
interface DeleteUserProps {
|
||||||
|
id: string
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteUser = ({ id, onSuccess }: DeleteUserProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast, showErrorToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
const { handleSubmit } = useForm()
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm()
|
|
||||||
|
|
||||||
const deleteUser = async (id: string) => {
|
const deleteUser = async (id: string) => {
|
||||||
await UsersService.deleteUser({ userId: id })
|
await UsersService.deleteUser({ userId: id })
|
||||||
@@ -35,10 +39,9 @@ const DeleteUser = ({ id }: { id: string }) => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("The user was deleted successfully")
|
showSuccessToast("The user was deleted successfully")
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
|
onSuccess()
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: handleError.bind(showErrorToast),
|
||||||
showErrorToast("An error occurred while deleting the user")
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries()
|
queryClient.invalidateQueries()
|
||||||
},
|
},
|
||||||
@@ -49,55 +52,43 @@ const DeleteUser = ({ id }: { id: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
size={{ base: "xs", md: "md" }}
|
<DropdownMenuItem
|
||||||
placement="center"
|
variant="destructive"
|
||||||
role="alertdialog"
|
onSelect={(e) => e.preventDefault()}
|
||||||
open={isOpen}
|
onClick={() => setIsOpen(true)}
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
>
|
||||||
>
|
<Trash2 />
|
||||||
<DialogTrigger asChild>
|
Delete User
|
||||||
<Button variant="ghost" size="sm" colorPalette="red">
|
</DropdownMenuItem>
|
||||||
<FiTrash2 fontSize="16px" />
|
<DialogContent className="sm:max-w-md">
|
||||||
Delete User
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Delete User</DialogTitle>
|
<DialogTitle>Delete User</DialogTitle>
|
||||||
</DialogHeader>
|
<DialogDescription>
|
||||||
<DialogBody>
|
|
||||||
<Text mb={4}>
|
|
||||||
All items associated with this user will also be{" "}
|
All items associated with this user will also be{" "}
|
||||||
<strong>permanently deleted.</strong> Are you sure? You will not
|
<strong>permanently deleted.</strong> Are you sure? You will not
|
||||||
be able to undo this action.
|
be able to undo this action.
|
||||||
</Text>
|
</DialogDescription>
|
||||||
</DialogBody>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogFooter gap={2}>
|
<DialogFooter className="mt-4">
|
||||||
<DialogActionTrigger asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
variant="subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActionTrigger>
|
</DialogClose>
|
||||||
<Button
|
<LoadingButton
|
||||||
variant="solid"
|
variant="destructive"
|
||||||
colorPalette="red"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isSubmitting}
|
loading={mutation.isPending}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
<DialogCloseTrigger />
|
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,221 +1,238 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Controller, type SubmitHandler, useForm } from "react-hook-form"
|
import { Pencil } from "lucide-react"
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DialogActionTrigger,
|
|
||||||
DialogRoot,
|
|
||||||
DialogTrigger,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { FaExchangeAlt } from "react-icons/fa"
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
import { type UserPublic, type UserUpdate, UsersService } from "@/client"
|
import { type UserPublic, UsersService } from "@/client"
|
||||||
import type { ApiError } from "@/client/core/ApiError"
|
import { Button } from "@/components/ui/button"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { emailPattern, handleError } from "@/utils"
|
|
||||||
import { Checkbox } from "../ui/checkbox"
|
|
||||||
import {
|
import {
|
||||||
DialogBody,
|
Dialog,
|
||||||
DialogCloseTrigger,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "../ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Field } from "../ui/field"
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
email: z.email({ message: "Invalid email address" }),
|
||||||
|
full_name: z.string().optional(),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, { message: "Password must be at least 8 characters" })
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
confirm_password: z.string().optional(),
|
||||||
|
is_superuser: z.boolean().optional(),
|
||||||
|
is_active: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => !data.password || data.password === data.confirm_password, {
|
||||||
|
message: "The passwords don't match",
|
||||||
|
path: ["confirm_password"],
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|
||||||
interface EditUserProps {
|
interface EditUserProps {
|
||||||
user: UserPublic
|
user: UserPublic
|
||||||
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserUpdateForm extends UserUpdate {
|
const EditUser = ({ user, onSuccess }: EditUserProps) => {
|
||||||
confirm_password?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditUser = ({ user }: EditUserProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
|
||||||
control,
|
const form = useForm<FormData>({
|
||||||
register,
|
resolver: zodResolver(formSchema),
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
getValues,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm<UserUpdateForm>({
|
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
defaultValues: user,
|
defaultValues: {
|
||||||
|
email: user.email,
|
||||||
|
full_name: user.full_name ?? undefined,
|
||||||
|
is_superuser: user.is_superuser,
|
||||||
|
is_active: user.is_active,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data: UserUpdateForm) =>
|
mutationFn: (data: FormData) =>
|
||||||
UsersService.updateUser({ userId: user.id, requestBody: data }),
|
UsersService.updateUser({ userId: user.id, requestBody: data }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("User updated successfully.")
|
showSuccessToast("User updated successfully")
|
||||||
reset()
|
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
|
onSuccess()
|
||||||
},
|
},
|
||||||
onError: (err: ApiError) => {
|
onError: handleError.bind(showErrorToast),
|
||||||
handleError(err)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["users"] })
|
queryClient.invalidateQueries({ queryKey: ["users"] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => {
|
const onSubmit = (data: FormData) => {
|
||||||
if (data.password === "") {
|
// exclude confirm_password from submission data and remove password if empty
|
||||||
data.password = undefined
|
const { confirm_password: _, ...submitData } = data
|
||||||
|
if (!submitData.password) {
|
||||||
|
delete submitData.password
|
||||||
}
|
}
|
||||||
mutation.mutate(data)
|
mutation.mutate(submitData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
size={{ base: "xs", md: "md" }}
|
<DropdownMenuItem
|
||||||
placement="center"
|
onSelect={(e) => e.preventDefault()}
|
||||||
open={isOpen}
|
onClick={() => setIsOpen(true)}
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
>
|
||||||
>
|
<Pencil />
|
||||||
<DialogTrigger asChild>
|
Edit User
|
||||||
<Button variant="ghost" size="sm">
|
</DropdownMenuItem>
|
||||||
<FaExchangeAlt fontSize="16px" />
|
<DialogContent className="sm:max-w-md">
|
||||||
Edit User
|
<Form {...form}>
|
||||||
</Button>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
</DialogTrigger>
|
<DialogHeader>
|
||||||
<DialogContent>
|
<DialogTitle>Edit User</DialogTitle>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<DialogDescription>
|
||||||
<DialogHeader>
|
Update the user details below.
|
||||||
<DialogTitle>Edit User</DialogTitle>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogBody>
|
<div className="grid gap-4 py-4">
|
||||||
<Text mb={4}>Update the user details below.</Text>
|
<FormField
|
||||||
<VStack gap={4}>
|
control={form.control}
|
||||||
<Field
|
name="email"
|
||||||
required
|
render={({ field }) => (
|
||||||
invalid={!!errors.email}
|
<FormItem>
|
||||||
errorText={errors.email?.message}
|
<FormLabel>
|
||||||
label="Email"
|
Email <span className="text-destructive">*</span>
|
||||||
>
|
</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
id="email"
|
<Input
|
||||||
{...register("email", {
|
placeholder="Email"
|
||||||
required: "Email is required",
|
type="email"
|
||||||
pattern: emailPattern,
|
{...field}
|
||||||
})}
|
required
|
||||||
placeholder="Email"
|
/>
|
||||||
type="email"
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
</Field>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
invalid={!!errors.full_name}
|
control={form.control}
|
||||||
errorText={errors.full_name?.message}
|
name="full_name"
|
||||||
label="Full Name"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
<Input
|
<FormLabel>Full Name</FormLabel>
|
||||||
id="name"
|
<FormControl>
|
||||||
{...register("full_name")}
|
<Input placeholder="Full name" type="text" {...field} />
|
||||||
placeholder="Full name"
|
</FormControl>
|
||||||
type="text"
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</Field>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
required
|
control={form.control}
|
||||||
invalid={!!errors.password}
|
name="password"
|
||||||
errorText={errors.password?.message}
|
render={({ field }) => (
|
||||||
label="Set Password"
|
<FormItem>
|
||||||
>
|
<FormLabel>Set Password</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
id="password"
|
<Input
|
||||||
{...register("password", {
|
placeholder="Password"
|
||||||
minLength: {
|
type="password"
|
||||||
value: 8,
|
{...field}
|
||||||
message: "Password must be at least 8 characters",
|
/>
|
||||||
},
|
</FormControl>
|
||||||
})}
|
<FormMessage />
|
||||||
placeholder="Password"
|
</FormItem>
|
||||||
type="password"
|
)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
required
|
control={form.control}
|
||||||
invalid={!!errors.confirm_password}
|
name="confirm_password"
|
||||||
errorText={errors.confirm_password?.message}
|
render={({ field }) => (
|
||||||
label="Confirm Password"
|
<FormItem>
|
||||||
>
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
id="confirm_password"
|
<Input
|
||||||
{...register("confirm_password", {
|
placeholder="Password"
|
||||||
validate: (value) =>
|
type="password"
|
||||||
value === getValues().password ||
|
{...field}
|
||||||
"The passwords do not match",
|
/>
|
||||||
})}
|
</FormControl>
|
||||||
placeholder="Password"
|
<FormMessage />
|
||||||
type="password"
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
</Field>
|
/>
|
||||||
</VStack>
|
|
||||||
|
|
||||||
<Flex mt={4} direction="column" gap={4}>
|
<FormField
|
||||||
<Controller
|
control={form.control}
|
||||||
control={control}
|
|
||||||
name="is_superuser"
|
name="is_superuser"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Field disabled={field.disabled} colorPalette="teal">
|
<FormItem className="flex items-center gap-3 space-y-0">
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Checkbox
|
||||||
onCheckedChange={({ checked }) => field.onChange(checked)}
|
checked={field.value}
|
||||||
>
|
onCheckedChange={field.onChange}
|
||||||
Is superuser?
|
/>
|
||||||
</Checkbox>
|
</FormControl>
|
||||||
</Field>
|
<FormLabel className="font-normal">Is superuser?</FormLabel>
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={control}
|
<FormField
|
||||||
|
control={form.control}
|
||||||
name="is_active"
|
name="is_active"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Field disabled={field.disabled} colorPalette="teal">
|
<FormItem className="flex items-center gap-3 space-y-0">
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Checkbox
|
||||||
onCheckedChange={({ checked }) => field.onChange(checked)}
|
checked={field.value}
|
||||||
>
|
onCheckedChange={field.onChange}
|
||||||
Is active?
|
/>
|
||||||
</Checkbox>
|
</FormControl>
|
||||||
</Field>
|
<FormLabel className="font-normal">Is active?</FormLabel>
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</div>
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter gap={2}>
|
<DialogFooter>
|
||||||
<DialogActionTrigger asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
variant="subtle"
|
Cancel
|
||||||
colorPalette="gray"
|
</Button>
|
||||||
disabled={isSubmitting}
|
</DialogClose>
|
||||||
>
|
<LoadingButton type="submit" loading={mutation.isPending}>
|
||||||
Cancel
|
Save
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</DialogActionTrigger>
|
</DialogFooter>
|
||||||
<Button variant="solid" type="submit" loading={isSubmitting}>
|
</form>
|
||||||
Save
|
</Form>
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
<DialogCloseTrigger />
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { EllipsisVertical } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import type { UserPublic } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import useAuth from "@/hooks/useAuth"
|
||||||
|
import DeleteUser from "./DeleteUser"
|
||||||
|
import EditUser from "./EditUser"
|
||||||
|
|
||||||
|
interface UserActionsMenuProps {
|
||||||
|
user: UserPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserActionsMenu = ({ user }: UserActionsMenuProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { user: currentUser } = useAuth()
|
||||||
|
|
||||||
|
if (user.id === currentUser?.id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<EllipsisVertical />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<EditUser user={user} onSuccess={() => setOpen(false)} />
|
||||||
|
<DeleteUser id={user.id} onSuccess={() => setOpen(false)} />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
|
||||||
|
import type { UserPublic } from "@/client"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { UserActionsMenu } from "./UserActionsMenu"
|
||||||
|
|
||||||
|
export type UserTableData = UserPublic & {
|
||||||
|
isCurrentUser: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columns: ColumnDef<UserTableData>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "full_name",
|
||||||
|
header: "Full Name",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const fullName = row.original.full_name
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn("font-medium", !fullName && "text-muted-foreground")}
|
||||||
|
>
|
||||||
|
{fullName || "N/A"}
|
||||||
|
</span>
|
||||||
|
{row.original.isCurrentUser && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
You
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "email",
|
||||||
|
header: "Email",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-muted-foreground">{row.original.email}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "is_superuser",
|
||||||
|
header: "Role",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant={row.original.is_superuser ? "default" : "secondary"}>
|
||||||
|
{row.original.is_superuser ? "Superuser" : "User"}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "is_active",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"size-2 rounded-full",
|
||||||
|
row.original.is_active ? "bg-green-500" : "bg-gray-400",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className={row.original.is_active ? "" : "text-muted-foreground"}>
|
||||||
|
{row.original.is_active ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <span className="sr-only">Actions</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<UserActionsMenu user={row.original} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Monitor, Moon, Sun } from "lucide-react"
|
||||||
|
|
||||||
|
import { type Theme, useTheme } from "@/components/theme-provider"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar"
|
||||||
|
|
||||||
|
type LucideIcon = React.FC<React.SVGProps<SVGSVGElement>>
|
||||||
|
|
||||||
|
const ICON_MAP: Record<Theme, LucideIcon> = {
|
||||||
|
system: Monitor,
|
||||||
|
light: Sun,
|
||||||
|
dark: Moon,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarAppearance = () => {
|
||||||
|
const { isMobile } = useSidebar()
|
||||||
|
const { setTheme, theme } = useTheme()
|
||||||
|
const Icon = ICON_MAP[theme]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton tooltip="Appearance" data-testid="theme-button">
|
||||||
|
<Icon className="size-4 text-muted-foreground" />
|
||||||
|
<span>Appearance</span>
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
side={isMobile ? "top" : "right"}
|
||||||
|
align="end"
|
||||||
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-testid="light-mode"
|
||||||
|
onClick={() => setTheme("light")}
|
||||||
|
>
|
||||||
|
<Sun className="mr-2 h-4 w-4" />
|
||||||
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-testid="dark-mode"
|
||||||
|
onClick={() => setTheme("dark")}
|
||||||
|
>
|
||||||
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
|
<Monitor className="mr-2 h-4 w-4" />
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Appearance = () => {
|
||||||
|
const { setTheme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button data-testid="theme-button" variant="outline" size="icon">
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-testid="light-mode"
|
||||||
|
onClick={() => setTheme("light")}
|
||||||
|
>
|
||||||
|
<Sun className="mr-2 h-4 w-4" />
|
||||||
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-testid="dark-mode"
|
||||||
|
onClick={() => setTheme("dark")}
|
||||||
|
>
|
||||||
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
|
<Monitor className="mr-2 h-4 w-4" />
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Appearance } from "@/components/Common/Appearance"
|
||||||
|
import { Logo } from "@/components/Common/Logo"
|
||||||
|
import { Footer } from "./Footer"
|
||||||
|
|
||||||
|
interface AuthLayoutProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthLayout({ children }: AuthLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="grid min-h-svh lg:grid-cols-2">
|
||||||
|
<div className="bg-muted dark:bg-zinc-900 relative hidden lg:flex lg:items-center lg:justify-center">
|
||||||
|
<Logo variant="full" className="h-16" asLink={false} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Appearance />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center justify-center">
|
||||||
|
<div className="w-full max-w-xs">{children}</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id} className="hover:bg-transparent">
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow className="hover:bg-transparent">
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-32 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No results found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{table.getPageCount() > 1 && (
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 p-4 border-t bg-muted/20">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Showing{" "}
|
||||||
|
{table.getState().pagination.pageIndex *
|
||||||
|
table.getState().pagination.pageSize +
|
||||||
|
1}{" "}
|
||||||
|
to{" "}
|
||||||
|
{Math.min(
|
||||||
|
(table.getState().pagination.pageIndex + 1) *
|
||||||
|
table.getState().pagination.pageSize,
|
||||||
|
data.length,
|
||||||
|
)}{" "}
|
||||||
|
of{" "}
|
||||||
|
<span className="font-medium text-foreground">{data.length}</span>{" "}
|
||||||
|
entries
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<p className="text-sm text-muted-foreground">Rows per page</p>
|
||||||
|
<Select
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={table.getState().pagination.pageSize}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[5, 10, 25, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-x-6">
|
||||||
|
<div className="flex items-center gap-x-1 text-sm text-muted-foreground">
|
||||||
|
<span>Page</span>
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{table.getState().pagination.pageIndex + 1}
|
||||||
|
</span>
|
||||||
|
<span>of</span>
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{table.getPageCount()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to first page</span>
|
||||||
|
<ChevronsLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to previous page</span>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to next page</span>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to last page</span>
|
||||||
|
<ChevronsRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const ErrorComponent = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex min-h-screen items-center justify-center flex-col p-4"
|
||||||
|
data-testid="error-component"
|
||||||
|
>
|
||||||
|
<div className="flex items-center z-10">
|
||||||
|
<div className="flex flex-col ml-4 items-center justify-center p-4">
|
||||||
|
<span className="text-6xl md:text-8xl font-bold leading-none mb-4">
|
||||||
|
Error
|
||||||
|
</span>
|
||||||
|
<span className="text-2xl font-bold mb-2">Oops!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-lg text-muted-foreground mb-4 text-center z-10">
|
||||||
|
Something went wrong. Please try again.
|
||||||
|
</p>
|
||||||
|
<Link to="/">
|
||||||
|
<Button>Go Home</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorComponent
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { FaGithub, FaLinkedinIn } from "react-icons/fa"
|
||||||
|
import { FaXTwitter } from "react-icons/fa6"
|
||||||
|
|
||||||
|
const socialLinks = [
|
||||||
|
{ icon: FaGithub, href: "https://github.com/fastapi/fastapi", label: "GitHub" },
|
||||||
|
{ icon: FaXTwitter, href: "https://x.com/fastapi", label: "X" },
|
||||||
|
{ icon: FaLinkedinIn, href: "https://linkedin.com/company/fastapi", label: "LinkedIn" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="border-t py-4 px-6">
|
||||||
|
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Full Stack FastAPI Template - {currentYear}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{socialLinks.map(({ icon: Icon, href, label }) => (
|
||||||
|
<a
|
||||||
|
key={label}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label={label}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { IconButton } from "@chakra-ui/react"
|
|
||||||
import { BsThreeDotsVertical } from "react-icons/bs"
|
|
||||||
import { MenuContent, MenuRoot, MenuTrigger } from "../ui/menu"
|
|
||||||
|
|
||||||
import type { ItemPublic } from "@/client"
|
|
||||||
import DeleteItem from "../Items/DeleteItem"
|
|
||||||
import EditItem from "../Items/EditItem"
|
|
||||||
|
|
||||||
interface ItemActionsMenuProps {
|
|
||||||
item: ItemPublic
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => {
|
|
||||||
return (
|
|
||||||
<MenuRoot>
|
|
||||||
<MenuTrigger asChild>
|
|
||||||
<IconButton variant="ghost" color="inherit">
|
|
||||||
<BsThreeDotsVertical />
|
|
||||||
</IconButton>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuContent>
|
|
||||||
<EditItem item={item} />
|
|
||||||
<DeleteItem id={item.id} />
|
|
||||||
</MenuContent>
|
|
||||||
</MenuRoot>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
import { useTheme } from "@/components/theme-provider"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import icon from "/assets/images/fastapi-icon.svg"
|
||||||
|
import iconLight from "/assets/images/fastapi-icon-light.svg"
|
||||||
|
import logo from "/assets/images/fastapi-logo.svg"
|
||||||
|
import logoLight from "/assets/images/fastapi-logo-light.svg"
|
||||||
|
|
||||||
|
interface LogoProps {
|
||||||
|
variant?: "full" | "icon" | "responsive"
|
||||||
|
className?: string
|
||||||
|
asLink?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Logo({
|
||||||
|
variant = "full",
|
||||||
|
className,
|
||||||
|
asLink = true,
|
||||||
|
}: LogoProps) {
|
||||||
|
const { resolvedTheme } = useTheme()
|
||||||
|
const isDark = resolvedTheme === "dark"
|
||||||
|
|
||||||
|
const fullLogo = isDark ? logoLight : logo
|
||||||
|
const iconLogo = isDark ? iconLight : icon
|
||||||
|
|
||||||
|
const content =
|
||||||
|
variant === "responsive" ? (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={fullLogo}
|
||||||
|
alt="FastAPI"
|
||||||
|
className={cn(
|
||||||
|
"h-6 w-auto group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={iconLogo}
|
||||||
|
alt="FastAPI"
|
||||||
|
className={cn(
|
||||||
|
"size-5 hidden group-data-[collapsible=icon]:block",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={variant === "full" ? fullLogo : iconLogo}
|
||||||
|
alt="FastAPI"
|
||||||
|
className={cn(variant === "full" ? "h-6 w-auto" : "size-5", className)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!asLink) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Link to="/">{content}</Link>
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Flex, Image, useBreakpointValue } from "@chakra-ui/react"
|
|
||||||
import { Link } from "@tanstack/react-router"
|
|
||||||
|
|
||||||
import Logo from "/assets/images/fastapi-logo.svg"
|
|
||||||
import UserMenu from "./UserMenu"
|
|
||||||
|
|
||||||
function Navbar() {
|
|
||||||
const display = useBreakpointValue({ base: "none", md: "flex" })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
display={display}
|
|
||||||
justify="space-between"
|
|
||||||
position="sticky"
|
|
||||||
color="white"
|
|
||||||
align="center"
|
|
||||||
bg="bg.muted"
|
|
||||||
w="100%"
|
|
||||||
top={0}
|
|
||||||
p={4}
|
|
||||||
>
|
|
||||||
<Link to="/">
|
|
||||||
<Image src={Logo} alt="Logo" w="180px" maxW="2xs" px={2} />
|
|
||||||
</Link>
|
|
||||||
<Flex gap={2} alignItems="center">
|
|
||||||
<UserMenu />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Navbar
|
|
||||||
@@ -1,56 +1,30 @@
|
|||||||
import { Button, Center, Flex, Text } from "@chakra-ui/react"
|
|
||||||
import { Link } from "@tanstack/react-router"
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<Flex
|
className="flex min-h-screen items-center justify-center flex-col p-4"
|
||||||
height="100vh"
|
data-testid="not-found"
|
||||||
align="center"
|
>
|
||||||
justify="center"
|
<div className="flex items-center z-10">
|
||||||
flexDir="column"
|
<div className="flex flex-col ml-4 items-center justify-center p-4">
|
||||||
data-testid="not-found"
|
<span className="text-6xl md:text-8xl font-bold leading-none mb-4">
|
||||||
p={4}
|
404
|
||||||
>
|
</span>
|
||||||
<Flex alignItems="center" zIndex={1}>
|
<span className="text-2xl font-bold mb-2">Oops!</span>
|
||||||
<Flex flexDir="column" ml={4} align="center" justify="center" p={4}>
|
</div>
|
||||||
<Text
|
</div>
|
||||||
fontSize={{ base: "6xl", md: "8xl" }}
|
|
||||||
fontWeight="bold"
|
|
||||||
lineHeight="1"
|
|
||||||
mb={4}
|
|
||||||
>
|
|
||||||
404
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="2xl" fontWeight="bold" mb={2}>
|
|
||||||
Oops!
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Text
|
<p className="text-lg text-muted-foreground mb-4 text-center z-10">
|
||||||
fontSize="lg"
|
The page you are looking for was not found.
|
||||||
color="gray.600"
|
</p>
|
||||||
mb={4}
|
<div className="z-10">
|
||||||
textAlign="center"
|
<Link to="/">
|
||||||
zIndex={1}
|
<Button className="mt-4">Go Back</Button>
|
||||||
>
|
</Link>
|
||||||
The page you are looking for was not found.
|
</div>
|
||||||
</Text>
|
</div>
|
||||||
<Center zIndex={1}>
|
|
||||||
<Link to="/">
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
colorScheme="teal"
|
|
||||||
mt={4}
|
|
||||||
alignSelf="center"
|
|
||||||
>
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</Center>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Box, Flex, IconButton, Text } from "@chakra-ui/react"
|
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { FaBars } from "react-icons/fa"
|
|
||||||
import { FiLogOut } from "react-icons/fi"
|
|
||||||
|
|
||||||
import type { UserPublic } from "@/client"
|
|
||||||
import useAuth from "@/hooks/useAuth"
|
|
||||||
import {
|
|
||||||
DrawerBackdrop,
|
|
||||||
DrawerBody,
|
|
||||||
DrawerCloseTrigger,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerRoot,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from "../ui/drawer"
|
|
||||||
import SidebarItems from "./SidebarItems"
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
|
|
||||||
const { logout } = useAuth()
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
logout()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Mobile */}
|
|
||||||
<DrawerRoot
|
|
||||||
placement="start"
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(e) => setOpen(e.open)}
|
|
||||||
>
|
|
||||||
<DrawerBackdrop />
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
color="inherit"
|
|
||||||
display={{ base: "flex", md: "none" }}
|
|
||||||
aria-label="Open Menu"
|
|
||||||
position="absolute"
|
|
||||||
zIndex="100"
|
|
||||||
m={4}
|
|
||||||
>
|
|
||||||
<FaBars />
|
|
||||||
</IconButton>
|
|
||||||
</DrawerTrigger>
|
|
||||||
<DrawerContent maxW="280px">
|
|
||||||
<DrawerCloseTrigger />
|
|
||||||
<DrawerBody>
|
|
||||||
<Flex flexDir="column" justify="space-between">
|
|
||||||
<Box>
|
|
||||||
<SidebarItems />
|
|
||||||
<Flex
|
|
||||||
as="button"
|
|
||||||
onClick={handleLogout}
|
|
||||||
alignItems="center"
|
|
||||||
gap={4}
|
|
||||||
px={4}
|
|
||||||
py={2}
|
|
||||||
>
|
|
||||||
<FiLogOut />
|
|
||||||
<Text>Log Out</Text>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
{currentUser?.email && (
|
|
||||||
<Text fontSize="sm" p={2}>
|
|
||||||
Logged in as: {currentUser.email}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</DrawerBody>
|
|
||||||
<DrawerCloseTrigger />
|
|
||||||
</DrawerContent>
|
|
||||||
</DrawerRoot>
|
|
||||||
|
|
||||||
{/* Desktop */}
|
|
||||||
|
|
||||||
<Box
|
|
||||||
display={{ base: "none", md: "flex" }}
|
|
||||||
position="sticky"
|
|
||||||
bg="bg.subtle"
|
|
||||||
top={0}
|
|
||||||
minW="280px"
|
|
||||||
h="100vh"
|
|
||||||
p={4}
|
|
||||||
>
|
|
||||||
<Box w="100%">
|
|
||||||
<SidebarItems />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sidebar
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Box, Flex, Icon, Text } from "@chakra-ui/react"
|
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
|
||||||
import { Link as RouterLink } from "@tanstack/react-router"
|
|
||||||
import { FiBriefcase, FiHome, FiSettings, FiUsers } from "react-icons/fi"
|
|
||||||
import type { IconType } from "react-icons/lib"
|
|
||||||
|
|
||||||
import type { UserPublic } from "@/client"
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ icon: FiHome, title: "Dashboard", path: "/" },
|
|
||||||
{ icon: FiBriefcase, title: "Items", path: "/items" },
|
|
||||||
{ icon: FiSettings, title: "User Settings", path: "/settings" },
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SidebarItemsProps {
|
|
||||||
onClose?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Item {
|
|
||||||
icon: IconType
|
|
||||||
title: string
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarItems = ({ onClose }: SidebarItemsProps) => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
|
|
||||||
|
|
||||||
const finalItems: Item[] = currentUser?.is_superuser
|
|
||||||
? [...items, { icon: FiUsers, title: "Admin", path: "/admin" }]
|
|
||||||
: items
|
|
||||||
|
|
||||||
const listItems = finalItems.map(({ icon, title, path }) => (
|
|
||||||
<RouterLink key={title} to={path} onClick={onClose}>
|
|
||||||
<Flex
|
|
||||||
gap={4}
|
|
||||||
px={4}
|
|
||||||
py={2}
|
|
||||||
_hover={{
|
|
||||||
background: "gray.subtle",
|
|
||||||
}}
|
|
||||||
alignItems="center"
|
|
||||||
fontSize="sm"
|
|
||||||
>
|
|
||||||
<Icon as={icon} alignSelf="center" />
|
|
||||||
<Text ml={2}>{title}</Text>
|
|
||||||
</Flex>
|
|
||||||
</RouterLink>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text fontSize="xs" px={4} py={2} fontWeight="bold">
|
|
||||||
Menu
|
|
||||||
</Text>
|
|
||||||
<Box>{listItems}</Box>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SidebarItems
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { IconButton } from "@chakra-ui/react"
|
|
||||||
import { BsThreeDotsVertical } from "react-icons/bs"
|
|
||||||
import { MenuContent, MenuRoot, MenuTrigger } from "../ui/menu"
|
|
||||||
|
|
||||||
import type { UserPublic } from "@/client"
|
|
||||||
import DeleteUser from "../Admin/DeleteUser"
|
|
||||||
import EditUser from "../Admin/EditUser"
|
|
||||||
|
|
||||||
interface UserActionsMenuProps {
|
|
||||||
user: UserPublic
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserActionsMenu = ({ user, disabled }: UserActionsMenuProps) => {
|
|
||||||
return (
|
|
||||||
<MenuRoot>
|
|
||||||
<MenuTrigger asChild>
|
|
||||||
<IconButton variant="ghost" color="inherit" disabled={disabled}>
|
|
||||||
<BsThreeDotsVertical />
|
|
||||||
</IconButton>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuContent>
|
|
||||||
<EditUser user={user} />
|
|
||||||
<DeleteUser id={user.id} />
|
|
||||||
</MenuContent>
|
|
||||||
</MenuRoot>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Box, Button, Flex, Text } from "@chakra-ui/react"
|
|
||||||
import { Link } from "@tanstack/react-router"
|
|
||||||
import { FaUserAstronaut } from "react-icons/fa"
|
|
||||||
import { FiLogOut, FiUser } from "react-icons/fi"
|
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth"
|
|
||||||
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from "../ui/menu"
|
|
||||||
|
|
||||||
const UserMenu = () => {
|
|
||||||
const { user, logout } = useAuth()
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
logout()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Desktop */}
|
|
||||||
<Flex>
|
|
||||||
<MenuRoot>
|
|
||||||
<MenuTrigger asChild p={2}>
|
|
||||||
<Button
|
|
||||||
data-testid="user-menu"
|
|
||||||
variant="solid"
|
|
||||||
maxW="150px"
|
|
||||||
truncate
|
|
||||||
>
|
|
||||||
<FaUserAstronaut fontSize="18" />
|
|
||||||
<Text>{user?.full_name || "User"}</Text>
|
|
||||||
</Button>
|
|
||||||
</MenuTrigger>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
<Link to="settings">
|
|
||||||
<MenuItem
|
|
||||||
closeOnSelect
|
|
||||||
value="user-settings"
|
|
||||||
gap={2}
|
|
||||||
py={2}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<FiUser fontSize="18px" />
|
|
||||||
<Box flex="1">My Profile</Box>
|
|
||||||
</MenuItem>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
value="logout"
|
|
||||||
gap={2}
|
|
||||||
py={2}
|
|
||||||
onClick={handleLogout}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<FiLogOut />
|
|
||||||
Log Out
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuRoot>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserMenu
|
|
||||||
@@ -1,42 +1,49 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { type SubmitHandler, useForm } from "react-hook-form"
|
import { Plus } from "lucide-react"
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DialogActionTrigger,
|
|
||||||
DialogTitle,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { FaPlus } from "react-icons/fa"
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
import { type ItemCreate, ItemsService } from "@/client"
|
import { type ItemCreate, ItemsService } from "@/client"
|
||||||
import type { ApiError } from "@/client/core/ApiError"
|
import { Button } from "@/components/ui/button"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
|
||||||
import { handleError } from "@/utils"
|
|
||||||
import {
|
import {
|
||||||
DialogBody,
|
Dialog,
|
||||||
DialogCloseTrigger,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogRoot,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "../ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Field } from "../ui/field"
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
title: z.string().min(1, { message: "Title is required" }),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|
||||||
const AddItem = () => {
|
const AddItem = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
|
||||||
register,
|
const form = useForm<FormData>({
|
||||||
handleSubmit,
|
resolver: zodResolver(formSchema),
|
||||||
reset,
|
|
||||||
formState: { errors, isValid, isSubmitting },
|
|
||||||
} = useForm<ItemCreate>({
|
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -49,97 +56,88 @@ const AddItem = () => {
|
|||||||
mutationFn: (data: ItemCreate) =>
|
mutationFn: (data: ItemCreate) =>
|
||||||
ItemsService.createItem({ requestBody: data }),
|
ItemsService.createItem({ requestBody: data }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("Item created successfully.")
|
showSuccessToast("Item created successfully")
|
||||||
reset()
|
form.reset()
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
},
|
},
|
||||||
onError: (err: ApiError) => {
|
onError: handleError.bind(showErrorToast),
|
||||||
handleError(err)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["items"] })
|
queryClient.invalidateQueries({ queryKey: ["items"] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ItemCreate> = (data) => {
|
const onSubmit = (data: FormData) => {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
size={{ base: "xs", md: "md" }}
|
|
||||||
placement="center"
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button value="add-item" my={4}>
|
<Button className="my-4">
|
||||||
<FaPlus fontSize="16px" />
|
<Plus className="mr-2" />
|
||||||
Add Item
|
Add Item
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="sm:max-w-md">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>Add Item</DialogTitle>
|
||||||
<DialogTitle>Add Item</DialogTitle>
|
<DialogDescription>
|
||||||
</DialogHeader>
|
Fill in the details to add a new item.
|
||||||
<DialogBody>
|
</DialogDescription>
|
||||||
<Text mb={4}>Fill in the details to add a new item.</Text>
|
</DialogHeader>
|
||||||
<VStack gap={4}>
|
<Form {...form}>
|
||||||
<Field
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
required
|
<div className="grid gap-4 py-4">
|
||||||
invalid={!!errors.title}
|
<FormField
|
||||||
errorText={errors.title?.message}
|
control={form.control}
|
||||||
label="Title"
|
name="title"
|
||||||
>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem>
|
||||||
id="title"
|
<FormLabel>
|
||||||
{...register("title", {
|
Title <span className="text-destructive">*</span>
|
||||||
required: "Title is required.",
|
</FormLabel>
|
||||||
})}
|
<FormControl>
|
||||||
placeholder="Title"
|
<Input
|
||||||
type="text"
|
placeholder="Title"
|
||||||
/>
|
type="text"
|
||||||
</Field>
|
{...field}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
invalid={!!errors.description}
|
control={form.control}
|
||||||
errorText={errors.description?.message}
|
name="description"
|
||||||
label="Description"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
<Input
|
<FormLabel>Description</FormLabel>
|
||||||
id="description"
|
<FormControl>
|
||||||
{...register("description")}
|
<Input placeholder="Description" type="text" {...field} />
|
||||||
placeholder="Description"
|
</FormControl>
|
||||||
type="text"
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</Field>
|
)}
|
||||||
</VStack>
|
/>
|
||||||
</DialogBody>
|
</div>
|
||||||
|
|
||||||
<DialogFooter gap={2}>
|
<DialogFooter>
|
||||||
<DialogActionTrigger asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
variant="subtle"
|
Cancel
|
||||||
colorPalette="gray"
|
</Button>
|
||||||
disabled={isSubmitting}
|
</DialogClose>
|
||||||
>
|
<LoadingButton type="submit" loading={mutation.isPending}>
|
||||||
Cancel
|
Save
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</DialogActionTrigger>
|
</DialogFooter>
|
||||||
<Button
|
</form>
|
||||||
variant="solid"
|
</Form>
|
||||||
type="submit"
|
|
||||||
disabled={!isValid}
|
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
<DialogCloseTrigger />
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,34 @@
|
|||||||
import { Button, DialogTitle, Text } from "@chakra-ui/react"
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Trash2 } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { FiTrash2 } from "react-icons/fi"
|
|
||||||
|
|
||||||
import { ItemsService } from "@/client"
|
import { ItemsService } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
DialogActionTrigger,
|
Dialog,
|
||||||
DialogBody,
|
DialogClose,
|
||||||
DialogCloseTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogRoot,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
const DeleteItem = ({ id }: { id: string }) => {
|
interface DeleteItemProps {
|
||||||
|
id: string
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteItem = ({ id, onSuccess }: DeleteItemProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast, showErrorToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
const { handleSubmit } = useForm()
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm()
|
|
||||||
|
|
||||||
const deleteItem = async (id: string) => {
|
const deleteItem = async (id: string) => {
|
||||||
await ItemsService.deleteItem({ id: id })
|
await ItemsService.deleteItem({ id: id })
|
||||||
@@ -35,10 +39,9 @@ const DeleteItem = ({ id }: { id: string }) => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("The item was deleted successfully")
|
showSuccessToast("The item was deleted successfully")
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
|
onSuccess()
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: handleError.bind(showErrorToast),
|
||||||
showErrorToast("An error occurred while deleting the item")
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries()
|
queryClient.invalidateQueries()
|
||||||
},
|
},
|
||||||
@@ -49,55 +52,42 @@ const DeleteItem = ({ id }: { id: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
size={{ base: "xs", md: "md" }}
|
<DropdownMenuItem
|
||||||
placement="center"
|
variant="destructive"
|
||||||
role="alertdialog"
|
onSelect={(e) => e.preventDefault()}
|
||||||
open={isOpen}
|
onClick={() => setIsOpen(true)}
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
>
|
||||||
>
|
<Trash2 />
|
||||||
<DialogTrigger asChild>
|
Delete Item
|
||||||
<Button variant="ghost" size="sm" colorPalette="red">
|
</DropdownMenuItem>
|
||||||
<FiTrash2 fontSize="16px" />
|
<DialogContent className="sm:max-w-md">
|
||||||
Delete Item
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<DialogCloseTrigger />
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Delete Item</DialogTitle>
|
<DialogTitle>Delete Item</DialogTitle>
|
||||||
</DialogHeader>
|
<DialogDescription>
|
||||||
<DialogBody>
|
|
||||||
<Text mb={4}>
|
|
||||||
This item will be permanently deleted. Are you sure? You will not
|
This item will be permanently deleted. Are you sure? You will not
|
||||||
be able to undo this action.
|
be able to undo this action.
|
||||||
</Text>
|
</DialogDescription>
|
||||||
</DialogBody>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogFooter gap={2}>
|
<DialogFooter className="mt-4">
|
||||||
<DialogActionTrigger asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
variant="subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActionTrigger>
|
</DialogClose>
|
||||||
<Button
|
<LoadingButton
|
||||||
variant="solid"
|
variant="destructive"
|
||||||
colorPalette="red"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isSubmitting}
|
loading={mutation.isPending}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +1,144 @@
|
|||||||
import {
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
DialogActionTrigger,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Pencil } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { type SubmitHandler, useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { FaExchangeAlt } from "react-icons/fa"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { type ApiError, type ItemPublic, ItemsService } from "@/client"
|
import { type ItemPublic, ItemsService } from "@/client"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import { Button } from "@/components/ui/button"
|
||||||
import { handleError } from "@/utils"
|
|
||||||
import {
|
import {
|
||||||
DialogBody,
|
Dialog,
|
||||||
DialogCloseTrigger,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogRoot,
|
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
} from "@/components/ui/dialog"
|
||||||
} from "../ui/dialog"
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
||||||
import { Field } from "../ui/field"
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
title: z.string().min(1, { message: "Title is required" }),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|
||||||
interface EditItemProps {
|
interface EditItemProps {
|
||||||
item: ItemPublic
|
item: ItemPublic
|
||||||
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ItemUpdateForm {
|
const EditItem = ({ item, onSuccess }: EditItemProps) => {
|
||||||
title: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditItem = ({ item }: EditItemProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
|
||||||
register,
|
const form = useForm<FormData>({
|
||||||
handleSubmit,
|
resolver: zodResolver(formSchema),
|
||||||
reset,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm<ItemUpdateForm>({
|
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...item,
|
title: item.title,
|
||||||
description: item.description ?? undefined,
|
description: item.description ?? undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data: ItemUpdateForm) =>
|
mutationFn: (data: FormData) =>
|
||||||
ItemsService.updateItem({ id: item.id, requestBody: data }),
|
ItemsService.updateItem({ id: item.id, requestBody: data }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("Item updated successfully.")
|
showSuccessToast("Item updated successfully")
|
||||||
reset()
|
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
|
onSuccess()
|
||||||
},
|
},
|
||||||
onError: (err: ApiError) => {
|
onError: handleError.bind(showErrorToast),
|
||||||
handleError(err)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["items"] })
|
queryClient.invalidateQueries({ queryKey: ["items"] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ItemUpdateForm> = async (data) => {
|
const onSubmit = (data: FormData) => {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
size={{ base: "xs", md: "md" }}
|
<DropdownMenuItem
|
||||||
placement="center"
|
onSelect={(e) => e.preventDefault()}
|
||||||
open={isOpen}
|
onClick={() => setIsOpen(true)}
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
>
|
||||||
>
|
<Pencil />
|
||||||
<DialogTrigger asChild>
|
Edit Item
|
||||||
<Button variant="ghost">
|
</DropdownMenuItem>
|
||||||
<FaExchangeAlt fontSize="16px" />
|
<DialogContent className="sm:max-w-md">
|
||||||
Edit Item
|
<Form {...form}>
|
||||||
</Button>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
</DialogTrigger>
|
<DialogHeader>
|
||||||
<DialogContent>
|
<DialogTitle>Edit Item</DialogTitle>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<DialogDescription>
|
||||||
<DialogHeader>
|
Update the item details below.
|
||||||
<DialogTitle>Edit Item</DialogTitle>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogBody>
|
<div className="grid gap-4 py-4">
|
||||||
<Text mb={4}>Update the item details below.</Text>
|
<FormField
|
||||||
<VStack gap={4}>
|
control={form.control}
|
||||||
<Field
|
name="title"
|
||||||
required
|
render={({ field }) => (
|
||||||
invalid={!!errors.title}
|
<FormItem>
|
||||||
errorText={errors.title?.message}
|
<FormLabel>
|
||||||
label="Title"
|
Title <span className="text-destructive">*</span>
|
||||||
>
|
</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
id="title"
|
<Input placeholder="Title" type="text" {...field} />
|
||||||
{...register("title", {
|
</FormControl>
|
||||||
required: "Title is required",
|
<FormMessage />
|
||||||
})}
|
</FormItem>
|
||||||
placeholder="Title"
|
)}
|
||||||
type="text"
|
/>
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
<FormField
|
||||||
invalid={!!errors.description}
|
control={form.control}
|
||||||
errorText={errors.description?.message}
|
name="description"
|
||||||
label="Description"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
<Input
|
<FormLabel>Description</FormLabel>
|
||||||
id="description"
|
<FormControl>
|
||||||
{...register("description")}
|
<Input placeholder="Description" type="text" {...field} />
|
||||||
placeholder="Description"
|
</FormControl>
|
||||||
type="text"
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</Field>
|
)}
|
||||||
</VStack>
|
/>
|
||||||
</DialogBody>
|
</div>
|
||||||
|
|
||||||
<DialogFooter gap={2}>
|
<DialogFooter>
|
||||||
<ButtonGroup>
|
<DialogClose asChild>
|
||||||
<DialogActionTrigger asChild>
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActionTrigger>
|
</DialogClose>
|
||||||
<Button variant="solid" type="submit" loading={isSubmitting}>
|
<LoadingButton type="submit" loading={mutation.isPending}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</ButtonGroup>
|
</DialogFooter>
|
||||||
</DialogFooter>
|
</form>
|
||||||
</form>
|
</Form>
|
||||||
<DialogCloseTrigger />
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { EllipsisVertical } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import type { ItemPublic } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import DeleteItem from "../Items/DeleteItem"
|
||||||
|
import EditItem from "../Items/EditItem"
|
||||||
|
|
||||||
|
interface ItemActionsMenuProps {
|
||||||
|
item: ItemPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<EllipsisVertical />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<EditItem item={item} onSuccess={() => setOpen(false)} />
|
||||||
|
<DeleteItem id={item.id} onSuccess={() => setOpen(false)} />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { Check, Copy } from "lucide-react"
|
||||||
|
|
||||||
|
import type { ItemPublic } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ItemActionsMenu } from "./ItemActionsMenu"
|
||||||
|
|
||||||
|
function CopyId({ id }: { id: string }) {
|
||||||
|
const [copiedText, copy] = useCopyToClipboard()
|
||||||
|
const isCopied = copiedText === id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 group">
|
||||||
|
<span className="font-mono text-xs text-muted-foreground">{id}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={() => copy(id)}
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<Check className="size-3 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-3" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">Copy ID</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columns: ColumnDef<ItemPublic>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
cell: ({ row }) => <CopyId id={row.original.id} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "title",
|
||||||
|
header: "Title",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-medium">{row.original.title}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: "Description",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const description = row.original.description
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"max-w-xs truncate block text-muted-foreground",
|
||||||
|
!description && "italic",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description || "No description"}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <span className="sr-only">Actions</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<ItemActionsMenu item={row.original} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,34 +1,46 @@
|
|||||||
import { Skeleton, Table } from "@chakra-ui/react"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
|
||||||
const PendingItems = () => (
|
const PendingItems = () => (
|
||||||
<Table.Root size={{ base: "sm", md: "md" }}>
|
<Table>
|
||||||
<Table.Header>
|
<TableHeader>
|
||||||
<Table.Row>
|
<TableRow>
|
||||||
<Table.ColumnHeader w="30%">ID</Table.ColumnHeader>
|
<TableHead>ID</TableHead>
|
||||||
<Table.ColumnHeader w="30%">Title</Table.ColumnHeader>
|
<TableHead>Title</TableHead>
|
||||||
<Table.ColumnHeader w="30%">Description</Table.ColumnHeader>
|
<TableHead>Description</TableHead>
|
||||||
<Table.ColumnHeader w="10%">Actions</Table.ColumnHeader>
|
<TableHead>
|
||||||
</Table.Row>
|
<span className="sr-only">Actions</span>
|
||||||
</Table.Header>
|
</TableHead>
|
||||||
<Table.Body>
|
</TableRow>
|
||||||
{[...Array(5)].map((_, index) => (
|
</TableHeader>
|
||||||
<Table.Row key={index}>
|
<TableBody>
|
||||||
<Table.Cell>
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
<Skeleton h="20px" />
|
<TableRow key={index}>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<Skeleton className="h-4 w-64 font-mono" />
|
||||||
<Skeleton h="20px" />
|
</TableCell>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<Skeleton className="h-4 w-32" />
|
||||||
<Skeleton h="20px" />
|
</TableCell>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<Skeleton className="h-4 w-48" />
|
||||||
<Skeleton h="20px" />
|
</TableCell>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
</Table.Row>
|
<div className="flex justify-end">
|
||||||
|
<Skeleton className="size-8 rounded-md" />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</Table.Body>
|
</TableBody>
|
||||||
</Table.Root>
|
</Table>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default PendingItems
|
export default PendingItems
|
||||||
|
|||||||
@@ -1,38 +1,53 @@
|
|||||||
import { Skeleton, Table } from "@chakra-ui/react"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
|
||||||
const PendingUsers = () => (
|
const PendingUsers = () => (
|
||||||
<Table.Root size={{ base: "sm", md: "md" }}>
|
<Table>
|
||||||
<Table.Header>
|
<TableHeader>
|
||||||
<Table.Row>
|
<TableRow>
|
||||||
<Table.ColumnHeader w="20%">Full name</Table.ColumnHeader>
|
<TableHead>Full Name</TableHead>
|
||||||
<Table.ColumnHeader w="25%">Email</Table.ColumnHeader>
|
<TableHead>Email</TableHead>
|
||||||
<Table.ColumnHeader w="15%">Role</Table.ColumnHeader>
|
<TableHead>Role</TableHead>
|
||||||
<Table.ColumnHeader w="20%">Status</Table.ColumnHeader>
|
<TableHead>Status</TableHead>
|
||||||
<Table.ColumnHeader w="20%">Actions</Table.ColumnHeader>
|
<TableHead>
|
||||||
</Table.Row>
|
<span className="sr-only">Actions</span>
|
||||||
</Table.Header>
|
</TableHead>
|
||||||
<Table.Body>
|
</TableRow>
|
||||||
{[...Array(5)].map((_, index) => (
|
</TableHeader>
|
||||||
<Table.Row key={index}>
|
<TableBody>
|
||||||
<Table.Cell>
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
<Skeleton h="20px" />
|
<TableRow key={index}>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<Skeleton className="h-4 w-32" />
|
||||||
<Skeleton h="20px" />
|
</TableCell>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<Skeleton className="h-4 w-40" />
|
||||||
<Skeleton h="20px" />
|
</TableCell>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<Skeleton className="h-5 w-20 rounded-full" />
|
||||||
<Skeleton h="20px" />
|
</TableCell>
|
||||||
</Table.Cell>
|
<TableCell>
|
||||||
<Table.Cell>
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton h="20px" />
|
<Skeleton className="size-2 rounded-full" />
|
||||||
</Table.Cell>
|
<Skeleton className="h-4 w-12" />
|
||||||
</Table.Row>
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Skeleton className="size-8 rounded-md" />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</Table.Body>
|
</TableBody>
|
||||||
</Table.Root>
|
</Table>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default PendingUsers
|
export default PendingUsers
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Briefcase, Home, Users } from "lucide-react"
|
||||||
|
|
||||||
|
import { SidebarAppearance } from "@/components/Common/Appearance"
|
||||||
|
import { Logo } from "@/components/Common/Logo"
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarHeader,
|
||||||
|
} from "@/components/ui/sidebar"
|
||||||
|
import useAuth from "@/hooks/useAuth"
|
||||||
|
import { type Item, Main } from "./Main"
|
||||||
|
import { User } from "./User"
|
||||||
|
|
||||||
|
const baseItems: Item[] = [
|
||||||
|
{ icon: Home, title: "Dashboard", path: "/" },
|
||||||
|
{ icon: Briefcase, title: "Items", path: "/items" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function AppSidebar() {
|
||||||
|
const { user: currentUser } = useAuth()
|
||||||
|
|
||||||
|
const items = currentUser?.is_superuser
|
||||||
|
? [...baseItems, { icon: Users, title: "Admin", path: "/admin" }]
|
||||||
|
: baseItems
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar collapsible="icon">
|
||||||
|
<SidebarHeader className="px-4 py-6 group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:items-center">
|
||||||
|
<Logo variant="responsive" />
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<Main items={items} />
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarFooter>
|
||||||
|
<SidebarAppearance />
|
||||||
|
<User user={currentUser} />
|
||||||
|
</SidebarFooter>
|
||||||
|
</Sidebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppSidebar
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Link as RouterLink, useRouterState } from "@tanstack/react-router"
|
||||||
|
import type { LucideIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar"
|
||||||
|
|
||||||
|
export type Item = {
|
||||||
|
icon: LucideIcon
|
||||||
|
title: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MainProps {
|
||||||
|
items: Item[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Main({ items }: MainProps) {
|
||||||
|
const { isMobile, setOpenMobile } = useSidebar()
|
||||||
|
const router = useRouterState()
|
||||||
|
const currentPath = router.location.pathname
|
||||||
|
|
||||||
|
const handleMenuClick = () => {
|
||||||
|
if (isMobile) {
|
||||||
|
setOpenMobile(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{items.map((item) => {
|
||||||
|
const isActive = currentPath === item.path
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={item.title}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
tooltip={item.title}
|
||||||
|
isActive={isActive}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<RouterLink to={item.path} onClick={handleMenuClick}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Link as RouterLink } from "@tanstack/react-router"
|
||||||
|
import { ChevronsUpDown, LogOut, Settings } from "lucide-react"
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar"
|
||||||
|
import useAuth from "@/hooks/useAuth"
|
||||||
|
import { getInitials } from "@/utils"
|
||||||
|
|
||||||
|
interface UserInfoProps {
|
||||||
|
fullName?: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserInfo({ fullName, email }: UserInfoProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2.5 w-full min-w-0">
|
||||||
|
<Avatar className="size-8">
|
||||||
|
<AvatarFallback className="bg-zinc-600 text-white">
|
||||||
|
{getInitials(fullName || "User")}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col items-start min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate w-full">{fullName}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate w-full">{email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function User({ user }: { user: any }) {
|
||||||
|
const { logout } = useAuth()
|
||||||
|
const { isMobile, setOpenMobile } = useSidebar()
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
const handleMenuClick = () => {
|
||||||
|
if (isMobile) {
|
||||||
|
setOpenMobile(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleLogout = async () => {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
data-testid="user-menu"
|
||||||
|
>
|
||||||
|
<UserInfo fullName={user?.full_name} email={user?.email} />
|
||||||
|
<ChevronsUpDown className="ml-auto size-4 text-muted-foreground" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
|
<UserInfo fullName={user?.full_name} email={user?.email} />
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<RouterLink to="/settings" onClick={handleMenuClick}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Settings />
|
||||||
|
User Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</RouterLink>
|
||||||
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
|
<LogOut />
|
||||||
|
Log Out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Container, Heading, Stack } from "@chakra-ui/react"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
|
|
||||||
import { Radio, RadioGroup } from "@/components/ui/radio"
|
|
||||||
|
|
||||||
const Appearance = () => {
|
|
||||||
const { theme, setTheme } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Container maxW="full">
|
|
||||||
<Heading size="sm" py={4}>
|
|
||||||
Appearance
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={(e) => setTheme(e.value)}
|
|
||||||
value={theme}
|
|
||||||
colorPalette="teal"
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Radio value="system">System</Radio>
|
|
||||||
<Radio value="light">Light Mode</Radio>
|
|
||||||
<Radio value="dark">Dark Mode</Radio>
|
|
||||||
</Stack>
|
|
||||||
</RadioGroup>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default Appearance
|
|
||||||
@@ -1,92 +1,146 @@
|
|||||||
import { Box, Button, Container, Heading, VStack } from "@chakra-ui/react"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { type SubmitHandler, useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { FiLock } from "react-icons/fi"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { type ApiError, type UpdatePassword, UsersService } from "@/client"
|
import { type UpdatePassword, UsersService } from "@/client"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
import { confirmPasswordRules, handleError, passwordRules } from "@/utils"
|
import { handleError } from "@/utils"
|
||||||
import { PasswordInput } from "../ui/password-input"
|
|
||||||
|
|
||||||
interface UpdatePasswordForm extends UpdatePassword {
|
const formSchema = z
|
||||||
confirm_password: string
|
.object({
|
||||||
}
|
current_password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Password is required" })
|
||||||
|
.min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
new_password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Password is required" })
|
||||||
|
.min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
confirm_password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Password confirmation is required" }),
|
||||||
|
})
|
||||||
|
.refine((data) => data.new_password === data.confirm_password, {
|
||||||
|
message: "The passwords don't match",
|
||||||
|
path: ["confirm_password"],
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|
||||||
const ChangePassword = () => {
|
const ChangePassword = () => {
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
const form = useForm<FormData>({
|
||||||
register,
|
resolver: zodResolver(formSchema),
|
||||||
handleSubmit,
|
mode: "onSubmit",
|
||||||
reset,
|
|
||||||
getValues,
|
|
||||||
formState: { errors, isValid, isSubmitting },
|
|
||||||
} = useForm<UpdatePasswordForm>({
|
|
||||||
mode: "onBlur",
|
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
|
defaultValues: {
|
||||||
|
current_password: "",
|
||||||
|
new_password: "",
|
||||||
|
confirm_password: "",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data: UpdatePassword) =>
|
mutationFn: (data: UpdatePassword) =>
|
||||||
UsersService.updatePasswordMe({ requestBody: data }),
|
UsersService.updatePasswordMe({ requestBody: data }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("Password updated successfully.")
|
showSuccessToast("Password updated successfully")
|
||||||
reset()
|
form.reset()
|
||||||
},
|
|
||||||
onError: (err: ApiError) => {
|
|
||||||
handleError(err)
|
|
||||||
},
|
},
|
||||||
|
onError: handleError.bind(showErrorToast),
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<UpdatePasswordForm> = async (data) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="max-w-md">
|
||||||
<Container maxW="full">
|
<h3 className="text-lg font-semibold py-4">Change Password</h3>
|
||||||
<Heading size="sm" py={4}>
|
<Form {...form}>
|
||||||
Change Password
|
<form
|
||||||
</Heading>
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<Box
|
className="flex flex-col gap-4"
|
||||||
w={{ sm: "full", md: "300px" }}
|
|
||||||
as="form"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
>
|
||||||
<VStack gap={4}>
|
<FormField
|
||||||
<PasswordInput
|
control={form.control}
|
||||||
type="current_password"
|
name="current_password"
|
||||||
startElement={<FiLock />}
|
render={({ field, fieldState }) => (
|
||||||
{...register("current_password", passwordRules())}
|
<FormItem>
|
||||||
placeholder="Current Password"
|
<FormLabel>Current Password</FormLabel>
|
||||||
errors={errors}
|
<FormControl>
|
||||||
/>
|
<PasswordInput
|
||||||
<PasswordInput
|
data-testid="current-password-input"
|
||||||
type="new_password"
|
placeholder="••••••••"
|
||||||
startElement={<FiLock />}
|
aria-invalid={fieldState.invalid}
|
||||||
{...register("new_password", passwordRules())}
|
{...field}
|
||||||
placeholder="New Password"
|
/>
|
||||||
errors={errors}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
<PasswordInput
|
</FormItem>
|
||||||
type="confirm_password"
|
)}
|
||||||
startElement={<FiLock />}
|
/>
|
||||||
{...register("confirm_password", confirmPasswordRules(getValues))}
|
|
||||||
placeholder="Confirm Password"
|
<FormField
|
||||||
errors={errors}
|
control={form.control}
|
||||||
/>
|
name="new_password"
|
||||||
</VStack>
|
render={({ field, fieldState }) => (
|
||||||
<Button
|
<FormItem>
|
||||||
variant="solid"
|
<FormLabel>New Password</FormLabel>
|
||||||
mt={4}
|
<FormControl>
|
||||||
|
<PasswordInput
|
||||||
|
data-testid="new-password-input"
|
||||||
|
placeholder="••••••••"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirm_password"
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<PasswordInput
|
||||||
|
data-testid="confirm-password-input"
|
||||||
|
placeholder="••••••••"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isSubmitting}
|
loading={mutation.isPending}
|
||||||
disabled={!isValid}
|
className="self-start"
|
||||||
>
|
>
|
||||||
Save
|
Update Password
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</Box>
|
</form>
|
||||||
</Container>
|
</Form>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChangePassword
|
export default ChangePassword
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { Container, Heading, Text } from "@chakra-ui/react"
|
|
||||||
|
|
||||||
import DeleteConfirmation from "./DeleteConfirmation"
|
import DeleteConfirmation from "./DeleteConfirmation"
|
||||||
|
|
||||||
const DeleteAccount = () => {
|
const DeleteAccount = () => {
|
||||||
return (
|
return (
|
||||||
<Container maxW="full">
|
<div className="max-w-md mt-4 rounded-lg border border-destructive/50 p-4">
|
||||||
<Heading size="sm" py={4}>
|
<h3 className="font-semibold text-destructive">Delete Account</h3>
|
||||||
Delete Account
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
</Heading>
|
Permanently delete your account and all associated data.
|
||||||
<Text>
|
</p>
|
||||||
Permanently delete your data and everything associated with your
|
|
||||||
account.
|
|
||||||
</Text>
|
|
||||||
<DeleteConfirmation />
|
<DeleteConfirmation />
|
||||||
</Container>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeleteAccount
|
export default DeleteAccount
|
||||||
|
|||||||
@@ -1,44 +1,36 @@
|
|||||||
import { Button, ButtonGroup, Text } from "@chakra-ui/react"
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { useState } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
|
|
||||||
import { type ApiError, UsersService } from "@/client"
|
import { UsersService } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
DialogActionTrigger,
|
Dialog,
|
||||||
DialogBody,
|
DialogClose,
|
||||||
DialogCloseTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogRoot,
|
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
import useAuth from "@/hooks/useAuth"
|
import useAuth from "@/hooks/useAuth"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
import { handleError } from "@/utils"
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
const DeleteConfirmation = () => {
|
const DeleteConfirmation = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const {
|
const { handleSubmit } = useForm()
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm()
|
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () => UsersService.deleteUserMe(),
|
mutationFn: () => UsersService.deleteUserMe(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("Your account has been successfully deleted")
|
showSuccessToast("Your account has been successfully deleted")
|
||||||
setIsOpen(false)
|
|
||||||
logout()
|
logout()
|
||||||
},
|
},
|
||||||
onError: (err: ApiError) => {
|
onError: handleError.bind(showErrorToast),
|
||||||
handleError(err)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
|
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
|
||||||
},
|
},
|
||||||
@@ -49,60 +41,41 @@ const DeleteConfirmation = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog>
|
||||||
<DialogRoot
|
<DialogTrigger asChild>
|
||||||
size={{ base: "xs", md: "md" }}
|
<Button variant="destructive" className="mt-3">
|
||||||
role="alertdialog"
|
Delete Account
|
||||||
placement="center"
|
</Button>
|
||||||
open={isOpen}
|
</DialogTrigger>
|
||||||
onOpenChange={({ open }) => setIsOpen(open)}
|
<DialogContent>
|
||||||
>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<DialogTrigger asChild>
|
<DialogHeader>
|
||||||
<Button variant="solid" colorPalette="red" mt={4}>
|
<DialogTitle>Confirmation Required</DialogTitle>
|
||||||
Delete
|
<DialogDescription>
|
||||||
</Button>
|
All your account data will be{" "}
|
||||||
</DialogTrigger>
|
<strong>permanently deleted.</strong> If you are sure, please
|
||||||
|
click <strong>"Confirm"</strong> to proceed. This action cannot be
|
||||||
|
undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogFooter className="mt-4">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<DialogClose asChild>
|
||||||
<DialogCloseTrigger />
|
<Button variant="outline" disabled={mutation.isPending}>
|
||||||
<DialogHeader>
|
Cancel
|
||||||
<DialogTitle>Confirmation Required</DialogTitle>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogClose>
|
||||||
<DialogBody>
|
<LoadingButton
|
||||||
<Text mb={4}>
|
variant="destructive"
|
||||||
All your account data will be{" "}
|
type="submit"
|
||||||
<strong>permanently deleted.</strong> If you are sure, please
|
loading={mutation.isPending}
|
||||||
click <strong>"Confirm"</strong> to proceed. This action cannot
|
>
|
||||||
be undone.
|
Delete
|
||||||
</Text>
|
</LoadingButton>
|
||||||
</DialogBody>
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
<DialogFooter gap={2}>
|
</DialogContent>
|
||||||
<ButtonGroup>
|
</Dialog>
|
||||||
<DialogActionTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogActionTrigger>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
colorPalette="red"
|
|
||||||
type="submit"
|
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</DialogRoot>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,45 @@
|
|||||||
import {
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { type SubmitHandler, useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { UsersService, type UserUpdateMe } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
type ApiError,
|
Form,
|
||||||
type UserPublic,
|
FormControl,
|
||||||
type UserUpdateMe,
|
FormField,
|
||||||
UsersService,
|
FormItem,
|
||||||
} from "@/client"
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { LoadingButton } from "@/components/ui/loading-button"
|
||||||
import useAuth from "@/hooks/useAuth"
|
import useAuth from "@/hooks/useAuth"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
import { emailPattern, handleError } from "@/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Field } from "../ui/field"
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
full_name: z.string().max(30).optional(),
|
||||||
|
email: z.email({ message: "Invalid email address" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|
||||||
const UserInformation = () => {
|
const UserInformation = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showSuccessToast } = useCustomToast()
|
const { showSuccessToast, showErrorToast } = useCustomToast()
|
||||||
const [editMode, setEditMode] = useState(false)
|
const [editMode, setEditMode] = useState(false)
|
||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const {
|
|
||||||
register,
|
const form = useForm<FormData>({
|
||||||
handleSubmit,
|
resolver: zodResolver(formSchema),
|
||||||
reset,
|
|
||||||
getValues,
|
|
||||||
formState: { isSubmitting, errors, isDirty },
|
|
||||||
} = useForm<UserPublic>({
|
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
full_name: currentUser?.full_name,
|
full_name: currentUser?.full_name ?? undefined,
|
||||||
email: currentUser?.email,
|
email: currentUser?.email,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -50,102 +52,119 @@ const UserInformation = () => {
|
|||||||
mutationFn: (data: UserUpdateMe) =>
|
mutationFn: (data: UserUpdateMe) =>
|
||||||
UsersService.updateUserMe({ requestBody: data }),
|
UsersService.updateUserMe({ requestBody: data }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessToast("User updated successfully.")
|
showSuccessToast("User updated successfully")
|
||||||
},
|
toggleEditMode()
|
||||||
onError: (err: ApiError) => {
|
|
||||||
handleError(err)
|
|
||||||
},
|
},
|
||||||
|
onError: handleError.bind(showErrorToast),
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries()
|
queryClient.invalidateQueries()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<UserUpdateMe> = async (data) => {
|
const onSubmit = (data: FormData) => {
|
||||||
mutation.mutate(data)
|
const updateData: UserUpdateMe = {}
|
||||||
|
|
||||||
|
// only include fields that have changed
|
||||||
|
if (data.full_name !== currentUser?.full_name) {
|
||||||
|
updateData.full_name = data.full_name
|
||||||
|
}
|
||||||
|
if (data.email !== currentUser?.email) {
|
||||||
|
updateData.email = data.email
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation.mutate(updateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
reset()
|
form.reset()
|
||||||
toggleEditMode()
|
toggleEditMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="max-w-md">
|
||||||
<Container maxW="full">
|
<h3 className="text-lg font-semibold py-4">User Information</h3>
|
||||||
<Heading size="sm" py={4}>
|
<Form {...form}>
|
||||||
User Information
|
<form
|
||||||
</Heading>
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<Box
|
className="flex flex-col gap-4"
|
||||||
w={{ sm: "full", md: "50%" }}
|
|
||||||
as="form"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
>
|
||||||
<Field label="Full name">
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="full_name"
|
||||||
|
render={({ field }) =>
|
||||||
|
editMode ? (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Full name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="text" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
) : (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Full name</FormLabel>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"py-2 truncate max-w-sm",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value || "N/A"}
|
||||||
|
</p>
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) =>
|
||||||
|
editMode ? (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
) : (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<p className="py-2 truncate max-w-sm">{field.value}</p>
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<Input
|
<>
|
||||||
{...register("full_name", { maxLength: 30 })}
|
<LoadingButton
|
||||||
type="text"
|
type="submit"
|
||||||
size="md"
|
loading={mutation.isPending}
|
||||||
w="auto"
|
disabled={!form.formState.isDirty}
|
||||||
/>
|
>
|
||||||
|
Save
|
||||||
|
</LoadingButton>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Button type="button" onClick={toggleEditMode}>
|
||||||
fontSize="md"
|
Edit
|
||||||
py={2}
|
|
||||||
color={!currentUser?.full_name ? "gray" : "inherit"}
|
|
||||||
truncate
|
|
||||||
maxWidth="250px"
|
|
||||||
>
|
|
||||||
{currentUser?.full_name || "N/A"}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field
|
|
||||||
mt={4}
|
|
||||||
label="Email"
|
|
||||||
invalid={!!errors.email}
|
|
||||||
errorText={errors.email?.message}
|
|
||||||
>
|
|
||||||
{editMode ? (
|
|
||||||
<Input
|
|
||||||
{...register("email", {
|
|
||||||
required: "Email is required",
|
|
||||||
pattern: emailPattern,
|
|
||||||
})}
|
|
||||||
type="email"
|
|
||||||
size="md"
|
|
||||||
w="auto"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text fontSize="md" py={2} truncate maxWidth="250px">
|
|
||||||
{currentUser?.email}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Flex mt={4} gap={3}>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
onClick={toggleEditMode}
|
|
||||||
type={editMode ? "button" : "submit"}
|
|
||||||
loading={editMode ? isSubmitting : false}
|
|
||||||
disabled={editMode ? !isDirty || !getValues("email") : false}
|
|
||||||
>
|
|
||||||
{editMode ? "Save" : "Edit"}
|
|
||||||
</Button>
|
|
||||||
{editMode && (
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</div>
|
||||||
</Box>
|
</form>
|
||||||
</Container>
|
</Form>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
|
export type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTheme?: Theme
|
||||||
|
storageKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: Theme
|
||||||
|
resolvedTheme: "dark" | "light"
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ThemeProviderState = {
|
||||||
|
theme: "system",
|
||||||
|
resolvedTheme: "light",
|
||||||
|
setTheme: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||||
|
)
|
||||||
|
|
||||||
|
const getResolvedTheme = useCallback((theme: Theme): "dark" | "light" => {
|
||||||
|
if (theme === "system") {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
}
|
||||||
|
return theme
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">(() =>
|
||||||
|
getResolvedTheme(theme),
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateTheme = useCallback((newTheme: Theme) => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
|
if (newTheme === "system") {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
|
||||||
|
root.classList.add(systemTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(newTheme)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTheme(theme)
|
||||||
|
setResolvedTheme(getResolvedTheme(theme))
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
if (theme === "system") {
|
||||||
|
updateTheme("system")
|
||||||
|
setResolvedTheme(getResolvedTheme("system"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addEventListener("change", handleChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener("change", handleChange)
|
||||||
|
}
|
||||||
|
}, [theme, updateTheme, getResolvedTheme])
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
resolvedTheme,
|
||||||
|
setTheme: (theme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, theme)
|
||||||
|
setTheme(theme)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext)
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user