Compare commits
92
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d42cddbc | ||
|
|
790e272214 | ||
|
|
d0652f7cf5 | ||
|
|
4bd142e09c | ||
|
|
844a5c93f6 | ||
|
|
640d5e6a8e | ||
|
|
5d5c18b364 | ||
|
|
6deab2ecec | ||
|
|
220cb669a1 | ||
|
|
eb760da8b3 | ||
|
|
e561948657 | ||
|
|
7e91d12439 | ||
|
|
36f60fd844 | ||
|
|
10627453d5 | ||
|
|
c09da85f3a | ||
|
|
ada5597cf3 | ||
|
|
fca0aa8735 | ||
|
|
479f083ab8 | ||
|
|
866d3f674d | ||
|
|
e106eec0ee | ||
|
|
baabb43fff | ||
|
|
b0e0b1d43f | ||
|
|
cc403b89eb | ||
|
|
dd24925d90 | ||
|
|
984b29d951 | ||
|
|
2b2c0e7e3a | ||
|
|
e7b3be2edb | ||
|
|
3b1c564a5a | ||
|
|
e37325f5e0 | ||
|
|
8a14a6cb45 | ||
|
|
d111eee0e0 | ||
|
|
0b4ca93aa1 | ||
|
|
6a908397cd | ||
|
|
85689caf6c | ||
|
|
cab5d19141 | ||
|
|
5cbab28099 | ||
|
|
1dd9694719 | ||
|
|
681aee05f0 | ||
|
|
c3ed7090c2 | ||
|
|
a20248d0b6 | ||
|
|
5a95cd3753 | ||
|
|
efc0384b37 | ||
|
|
fb04875253 | ||
|
|
4689ec9933 | ||
|
|
d084dfb470 | ||
|
|
7de45e6fe5 | ||
|
|
f6d16fe44b | ||
|
|
340f5606a9 | ||
|
|
ca9478dce7 | ||
|
|
1bb3654bf7 | ||
|
|
d88c6027fe | ||
|
|
020cbd72d3 | ||
|
|
aed2025b30 | ||
|
|
54809823e9 | ||
|
|
4f92e5265a | ||
|
|
234266ebd9 | ||
|
|
6b28aa86d0 | ||
|
|
ed638d4767 | ||
|
|
a2f7d63d2a | ||
|
|
9639f44560 | ||
|
|
bc087e6e60 | ||
|
|
d347d4455b | ||
|
|
10572ca962 | ||
|
|
d350e48998 | ||
|
|
2c48b18968 | ||
|
|
069ed0a267 | ||
|
|
a3e53ec291 | ||
|
|
38a1c9ca83 | ||
|
|
b40deba9ff | ||
|
|
d5d59dd256 | ||
|
|
3453482ed4 | ||
|
|
d2c1dc3cc1 | ||
|
|
59bcbc8b4d | ||
|
|
b2c46d33d6 | ||
|
|
ca01a31fe5 | ||
|
|
deb5e9d960 | ||
|
|
16f12d98e6 | ||
|
|
10bdd74b2b | ||
|
|
a91955a35b | ||
|
|
87deba417f | ||
|
|
220f68ee04 | ||
|
|
d6fc46df3b | ||
|
|
7a8c50fa4c | ||
|
|
59eecaf842 | ||
|
|
879a77ff2a | ||
|
|
b53bcb77e9 | ||
|
|
3062ddafbe | ||
|
|
7f782220a1 | ||
|
|
aa2ec5d221 | ||
|
|
e6db47aaab | ||
|
|
f3d2026746 | ||
|
|
82cb74eb4e |
No files matched your search
@@ -33,6 +33,8 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.45.1
|
||||||
|
- 1.45.0
|
||||||
- 1.44.0
|
- 1.44.0
|
||||||
- 1.43.1
|
- 1.43.1
|
||||||
- 1.43.0
|
- 1.43.0
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Extract Build Artifact
|
||||||
|
description: Extracts artifacts from an existing Docker image to be used for from source installation
|
||||||
|
inputs:
|
||||||
|
digest:
|
||||||
|
description: Digest of Docker image to use
|
||||||
|
required: true
|
||||||
|
architecture:
|
||||||
|
description: Name of architecture, will be used to create directories (e.g. amd64, arm64)
|
||||||
|
required: true
|
||||||
|
release-tag:
|
||||||
|
description: Tag of the release to which the artifact will be attached
|
||||||
|
required: true
|
||||||
|
repository:
|
||||||
|
description: Repository to which the release belongs, e.g. owner/repo
|
||||||
|
required: true
|
||||||
|
token:
|
||||||
|
description: GitHub token with permissions to upload release assets
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Start docker container for ${{ inputs.architecture }}
|
||||||
|
run: |
|
||||||
|
docker run --name homarr \
|
||||||
|
-e "SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000" \
|
||||||
|
--detach --rm ${{ inputs.digest }}
|
||||||
|
shell: bash
|
||||||
|
- name: Extract build from ${{ inputs.architecture }} container
|
||||||
|
run: |
|
||||||
|
docker exec homarr cp /etc/nginx/templates/nginx.conf /app && \
|
||||||
|
docker exec homarr tar -czf extraction.tar.gz -C /app . && \
|
||||||
|
mkdir -p ${{ runner.temp }}/extraction/${{ inputs.architecture }} && \
|
||||||
|
docker cp homarr:/app/extraction.tar.gz ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-${{ inputs.architecture }}.tar.gz
|
||||||
|
shell: bash
|
||||||
|
- name: Stop ${{ inputs.architecture }} container
|
||||||
|
if: always()
|
||||||
|
run: docker container remove --force --volumes homarr
|
||||||
|
shell: bash
|
||||||
|
- name: Add build archive to release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
run: gh release upload --repo ${{ inputs.repository }} ${{ inputs.release-tag }} ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-${{ inputs.architecture }}.tar.gz --clobber
|
||||||
|
shell: bash
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
format:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
- name: Copy env
|
- name: Copy env
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Upload Crowdin translations
|
- name: Upload Crowdin translations
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
||||||
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
||||||
|
skipped: ${{ env.SKIP_RELEASE }}
|
||||||
steps:
|
steps:
|
||||||
- run: echo "Skipping release for workflow_dispatch event"
|
- run: echo "Skipping release for workflow_dispatch event"
|
||||||
if: env.SKIP_RELEASE == 'true'
|
if: env.SKIP_RELEASE == 'true'
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
|
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
digest: ${{ steps.build.outputs.digest }}
|
digest: ${{ steps.build.outputs.digest }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.release.outputs.git_ref }}
|
ref: ${{ needs.release.outputs.git_ref }}
|
||||||
|
|
||||||
@@ -146,7 +146,6 @@ jobs:
|
|||||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||||
env:
|
env:
|
||||||
SKIP_ENV_VALIDATION: true
|
SKIP_ENV_VALIDATION: true
|
||||||
|
|
||||||
build-arm64:
|
build-arm64:
|
||||||
name: Build docker image for arm64
|
name: Build docker image for arm64
|
||||||
needs: release
|
needs: release
|
||||||
@@ -154,7 +153,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
digest: ${{ steps.build.outputs.digest }}
|
digest: ${{ steps.build.outputs.digest }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.release.outputs.git_ref }}
|
ref: ${{ needs.release.outputs.git_ref }}
|
||||||
|
|
||||||
@@ -185,10 +184,38 @@ jobs:
|
|||||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||||
env:
|
env:
|
||||||
SKIP_ENV_VALIDATION: true
|
SKIP_ENV_VALIDATION: true
|
||||||
|
extract-asset-amd64:
|
||||||
|
name: Extract amd64 asset from docker image
|
||||||
|
needs: [release, build-amd64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract amd64
|
||||||
|
if: needs.release.outputs.skipped == 'false'
|
||||||
|
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
|
||||||
|
with:
|
||||||
|
digest: "${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }}"
|
||||||
|
architecture: amd64
|
||||||
|
release-tag: ${{ needs.release.outputs.version }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
|
||||||
|
extract-asset-arm64:
|
||||||
|
name: Extract arm64 asset from docker image
|
||||||
|
needs: [release, build-arm64]
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- name: Extract arm64
|
||||||
|
if: needs.release.outputs.skipped == 'false'
|
||||||
|
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
|
||||||
|
with:
|
||||||
|
digest: "${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}"
|
||||||
|
architecture: arm64
|
||||||
|
release-tag: ${{ needs.release.outputs.version }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
repository: ${{ github.repository }}
|
||||||
publish:
|
publish:
|
||||||
name: Complete deployment and notify
|
name: Complete deployment and notify
|
||||||
needs: [release, build-amd64, build-arm64]
|
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NEXT_VERSION: ${{ needs.release.outputs.version }}
|
NEXT_VERSION: ${{ needs.release.outputs.version }}
|
||||||
@@ -222,6 +249,13 @@ jobs:
|
|||||||
${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }} \
|
${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }} \
|
||||||
${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}
|
${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}
|
||||||
|
|
||||||
|
- name: Publish release
|
||||||
|
if: needs.release.outputs.skipped == 'false'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh release edit --repo ${{ github.repository }} ${{ needs.release.outputs.version }} --draft=false
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get Next Version
|
- name: Get Next Version
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
renovate-validate:
|
renovate-validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- run: |
|
- run: |
|
||||||
npx --yes --package renovate -- \
|
npx --yes --package renovate -- \
|
||||||
renovate-config-validator --strict .github/renovate.json5
|
renovate-config-validator --strict .github/renovate.json5
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
permission-contents: write # required to commit to branch
|
permission-contents: write # required to commit to branch
|
||||||
permission-pull-requests: write # required to create pr & enable automerge
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.obtainToken.outputs.token }}
|
token: ${{ steps.obtainToken.outputs.token }}
|
||||||
- name: Setup
|
- name: Setup
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: create-pull-request
|
id: create-pull-request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.obtainToken.outputs.token }}
|
token: ${{ steps.obtainToken.outputs.token }}
|
||||||
branch: update-bug-report-template
|
branch: update-bug-report-template
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
permission-pull-requests: write # required to create pr & enable automerge
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: create-pull-request
|
id: create-pull-request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.obtainToken.outputs.token }}
|
token: ${{ steps.obtainToken.outputs.token }}
|
||||||
branch: update-contributors
|
branch: update-contributors
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
permission-contents: write # required to commit to branch
|
permission-contents: write # required to commit to branch
|
||||||
permission-pull-requests: write # required to create pr & enable automerge
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
- name: Setup
|
- name: Setup
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: create-pull-request
|
id: create-pull-request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.obtainToken.outputs.token }}
|
token: ${{ steps.obtainToken.outputs.token }}
|
||||||
branch: update-integrations-readme
|
branch: update-integrations-readme
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
{
|
{
|
||||||
"successComment": false,
|
"successComment": false,
|
||||||
"failComment": false,
|
"failComment": false,
|
||||||
|
"draftRelease": true,
|
||||||
"releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>"
|
"releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,67 @@
|
|||||||
|
## [1.46.0](https://github.com/homarr-labs/homarr/compare/v1.45.3...v1.46.0) (2025-12-12)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **docker:** add widget options to sort containers ([#4636](https://github.com/homarr-labs/homarr/issues/4636)) ([dd24925](https://github.com/homarr-labs/homarr/commit/dd24925d906dc13d73c52a47fdd07bec89798789))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add autocomplete and id attributes for password manager compatibility ([#4602](https://github.com/homarr-labs/homarr/issues/4602)) ([681aee0](https://github.com/homarr-labs/homarr/commit/681aee05f07a6e09371ed089d2b9ef361494c416))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.90.12 ([#4615](https://github.com/homarr-labs/homarr/issues/4615)) ([efc0384](https://github.com/homarr-labs/homarr/commit/efc0384b372fadafb65f61f715f08edd2a8f8573))
|
||||||
|
* **deps:** update dependency better-sqlite3 to ^12.5.0 ([#4569](https://github.com/homarr-labs/homarr/issues/4569)) ([a20248d](https://github.com/homarr-labs/homarr/commit/a20248d0b689ab1ca475f606edfca458897ce442))
|
||||||
|
* **deps:** update dependency drizzle-kit to ^0.31.8 ([#4618](https://github.com/homarr-labs/homarr/issues/4618)) ([5cbab28](https://github.com/homarr-labs/homarr/commit/5cbab28099d41b8ade6cc8a42e69c4191310296c))
|
||||||
|
* **deps:** update dependency drizzle-orm to ^0.45.0 ([#4619](https://github.com/homarr-labs/homarr/issues/4619)) ([cab5d19](https://github.com/homarr-labs/homarr/commit/cab5d19141a38fbb977f4ef6e9c2ddbfdfea6aff))
|
||||||
|
* **deps:** update dependency ldapts to v8.0.12 ([#4613](https://github.com/homarr-labs/homarr/issues/4613)) ([4689ec9](https://github.com/homarr-labs/homarr/commit/4689ec9933e5286c0d19c66989023b5b3376ffb1))
|
||||||
|
* **deps:** update dependency ldapts to v8.0.13 ([#4626](https://github.com/homarr-labs/homarr/issues/4626)) ([8a14a6c](https://github.com/homarr-labs/homarr/commit/8a14a6cb456a6f16975372f64451e3b54ebfd88d))
|
||||||
|
* **deps:** update dependency ldapts to v8.0.14 ([#4627](https://github.com/homarr-labs/homarr/issues/4627)) ([e37325f](https://github.com/homarr-labs/homarr/commit/e37325f5e0314b13d031fba1bff591b881baf8d1))
|
||||||
|
* **deps:** update dependency winston to v3.19.0 ([#4632](https://github.com/homarr-labs/homarr/issues/4632)) ([3b1c564](https://github.com/homarr-labs/homarr/commit/3b1c564a5abbf1d1fca6ecab46e173c8f6938130))
|
||||||
|
* **deps:** update next monorepo to v16.0.10 [security] ([#4634](https://github.com/homarr-labs/homarr/issues/4634)) ([2b2c0e7](https://github.com/homarr-labs/homarr/commit/2b2c0e7e3a813525bd977276df6e5594bc283055))
|
||||||
|
* **deps:** update react monorepo to v19.2.1 ([#4608](https://github.com/homarr-labs/homarr/issues/4608)) ([f6d16fe](https://github.com/homarr-labs/homarr/commit/f6d16fe44bf239fb85a4ee3e9ae6491131315dce))
|
||||||
|
* **deps:** update tiptap monorepo to v3 (major) ([#3612](https://github.com/homarr-labs/homarr/issues/3612)) ([d111eee](https://github.com/homarr-labs/homarr/commit/d111eee0e0a41e55de87049f3cb0a9154ae82ea1))
|
||||||
|
* **deps:** update turbo monorepo to ^2.6.2 ([#4612](https://github.com/homarr-labs/homarr/issues/4612)) ([7de45e6](https://github.com/homarr-labs/homarr/commit/7de45e6fe50ccf4333959f924889463a22316984))
|
||||||
|
* **deps:** update turbo monorepo to ^2.6.3 ([#4622](https://github.com/homarr-labs/homarr/issues/4622)) ([85689ca](https://github.com/homarr-labs/homarr/commit/85689caf6c25b80cb4ce04a0baf2980a1a4deece))
|
||||||
|
* format issues ([1dd9694](https://github.com/homarr-labs/homarr/commit/1dd9694719ec4dcefe3c8d221015ba9aecede78c))
|
||||||
|
* improve run script ([#4609](https://github.com/homarr-labs/homarr/issues/4609)) ([d084dfb](https://github.com/homarr-labs/homarr/commit/d084dfb4705fca68bb9f9465ec9564050d0b04b2))
|
||||||
|
* **plex:** test connection not fully checking connection ([#4617](https://github.com/homarr-labs/homarr/issues/4617)) ([6a90839](https://github.com/homarr-labs/homarr/commit/6a908397cd8a6189123b91ea9d68f683af2bed05))
|
||||||
|
* **transmission:** total download incorrect for cross seeded torrents ([#3211](https://github.com/homarr-labs/homarr/issues/3211)) ([0b4ca93](https://github.com/homarr-labs/homarr/commit/0b4ca93aa13e9838b481daed5703640d30a52ffc))
|
||||||
|
|
||||||
|
## [1.45.3](https://github.com/homarr-labs/homarr/compare/v1.45.2...v1.45.3) (2025-12-08)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency glob to v13 ([#4584](https://github.com/homarr-labs/homarr/issues/4584)) ([020cbd7](https://github.com/homarr-labs/homarr/commit/020cbd72d387c9f50f4eddcbc2685829cf7787de))
|
||||||
|
* **deps:** update dependency jotai to ^2.15.2 ([#4597](https://github.com/homarr-labs/homarr/issues/4597)) ([ed638d4](https://github.com/homarr-labs/homarr/commit/ed638d47679594eab3cc19a12473722644ebf694))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.7 ([#4598](https://github.com/homarr-labs/homarr/issues/4598)) ([6b28aa8](https://github.com/homarr-labs/homarr/commit/6b28aa86d0bad29e4806592079a7f5d2e22b5ade))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.8 ([#4606](https://github.com/homarr-labs/homarr/issues/4606)) ([4f92e52](https://github.com/homarr-labs/homarr/commit/4f92e5265a40c636f303406e728e88cc056da23c))
|
||||||
|
* **deps:** update dependency prettier to ^3.7.4 ([#4582](https://github.com/homarr-labs/homarr/issues/4582)) ([5480982](https://github.com/homarr-labs/homarr/commit/54809823e9edeacc245b560747b622aba683b3bf))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.48.1 ([#4438](https://github.com/homarr-labs/homarr/issues/4438)) ([d88c602](https://github.com/homarr-labs/homarr/commit/d88c6027fe3accf7a6872ada8aa2724d30bf691c))
|
||||||
|
|
||||||
|
## [1.45.2](https://github.com/homarr-labs/homarr/compare/v1.45.1...v1.45.2) (2025-12-05)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add ipv6 support for internal nginx proxy ([2c48b18](https://github.com/homarr-labs/homarr/commit/2c48b18968a74cb7150c246ebb2505d07764f109))
|
||||||
|
* disable react-compiler due to issue with mantine-react-table data updates ([#4593](https://github.com/homarr-labs/homarr/issues/4593)) ([10572ca](https://github.com/homarr-labs/homarr/commit/10572ca962081961fb63b3ad9f164c610edf35e4))
|
||||||
|
|
||||||
|
## [1.45.1](https://github.com/homarr-labs/homarr/compare/v1.45.0...v1.45.1) (2025-12-03)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency chroma-js to ^3.1.4 ([#4570](https://github.com/homarr-labs/homarr/issues/4570)) ([d6fc46d](https://github.com/homarr-labs/homarr/commit/d6fc46df3b5e2dd79ff6f9a3dd3a3e2050a0eeef))
|
||||||
|
* **deps:** update dependency chroma-js to ^3.2.0 ([#4585](https://github.com/homarr-labs/homarr/issues/4585)) ([b2c46d3](https://github.com/homarr-labs/homarr/commit/b2c46d33d6cc769e89e208fd29d3a1d143cf74c7))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^2.33.0 ([#4581](https://github.com/homarr-labs/homarr/issues/4581)) ([ca01a31](https://github.com/homarr-labs/homarr/commit/ca01a31fe5cbce53eb0e6bd1a943320c092161ef))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.6 ([#4571](https://github.com/homarr-labs/homarr/issues/4571)) ([3062dda](https://github.com/homarr-labs/homarr/commit/3062ddafbe0bc1dbac9259af4036f0fa32d85237))
|
||||||
|
* **deps:** update dependency sass to ^1.94.2 ([#4572](https://github.com/homarr-labs/homarr/issues/4572)) ([7a8c50f](https://github.com/homarr-labs/homarr/commit/7a8c50fa4c5224dd74af3c17f56e2e5b8d6a4f43))
|
||||||
|
* **deps:** update dependency superjson to v2.2.6 ([#4573](https://github.com/homarr-labs/homarr/issues/4573)) ([deb5e9d](https://github.com/homarr-labs/homarr/commit/deb5e9d96092069e296ba473eaad58367b516f85))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.30.3 ([#4574](https://github.com/homarr-labs/homarr/issues/4574)) ([d2c1dc3](https://github.com/homarr-labs/homarr/commit/d2c1dc3cc1dd72fc9fc8ae9613467fa0b0b8d75a))
|
||||||
|
* **deps:** update dependency zod to ^4.1.13 ([#4575](https://github.com/homarr-labs/homarr/issues/4575)) ([879a77f](https://github.com/homarr-labs/homarr/commit/879a77ff2a81b0d0a17d1487eaf6d8539a955301))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.9 ([#4576](https://github.com/homarr-labs/homarr/issues/4576)) ([d5d59dd](https://github.com/homarr-labs/homarr/commit/d5d59dd2561f0be2f4f4c689f33c6a9a7840a21c))
|
||||||
|
* **deps:** update nextjs monorepo to v16.0.5 ([#4577](https://github.com/homarr-labs/homarr/issues/4577)) ([16f12d9](https://github.com/homarr-labs/homarr/commit/16f12d98e6cdae50fe2989db0911cb1350fcb93b))
|
||||||
|
* **deps:** update tanstack-query monorepo ([#4578](https://github.com/homarr-labs/homarr/issues/4578)) ([59bcbc8](https://github.com/homarr-labs/homarr/commit/59bcbc8b4de4a0a8cee8adf6336c7c10477c766a))
|
||||||
|
* **deps:** update testcontainers-node monorepo to ^11.9.0 ([#4583](https://github.com/homarr-labs/homarr/issues/4583)) ([10bdd74](https://github.com/homarr-labs/homarr/commit/10bdd74b2b287cb08ec0926147ac0e7fcfd492bb))
|
||||||
|
* **deps:** update trpc monorepo to ^11.7.2 ([#4579](https://github.com/homarr-labs/homarr/issues/4579)) ([3453482](https://github.com/homarr-labs/homarr/commit/3453482ed43644025c39dee8e20549e1beb86be0))
|
||||||
|
* **security:** rce vulnerability in nextjs ([b40deba](https://github.com/homarr-labs/homarr/commit/b40deba9ff1a3d629b6027565378d9f3ad2f1980))
|
||||||
|
|
||||||
## [1.45.0](https://github.com/homarr-labs/homarr/compare/v1.44.0...v1.45.0) (2025-11-28)
|
## [1.45.0](https://github.com/homarr-labs/homarr/compare/v1.44.0...v1.45.0) (2025-11-28)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const withNextIntl = createNextIntlPlugin({
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
reactCompiler: true,
|
// react compiler breaks mantine-react-table, so disabled for now
|
||||||
|
//reactCompiler: true,
|
||||||
/** We already do typechecking as separate tasks in CI */
|
/** We already do typechecking as separate tasks in CI */
|
||||||
typescript: { ignoreBuildErrors: true },
|
typescript: { ignoreBuildErrors: true },
|
||||||
/**
|
/**
|
||||||
|
|||||||
+26
-26
@@ -50,61 +50,61 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^8.3.8",
|
"@mantine/colors-generator": "^8.3.10",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"@mantine/dropzone": "^8.3.8",
|
"@mantine/dropzone": "^8.3.10",
|
||||||
"@mantine/hooks": "^8.3.8",
|
"@mantine/hooks": "^8.3.10",
|
||||||
"@mantine/modals": "^8.3.8",
|
"@mantine/modals": "^8.3.10",
|
||||||
"@mantine/tiptap": "^8.3.8",
|
"@mantine/tiptap": "^8.3.10",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@tanstack/react-query": "^5.90.10",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "^5.91.1",
|
||||||
"@tanstack/react-query-next-experimental": "^5.91.0",
|
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.2",
|
||||||
"@trpc/next": "^11.7.1",
|
"@trpc/next": "^11.7.2",
|
||||||
"@trpc/react-query": "^11.7.1",
|
"@trpc/react-query": "^11.7.2",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.2",
|
||||||
"@xterm/addon-canvas": "^0.7.0",
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"chroma-js": "^3.1.2",
|
"chroma-js": "^3.2.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"glob": "^12.0.0",
|
"glob": "^13.0.0",
|
||||||
"isomorphic-dompurify": "^2.32.0",
|
"isomorphic-dompurify": "^2.34.0",
|
||||||
"jotai": "^2.15.1",
|
"jotai": "^2.15.2",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.94.1",
|
"sass": "^1.95.0",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.6",
|
||||||
"swagger-ui-react": "^5.30.2",
|
"swagger-ui-react": "^5.30.3",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "3.1.2",
|
"@types/chroma-js": "3.1.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.2",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.2.6",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,16 +64,23 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
|||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<TextInput label={t("field.username.label")} autoComplete="off" {...form.getInputProps("username")} />
|
<TextInput
|
||||||
|
label={t("field.username.label")}
|
||||||
|
id="username"
|
||||||
|
autoComplete="username"
|
||||||
|
{...form.getInputProps("username")}
|
||||||
|
/>
|
||||||
<CustomPasswordInput
|
<CustomPasswordInput
|
||||||
withPasswordRequirements
|
withPasswordRequirements
|
||||||
label={t("field.password.label")}
|
label={t("field.password.label")}
|
||||||
|
id="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label={t("field.passwordConfirm.label")}
|
label={t("field.passwordConfirm.label")}
|
||||||
|
id="password-confirm"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
{...form.getInputProps("confirmPassword")}
|
{...form.getInputProps("confirmPassword")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -116,8 +116,18 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
|
|||||||
<>
|
<>
|
||||||
<form onSubmit={form.onSubmit((credentials) => void signInAsync(credentials.provider, credentials))}>
|
<form onSubmit={form.onSubmit((credentials) => void signInAsync(credentials.provider, credentials))}>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<TextInput label={t("field.username.label")} {...form.getInputProps("name")} />
|
<TextInput
|
||||||
<PasswordInput label={t("field.password.label")} {...form.getInputProps("password")} />
|
label={t("field.username.label")}
|
||||||
|
id="username"
|
||||||
|
autoComplete="username"
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label={t("field.password.label")}
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
|
||||||
{providers.includes("credentials") && (
|
{providers.includes("credentials") && (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
import { applyAutoLayoutToBoard, clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useSession } from "@homarr/auth/client";
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||||
@@ -165,7 +165,7 @@ const EditModeMenu = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (isEditMode) return saveBoard(applyAutoLayoutToBoard(board));
|
if (isEditMode) return saveBoard(board);
|
||||||
open();
|
open();
|
||||||
}, [board, isEditMode, saveBoard, open]);
|
}, [board, isEditMode, saveBoard, open]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core";
|
||||||
Badge,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
ChipGroup,
|
|
||||||
Fieldset,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
Input,
|
|
||||||
InputWrapper,
|
|
||||||
NumberInput,
|
|
||||||
RadioGroup,
|
|
||||||
SimpleGrid,
|
|
||||||
Slider,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
} from "@mantine/core";
|
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { boardLayoutModes } from "@homarr/definitions";
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { CustomRadioCard } from "@homarr/ui";
|
|
||||||
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
@@ -45,8 +25,6 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
layouts: board.layouts,
|
layouts: board.layouts,
|
||||||
layoutMode: board.layoutMode,
|
|
||||||
baseLayoutId: board.baseLayoutId,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,53 +38,6 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<RadioGroup {...form.getInputProps("layoutMode")} label={t("board.field.layoutMode.label")}>
|
|
||||||
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
|
|
||||||
{boardLayoutModes.values.map((layoutMode) => (
|
|
||||||
<Box w="100%" key={layoutMode} pos="relative">
|
|
||||||
<CustomRadioCard
|
|
||||||
value={layoutMode}
|
|
||||||
label={t(`board.field.layoutMode.option.${layoutMode}.label`)}
|
|
||||||
description={t(`board.field.layoutMode.option.${layoutMode}.description`)}
|
|
||||||
/>
|
|
||||||
{layoutMode === boardLayoutModes.defaultValue && (
|
|
||||||
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
|
|
||||||
{t("common.select.badge.recommended")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
{form.values.layoutMode === "auto" && (
|
|
||||||
<InputWrapper label={t("board.field.baseLayout.label")} description={t("board.field.baseLayout.description")}>
|
|
||||||
<ChipGroup {...form.getInputProps("baseLayoutId")} multiple={false}>
|
|
||||||
<SimpleGrid
|
|
||||||
cols={{
|
|
||||||
lg: Math.min(6, form.values.layouts.length),
|
|
||||||
sm: Math.min(4, form.values.layouts.length),
|
|
||||||
xs: Math.min(2, form.values.layouts.length),
|
|
||||||
}}
|
|
||||||
spacing="md"
|
|
||||||
mt="sm"
|
|
||||||
>
|
|
||||||
{form.values.layouts.map((layout) => (
|
|
||||||
<Chip
|
|
||||||
key={layout.id}
|
|
||||||
value={layout.id}
|
|
||||||
w="100%"
|
|
||||||
size="sm"
|
|
||||||
styles={{ label: { width: "100%", justifyContent: "center" } }}
|
|
||||||
>
|
|
||||||
{layout.name}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</ChipGroup>
|
|
||||||
</InputWrapper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
|
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
|
||||||
@@ -151,21 +82,18 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
{form.values.layouts.length >= 2 && form.values.baseLayoutId !== layout.id && (
|
{form.values.layouts.length >= 2 && (
|
||||||
<Group justify="end">
|
<Group justify="end">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.setValues((previous) => {
|
form.setValues((previous) =>
|
||||||
if (!previous.layouts) return previous;
|
previous.layouts !== undefined && previous.layouts.length >= 2
|
||||||
if (previous.layouts.length < 2) return previous;
|
? {
|
||||||
if (previous.baseLayoutId === layout.id) return previous;
|
layouts: form.values.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
||||||
|
}
|
||||||
return {
|
: previous,
|
||||||
...previous,
|
);
|
||||||
layouts: previous.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("common.action.remove")}
|
{t("common.action.remove")}
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import { groupCreateSchema } from "@homarr/validation/group";
|
|||||||
|
|
||||||
export const InitGroup = () => {
|
export const InitGroup = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/init");
|
||||||
|
},
|
||||||
|
});
|
||||||
const form = useZodForm(groupCreateSchema, {
|
const form = useZodForm(groupCreateSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -21,9 +25,6 @@ export const InitGroup = () => {
|
|||||||
|
|
||||||
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
|
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values, {
|
||||||
async onSuccess() {
|
|
||||||
await revalidatePathActionAsync("/init");
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
if (error.data?.code === "CONFLICT") {
|
if (error.data?.code === "CONFLICT") {
|
||||||
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") });
|
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") });
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { settingsInitSchema } from "@homarr/validation/settings";
|
|||||||
export const InitSettings = () => {
|
export const InitSettings = () => {
|
||||||
const tSection = useScopedI18n("management.page.settings.section");
|
const tSection = useScopedI18n("management.page.settings.section");
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/init");
|
||||||
|
},
|
||||||
|
});
|
||||||
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
|
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
|
||||||
|
|
||||||
form.watch("analytics.enableGeneral", ({ value }) => {
|
form.watch("analytics.enableGeneral", ({ value }) => {
|
||||||
@@ -31,11 +35,7 @@ export const InitSettings = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
|
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values);
|
||||||
async onSuccess() {
|
|
||||||
await revalidatePathActionAsync("/init");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,19 +26,19 @@ export const InitUserForm = () => {
|
|||||||
|
|
||||||
const handleSubmitAsync = async (values: FormType) => {
|
const handleSubmitAsync = async (values: FormType) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values, {
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: tUser("notification.success.title"),
|
title: tUser("notification.success.title"),
|
||||||
message: tUser("notification.success.message"),
|
message: tUser("notification.success.message"),
|
||||||
});
|
});
|
||||||
|
|
||||||
await signIn("credentials", {
|
void signIn("credentials", {
|
||||||
name: values.username,
|
name: values.username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
|
}).then(async () => {
|
||||||
|
await revalidatePathActionAsync("/init");
|
||||||
});
|
});
|
||||||
|
|
||||||
await revalidatePathActionAsync("/init");
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
@@ -84,9 +84,6 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
|
|||||||
id: board.id,
|
id: board.id,
|
||||||
name: board.name,
|
name: board.name,
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
|
||||||
await revalidatePathActionAsync("/manage/boards");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, [board.id, board.name, openDuplicateModal]);
|
}, [board.id, board.name, openDuplicateModal]);
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,11 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
|
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/manage/integrations");
|
||||||
|
},
|
||||||
|
});
|
||||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
||||||
|
|
||||||
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
|
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
|
||||||
@@ -116,7 +120,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
app,
|
app,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSuccess(data) {
|
onSuccess(data) {
|
||||||
// We do it this way as we are unable to send a typesafe error through onError
|
// We do it this way as we are unable to send a typesafe error through onError
|
||||||
if (data?.error) {
|
if (data?.error) {
|
||||||
setError(data.error);
|
setError(data.error);
|
||||||
@@ -132,7 +136,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
message: t("integration.page.create.notification.success.message"),
|
message: t("integration.page.create.notification.success.message"),
|
||||||
});
|
});
|
||||||
|
|
||||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
router.push("/manage/integrations");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
+6
-3
@@ -15,7 +15,11 @@ interface RemoveCertificateProps {
|
|||||||
|
|
||||||
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
|
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||||
|
},
|
||||||
|
});
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -27,12 +31,11 @@ export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
|||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{ fileName },
|
{ fileName },
|
||||||
{
|
{
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("certificate.action.remove.notification.success.title"),
|
title: t("certificate.action.remove.notification.success.title"),
|
||||||
message: t("certificate.action.remove.notification.success.message"),
|
message: t("certificate.action.remove.notification.success.message"),
|
||||||
});
|
});
|
||||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
+6
-3
@@ -15,7 +15,11 @@ interface RemoveHostnameActionIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
|
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
|
||||||
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation();
|
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
|
||||||
|
},
|
||||||
|
});
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
@@ -26,8 +30,7 @@ export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) =
|
|||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await mutateAsync(input, {
|
await mutateAsync(input, {
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
|
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("certificate.action.removeHostname.notification.success.title"),
|
title: t("certificate.action.removeHostname.notification.success.title"),
|
||||||
message: t("certificate.action.removeHostname.notification.success.message"),
|
message: t("certificate.action.removeHostname.notification.success.message"),
|
||||||
|
|||||||
@@ -206,15 +206,16 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
|
|||||||
const t = useScopedI18n("docker.action");
|
const t = useScopedI18n("docker.action");
|
||||||
const utils = clientApi.useUtils();
|
const utils = clientApi.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
|
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation({
|
||||||
|
async onSettled() {
|
||||||
|
await utils.docker.getContainers.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleClickAsync = async () => {
|
const handleClickAsync = async () => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{ ids: props.selectedIds },
|
{ ids: props.selectedIds },
|
||||||
{
|
{
|
||||||
async onSettled() {
|
|
||||||
await utils.docker.getContainers.invalidate();
|
|
||||||
},
|
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t(`${props.action}.notification.success.title`),
|
title: t(`${props.action}.notification.success.title`),
|
||||||
|
|||||||
+8
-7
@@ -18,7 +18,12 @@ interface UserProfileAvatarForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||||
const { mutate } = clientApi.user.setProfileImage.useMutation();
|
const { mutate } = clientApi.user.setProfileImage.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
// Revalidate all as the avatar is used in multiple places
|
||||||
|
await revalidatePathActionAsync("/");
|
||||||
|
},
|
||||||
|
});
|
||||||
const [opened, { toggle }] = useDisclosure(false);
|
const [opened, { toggle }] = useDisclosure(false);
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -38,9 +43,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
image: base64Url,
|
image: base64Url,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
// Revalidate all as the avatar is used in multiple places
|
|
||||||
await revalidatePathActionAsync("/");
|
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: tManageAvatar("changeImage.notification.success.message"),
|
message: tManageAvatar("changeImage.notification.success.message"),
|
||||||
});
|
});
|
||||||
@@ -74,9 +77,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
image: null,
|
image: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
// Revalidate all as the avatar is used in multiple places
|
|
||||||
await revalidatePathActionAsync("/");
|
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: tManageAvatar("removeImage.notification.success.message"),
|
message: tManageAvatar("removeImage.notification.success.message"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
|||||||
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
|
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
|
||||||
const { openModal } = useModalAction(UserSelectModal);
|
const { openModal } = useModalAction(UserSelectModal);
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const { mutateAsync } = clientApi.group.transferOwnership.useMutation();
|
const { mutateAsync } = clientApi.group.transferOwnership.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleTransfer = useCallback(() => {
|
const handleTransfer = useCallback(() => {
|
||||||
openModal(
|
openModal(
|
||||||
@@ -47,7 +51,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
|||||||
userId: id,
|
userId: id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
setInnerOwnerId(id);
|
setInnerOwnerId(id);
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: tRoot("common.notification.transfer.success"),
|
title: tRoot("common.notification.transfer.success"),
|
||||||
@@ -56,7 +60,6 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
|||||||
user: name,
|
user: name,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
|
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
@@ -56,16 +56,19 @@ export const GroupsTable = ({ groups, initialGroupIds, hasFilter }: GroupsTableP
|
|||||||
() => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index),
|
() => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index),
|
||||||
[groupIds, initialGroupIds],
|
[groupIds, initialGroupIds],
|
||||||
);
|
);
|
||||||
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation();
|
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/manage/users/groups");
|
||||||
|
},
|
||||||
|
});
|
||||||
const handleSavePositionsAsync = async () => {
|
const handleSavePositionsAsync = async () => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{ positions: groupIds },
|
{ positions: groupIds },
|
||||||
{
|
{
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: t("group.action.changePosition.notification.success.message"),
|
message: t("group.action.changePosition.notification.success.message"),
|
||||||
});
|
});
|
||||||
await revalidatePathActionAsync("/manage/users/groups");
|
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export class BoardMockBuilder {
|
|||||||
private readonly board: Board;
|
private readonly board: Board;
|
||||||
|
|
||||||
constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) {
|
constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) {
|
||||||
const baseLayoutId = createId();
|
|
||||||
this.board = {
|
this.board = {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
backgroundImageRepeat: "no-repeat",
|
backgroundImageRepeat: "no-repeat",
|
||||||
@@ -42,14 +41,12 @@ export class BoardMockBuilder {
|
|||||||
items: [],
|
items: [],
|
||||||
layouts: [
|
layouts: [
|
||||||
{
|
{
|
||||||
id: baseLayoutId,
|
id: createId(),
|
||||||
name: "Base",
|
name: "Base",
|
||||||
columnCount: 12,
|
columnCount: 12,
|
||||||
breakpoint: 0,
|
breakpoint: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
baseLayoutId,
|
|
||||||
layoutMode: "auto",
|
|
||||||
...board,
|
...board,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,18 +40,18 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.6",
|
||||||
"undici": "7.16.0"
|
"undici": "7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.2",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,9 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -5,6 +5,7 @@ events {
|
|||||||
http {
|
http {
|
||||||
server {
|
server {
|
||||||
listen 7575;
|
listen 7575;
|
||||||
|
listen [::]:7575;
|
||||||
|
|
||||||
# Route websockets traffic to port 3001
|
# Route websockets traffic to port 3001
|
||||||
location /websockets {
|
location /websockets {
|
||||||
@@ -25,4 +26,4 @@ http {
|
|||||||
client_max_body_size 32M;
|
client_max_body_size 32M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+17
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.45.0",
|
"version": "1.46.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -42,27 +42,27 @@
|
|||||||
"@semantic-release/github": "^12.0.2",
|
"@semantic-release/github": "^12.0.2",
|
||||||
"@semantic-release/npm": "^13.1.2",
|
"@semantic-release/npm": "^13.1.2",
|
||||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||||
"@testcontainers/redis": "^11.8.1",
|
"@testcontainers/redis": "^11.9.0",
|
||||||
"@turbo/gen": "^2.6.1",
|
"@turbo/gen": "^2.6.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"@vitest/coverage-v8": "^4.0.10",
|
"@vitest/coverage-v8": "^4.0.15",
|
||||||
"@vitest/ui": "^4.0.10",
|
"@vitest/ui": "^4.0.15",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.3.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"semantic-release": "^25.0.2",
|
"semantic-release": "^25.0.2",
|
||||||
"testcontainers": "^11.8.1",
|
"testcontainers": "^11.9.0",
|
||||||
"turbo": "^2.6.1",
|
"turbo": "^2.6.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^4.0.10"
|
"vitest": "^4.0.15"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.22.0",
|
"packageManager": "pnpm@10.25.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.11.1",
|
"node": ">=24.11.1",
|
||||||
"pnpm": ">=10.22.0"
|
"pnpm": ">=10.25.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
@@ -82,20 +82,20 @@
|
|||||||
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
|
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
|
||||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||||
"esbuild@<=0.24.2": ">=0.27.0",
|
"esbuild@<=0.24.2": ">=0.27.1",
|
||||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
||||||
"hono@<4.6.5": ">=4.10.6",
|
"hono@<4.6.5": ">=4.10.8",
|
||||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
||||||
"prismjs@<1.30.0": ">=1.30.0",
|
"prismjs@<1.30.0": ">=1.30.0",
|
||||||
"proxmox-api>undici": "7.16.0",
|
"proxmox-api>undici": "7.16.0",
|
||||||
"react-is": "^19.2.0",
|
"react-is": "^19.2.1",
|
||||||
"rollup@>=4.0.0 <4.22.4": ">=4.53.3",
|
"rollup@>=4.0.0 <4.22.4": ">=4.53.3",
|
||||||
"sha.js@<=2.4.11": ">=2.4.12",
|
"sha.js@<=2.4.11": ">=2.4.12",
|
||||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||||
"tmp@<=0.2.3": ">=0.2.5",
|
"tmp@<=0.2.3": ">=0.2.5",
|
||||||
"vite@>=5.0.0 <=5.4.18": ">=7.2.2"
|
"vite@>=5.0.0 <=5.4.18": ">=7.2.7"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@umami/node": "^0.4.0",
|
"@umami/node": "^0.4.0",
|
||||||
"superjson": "2.2.5"
|
"superjson": "2.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
+11
-11
@@ -43,25 +43,25 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@tanstack/react-query": "^5.90.10",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.2",
|
||||||
"@trpc/react-query": "^11.7.1",
|
"@trpc/react-query": "^11.7.2",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.2",
|
||||||
"@trpc/tanstack-react-query": "^11.7.1",
|
"@trpc/tanstack-react-query": "^11.7.2",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.6",
|
||||||
"trpc-to-openapi": "^3.1.0",
|
"trpc-to-openapi": "^3.1.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,5 +17,3 @@ export const fetchApi = createTRPCClient<AppRouter>({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export { applyAutoLayoutToBoard } from "./router/board/board-layout";
|
|
||||||
@@ -5,7 +5,7 @@ import { z } from "zod/v4";
|
|||||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import type { DeviceType } from "@homarr/common/server";
|
import type { DeviceType } from "@homarr/common/server";
|
||||||
import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
|
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||||
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
||||||
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
@@ -27,11 +27,13 @@ import {
|
|||||||
sections,
|
sections,
|
||||||
users,
|
users,
|
||||||
} from "@homarr/db/schema";
|
} from "@homarr/db/schema";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import {
|
import {
|
||||||
emptySuperJSON,
|
emptySuperJSON,
|
||||||
everyoneGroup,
|
everyoneGroup,
|
||||||
getPermissionsWithChildren,
|
getPermissionsWithChildren,
|
||||||
getPermissionsWithParents,
|
getPermissionsWithParents,
|
||||||
|
widgetKinds,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { importOldmarrAsync } from "@homarr/old-import";
|
import { importOldmarrAsync } from "@homarr/old-import";
|
||||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||||
@@ -48,11 +50,13 @@ import {
|
|||||||
boardSaveSchema,
|
boardSaveSchema,
|
||||||
} from "@homarr/validation/board";
|
} from "@homarr/validation/board";
|
||||||
import { byIdSchema } from "@homarr/validation/common";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
|
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||||
|
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||||
|
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||||
import { applyAutoLayoutToBoard, getUpdatedBoardLayout } from "./board/board-layout";
|
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
||||||
import { getFullBoardWithWhereAsync } from "./board/full-board-query";
|
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
exists: permissionRequiredProcedure
|
exists: permissionRequiredProcedure
|
||||||
@@ -275,7 +279,6 @@ export const boardRouter = createTRPCRouter({
|
|||||||
name: input.name,
|
name: input.name,
|
||||||
isPublic: input.isPublic,
|
isPublic: input.isPublic,
|
||||||
creatorId: ctx.session.user.id,
|
creatorId: ctx.session.user.id,
|
||||||
layoutMode: input.layoutMode,
|
|
||||||
});
|
});
|
||||||
createBoardCollection.sections.push({
|
createBoardCollection.sections.push({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -284,9 +287,8 @@ export const boardRouter = createTRPCRouter({
|
|||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
boardId,
|
boardId,
|
||||||
});
|
});
|
||||||
const baseLayoutId = createId();
|
|
||||||
createBoardCollection.layouts.push({
|
createBoardCollection.layouts.push({
|
||||||
id: baseLayoutId,
|
id: createId(),
|
||||||
name: "Base",
|
name: "Base",
|
||||||
columnCount: input.columnCount,
|
columnCount: input.columnCount,
|
||||||
breakpoint: 0,
|
breakpoint: 0,
|
||||||
@@ -294,10 +296,6 @@ export const boardRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await createBoardCollection.insertAllAsync(ctx.db);
|
await createBoardCollection.insertAllAsync(ctx.db);
|
||||||
// Define the base layout (only for automatic layout mode)
|
|
||||||
if (input.layoutMode === "auto") {
|
|
||||||
await ctx.db.update(boards).set({ baseLayoutId }).where(eq(boards.id, boardId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user?.homeBoardId) {
|
if (!user?.homeBoardId) {
|
||||||
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
||||||
@@ -584,15 +582,13 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||||
|
|
||||||
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||||
return applyAutoLayoutToBoard(board);
|
|
||||||
}),
|
}),
|
||||||
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
||||||
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||||
|
|
||||||
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||||
return applyAutoLayoutToBoard(board);
|
|
||||||
}),
|
}),
|
||||||
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||||
@@ -600,32 +596,13 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||||
|
|
||||||
const addedLayouts = filterAddedItems(input.layouts, board.layouts);
|
const addedLayouts = filterAddedItems(input.layouts, board.layouts);
|
||||||
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
|
||||||
|
|
||||||
if (input.layoutMode === "auto" && input.baseLayoutId === null) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Base layout must be set when using automatic layout mode",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removedLayouts.some((layout) => layout.id === input.baseLayoutId)) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Cannot remove base layout",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
|
const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
|
||||||
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
|
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
|
||||||
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
|
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||||
|
|
||||||
let baseLayoutId = input.baseLayoutId;
|
|
||||||
for (const addedLayout of addedLayouts) {
|
for (const addedLayout of addedLayouts) {
|
||||||
const layoutId = createId();
|
const layoutId = createId();
|
||||||
if (addedLayout.id === input.baseLayoutId) {
|
|
||||||
baseLayoutId = layoutId;
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutsToInsert.push({
|
layoutsToInsert.push({
|
||||||
id: layoutId,
|
id: layoutId,
|
||||||
@@ -732,18 +709,11 @@ export const boardRouter = createTRPCRouter({
|
|||||||
.where(eq(layouts.id, updatedLayout.id));
|
.where(eq(layouts.id, updatedLayout.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
||||||
const removedLayoutIds = removedLayouts.map((layout) => layout.id);
|
const removedLayoutIds = removedLayouts.map((layout) => layout.id);
|
||||||
if (removedLayoutIds.length > 0) {
|
if (removedLayoutIds.length > 0) {
|
||||||
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
|
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db
|
|
||||||
.update(boards)
|
|
||||||
.set({
|
|
||||||
baseLayoutId,
|
|
||||||
layoutMode: input.layoutMode,
|
|
||||||
})
|
|
||||||
.where(eq(boards.id, input.id));
|
|
||||||
}),
|
}),
|
||||||
savePartialBoardSettings: protectedProcedure
|
savePartialBoardSettings: protectedProcedure
|
||||||
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||||
@@ -1438,6 +1408,244 @@ const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredId
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUpdatedBoardLayout = (
|
||||||
|
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||||
|
options: {
|
||||||
|
previous: {
|
||||||
|
layoutId: string;
|
||||||
|
columnCount: number;
|
||||||
|
};
|
||||||
|
current: {
|
||||||
|
layoutId: string;
|
||||||
|
columnCount: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
||||||
|
const sectionLayoutsCollection: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||||
|
|
||||||
|
const elements = getElementsForLayout(board, options.previous.layoutId);
|
||||||
|
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
||||||
|
|
||||||
|
for (const rootSection of rootSections) {
|
||||||
|
const result = generateResponsiveGridFor({
|
||||||
|
items: elements,
|
||||||
|
previousWidth: options.previous.columnCount,
|
||||||
|
width: options.current.columnCount,
|
||||||
|
sectionId: rootSection.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
itemSectionLayoutsCollection.push(
|
||||||
|
...board.items
|
||||||
|
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
||||||
|
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
||||||
|
|
||||||
|
if (!currentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemId: item.id,
|
||||||
|
layoutId: options.current.layoutId,
|
||||||
|
sectionId: currentElement.sectionId,
|
||||||
|
height: currentElement.height,
|
||||||
|
width: currentElement.width,
|
||||||
|
xOffset: currentElement.xOffset,
|
||||||
|
yOffset: currentElement.yOffset,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => item !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
sectionLayoutsCollection.push(
|
||||||
|
...board.sections
|
||||||
|
.filter((section) => section.kind === "dynamic")
|
||||||
|
.map((section): InferInsertModel<typeof sectionLayouts> | null => {
|
||||||
|
const currentElement = result.items.find(
|
||||||
|
(element) => element.type === "section" && element.id === section.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
layoutId: options.current.layoutId,
|
||||||
|
sectionId: section.id,
|
||||||
|
parentSectionId: currentElement.sectionId,
|
||||||
|
height: currentElement.height,
|
||||||
|
width: currentElement.width,
|
||||||
|
xOffset: currentElement.xOffset,
|
||||||
|
yOffset: currentElement.yOffset,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((section) => section !== null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemSectionLayouts: itemSectionLayoutsCollection,
|
||||||
|
sectionLayouts: sectionLayoutsCollection,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
||||||
|
const sectionElements = board.sections
|
||||||
|
.filter((section) => section.kind === "dynamic")
|
||||||
|
.map((section) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: section.id,
|
||||||
|
type: "section" as const,
|
||||||
|
height: clonedLayout.height,
|
||||||
|
width: clonedLayout.width,
|
||||||
|
xOffset: clonedLayout.xOffset,
|
||||||
|
yOffset: clonedLayout.yOffset,
|
||||||
|
sectionId: clonedLayout.parentSectionId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemElements = board.items.map((item) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
type: "item" as const,
|
||||||
|
height: clonedLayout.height,
|
||||||
|
width: clonedLayout.width,
|
||||||
|
xOffset: clonedLayout.xOffset,
|
||||||
|
yOffset: clonedLayout.yOffset,
|
||||||
|
sectionId: clonedLayout.sectionId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...itemElements, ...sectionElements];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||||
|
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||||
|
where: eq(groupMembers.userId, userId ?? ""),
|
||||||
|
});
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where,
|
||||||
|
with: {
|
||||||
|
creator: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
collapseStates: {
|
||||||
|
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||||
|
},
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
with: {
|
||||||
|
integrations: {
|
||||||
|
with: {
|
||||||
|
integration: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layouts: true,
|
||||||
|
userPermissions: {
|
||||||
|
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||||
|
columns: {
|
||||||
|
permission: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupPermissions: {
|
||||||
|
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!board) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Board not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sections, items, layouts, ...otherBoardProperties } = board;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...otherBoardProperties,
|
||||||
|
layouts: layouts
|
||||||
|
.map(({ boardId: _, ...layout }) => layout)
|
||||||
|
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||||
|
sections: sections.map(({ collapseStates, ...section }) =>
|
||||||
|
parseSection({
|
||||||
|
...section,
|
||||||
|
xOffset: section.xOffset,
|
||||||
|
yOffset: section.yOffset,
|
||||||
|
options: superjson.parse(section.options ?? emptySuperJSON),
|
||||||
|
layouts: section.layouts.map((layout) => ({
|
||||||
|
xOffset: layout.xOffset,
|
||||||
|
yOffset: layout.yOffset,
|
||||||
|
width: layout.width,
|
||||||
|
height: layout.height,
|
||||||
|
parentSectionId: layout.parentSectionId,
|
||||||
|
layoutId: layout.layoutId,
|
||||||
|
})),
|
||||||
|
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
||||||
|
parseItem({
|
||||||
|
...item,
|
||||||
|
layouts: item.layouts.map((layout) => ({
|
||||||
|
xOffset: layout.xOffset,
|
||||||
|
yOffset: layout.yOffset,
|
||||||
|
width: layout.width,
|
||||||
|
height: layout.height,
|
||||||
|
layoutId: layout.layoutId,
|
||||||
|
sectionId: layout.sectionId,
|
||||||
|
})),
|
||||||
|
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||||
|
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||||
|
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||||
|
z.object({
|
||||||
|
kind: z.literal(kind),
|
||||||
|
options: z.record(z.string(), z.unknown()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||||
|
|
||||||
|
const parseItem = (item: unknown) => {
|
||||||
|
const result = outputItemSchema.safeParse(item);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error.message);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseSection = (section: unknown) => {
|
||||||
|
const result = sectionSchema.safeParse(section);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error.message);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
|
|
||||||
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||||
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
||||||
|
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
import type { Modify } from "@homarr/common/types";
|
|
||||||
import type { InferInsertModel } from "@homarr/db";
|
|
||||||
import type { itemLayouts, sectionLayouts } from "@homarr/db/schema";
|
|
||||||
|
|
||||||
import type { getFullBoardWithWhereAsync } from "./full-board-query";
|
|
||||||
import { generateResponsiveGridFor } from "./grid-algorithm";
|
|
||||||
|
|
||||||
type DynamicSectionLayout = Modify<InferInsertModel<typeof sectionLayouts>, { parentSectionId: string }>;
|
|
||||||
|
|
||||||
export const getUpdatedBoardLayout = (
|
|
||||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
|
||||||
options: {
|
|
||||||
previous: {
|
|
||||||
layoutId: string;
|
|
||||||
columnCount: number;
|
|
||||||
};
|
|
||||||
current: {
|
|
||||||
layoutId: string;
|
|
||||||
columnCount: number;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
|
||||||
const sectionLayoutsCollection: DynamicSectionLayout[] = [];
|
|
||||||
|
|
||||||
const elements = getElementsForLayout(board, options.previous.layoutId);
|
|
||||||
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
|
||||||
|
|
||||||
for (const rootSection of rootSections) {
|
|
||||||
const result = generateResponsiveGridFor({
|
|
||||||
items: elements,
|
|
||||||
previousWidth: options.previous.columnCount,
|
|
||||||
width: options.current.columnCount,
|
|
||||||
sectionId: rootSection.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
itemSectionLayoutsCollection.push(
|
|
||||||
...board.items
|
|
||||||
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
|
||||||
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
|
||||||
|
|
||||||
if (!currentElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
itemId: item.id,
|
|
||||||
layoutId: options.current.layoutId,
|
|
||||||
sectionId: currentElement.sectionId,
|
|
||||||
height: currentElement.height,
|
|
||||||
width: currentElement.width,
|
|
||||||
xOffset: currentElement.xOffset,
|
|
||||||
yOffset: currentElement.yOffset,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item) => item !== null),
|
|
||||||
);
|
|
||||||
|
|
||||||
sectionLayoutsCollection.push(
|
|
||||||
...board.sections
|
|
||||||
.filter((section) => section.kind === "dynamic")
|
|
||||||
.map((section): DynamicSectionLayout | null => {
|
|
||||||
const currentElement = result.items.find(
|
|
||||||
(element) => element.type === "section" && element.id === section.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
layoutId: options.current.layoutId,
|
|
||||||
sectionId: section.id,
|
|
||||||
parentSectionId: currentElement.sectionId,
|
|
||||||
height: currentElement.height,
|
|
||||||
width: currentElement.width,
|
|
||||||
xOffset: currentElement.xOffset,
|
|
||||||
yOffset: currentElement.yOffset,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((section) => section !== null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
itemSectionLayouts: itemSectionLayoutsCollection,
|
|
||||||
sectionLayouts: sectionLayoutsCollection,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
|
||||||
const sectionElements = board.sections
|
|
||||||
.filter((section) => section.kind === "dynamic")
|
|
||||||
.map((section) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: section.id,
|
|
||||||
type: "section" as const,
|
|
||||||
height: clonedLayout.height,
|
|
||||||
width: clonedLayout.width,
|
|
||||||
xOffset: clonedLayout.xOffset,
|
|
||||||
yOffset: clonedLayout.yOffset,
|
|
||||||
sectionId: clonedLayout.parentSectionId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const itemElements = board.items.map((item) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
type: "item" as const,
|
|
||||||
height: clonedLayout.height,
|
|
||||||
width: clonedLayout.width,
|
|
||||||
xOffset: clonedLayout.xOffset,
|
|
||||||
yOffset: clonedLayout.yOffset,
|
|
||||||
sectionId: clonedLayout.sectionId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...itemElements, ...sectionElements];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const applyAutoLayoutToBoard = (
|
|
||||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
|
||||||
): Awaited<ReturnType<typeof getFullBoardWithWhereAsync>> => {
|
|
||||||
if (board.layoutMode === "custom") return board;
|
|
||||||
|
|
||||||
const baseLayout = board.layouts.find((layout) => layout.id === board.baseLayoutId) ?? null;
|
|
||||||
if (!baseLayout) return board;
|
|
||||||
|
|
||||||
const updatedLayoutMap = new Map<string, ReturnType<typeof getUpdatedBoardLayout>>();
|
|
||||||
for (const layout of board.layouts) {
|
|
||||||
if (layout.id === baseLayout.id) continue;
|
|
||||||
|
|
||||||
updatedLayoutMap.set(
|
|
||||||
layout.id,
|
|
||||||
getUpdatedBoardLayout(board, {
|
|
||||||
previous: {
|
|
||||||
layoutId: baseLayout.id,
|
|
||||||
columnCount: baseLayout.columnCount,
|
|
||||||
},
|
|
||||||
current: {
|
|
||||||
layoutId: layout.id,
|
|
||||||
columnCount: layout.columnCount,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...board,
|
|
||||||
items: board.items.map((item) => ({
|
|
||||||
...item,
|
|
||||||
layouts: item.layouts.map(
|
|
||||||
(layoutSection) =>
|
|
||||||
updatedLayoutMap
|
|
||||||
.get(layoutSection.layoutId)
|
|
||||||
?.itemSectionLayouts.find(
|
|
||||||
(updatedLayoutSection) =>
|
|
||||||
updatedLayoutSection.itemId === item.id && updatedLayoutSection.layoutId === layoutSection.layoutId,
|
|
||||||
) ?? layoutSection,
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
sections: board.sections.map((section) =>
|
|
||||||
"layouts" in section
|
|
||||||
? {
|
|
||||||
...section,
|
|
||||||
layouts: section.layouts.map(
|
|
||||||
(sectionLayout) =>
|
|
||||||
updatedLayoutMap
|
|
||||||
.get(sectionLayout.layoutId)
|
|
||||||
?.sectionLayouts.find(
|
|
||||||
(updatedSectionLayout) =>
|
|
||||||
updatedSectionLayout.sectionId === section.id &&
|
|
||||||
updatedSectionLayout.layoutId === sectionLayout.layoutId,
|
|
||||||
) ?? sectionLayout,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: section,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import superjson from "superjson";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import type { Database, SQL } from "@homarr/db";
|
|
||||||
import { eq, inArray } from "@homarr/db";
|
|
||||||
import { boardGroupPermissions, boardUserPermissions, groupMembers, sectionCollapseStates } from "@homarr/db/schema";
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
|
||||||
import { emptySuperJSON, widgetKinds } from "@homarr/definitions";
|
|
||||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
|
||||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
|
||||||
|
|
||||||
export const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
|
||||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
|
||||||
where: eq(groupMembers.userId, userId ?? ""),
|
|
||||||
});
|
|
||||||
const board = await db.query.boards.findFirst({
|
|
||||||
where,
|
|
||||||
with: {
|
|
||||||
creator: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
image: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sections: {
|
|
||||||
with: {
|
|
||||||
collapseStates: {
|
|
||||||
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
|
||||||
},
|
|
||||||
layouts: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
with: {
|
|
||||||
integrations: {
|
|
||||||
with: {
|
|
||||||
integration: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layouts: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layouts: true,
|
|
||||||
userPermissions: {
|
|
||||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
|
||||||
columns: {
|
|
||||||
permission: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupPermissions: {
|
|
||||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!board) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Board not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sections, items, layouts, ...otherBoardProperties } = board;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...otherBoardProperties,
|
|
||||||
layouts: layouts
|
|
||||||
.map(({ boardId: _, ...layout }) => layout)
|
|
||||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
|
||||||
sections: sections.map(({ collapseStates, ...section }) =>
|
|
||||||
parseSection({
|
|
||||||
...section,
|
|
||||||
xOffset: section.xOffset,
|
|
||||||
yOffset: section.yOffset,
|
|
||||||
options: superjson.parse(section.options ?? emptySuperJSON),
|
|
||||||
layouts: section.layouts.map((layout) => ({
|
|
||||||
xOffset: layout.xOffset,
|
|
||||||
yOffset: layout.yOffset,
|
|
||||||
width: layout.width,
|
|
||||||
height: layout.height,
|
|
||||||
parentSectionId: layout.parentSectionId,
|
|
||||||
layoutId: layout.layoutId,
|
|
||||||
})),
|
|
||||||
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
|
||||||
parseItem({
|
|
||||||
...item,
|
|
||||||
layouts: item.layouts.map((layout) => ({
|
|
||||||
xOffset: layout.xOffset,
|
|
||||||
yOffset: layout.yOffset,
|
|
||||||
width: layout.width,
|
|
||||||
height: layout.height,
|
|
||||||
layoutId: layout.layoutId,
|
|
||||||
sectionId: layout.sectionId,
|
|
||||||
})),
|
|
||||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
|
||||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
|
||||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
|
||||||
z.object({
|
|
||||||
kind: z.literal(kind),
|
|
||||||
options: z.record(z.string(), z.unknown()),
|
|
||||||
});
|
|
||||||
|
|
||||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
|
||||||
|
|
||||||
const parseItem = (item: unknown) => {
|
|
||||||
const result = outputItemSchema.safeParse(item);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error.message);
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseSection = (section: unknown) => {
|
|
||||||
const result = sectionSchema.safeParse(section);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error.message);
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
};
|
|
||||||
@@ -56,7 +56,8 @@ export const generateResponsiveGridFor = ({
|
|||||||
dynamicSectionHeightMap.set(dynamicSection.id, result.height);
|
dynamicSectionHeightMap.set(dynamicSection.id, result.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (width === previousWidth) {
|
// Return same positions for items in the current section
|
||||||
|
if (width >= previousWidth) {
|
||||||
return {
|
return {
|
||||||
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
|
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
|
||||||
items: newItems.concat(normalizedItems),
|
items: newItems.concat(normalizedItems),
|
||||||
|
|||||||
@@ -99,12 +99,11 @@ qlllmmmmmnnrrrst
|
|||||||
ulllmmmmmnnrrrvw
|
ulllmmmmmnnrrrvw
|
||||||
xyz äö`;
|
xyz äö`;
|
||||||
|
|
||||||
const eighteenColumns = `
|
// Just add two empty columns to the right
|
||||||
abbccccddddeeefghi
|
const eighteenColumns = sixteenColumns
|
||||||
jbbccccddddeeeklll
|
.split("\n")
|
||||||
mmmmmnnopqseeetlll
|
.map((line, index) => (index === 0 ? line : `${line} `))
|
||||||
mmmmmnnrrruvwxylll
|
.join("\n");
|
||||||
mmmmmnnrrrzäö `;
|
|
||||||
|
|
||||||
const tenColumns = `
|
const tenColumns = `
|
||||||
abbcccceee
|
abbcccceee
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { escapeForRegEx } from "@tiptap/react";
|
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
@@ -10,9 +9,9 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
|||||||
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
|
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
|
||||||
if (!versionFilter) return undefined;
|
if (!versionFilter) return undefined;
|
||||||
|
|
||||||
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
|
const escapedPrefix = versionFilter.prefix ? RegExp.escape(versionFilter.prefix) : "";
|
||||||
const precision = "[0-9]+\\.".repeat(versionFilter.precision).slice(0, -2);
|
const precision = "[0-9]+\\.".repeat(versionFilter.precision).slice(0, -2);
|
||||||
const escapedSuffix = versionFilter.suffix ? escapeForRegEx(versionFilter.suffix) : "";
|
const escapedSuffix = versionFilter.suffix ? RegExp.escape(versionFilter.suffix) : "";
|
||||||
|
|
||||||
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
|
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,12 +34,12 @@
|
|||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "8.0.9",
|
"ldapts": "8.0.18",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "6.0.0",
|
||||||
"@types/cookies": "0.9.2",
|
"@types/cookies": "0.9.2",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
|
|||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import type { userSignInSchema } from "@homarr/validation/user";
|
import type { ldapSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { env } from "../../../env";
|
import { env } from "../../../env";
|
||||||
import { LdapClient } from "../ldap-client";
|
import { LdapClient } from "../ldap-client";
|
||||||
|
|
||||||
export const authorizeWithLdapCredentialsAsync = async (
|
export const authorizeWithLdapCredentialsAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
credentials: z.infer<typeof userSignInSchema>,
|
credentials: z.infer<typeof ldapSignInSchema>,
|
||||||
) => {
|
) => {
|
||||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
||||||
const client = new LdapClient();
|
const client = new LdapClient();
|
||||||
@@ -38,7 +38,14 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
attributes: [env.AUTH_LDAP_USERNAME_ATTRIBUTE, env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
attributes: [env.AUTH_LDAP_USERNAME_ATTRIBUTE, env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((entries) => entries.at(0));
|
.then((entries) => {
|
||||||
|
if (entries.length > 1) {
|
||||||
|
logger.warn(`Multiple LDAP users found for ${credentials.name}, expected only one.`);
|
||||||
|
throw new CredentialsSignin();
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.at(0);
|
||||||
|
});
|
||||||
|
|
||||||
if (!ldapUser) {
|
if (!ldapUser) {
|
||||||
logger.warn(`User ${credentials.name} not found in LDAP`);
|
logger.warn(`User ${credentials.name} not found in LDAP`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type Credentials from "@auth/core/providers/credentials";
|
import type Credentials from "@auth/core/providers/credentials";
|
||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { userSignInSchema } from "@homarr/validation/user";
|
import { ldapSignInSchema, userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
||||||
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
||||||
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
|
|||||||
name: "Ldap",
|
name: "Ldap",
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
const data = await userSignInSchema.parseAsync(credentials);
|
const data = await ldapSignInSchema.parseAsync(credentials);
|
||||||
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
||||||
},
|
},
|
||||||
}) satisfies CredentialsConfiguration;
|
}) satisfies CredentialsConfiguration;
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { usePathname } from "next/navigation";
|
|||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|
||||||
import { useEditMode } from "./edit-mode";
|
|
||||||
import { updateBoardName } from "./updater";
|
import { updateBoardName } from "./updater";
|
||||||
|
|
||||||
const BoardContext = createContext<{
|
const BoardContext = createContext<{
|
||||||
@@ -84,12 +83,10 @@ export const getCurrentLayout = (board: RouterOutputs["board"]["getBoardByName"]
|
|||||||
export const useCurrentLayout = () => {
|
export const useCurrentLayout = () => {
|
||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
|
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
|
||||||
const [isEditMode] = useEditMode();
|
|
||||||
|
|
||||||
const onResize = useCallback(() => {
|
const onResize = useCallback(() => {
|
||||||
if (isEditMode && board.layoutMode === "auto") return;
|
|
||||||
setCurrentLayout(getCurrentLayout(board));
|
setCurrentLayout(getCurrentLayout(board));
|
||||||
}, [board, isEditMode]);
|
}, [board]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -100,10 +97,6 @@ export const useCurrentLayout = () => {
|
|||||||
};
|
};
|
||||||
}, [onResize]);
|
}, [onResize]);
|
||||||
|
|
||||||
if (board.layoutMode === "auto" && isEditMode && board.baseLayoutId !== null) {
|
|
||||||
return board.baseLayoutId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentLayout;
|
return currentLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dns-caching": "^0.2.7",
|
"dns-caching": "^0.2.9",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"octokit": "^5.0.5",
|
"octokit": "^5.0.5",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"undici": "7.16.0",
|
"undici": "7.16.0",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.13",
|
||||||
"zod-validation-error": "^5.0.0"
|
"zod-validation-error": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -29,20 +29,20 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@tanstack/react-query": "^5.90.10",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.2",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.2",
|
||||||
"@trpc/tanstack-react-query": "^11.7.1",
|
"@trpc/tanstack-react-query": "^11.7.2",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "19.2.6",
|
"@types/react": "19.2.7",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE `board` ADD `layout_mode` varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `board` ADD `base_layout_id` varchar(64);--> statement-breakpoint
|
|
||||||
ALTER TABLE `board` ADD CONSTRAINT `board_base_layout_id_layout_id_fk` FOREIGN KEY (`base_layout_id`) REFERENCES `layout`(`id`) ON DELETE set null ON UPDATE no action;
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -260,13 +260,6 @@
|
|||||||
"when": 1760968518445,
|
"when": 1760968518445,
|
||||||
"tag": "0036_add_app_reference_to_integration",
|
"tag": "0036_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 37,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1764190331048,
|
|
||||||
"tag": "0037_add_auto_layout_mode",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE "board" ADD COLUMN "layout_mode" varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "board" ADD COLUMN "base_layout_id" varchar(64);--> statement-breakpoint
|
|
||||||
ALTER TABLE "board" ADD CONSTRAINT "board_base_layout_id_layout_id_fk" FOREIGN KEY ("base_layout_id") REFERENCES "public"."layout"("id") ON DELETE set null ON UPDATE no action;
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -22,13 +22,6 @@
|
|||||||
"when": 1760968530084,
|
"when": 1760968530084,
|
||||||
"tag": "0002_add_app_reference_to_integration",
|
"tag": "0002_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1764190350662,
|
|
||||||
"tag": "0003_add_auto_layout_mode",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE `board` ADD `layout_mode` text DEFAULT 'custom' NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `board` ADD `base_layout_id` text REFERENCES layout(id);
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -246,13 +246,6 @@
|
|||||||
"when": 1760968503571,
|
"when": 1760968503571,
|
||||||
"tag": "0034_add_app_reference_to_integration",
|
"tag": "0034_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 35,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1764190313293,
|
|
||||||
"tag": "0035_add_auto_layout_mode",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -49,18 +49,18 @@
|
|||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"@testcontainers/mysql": "^11.8.1",
|
"@testcontainers/mysql": "^11.9.0",
|
||||||
"@testcontainers/postgresql": "^11.8.1",
|
"@testcontainers/postgresql": "^11.9.0",
|
||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.5.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "^0.31.7",
|
"drizzle-kit": "^0.31.8",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.45.0",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"mysql2": "3.15.3",
|
"mysql2": "3.15.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"superjson": "2.2.5"
|
"superjson": "2.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -69,9 +69,9 @@
|
|||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.15.6",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,16 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/mysql-core";
|
} from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
backgroundImageAttachments,
|
||||||
|
backgroundImageRepeats,
|
||||||
|
backgroundImageSizes,
|
||||||
|
emptySuperJSON,
|
||||||
|
} from "@homarr/definitions";
|
||||||
import type {
|
import type {
|
||||||
BackgroundImageAttachment,
|
BackgroundImageAttachment,
|
||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
BoardLayoutMode,
|
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
@@ -34,12 +39,6 @@ import type {
|
|||||||
SupportedAuthProvider,
|
SupportedAuthProvider,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import {
|
|
||||||
backgroundImageAttachments,
|
|
||||||
backgroundImageRepeats,
|
|
||||||
backgroundImageSizes,
|
|
||||||
emptySuperJSON,
|
|
||||||
} from "@homarr/definitions";
|
|
||||||
|
|
||||||
const customBlob = customType<{ data: Buffer }>({
|
const customBlob = customType<{ data: Buffer }>({
|
||||||
dataType() {
|
dataType() {
|
||||||
@@ -290,11 +289,6 @@ export const boards = mysqlTable("board", {
|
|||||||
iconColor: text(),
|
iconColor: text(),
|
||||||
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
||||||
disableStatus: boolean().default(false).notNull(),
|
disableStatus: boolean().default(false).notNull(),
|
||||||
// This has to be custom as the previous behaviour was custom
|
|
||||||
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
|
|
||||||
baseLayoutId: varchar({ length: 64 }).references((): AnyMySqlColumn => layouts.id, {
|
|
||||||
onDelete: "set null",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const boardUserPermissions = mysqlTable(
|
export const boardUserPermissions = mysqlTable(
|
||||||
@@ -684,10 +678,6 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
|||||||
userPermissions: many(boardUserPermissions),
|
userPermissions: many(boardUserPermissions),
|
||||||
groupPermissions: many(boardGroupPermissions),
|
groupPermissions: many(boardGroupPermissions),
|
||||||
layouts: many(layouts),
|
layouts: many(layouts),
|
||||||
baseLayout: one(layouts, {
|
|
||||||
fields: [boards.baseLayoutId],
|
|
||||||
references: [layouts.id],
|
|
||||||
}),
|
|
||||||
groupHomes: many(groups, {
|
groupHomes: many(groups, {
|
||||||
relationName: "groupRelations__board__homeBoardId",
|
relationName: "groupRelations__board__homeBoardId",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
backgroundImageAttachments,
|
||||||
|
backgroundImageRepeats,
|
||||||
|
backgroundImageSizes,
|
||||||
|
emptySuperJSON,
|
||||||
|
} from "@homarr/definitions";
|
||||||
import type {
|
import type {
|
||||||
BackgroundImageAttachment,
|
BackgroundImageAttachment,
|
||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
BoardLayoutMode,
|
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
@@ -33,12 +38,6 @@ import type {
|
|||||||
SupportedAuthProvider,
|
SupportedAuthProvider,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import {
|
|
||||||
backgroundImageAttachments,
|
|
||||||
backgroundImageRepeats,
|
|
||||||
backgroundImageSizes,
|
|
||||||
emptySuperJSON,
|
|
||||||
} from "@homarr/definitions";
|
|
||||||
|
|
||||||
const customBlob = customType<{ data: Buffer }>({
|
const customBlob = customType<{ data: Buffer }>({
|
||||||
dataType() {
|
dataType() {
|
||||||
@@ -289,11 +288,6 @@ export const boards = pgTable("board", {
|
|||||||
iconColor: text(),
|
iconColor: text(),
|
||||||
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
||||||
disableStatus: boolean().default(false).notNull(),
|
disableStatus: boolean().default(false).notNull(),
|
||||||
// This has to be custom as the previous behaviour was custom
|
|
||||||
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
|
|
||||||
baseLayoutId: varchar({ length: 64 }).references((): AnyPgColumn => layouts.id, {
|
|
||||||
onDelete: "set null",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const boardUserPermissions = pgTable(
|
export const boardUserPermissions = pgTable(
|
||||||
@@ -683,10 +677,6 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
|||||||
userPermissions: many(boardUserPermissions),
|
userPermissions: many(boardUserPermissions),
|
||||||
groupPermissions: many(boardGroupPermissions),
|
groupPermissions: many(boardGroupPermissions),
|
||||||
layouts: many(layouts),
|
layouts: many(layouts),
|
||||||
baseLayout: one(layouts, {
|
|
||||||
fields: [boards.baseLayoutId],
|
|
||||||
references: [layouts.id],
|
|
||||||
}),
|
|
||||||
groupHomes: many(groups, {
|
groupHomes: many(groups, {
|
||||||
relationName: "groupRelations__board__homeBoardId",
|
relationName: "groupRelations__board__homeBoardId",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import { relations, sql } from "drizzle-orm";
|
|||||||
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
||||||
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
backgroundImageAttachments,
|
||||||
|
backgroundImageRepeats,
|
||||||
|
backgroundImageSizes,
|
||||||
|
emptySuperJSON,
|
||||||
|
} from "@homarr/definitions";
|
||||||
import type {
|
import type {
|
||||||
BackgroundImageAttachment,
|
BackgroundImageAttachment,
|
||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
BoardLayoutMode,
|
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
@@ -22,12 +27,6 @@ import type {
|
|||||||
SupportedAuthProvider,
|
SupportedAuthProvider,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import {
|
|
||||||
backgroundImageAttachments,
|
|
||||||
backgroundImageRepeats,
|
|
||||||
backgroundImageSizes,
|
|
||||||
emptySuperJSON,
|
|
||||||
} from "@homarr/definitions";
|
|
||||||
|
|
||||||
export const apiKeys = sqliteTable("apiKey", {
|
export const apiKeys = sqliteTable("apiKey", {
|
||||||
id: text().notNull().primaryKey(),
|
id: text().notNull().primaryKey(),
|
||||||
@@ -275,11 +274,6 @@ export const boards = sqliteTable("board", {
|
|||||||
iconColor: text(),
|
iconColor: text(),
|
||||||
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
||||||
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
|
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
|
||||||
// This has to be custom as the previous behaviour was custom
|
|
||||||
layoutMode: text().$type<BoardLayoutMode>().default("custom").notNull(),
|
|
||||||
baseLayoutId: text().references((): AnySQLiteColumn => layouts.id, {
|
|
||||||
onDelete: "set null",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const boardUserPermissions = sqliteTable(
|
export const boardUserPermissions = sqliteTable(
|
||||||
@@ -669,10 +663,6 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
|||||||
userPermissions: many(boardUserPermissions),
|
userPermissions: many(boardUserPermissions),
|
||||||
groupPermissions: many(boardGroupPermissions),
|
groupPermissions: many(boardGroupPermissions),
|
||||||
layouts: many(layouts),
|
layouts: many(layouts),
|
||||||
baseLayout: one(layouts, {
|
|
||||||
fields: [boards.baseLayoutId],
|
|
||||||
references: [layouts.id],
|
|
||||||
}),
|
|
||||||
groupHomes: many(groups, {
|
groupHomes: many(groups, {
|
||||||
relationName: "groupRelations__board__homeBoardId",
|
relationName: "groupRelations__board__homeBoardId",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"fast-xml-parser": "^5.3.2",
|
"fast-xml-parser": "^5.3.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ export const backgroundImageRepeats = createDefinition(["repeat", "repeat-x", "r
|
|||||||
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
|
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
|
||||||
defaultValue: "cover",
|
defaultValue: "cover",
|
||||||
});
|
});
|
||||||
export const boardLayoutModes = createDefinition(["auto", "custom"], { defaultValue: "auto" });
|
|
||||||
|
|
||||||
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
|
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
|
||||||
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
|
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
|
||||||
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
|
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
|
||||||
export type BoardLayoutMode = inferDefinitionType<typeof boardLayoutModes>;
|
|
||||||
@@ -144,6 +144,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/getting-started/installation/proxmox"
|
| "/docs/getting-started/installation/proxmox"
|
||||||
| "/docs/getting-started/installation/qnap"
|
| "/docs/getting-started/installation/qnap"
|
||||||
| "/docs/getting-started/installation/railway"
|
| "/docs/getting-started/installation/railway"
|
||||||
|
| "/docs/getting-started/installation/runtipi"
|
||||||
| "/docs/getting-started/installation/saltbox"
|
| "/docs/getting-started/installation/saltbox"
|
||||||
| "/docs/getting-started/installation/source"
|
| "/docs/getting-started/installation/source"
|
||||||
| "/docs/getting-started/installation/synology"
|
| "/docs/getting-started/installation/synology"
|
||||||
|
|||||||
@@ -26,9 +26,9 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/form": "^8.3.8",
|
"@mantine/form": "^8.3.10",
|
||||||
"mantine-form-zod-resolver": "^1.3.0",
|
"mantine-form-zod-resolver": "^1.3.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { ChangeEvent, FocusEvent } from "react";
|
import type { ChangeEvent, FocusEvent } from "react";
|
||||||
|
|
||||||
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement>
|
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement> extends BasePropsFor<
|
||||||
extends BasePropsFor<TOnChangeArg, TComponent> {
|
TOnChangeArg,
|
||||||
|
TComponent
|
||||||
|
> {
|
||||||
value?: T;
|
value?: T;
|
||||||
defaultValue?: T;
|
defaultValue?: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -16,7 +16,19 @@ interface UploadMediaProps {
|
|||||||
|
|
||||||
export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }: UploadMediaProps) => {
|
export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }: UploadMediaProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
|
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation({
|
||||||
|
async onSuccess(mediaIds) {
|
||||||
|
await onSuccess?.(
|
||||||
|
mediaIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
url: `/api/user-medias/${id}`,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async onSettled() {
|
||||||
|
await onSettled?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleFileUploadAsync = async (files: File[] | File | null) => {
|
const handleFileUploadAsync = async (files: File[] | File | null) => {
|
||||||
if (!files || (Array.isArray(files) && files.length === 0)) return;
|
if (!files || (Array.isArray(files) && files.length === 0)) return;
|
||||||
@@ -24,25 +36,16 @@ export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
filesArray.forEach((file) => formData.append("files", file));
|
filesArray.forEach((file) => formData.append("files", file));
|
||||||
await mutateAsync(formData, {
|
await mutateAsync(formData, {
|
||||||
async onSuccess(mediaIds) {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: t("media.action.upload.notification.success.message"),
|
message: t("media.action.upload.notification.success.message"),
|
||||||
});
|
});
|
||||||
await onSuccess?.(
|
|
||||||
mediaIds.map((id) => ({
|
|
||||||
id,
|
|
||||||
url: `/api/user-medias/${id}`,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
message: t("media.action.upload.notification.error.message"),
|
message: t("media.action.upload.notification.error.message"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async onSettled() {
|
|
||||||
await onSettled?.();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"tsdav": "^2.1.6",
|
"tsdav": "^2.1.6",
|
||||||
"undici": "7.16.0",
|
"undici": "7.16.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export class TransmissionIntegration extends Integration implements IDownloadCli
|
|||||||
name: torrent.name,
|
name: torrent.name,
|
||||||
size: torrent.totalSize,
|
size: torrent.totalSize,
|
||||||
sent: torrent.uploadedEver,
|
sent: torrent.uploadedEver,
|
||||||
|
received: torrent.downloadedEver,
|
||||||
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
|
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
|
||||||
upSpeed: torrent.rateUpload,
|
upSpeed: torrent.rateUpload,
|
||||||
time:
|
time:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const downloadClientItemSchema = z.object({
|
|||||||
size: z.number(),
|
size: z.number(),
|
||||||
/** Total uploaded in Bytes, only required for Torrent items */
|
/** Total uploaded in Bytes, only required for Torrent items */
|
||||||
sent: z.number().optional(),
|
sent: z.number().optional(),
|
||||||
|
/** Total downloaded in Bytes, only required for Torrent items */
|
||||||
|
received: z.number().optional(),
|
||||||
/** Download speed in Bytes/s, only required if not complete
|
/** Download speed in Bytes/s, only required if not complete
|
||||||
* (Says 0 only if it should be downloading but isn't) */
|
* (Says 0 only if it should be downloading but isn't) */
|
||||||
downSpeed: z.number().optional(),
|
downSpeed: z.number().optional(),
|
||||||
|
|||||||
@@ -169,17 +169,15 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
|
|||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const token = super.getSecretValue("apiKey");
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
const response = await input.fetchAsync(this.url("/"), {
|
const response = await input.fetchAsync(super.url("/prefs"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Plex-Token": token,
|
"X-Plex-Token": token,
|
||||||
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) return TestConnectionError.StatusResult(response);
|
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||||
|
|
||||||
const result = await response.text();
|
|
||||||
|
|
||||||
await PlexIntegration.parseXmlAsync<PlexResponse>(result);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.6",
|
||||||
"winston": "3.18.3",
|
"winston": "3.19.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/forms-collection": "workspace:^0.1.0",
|
"@homarr/forms-collection": "workspace:^0.1.0",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
@@ -34,13 +33,13 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { z } from "zod/v4";
|
|||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import type { MaybePromise } from "@homarr/common/types";
|
|
||||||
import { AppForm } from "@homarr/forms-collection";
|
import { AppForm } from "@homarr/forms-collection";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
@@ -10,7 +9,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
|||||||
import type { appManageSchema } from "@homarr/validation/app";
|
import type { appManageSchema } from "@homarr/validation/app";
|
||||||
|
|
||||||
interface QuickAddAppModalProps {
|
interface QuickAddAppModalProps {
|
||||||
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => MaybePromise<void>;
|
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
|
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
|
||||||
@@ -28,13 +27,13 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
|
|||||||
|
|
||||||
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
|
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
|
||||||
mutate(values, {
|
mutate(values, {
|
||||||
async onSuccess(app) {
|
onSuccess(app) {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: tScoped("success.title"),
|
title: tScoped("success.title"),
|
||||||
message: tScoped("success.message"),
|
message: tScoped("success.message"),
|
||||||
});
|
});
|
||||||
|
|
||||||
await innerProps.onClose(app);
|
innerProps.onClose(app);
|
||||||
actions.closeModal();
|
actions.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import {
|
import { Button, Group, InputWrapper, Slider, Stack, Switch, TextInput } from "@mantine/core";
|
||||||
Badge,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Fieldset,
|
|
||||||
Group,
|
|
||||||
InputWrapper,
|
|
||||||
RadioGroup,
|
|
||||||
SimpleGrid,
|
|
||||||
Slider,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
TextInput,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
|
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { boardLayoutModes } from "@homarr/definitions";
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { CustomRadioCard } from "@homarr/ui";
|
|
||||||
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
|
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
export const AddBoardModal = createModal(({ actions }) => {
|
export const AddBoardModal = createModal(({ actions }) => {
|
||||||
@@ -33,7 +18,6 @@ export const AddBoardModal = createModal(({ actions }) => {
|
|||||||
name: "",
|
name: "",
|
||||||
columnCount: 10,
|
columnCount: 10,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
layoutMode: boardLayoutModes.defaultValue,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate, isPending } = clientApi.board.createBoard.useMutation({
|
const { mutate, isPending } = clientApi.board.createBoard.useMutation({
|
||||||
@@ -80,38 +64,14 @@ export const AddBoardModal = createModal(({ actions }) => {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
|
||||||
<Fieldset legend={t("board.setting.section.layout.title")}>
|
<Slider
|
||||||
<Stack gap="sm">
|
min={boardColumnCountSchema.minValue ?? undefined}
|
||||||
<RadioGroup {...form.getInputProps("layoutMode")}>
|
max={boardColumnCountSchema.maxValue ?? undefined}
|
||||||
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
|
step={1}
|
||||||
{boardLayoutModes.values.map((mode) => (
|
{...form.getInputProps("columnCount")}
|
||||||
<Box w="100%" key={mode} pos="relative">
|
/>
|
||||||
<CustomRadioCard
|
</InputWrapper>
|
||||||
value={mode}
|
|
||||||
label={t(`board.field.layoutMode.option.${mode}.label`)}
|
|
||||||
description={t(`board.field.layoutMode.option.${mode}.description`)}
|
|
||||||
/>
|
|
||||||
{mode === boardLayoutModes.defaultValue && (
|
|
||||||
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
|
|
||||||
{t("common.select.badge.recommended")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
|
|
||||||
<Slider
|
|
||||||
min={boardColumnCountSchema.minValue ?? undefined}
|
|
||||||
max={boardColumnCountSchema.maxValue ?? undefined}
|
|
||||||
step={1}
|
|
||||||
{...form.getInputProps("columnCount")}
|
|
||||||
/>
|
|
||||||
</InputWrapper>
|
|
||||||
</Stack>
|
|
||||||
</Fieldset>
|
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={t("board.field.isPublic.label")}
|
label={t("board.field.isPublic.label")}
|
||||||
@@ -132,7 +92,6 @@ export const AddBoardModal = createModal(({ actions }) => {
|
|||||||
);
|
);
|
||||||
}).withOptions({
|
}).withOptions({
|
||||||
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
||||||
size: "lg",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useBoardNameStatus = (name: string) => {
|
export const useBoardNameStatus = (name: string) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import type { MaybePromise } from "@homarr/common/types";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
@@ -15,7 +15,6 @@ interface InnerProps {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
onSuccess: () => MaybePromise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||||
@@ -27,7 +26,11 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const boardNameStatus = useBoardNameStatus(form.values.name);
|
const boardNameStatus = useBoardNameStatus(form.values.name);
|
||||||
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation();
|
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/manage/boards");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -40,13 +43,12 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
|
|||||||
id: innerProps.board.id,
|
id: innerProps.board.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
actions.closeModal();
|
actions.closeModal();
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("board.action.duplicate.notification.success.title"),
|
title: t("board.action.duplicate.notification.success.title"),
|
||||||
message: t("board.action.duplicate.notification.success.message"),
|
message: t("board.action.duplicate.notification.success.message"),
|
||||||
});
|
});
|
||||||
await innerProps.onSuccess();
|
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ export const ImportBoardModal = createModal(({ actions }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation();
|
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await revalidatePathActionAsync("/manage/boards");
|
||||||
|
},
|
||||||
|
});
|
||||||
const boardNameStatus = useBoardNameStatus(form.values.configuration.name);
|
const boardNameStatus = useBoardNameStatus(form.values.configuration.name);
|
||||||
|
|
||||||
const handleSubmitAsync = async (values: { file: File; configuration: OldmarrImportConfiguration }) => {
|
const handleSubmitAsync = async (values: { file: File; configuration: OldmarrImportConfiguration }) => {
|
||||||
@@ -74,9 +78,8 @@ export const ImportBoardModal = createModal(({ actions }) => {
|
|||||||
formData.set("configuration", JSON.stringify(values.configuration));
|
formData.set("configuration", JSON.stringify(values.configuration));
|
||||||
|
|
||||||
await mutateAsync(formData, {
|
await mutateAsync(formData, {
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
actions.closeModal();
|
actions.closeModal();
|
||||||
await revalidatePathActionAsync("/manage/boards");
|
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: tOldImport("notification.success.title"),
|
title: tOldImport("notification.success.title"),
|
||||||
message: tOldImport("notification.success.message"),
|
message: tOldImport("notification.success.message"),
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
|
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
await innerProps.onSuccess?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -35,12 +39,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.set("file", values.file);
|
formData.set("file", values.file);
|
||||||
await mutateAsync(formData, {
|
await mutateAsync(formData, {
|
||||||
async onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("certificate.action.create.notification.success.title"),
|
title: t("certificate.action.create.notification.success.title"),
|
||||||
message: t("certificate.action.create.notification.success.message"),
|
message: t("certificate.action.create.notification.success.message"),
|
||||||
});
|
});
|
||||||
await innerProps.onSuccess?.();
|
|
||||||
actions.closeModal();
|
actions.closeModal();
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"@mantine/hooks": "^8.3.8",
|
"@mantine/hooks": "^8.3.10",
|
||||||
"react": "19.2.0"
|
"react": "19.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^8.3.8",
|
"@mantine/notifications": "^8.3.10",
|
||||||
"@tabler/icons-react": "^3.35.0"
|
"@tabler/icons-react": "^3.35.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -37,14 +37,14 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"@mantine/hooks": "^8.3.8",
|
"@mantine/hooks": "^8.3.10",
|
||||||
"adm-zip": "0.5.16",
|
"adm-zip": "0.5.16",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.6",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.13",
|
||||||
"zod-form-data": "^3.0.1"
|
"zod-form-data": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export const ImportTokenModal = createModal<InnerProps>(({ actions, innerProps }
|
|||||||
mutate(
|
mutate(
|
||||||
{ checksum: innerProps.checksum, token: values.token },
|
{ checksum: innerProps.checksum, token: values.token },
|
||||||
{
|
{
|
||||||
async onSuccess(isValid) {
|
onSuccess(isValid) {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
actions.closeModal();
|
actions.closeModal();
|
||||||
await innerProps.onSuccessAsync(values.token);
|
void innerProps.onSuccessAsync(values.token);
|
||||||
} else {
|
} else {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
title: tTokenModal("notification.error.title"),
|
title: tTokenModal("notification.error.title"),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@homarr/definitions": "workspace:^",
|
"@homarr/definitions": "workspace:^",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
"superjson": "2.2.5"
|
"superjson": "2.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -32,9 +32,9 @@
|
|||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"octokit": "^5.0.5",
|
"octokit": "^5.0.5",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.6",
|
||||||
"undici": "7.16.0",
|
"undici": "7.16.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/dates": "^8.3.8",
|
"@mantine/dates": "^8.3.10",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -33,14 +33,14 @@
|
|||||||
"@homarr/settings": "workspace:^0.1.0",
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.8",
|
"@mantine/core": "^8.3.10",
|
||||||
"@mantine/hooks": "^8.3.8",
|
"@mantine/hooks": "^8.3.10",
|
||||||
"@mantine/spotlight": "^8.3.8",
|
"@mantine/spotlight": "^8.3.10",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"jotai": "^2.15.1",
|
"jotai": "^2.15.2",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -32,10 +32,10 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "16.0.3",
|
"next": "16.0.10",
|
||||||
"next-intl": "4.5.5",
|
"next-intl": "4.5.8",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1939,7 +1939,23 @@
|
|||||||
"dockerContainers": {
|
"dockerContainers": {
|
||||||
"name": "",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"option": {},
|
"option": {
|
||||||
|
"enableRowSorting": {
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"defaultSort": {
|
||||||
|
"label": "",
|
||||||
|
"option": {
|
||||||
|
"name": "",
|
||||||
|
"state": "",
|
||||||
|
"cpuUsage": "",
|
||||||
|
"memoryUsage": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"descendingDefaultSort": {
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": ""
|
"internalServerError": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1939,7 +1939,23 @@
|
|||||||
"dockerContainers": {
|
"dockerContainers": {
|
||||||
"name": "Docker 状态",
|
"name": "Docker 状态",
|
||||||
"description": "您的容器状态 (这个小部件只能用管理员权限添加)",
|
"description": "您的容器状态 (这个小部件只能用管理员权限添加)",
|
||||||
"option": {},
|
"option": {
|
||||||
|
"enableRowSorting": {
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"defaultSort": {
|
||||||
|
"label": "",
|
||||||
|
"option": {
|
||||||
|
"name": "",
|
||||||
|
"state": "",
|
||||||
|
"cpuUsage": "",
|
||||||
|
"memoryUsage": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"descendingDefaultSort": {
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": "获取容器统计信息失败"
|
"internalServerError": "获取容器统计信息失败"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1939,7 +1939,23 @@
|
|||||||
"dockerContainers": {
|
"dockerContainers": {
|
||||||
"name": "crwdns3424:0crwdne3424:0",
|
"name": "crwdns3424:0crwdne3424:0",
|
||||||
"description": "crwdns3426:0crwdne3426:0",
|
"description": "crwdns3426:0crwdne3426:0",
|
||||||
"option": {},
|
"option": {
|
||||||
|
"enableRowSorting": {
|
||||||
|
"label": "crwdns3808:0crwdne3808:0"
|
||||||
|
},
|
||||||
|
"defaultSort": {
|
||||||
|
"label": "crwdns3810:0crwdne3810:0",
|
||||||
|
"option": {
|
||||||
|
"name": "crwdns3812:0crwdne3812:0",
|
||||||
|
"state": "crwdns3814:0crwdne3814:0",
|
||||||
|
"cpuUsage": "crwdns3816:0crwdne3816:0",
|
||||||
|
"memoryUsage": "crwdns3818:0crwdne3818:0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"descendingDefaultSort": {
|
||||||
|
"label": "crwdns3820:0crwdne3820:0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": "crwdns3428:0crwdne3428:0"
|
"internalServerError": "crwdns3428:0crwdne3428:0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1939,7 +1939,23 @@
|
|||||||
"dockerContainers": {
|
"dockerContainers": {
|
||||||
"name": "",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"option": {},
|
"option": {
|
||||||
|
"enableRowSorting": {
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"defaultSort": {
|
||||||
|
"label": "",
|
||||||
|
"option": {
|
||||||
|
"name": "",
|
||||||
|
"state": "",
|
||||||
|
"cpuUsage": "",
|
||||||
|
"memoryUsage": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"descendingDefaultSort": {
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": ""
|
"internalServerError": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1939,7 +1939,23 @@
|
|||||||
"dockerContainers": {
|
"dockerContainers": {
|
||||||
"name": "Docker statistik",
|
"name": "Docker statistik",
|
||||||
"description": "Statistik for dine containere (Denne widget kan kun tilføjes med administratorrettigheder)",
|
"description": "Statistik for dine containere (Denne widget kan kun tilføjes med administratorrettigheder)",
|
||||||
"option": {},
|
"option": {
|
||||||
|
"enableRowSorting": {
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"defaultSort": {
|
||||||
|
"label": "",
|
||||||
|
"option": {
|
||||||
|
"name": "",
|
||||||
|
"state": "",
|
||||||
|
"cpuUsage": "",
|
||||||
|
"memoryUsage": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"descendingDefaultSort": {
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": "Kunne ikke hente containerstatistik"
|
"internalServerError": "Kunne ikke hente containerstatistik"
|
||||||
}
|
}
|
||||||
@@ -2232,7 +2248,7 @@
|
|||||||
"unknown": "Ukendt",
|
"unknown": "Ukendt",
|
||||||
"pending": "Afventende",
|
"pending": "Afventende",
|
||||||
"processing": "Behandler",
|
"processing": "Behandler",
|
||||||
"requested": "",
|
"requested": "Anmodet",
|
||||||
"partiallyAvailable": "Delvis",
|
"partiallyAvailable": "Delvis",
|
||||||
"available": "Tilgængelig",
|
"available": "Tilgængelig",
|
||||||
"blacklisted": "Blacklistet",
|
"blacklisted": "Blacklistet",
|
||||||
|
|||||||
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user