Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0de853ff37 | ||
|
|
a438a23f4e | ||
|
|
8dd7fe297c | ||
|
|
780efab80c | ||
|
|
a4e9abcda1 | ||
|
|
55b1afbb21 | ||
|
|
b447fc9fe4 | ||
|
|
51ebaba399 | ||
|
|
9d4aea2c41 | ||
|
|
4dcdac6621 | ||
|
|
84bc242834 | ||
|
|
f840f4c600 | ||
|
|
5c8d7fcc15 | ||
|
|
93dc3ae97e | ||
|
|
7cb0aa70f1 | ||
|
|
5c219a8b59 | ||
|
|
50970e431d | ||
|
|
af67bf26d6 | ||
|
|
0d5487366c | ||
|
|
f0daef237c | ||
|
|
54a62c49cf | ||
|
|
92f70f5a03 | ||
|
|
c43a2f0488 | ||
|
|
1146c59a41 | ||
|
|
af1f8eaf76 | ||
|
|
dd2ca13004 | ||
|
|
b38de29a88 | ||
|
|
2bb44e1666 | ||
|
|
57b523fa80 | ||
|
|
ecbd2d14da | ||
|
|
bbac4128eb | ||
|
|
5aaca9261f | ||
|
|
963d77463a | ||
|
|
b28274fa85 | ||
|
|
3e85f4e44d | ||
|
|
614f8077e1 | ||
|
|
b36b5c1eb1 | ||
|
|
b4b19f486e | ||
|
|
32cf09a62e | ||
|
|
a1ba4372da | ||
|
|
4e153b8046 | ||
|
|
ac788650fb | ||
|
|
a9b0e26c8d | ||
|
|
132db15424 | ||
|
|
860b73fa3f | ||
|
|
e4dd19b574 | ||
|
|
1b4ed02010 | ||
|
|
47b7cbb086 | ||
|
|
7a6477a272 | ||
|
|
bff536008c | ||
|
|
8b040eddef | ||
|
|
ce87d62b35 | ||
|
|
f88cc7381b | ||
|
|
cb54de524b | ||
|
|
7d8c7b9c2d | ||
|
|
73f7d885cd | ||
|
|
470d27e091 | ||
|
|
99c8e5b609 | ||
|
|
b413e2ec7d | ||
|
|
29d4f9e003 | ||
|
|
1f54bf5b9e | ||
|
|
4d8c22a4e6 | ||
|
|
3e9e1e5e4f | ||
|
|
582b981e69 | ||
|
|
f8ec4a43cf | ||
|
|
2a8809551c | ||
|
|
9df59296ff | ||
|
|
701538fcb5 | ||
|
|
0e82f2d199 | ||
|
|
a2b5d60aa8 | ||
|
|
02dabfcd01 | ||
|
|
931f312291 | ||
|
|
175927970b | ||
|
|
c922c5b083 | ||
|
|
73bbcfeee8 | ||
|
|
7a2d97ba91 | ||
|
|
e47d497560 | ||
|
|
b9271ba14a | ||
|
|
9d85677046 | ||
|
|
b30c192b8f | ||
|
|
d4f04da709 | ||
|
|
7960a4cea1 | ||
|
|
243d7e9dd4 | ||
|
|
bdefff636b | ||
|
|
55feb9dff8 | ||
|
|
45af6442a1 | ||
|
|
72debef4b4 | ||
|
|
25635db17e | ||
|
|
1897752e38 | ||
|
|
554853ffbd | ||
|
|
e40237e4a5 | ||
|
|
fa6262cf5e | ||
|
|
49bd02e869 | ||
|
|
3c8fbe371b | ||
|
|
81a8cf4b36 | ||
|
|
b5c008b4ad | ||
|
|
343438391b | ||
|
|
8400b9fb11 | ||
|
|
fe65e73fff | ||
|
|
e023edf9d6 | ||
|
|
927913cf4e | ||
|
|
07d8103051 | ||
|
|
fe08ccf589 | ||
|
|
c55207bd59 | ||
|
|
e35811c921 | ||
|
|
e481e116ff | ||
|
|
32be09a4a7 |
No files matched your search
@@ -31,6 +31,11 @@ body:
|
|||||||
label: Version
|
label: Version
|
||||||
description: What version of Homarr are you running?
|
description: What version of Homarr are you running?
|
||||||
options:
|
options:
|
||||||
|
- 1.3.0
|
||||||
|
- 1.2.0
|
||||||
|
- 1.1.0
|
||||||
|
- 1.0.1
|
||||||
|
- 1.0.0
|
||||||
- 1.0.0-beta
|
- 1.0.0-beta
|
||||||
- Other (describe in "additional information")
|
- Other (describe in "additional information")
|
||||||
default: 0
|
default: 0
|
||||||
|
|||||||
@@ -1,3 +1,96 @@
|
|||||||
|
## [1.3.1](https://github.com/homarr-labs/homarr/compare/v1.3.0...v1.3.1) (2025-01-28)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* app url line clamp ([#2170](https://github.com/homarr-labs/homarr/issues/2170)) ([780efab](https://github.com/homarr-labs/homarr/commit/780efab80cdc5446a6468d60afe2c1d9ecca0f1e))
|
||||||
|
* automatic search engine creation ([#2172](https://github.com/homarr-labs/homarr/issues/2172)) ([8dd7fe2](https://github.com/homarr-labs/homarr/commit/8dd7fe297c431bbe448bc47b381370ade7c4d3d9))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.18.3 ([#2164](https://github.com/homarr-labs/homarr/issues/2164)) ([a4e9abc](https://github.com/homarr-labs/homarr/commit/a4e9abcda192a96754078eece4343c2c1c9fa032))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.22.0 ([#2149](https://github.com/homarr-labs/homarr/issues/2149)) ([9d4aea2](https://github.com/homarr-labs/homarr/commit/9d4aea2c4187ddda6a0484d0f6a0403c04961dde))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.65.1 ([#2161](https://github.com/homarr-labs/homarr/issues/2161)) ([55b1afb](https://github.com/homarr-labs/homarr/commit/55b1afbb2167ccb6f5115cbb1291dfe561a5aa05))
|
||||||
|
|
||||||
|
## [1.3.0](https://github.com/homarr-labs/homarr/compare/v1.2.0...v1.3.0) (2025-01-27)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add opacity for categories ([#2132](https://github.com/homarr-labs/homarr/issues/2132)) ([1146c59](https://github.com/homarr-labs/homarr/commit/1146c59a41ddd4b4cda14f6601437cd9bceb305a))
|
||||||
|
* **category:** save collapse state for signed in users ([#2134](https://github.com/homarr-labs/homarr/issues/2134)) ([7cb0aa7](https://github.com/homarr-labs/homarr/commit/7cb0aa70f11253aa4016d928f326eb71183626f6))
|
||||||
|
* log body integration test failure ([#2151](https://github.com/homarr-labs/homarr/issues/2151)) ([5c8d7fc](https://github.com/homarr-labs/homarr/commit/5c8d7fcc153ba4b94cd88c6ca8b22962d812b2b4))
|
||||||
|
* **user:** add search in new tab preference ([#2125](https://github.com/homarr-labs/homarr/issues/2125)) ([92f70f5](https://github.com/homarr-labs/homarr/commit/92f70f5a03048c7b269604f1ce33313ecf23a64b))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* app url not required ([#2130](https://github.com/homarr-labs/homarr/issues/2130)) ([b38de29](https://github.com/homarr-labs/homarr/commit/b38de29a882ce26a9326f7f65403ea3916a58cac))
|
||||||
|
* bookmarks show unauthorized when not logged in ([#2123](https://github.com/homarr-labs/homarr/issues/2123)) ([2bb44e1](https://github.com/homarr-labs/homarr/commit/2bb44e16669f94d3d7500e828b1ab66b98c54bc7))
|
||||||
|
* **deps:** update dependency @t3-oss/env-nextjs to ^0.12.0 ([#2117](https://github.com/homarr-labs/homarr/issues/2117)) ([963d774](https://github.com/homarr-labs/homarr/commit/963d77463a5206ac1cb5d035b52271c6bbbd5e73))
|
||||||
|
* **deps:** update dependency drizzle-kit to ^0.30.3 ([#2142](https://github.com/homarr-labs/homarr/issues/2142)) ([0d54873](https://github.com/homarr-labs/homarr/commit/0d5487366c2616738b2542838f5cbd9c6d628973))
|
||||||
|
* **deps:** update dependency drizzle-orm to ^0.39.0 ([#2144](https://github.com/homarr-labs/homarr/issues/2144)) ([f0daef2](https://github.com/homarr-labs/homarr/commit/f0daef237c08c866f1f2aaefc113e70838b76be4))
|
||||||
|
* **deps:** update dependency drizzle-zod to ^0.7.0 ([#2143](https://github.com/homarr-labs/homarr/issues/2143)) ([af67bf2](https://github.com/homarr-labs/homarr/commit/af67bf26d674539abd7a0fdc54d7764d90f7007c))
|
||||||
|
* **deps:** update mantine monorepo to ^7.16.2 ([#2129](https://github.com/homarr-labs/homarr/issues/2129)) ([bbac412](https://github.com/homarr-labs/homarr/commit/bbac4128eb36d934310a83087c733338ae2b9b9c))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.65.0 ([#2146](https://github.com/homarr-labs/homarr/issues/2146)) ([50970e4](https://github.com/homarr-labs/homarr/commit/50970e431d7654d2db43a4b1d55cc4f7d15a4ac2))
|
||||||
|
* **deps:** update testcontainers-node monorepo to ^10.17.2 ([#2148](https://github.com/homarr-labs/homarr/issues/2148)) ([5c219a8](https://github.com/homarr-labs/homarr/commit/5c219a8b595f90c53189569bd63fb85db8eab882))
|
||||||
|
* multi-text-input not saving on blur ([#2128](https://github.com/homarr-labs/homarr/issues/2128)) ([dd2ca13](https://github.com/homarr-labs/homarr/commit/dd2ca1300422a6ad477089efe905901515eebaf6))
|
||||||
|
* split up proxmox secrets are incorrectly migrated ([#2124](https://github.com/homarr-labs/homarr/issues/2124)) ([57b523f](https://github.com/homarr-labs/homarr/commit/57b523fa805de56d8795169a535e5e45d7c22ad7))
|
||||||
|
* unable to update integration when encryption key changed ([#2133](https://github.com/homarr-labs/homarr/issues/2133)) ([93dc3ae](https://github.com/homarr-labs/homarr/commit/93dc3ae97ed97691c304c9b6de9245930d5461ae))
|
||||||
|
* websocket is not keep alive ([#2131](https://github.com/homarr-labs/homarr/issues/2131)) ([af1f8ea](https://github.com/homarr-labs/homarr/commit/af1f8eaf76062a9fde5570097fc6a75742174d97))
|
||||||
|
|
||||||
|
## [1.2.0](https://github.com/homarr-labs/homarr/compare/v1.1.0...v1.2.0) (2025-01-24)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add bookmarks options (apps) ([#2048](https://github.com/homarr-labs/homarr/issues/2048)) ([132db15](https://github.com/homarr-labs/homarr/commit/132db154242959dc2f16add4e58b0bdb7b96fb8c))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* date & time widget padding cut off ([#2089](https://github.com/homarr-labs/homarr/issues/2089)) ([e4dd19b](https://github.com/homarr-labs/homarr/commit/e4dd19b574bfe0dcbcd92a3c5993aef550683fc3))
|
||||||
|
* **deps:** update turbo monorepo to ^2.3.4 ([#2085](https://github.com/homarr-labs/homarr/issues/2085)) ([8b040ed](https://github.com/homarr-labs/homarr/commit/8b040eddef8dbc1269489038bc503dcf23ba9580))
|
||||||
|
* issue with foreign key constraint do to empty string for home board ([#2088](https://github.com/homarr-labs/homarr/issues/2088)) ([ac78865](https://github.com/homarr-labs/homarr/commit/ac788650fb60d7aca889ec270dc29edb7ee53123))
|
||||||
|
* system health monitoring gets proxmox id passed ([#2094](https://github.com/homarr-labs/homarr/issues/2094)) ([a1ba437](https://github.com/homarr-labs/homarr/commit/a1ba4372da3430777a85dc83bbfc59f4b03d65fb))
|
||||||
|
* url-encode nzbget credentials ([#2093](https://github.com/homarr-labs/homarr/issues/2093)) ([4e153b8](https://github.com/homarr-labs/homarr/commit/4e153b804664e4fbc63ee5eb945c8096c809c6d5))
|
||||||
|
|
||||||
|
## [1.1.0](https://github.com/homarr-labs/homarr/compare/v1.0.1...v1.1.0) (2025-01-22)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add dropdown list for switching boards ([#2042](https://github.com/homarr-labs/homarr/issues/2042)) ([02dabfc](https://github.com/homarr-labs/homarr/commit/02dabfcd01e372ada7d80a0501b708b171c8fbc6))
|
||||||
|
* add initial focus on new integration search ([#2015](https://github.com/homarr-labs/homarr/issues/2015)) ([b30c192](https://github.com/homarr-labs/homarr/commit/b30c192b8fbbbfcff8eb09b8f1654eb8dd90b56d))
|
||||||
|
* add support for calendar without integrations, add today mark ([#2016](https://github.com/homarr-labs/homarr/issues/2016)) ([9d85677](https://github.com/homarr-labs/homarr/commit/9d8567704670a9ab471ac3e246c5ba85f908cfd9))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add item does not respect dynamic sections ([#2010](https://github.com/homarr-labs/homarr/issues/2010)) ([d4f04da](https://github.com/homarr-labs/homarr/commit/d4f04da70910b6adbf3764de1499dbd46fe9b6d5))
|
||||||
|
* add missing request handler for rss feeds ([#2006](https://github.com/homarr-labs/homarr/issues/2006)) ([99c8e5b](https://github.com/homarr-labs/homarr/commit/99c8e5b609b17dfe443b35bfff2b883845dc62fe))
|
||||||
|
* certificate card delete button not reachable caused by overflow ([#2037](https://github.com/homarr-labs/homarr/issues/2037)) ([c922c5b](https://github.com/homarr-labs/homarr/commit/c922c5b08387b01136332876782fed1444203096))
|
||||||
|
* change error when section for item not found to logged warning and remove item ([#2044](https://github.com/homarr-labs/homarr/issues/2044)) ([b413e2e](https://github.com/homarr-labs/homarr/commit/b413e2ec7dd37c43665373be914ffa414599a206))
|
||||||
|
* copy only working for https and localhost, adding open in new tab ([#2035](https://github.com/homarr-labs/homarr/issues/2035)) ([7a2d97b](https://github.com/homarr-labs/homarr/commit/7a2d97ba9119e046c06365146fc4efd080f6f6df))
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.29.0 ([#2057](https://github.com/homarr-labs/homarr/issues/2057)) ([2a88095](https://github.com/homarr-labs/homarr/commit/2a8809551cfc4f4c09aa5c01cac05914fa913769))
|
||||||
|
* **deps:** update dependency better-sqlite3 to ^11.8.1 ([#2005](https://github.com/homarr-labs/homarr/issues/2005)) ([49bd02e](https://github.com/homarr-labs/homarr/commit/49bd02e869081ec325bd8b4ba2909603a82cd7df))
|
||||||
|
* **deps:** update dependency jotai to ^2.11.1 ([#2026](https://github.com/homarr-labs/homarr/issues/2026)) ([45af644](https://github.com/homarr-labs/homarr/commit/45af6442a1b84be399ddd281a17227a4184490be))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.21.0 ([#2032](https://github.com/homarr-labs/homarr/issues/2032)) ([55feb9d](https://github.com/homarr-labs/homarr/commit/55feb9dff87088d7f6d006bcecc61b0435edbb6f))
|
||||||
|
* **deps:** update dependency undici to v7.3.0 ([#2061](https://github.com/homarr-labs/homarr/issues/2061)) ([3e9e1e5](https://github.com/homarr-labs/homarr/commit/3e9e1e5e4f737f8bdb1046585463c987fe43c3d5))
|
||||||
|
* **deps:** update mantine monorepo to ^7.16.1 ([#2014](https://github.com/homarr-labs/homarr/issues/2014)) ([1897752](https://github.com/homarr-labs/homarr/commit/1897752e3893804a5082f34b20929ef007825506))
|
||||||
|
* **deps:** update nextjs monorepo to ^15.1.6 ([#2063](https://github.com/homarr-labs/homarr/issues/2063)) ([4d8c22a](https://github.com/homarr-labs/homarr/commit/4d8c22a4e684b4f921967992d04d2461eb2e52db))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.64.2 ([#2013](https://github.com/homarr-labs/homarr/issues/2013)) ([554853f](https://github.com/homarr-labs/homarr/commit/554853ffbda202eff559b5dafa330a15e2649f17))
|
||||||
|
* **deps:** update testcontainers-node monorepo to ^10.17.1 ([#2047](https://github.com/homarr-labs/homarr/issues/2047)) ([701538f](https://github.com/homarr-labs/homarr/commit/701538fcb5c546478f5d31b2869b933e7544cc3e))
|
||||||
|
* **deps:** update tiptap monorepo to v2.11.3 ([#2067](https://github.com/homarr-labs/homarr/issues/2067)) ([1f54bf5](https://github.com/homarr-labs/homarr/commit/1f54bf5b9e24b6d063f5ce9165c87d2d036fb8c6))
|
||||||
|
* docker hosts and ports env variable wrongly used ([#2050](https://github.com/homarr-labs/homarr/issues/2050)) ([73f7d88](https://github.com/homarr-labs/homarr/commit/73f7d885cddbbaa2ac879953fe5ee24a6abe582c))
|
||||||
|
* empty env variables should be ignored ([#2040](https://github.com/homarr-labs/homarr/issues/2040)) ([1759279](https://github.com/homarr-labs/homarr/commit/175927970b25648104fac5a31649471b3ebeaa9e))
|
||||||
|
* entity state and execute automation cards aren't movable ([#2043](https://github.com/homarr-labs/homarr/issues/2043)) ([29d4f9e](https://github.com/homarr-labs/homarr/commit/29d4f9e00313fe3e7ae207547cbd7a143b314535))
|
||||||
|
* health monitoring cpu temp ring when no data, disks health when no data ([#2029](https://github.com/homarr-labs/homarr/issues/2029)) ([bdefff6](https://github.com/homarr-labs/homarr/commit/bdefff636b06cb33020765e606bfe8e41c931722))
|
||||||
|
* hide current language switch in other users preferences ([#2069](https://github.com/homarr-labs/homarr/issues/2069)) ([470d27e](https://github.com/homarr-labs/homarr/commit/470d27e091a006af8a8c3ceccfc439902ebc5f18))
|
||||||
|
* unable to change home board in user preferences ([#2011](https://github.com/homarr-labs/homarr/issues/2011)) ([7960a4c](https://github.com/homarr-labs/homarr/commit/7960a4cea11d839f5a0d1889e98bb949c11b68fa))
|
||||||
|
* unable to move items with touch devices ([#2038](https://github.com/homarr-labs/homarr/issues/2038)) ([931f312](https://github.com/homarr-labs/homarr/commit/931f312291c0a6f1f22a26adf89dfbf912e4fe5e))
|
||||||
|
* url of batch link can get to long with 414 http code ([#2036](https://github.com/homarr-labs/homarr/issues/2036)) ([73bbcfe](https://github.com/homarr-labs/homarr/commit/73bbcfeee86bf0950145070a6a2ce528b708fe7c))
|
||||||
|
* User options switch on load value always false ([#1995](https://github.com/homarr-labs/homarr/issues/1995)) ([fa6262c](https://github.com/homarr-labs/homarr/commit/fa6262cf5e780fc3c5b445f988de9d9eb5b86ab5))
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/homarr-labs/homarr/compare/v1.0.0...v1.0.1) (2025-01-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add error from ldap bind operation to log statement ([#1980](https://github.com/homarr-labs/homarr/issues/1980)) ([927913c](https://github.com/homarr-labs/homarr/commit/927913cf4e0a6e4f3a6b7dbf9ae9034d0780b5fb))
|
||||||
|
* add missing old-import proxmox integration kind mapping ([#1979](https://github.com/homarr-labs/homarr/issues/1979)) ([e023edf](https://github.com/homarr-labs/homarr/commit/e023edf9d62417b054eb565a7f794267539eb992))
|
||||||
|
* **deps:** update dependency flag-icons to ^7.3.2 ([#1981](https://github.com/homarr-labs/homarr/issues/1981)) ([c55207b](https://github.com/homarr-labs/homarr/commit/c55207bd59f0c51869e8fdb36e083a8327ca46b1))
|
||||||
|
* **deps:** update nextjs monorepo to ^15.1.5 ([#1978](https://github.com/homarr-labs/homarr/issues/1978)) ([e35811c](https://github.com/homarr-labs/homarr/commit/e35811c921aea8c96dca398841ece396d71d4979))
|
||||||
|
|
||||||
## 1.0.0 (2025-01-17)
|
## 1.0.0 (2025-01-17)
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
### ⚠ BREAKING CHANGES
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.13.0-alpine AS base
|
FROM node:22.13.1-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import "@homarr/auth/env";
|
import "@homarr/auth/env";
|
||||||
import "@homarr/db/env";
|
import "@homarr/db/env";
|
||||||
import "@homarr/common/env";
|
import "@homarr/common/env";
|
||||||
|
import "@homarr/docker/env";
|
||||||
|
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
import MillionLint from "@million/lint";
|
import MillionLint from "@million/lint";
|
||||||
|
|||||||
+25
-21
@@ -23,8 +23,10 @@
|
|||||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/gridstack": "^1.11.3",
|
"@homarr/gridstack": "^1.12.0",
|
||||||
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
@@ -34,23 +36,24 @@
|
|||||||
"@homarr/old-schema": "workspace:^0.1.0",
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/spotlight": "workspace:^0.1.0",
|
"@homarr/spotlight": "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",
|
||||||
"@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": "^7.16.0",
|
"@mantine/colors-generator": "^7.16.2",
|
||||||
"@mantine/core": "^7.16.0",
|
"@mantine/core": "^7.16.2",
|
||||||
"@mantine/dropzone": "^7.16.0",
|
"@mantine/dropzone": "^7.16.2",
|
||||||
"@mantine/hooks": "^7.16.0",
|
"@mantine/hooks": "^7.16.2",
|
||||||
"@mantine/modals": "^7.16.0",
|
"@mantine/modals": "^7.16.2",
|
||||||
"@mantine/tiptap": "^7.16.0",
|
"@mantine/tiptap": "^7.16.2",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"@tabler/icons-react": "^3.28.1",
|
"@tabler/icons-react": "^3.29.0",
|
||||||
"@tanstack/react-query": "^5.64.1",
|
"@tanstack/react-query": "^5.65.1",
|
||||||
"@tanstack/react-query-devtools": "^5.64.1",
|
"@tanstack/react-query-devtools": "^5.65.1",
|
||||||
"@tanstack/react-query-next-experimental": "5.64.1",
|
"@tanstack/react-query-next-experimental": "^5.65.1",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/next": "next",
|
"@trpc/next": "next",
|
||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
@@ -62,11 +65,11 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"flag-icons": "^7.3.1",
|
"flag-icons": "^7.3.2",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"jotai": "^2.11.0",
|
"jotai": "^2.11.1",
|
||||||
"mantine-react-table": "2.0.0-beta.8",
|
"mantine-react-table": "2.0.0-beta.8",
|
||||||
"next": "15.1.4",
|
"next": "15.1.6",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
@@ -75,21 +78,22 @@
|
|||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.83.4",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"swagger-ui-react": "^5.18.2",
|
"swagger-ui-react": "^5.18.3",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"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.0",
|
"@types/chroma-js": "3.1.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.12.0",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.0.7",
|
"@types/react": "19.0.8",
|
||||||
"@types/react-dom": "19.0.3",
|
"@types/react-dom": "19.0.3",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.19.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.19.0",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
|||||||
false: unstable_httpBatchStreamLink({
|
false: unstable_httpBatchStreamLink({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
url: getTrpcUrl(),
|
url: getTrpcUrl(),
|
||||||
|
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
|
||||||
headers: createHeadersCallbackForSource("nextjs-react (json)"),
|
headers: createHeadersCallbackForSource("nextjs-react (json)"),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput } from "@homarr/ui";
|
import { CustomPasswordInput } from "@homarr/ui";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface RegistrationFormProps {
|
interface RegistrationFormProps {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
|
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { signIn } from "@homarr/auth/client";
|
import { signIn } from "@homarr/auth/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
@@ -12,7 +13,7 @@ import type { useForm } from "@homarr/form";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
providers: string[];
|
providers: string[];
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
|
|||||||
return {
|
return {
|
||||||
title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })),
|
title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })),
|
||||||
icons: {
|
icons: {
|
||||||
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
|
icon: board.faviconImageUrl ?? undefined,
|
||||||
apple: board.faviconImageUrl ? board.faviconImageUrl : undefined,
|
apple: board.faviconImageUrl ?? undefined,
|
||||||
},
|
},
|
||||||
appleWebApp: {
|
appleWebApp: {
|
||||||
startupImage: { url: board.faviconImageUrl ? board.faviconImageUrl : "/logo/logo.png" },
|
startupImage: { url: board.faviconImageUrl ?? "/logo/logo.png" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type { MouseEvent } from "react";
|
import type { MouseEvent } from "react";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Group, Menu } from "@mantine/core";
|
import { Group, Menu } from "@mantine/core";
|
||||||
import { useHotkeys } from "@mantine/hooks";
|
import { useHotkeys } from "@mantine/hooks";
|
||||||
@@ -9,9 +10,11 @@ import {
|
|||||||
IconBox,
|
IconBox,
|
||||||
IconBoxAlignTop,
|
IconBoxAlignTop,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
IconLayoutBoard,
|
||||||
IconPencil,
|
IconPencil,
|
||||||
IconPencilOff,
|
IconPencilOff,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
|
IconReplace,
|
||||||
IconResize,
|
IconResize,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
@@ -49,6 +52,8 @@ export const BoardContentHeaderActions = () => {
|
|||||||
<HeaderButton href={`/boards/${board.name}/settings`}>
|
<HeaderButton href={`/boards/${board.name}/settings`}>
|
||||||
<IconSettings stroke={1.5} />
|
<IconSettings stroke={1.5} />
|
||||||
</HeaderButton>
|
</HeaderButton>
|
||||||
|
|
||||||
|
<SelectBoardsMenu />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -151,6 +156,32 @@ const EditModeMenu = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SelectBoardsMenu = () => {
|
||||||
|
const { data: boards = [] } = clientApi.board.getAllBoards.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu position="bottom-end" withArrow>
|
||||||
|
<Menu.Target>
|
||||||
|
<HeaderButton w="auto" px={4}>
|
||||||
|
<IconReplace stroke={1.5} />
|
||||||
|
</HeaderButton>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown style={{ transform: "translate(-7px, 0)" }}>
|
||||||
|
{boards.map((board) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={board.id}
|
||||||
|
component={Link}
|
||||||
|
href={`/boards/${board.name}`}
|
||||||
|
leftSection={<IconLayoutBoard size={20} />}
|
||||||
|
>
|
||||||
|
{board.name}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const usePreventLeaveWithDirty = (isDirty: boolean) => {
|
const usePreventLeaveWithDirty = (isDirty: boolean) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import { Button, Card, Stack, TextInput } from "@mantine/core";
|
import { Button, Card, Stack, TextInput } from "@mantine/core";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
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 { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
export const InitGroup = () => {
|
export const InitGroup = () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { startTransition } from "react";
|
import { startTransition } from "react";
|
||||||
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
|
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
@@ -11,7 +12,6 @@ import type { CheckboxProps } from "@homarr/form/types";
|
|||||||
import { defaultServerSettings } from "@homarr/server-settings";
|
import { defaultServerSettings } from "@homarr/server-settings";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
export const InitSettings = () => {
|
export const InitSettings = () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
@@ -8,7 +9,6 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput } from "@homarr/ui";
|
import { CustomPasswordInput } from "@homarr/ui";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
export const InitUserForm = () => {
|
export const InitUserForm = () => {
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import "~/styles/scroll-area.scss";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
|
||||||
|
import { api } from "@homarr/api/server";
|
||||||
import { env } from "@homarr/auth/env";
|
import { env } from "@homarr/auth/env";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
import { getServerSettingsAsync } from "@homarr/db/queries";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
|
import { SettingsProvider } from "@homarr/settings";
|
||||||
import { SpotlightProvider } from "@homarr/spotlight";
|
import { SpotlightProvider } from "@homarr/spotlight";
|
||||||
import type { SupportedLanguage } from "@homarr/translation";
|
import type { SupportedLanguage } from "@homarr/translation";
|
||||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||||
@@ -73,6 +77,8 @@ export default async function Layout(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
const user = session ? await api.user.getById({ userId: session.user.id }).catch(() => null) : null;
|
||||||
|
const serverSettings = await getServerSettingsAsync(db);
|
||||||
const colorScheme = await getCurrentColorSchemeAsync();
|
const colorScheme = await getCurrentColorSchemeAsync();
|
||||||
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
|
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
|
||||||
const i18nMessages = await getI18nMessages();
|
const i18nMessages = await getI18nMessages();
|
||||||
@@ -81,6 +87,19 @@ export default async function Layout(props: {
|
|||||||
(innerProps) => {
|
(innerProps) => {
|
||||||
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
|
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
|
||||||
},
|
},
|
||||||
|
(innerProps) => (
|
||||||
|
<SettingsProvider
|
||||||
|
user={user}
|
||||||
|
serverSettings={{
|
||||||
|
board: {
|
||||||
|
homeBoardId: serverSettings.board.homeBoardId,
|
||||||
|
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
|
||||||
|
},
|
||||||
|
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
|
||||||
|
}}
|
||||||
|
{...innerProps}
|
||||||
|
/>
|
||||||
|
),
|
||||||
(innerProps) => <JotaiProvider {...innerProps} />,
|
(innerProps) => <JotaiProvider {...innerProps} />,
|
||||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||||
(innerProps) => <DayJsLoader {...innerProps} />,
|
(innerProps) => <DayJsLoader {...innerProps} />,
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { IconPicker } from "~/components/icons/picker/icon-picker";
|
import { IconPicker } from "~/components/icons/picker/icon-picker";
|
||||||
@@ -25,11 +25,11 @@ export const AppForm = (props: AppFormProps) => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const form = useZodForm(validation.app.manage, {
|
const form = useZodForm(validation.app.manage, {
|
||||||
initialValues: initialValues ?? {
|
initialValues: {
|
||||||
name: "",
|
name: initialValues?.name ?? "",
|
||||||
description: "",
|
description: initialValues?.description ?? "",
|
||||||
iconUrl: "",
|
iconUrl: initialValues?.iconUrl ?? "",
|
||||||
href: "",
|
href: initialValues?.href ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -9,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { AppForm } from "../../_form";
|
import { AppForm } from "../../_form";
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
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 { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { AppForm } from "../_form";
|
import { AppForm } from "../_form";
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconBox, IconPencil } from "@tabler/icons-react";
|
import { IconBox, IconPencil } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -9,7 +10,6 @@ import { auth } from "@homarr/auth/next";
|
|||||||
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||||
import { SearchInput, TablePagination } from "@homarr/ui";
|
import { SearchInput, TablePagination } from "@homarr/ui";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { ManageContainer } from "~/components/manage/manage-container";
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||||
@@ -80,7 +80,7 @@ const AppCard = async ({ app }: AppCardProps) => {
|
|||||||
return (
|
return (
|
||||||
<Card withBorder>
|
<Card withBorder>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group align="top" justify="start" wrap="nowrap">
|
<Group align="top" justify="start" wrap="nowrap" style={{ flex: "1" }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
src={app.iconUrl}
|
src={app.iconUrl}
|
||||||
@@ -91,7 +91,7 @@ const AppCard = async ({ app }: AppCardProps) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack gap={0}>
|
<Stack gap={0} style={{ flex: "1" }}>
|
||||||
<Text fw={500} lineClamp={1}>
|
<Text fw={500} lineClamp={1}>
|
||||||
{app.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -101,7 +101,7 @@ const AppCard = async ({ app }: AppCardProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{app.href && (
|
{app.href && (
|
||||||
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
|
<Anchor href={app.href} lineClamp={1} size="sm" style={{ wordBreak: "break-all" }}>
|
||||||
{app.href}
|
{app.href}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+4
-4
@@ -3,6 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -13,7 +14,6 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie
|
|||||||
import { useConfirmModal } from "@homarr/modals";
|
import { useConfirmModal } 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 type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
||||||
@@ -70,9 +70,9 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
if (testConnectionError) {
|
if (testConnectionError) {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
|
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
|
||||||
message: testConnectionError.message
|
message:
|
||||||
? testConnectionError.message
|
testConnectionError.message ??
|
||||||
: t(`integration.testConnection.notification.${testConnectionError.key}.message`),
|
t(`integration.testConnection.notification.${testConnectionError.key}.message`),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const IntegrationCreateDropdownContent = () => {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const filteredKinds = useMemo(() => {
|
const filteredKinds = useMemo(() => {
|
||||||
return integrationKinds.filter((kind) => kind.includes(search.toLowerCase()));
|
return integrationKinds.filter((kind) =>
|
||||||
|
getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
|
||||||
|
);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
const handleSearch = React.useCallback(
|
const handleSearch = React.useCallback(
|
||||||
@@ -29,6 +31,7 @@ export const IntegrationCreateDropdownContent = () => {
|
|||||||
leftSection={<IconSearch stroke={1.5} size={20} />}
|
leftSection={<IconSearch stroke={1.5} size={20} />}
|
||||||
placeholder={t("integration.page.list.search")}
|
placeholder={t("integration.page.list.search")}
|
||||||
value={search}
|
value={search}
|
||||||
|
data-autofocus
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
@@ -15,7 +16,6 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
||||||
@@ -63,9 +63,9 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
if (testConnectionError) {
|
if (testConnectionError) {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
|
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
|
||||||
message: testConnectionError.message
|
message:
|
||||||
? testConnectionError.message
|
testConnectionError.message ??
|
||||||
: t(`integration.testConnection.notification.${testConnectionError.key}.message`),
|
t(`integration.testConnection.notification.${testConnectionError.key}.message`),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Container, Group, Stack, Title } from "@mantine/core";
|
import { Container, Group, Stack, Title } from "@mantine/core";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import type { IntegrationKind } from "@homarr/definitions";
|
import type { IntegrationKind } from "@homarr/definitions";
|
||||||
@@ -7,7 +8,6 @@ import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
|||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
import { IntegrationAvatar } from "@homarr/ui";
|
import { IntegrationAvatar } from "@homarr/ui";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { validation } from "@homarr/validation";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { NewIntegrationForm } from "./_integration-new-form";
|
import { NewIntegrationForm } from "./_integration-new-form";
|
||||||
|
|||||||
@@ -102,7 +102,15 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
|
|||||||
|
|
||||||
const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
|
const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<Menu width={256} trapFocus position="bottom-end" withinPortal shadow="md" keepMounted={false}>
|
<Menu
|
||||||
|
width={256}
|
||||||
|
trapFocus
|
||||||
|
position="bottom-end"
|
||||||
|
withinPortal
|
||||||
|
shadow="md"
|
||||||
|
keepMounted={false}
|
||||||
|
withInitialFocusPlaceholder={false}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<MenuDropdown>
|
<MenuDropdown>
|
||||||
<IntegrationCreateDropdownContent />
|
<IntegrationCreateDropdownContent />
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
|||||||
{
|
{
|
||||||
label: t("items.help.items.submitIssue"),
|
label: t("items.help.items.submitIssue"),
|
||||||
icon: IconBrandGithub,
|
icon: IconBrandGithub,
|
||||||
href: "https://github.com/ajnart/homarr/issues/new/choose",
|
href: "https://github.com/homarr-labs/homarr/issues/new/choose",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
|||||||
{
|
{
|
||||||
label: t("items.help.items.sourceCode"),
|
label: t("items.help.items.sourceCode"),
|
||||||
icon: IconGitFork,
|
icon: IconGitFork,
|
||||||
href: "https://github.com/ajnart/homarr",
|
href: "https://github.com/homarr-labs/homarr",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export const CopyMedia = ({ media }: CopyMediaProps) => {
|
|||||||
|
|
||||||
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
|
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
|
||||||
|
|
||||||
|
// Clipboard only works on localhost or secure connections (https)
|
||||||
|
if (url.startsWith("http://") && !url.startsWith("http://localhost")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CopyButton value={url}>
|
<CopyButton value={url}>
|
||||||
{({ copy, copied }) => (
|
{({ copy, copied }) => (
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Anchor,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconExternalLink } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { humanFileSize } from "@homarr/common";
|
import { humanFileSize } from "@homarr/common";
|
||||||
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||||
|
import { createLocalImageUrl } from "@homarr/icons/local";
|
||||||
import { getI18n } from "@homarr/translation/server";
|
import { getI18n } from "@homarr/translation/server";
|
||||||
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
|
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { ManageContainer } from "~/components/manage/manage-container";
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
@@ -91,13 +106,14 @@ interface RowProps {
|
|||||||
|
|
||||||
const Row = async ({ media }: RowProps) => {
|
const Row = async ({ media }: RowProps) => {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
const t = await getI18n();
|
||||||
const canDelete = media.creatorId === session?.user.id || session?.user.permissions.includes("media-full-all");
|
const canDelete = media.creatorId === session?.user.id || session?.user.permissions.includes("media-full-all");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd w={64}>
|
<TableTd w={64}>
|
||||||
<Image
|
<Image
|
||||||
src={`/api/user-medias/${media.id}`}
|
src={createLocalImageUrl(media.id)}
|
||||||
alt={media.name}
|
alt={media.name}
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
@@ -121,6 +137,17 @@ const Row = async ({ media }: RowProps) => {
|
|||||||
<TableTd w={64}>
|
<TableTd w={64}>
|
||||||
<Group wrap="nowrap" gap="xs">
|
<Group wrap="nowrap" gap="xs">
|
||||||
<CopyMedia media={media} />
|
<CopyMedia media={media} />
|
||||||
|
<Tooltip label={t("media.action.open.label")} openDelay={500}>
|
||||||
|
<ActionIcon
|
||||||
|
component="a"
|
||||||
|
href={createLocalImageUrl(media.id)}
|
||||||
|
target="_blank"
|
||||||
|
color="gray"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<IconExternalLink size={16} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
{canDelete && <DeleteMedia media={media} />}
|
{canDelete && <DeleteMedia media={media} />}
|
||||||
</Group>
|
</Group>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import Link from "next/link";
|
|||||||
import type { SegmentedControlItem } from "@mantine/core";
|
import type { SegmentedControlItem } from "@mantine/core";
|
||||||
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
|
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
|
||||||
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
|
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { searchEngineTypes } from "@homarr/definitions";
|
import { searchEngineTypes } from "@homarr/definitions";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { IconPicker } from "~/components/icons/picker/icon-picker";
|
import { IconPicker } from "~/components/icons/picker/icon-picker";
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -9,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { SearchEngineForm } from "../../_form";
|
import { SearchEngineForm } from "../../_form";
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
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 { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { SearchEngineForm } from "../_form";
|
import { SearchEngineForm } from "../_form";
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconPencil, IconSearch } from "@tabler/icons-react";
|
import { IconPencil, IconSearch } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -9,7 +10,6 @@ import { auth } from "@homarr/auth/next";
|
|||||||
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||||
import { SearchInput, TablePagination } from "@homarr/ui";
|
import { SearchInput, TablePagination } from "@homarr/ui";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { ManageContainer } from "~/components/manage/manage-container";
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Button, FileInput, Group, Stack } from "@mantine/core";
|
import { Button, FileInput, Group, Stack } from "@mantine/core";
|
||||||
import { IconCertificate } from "@tabler/icons-react";
|
import { IconCertificate } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
@@ -9,7 +10,7 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { createModal, useModalAction } from "@homarr/modals";
|
import { createModal, useModalAction } 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 { superRefineCertificateFile, z } from "@homarr/validation";
|
import { superRefineCertificateFile } from "@homarr/validation";
|
||||||
|
|
||||||
export const AddCertificateButton = () => {
|
export const AddCertificateButton = () => {
|
||||||
const { openModal } = useModalAction(AddCertificateModal);
|
const { openModal } = useModalAction(AddCertificateModal);
|
||||||
|
|||||||
@@ -59,10 +59,17 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
|
|||||||
{x509Certificates.map((cert) => (
|
{x509Certificates.map((cert) => (
|
||||||
<Card key={cert.x509.fingerprint} withBorder>
|
<Card key={cert.x509.fingerprint} withBorder>
|
||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
<IconCertificate color={getMantineColor(iconColor(cert.x509.validToDate), 6)} size={32} stroke={1.5} />
|
<IconCertificate
|
||||||
<Stack flex={1} gap="xs">
|
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
|
||||||
<Group justify="space-between">
|
style={{ minWidth: 32 }}
|
||||||
<Text fw={500}>{cert.x509.subject}</Text>
|
size={32}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
<Stack flex={1} gap="xs" maw="calc(100% - 48px)">
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
|
||||||
|
{cert.x509.subject}
|
||||||
|
</Text>
|
||||||
<Text c="gray.6" ta="end" size="sm">
|
<Text c="gray.6" ta="end" size="sm">
|
||||||
{cert.fileName}
|
{cert.fileName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { MantineReactTable } from "mantine-react-table";
|
|||||||
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 { useTimeAgo } from "@homarr/common";
|
import { useTimeAgo } from "@homarr/common";
|
||||||
import type { DockerContainerState } from "@homarr/definitions";
|
import type { ContainerState } from "@homarr/docker";
|
||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
|
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
@@ -246,9 +246,9 @@ const containerStates = {
|
|||||||
exited: "red",
|
exited: "red",
|
||||||
removing: "pink",
|
removing: "pink",
|
||||||
dead: "dark",
|
dead: "dark",
|
||||||
} satisfies Record<DockerContainerState, MantineColor>;
|
} satisfies Record<ContainerState, MantineColor>;
|
||||||
|
|
||||||
const ContainerStateBadge = ({ state }: { state: DockerContainerState }) => {
|
const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
|
||||||
const t = useScopedI18n("docker.field.state.option");
|
const t = useScopedI18n("docker.field.state.option");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Select, Stack } from "@mantine/core";
|
import { Button, Group, Select, Stack } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -8,7 +9,6 @@ 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";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface ChangeHomeBoardFormProps {
|
interface ChangeHomeBoardFormProps {
|
||||||
@@ -39,8 +39,8 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
|
|||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.changeHomeBoards, {
|
const form = useZodForm(validation.user.changeHomeBoards, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
homeBoardId: user.homeBoardId ?? "",
|
homeBoardId: user.homeBoardId,
|
||||||
mobileHomeBoardId: user.mobileHomeBoardId ?? "",
|
mobileHomeBoardId: user.mobileHomeBoardId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+22
-11
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Select, Stack } from "@mantine/core";
|
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -8,37 +9,38 @@ 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";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface ChangeDefaultSearchEngineFormProps {
|
interface ChangeSearchPreferencesFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
searchEnginesData: { value: string; label: string }[];
|
searchEnginesData: { value: string; label: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: ChangeDefaultSearchEngineFormProps) => {
|
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.user.changeDefaultSearchEngine.useMutation({
|
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
|
||||||
async onSettled() {
|
async onSettled() {
|
||||||
await revalidatePathActionAsync(`/manage/users/${user.id}`);
|
await revalidatePathActionAsync(`/manage/users/${user.id}`);
|
||||||
},
|
},
|
||||||
onSuccess(_, variables) {
|
onSuccess(_, variables) {
|
||||||
form.setInitialValues({
|
form.setInitialValues({
|
||||||
defaultSearchEngineId: variables.defaultSearchEngineId,
|
defaultSearchEngineId: variables.defaultSearchEngineId,
|
||||||
|
openInNewTab: variables.openInNewTab,
|
||||||
});
|
});
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: t("user.action.changeDefaultSearchEngine.notification.success.message"),
|
message: t("user.action.changeSearchPreferences.notification.success.message"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
message: t("user.action.changeDefaultSearchEngine.notification.error.message"),
|
message: t("user.action.changeSearchPreferences.notification.error.message"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.changeDefaultSearchEngine, {
|
const form = useZodForm(validation.user.changeSearchPreferences, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
defaultSearchEngineId: user.defaultSearchEngineId ?? "",
|
defaultSearchEngineId: user.defaultSearchEngineId,
|
||||||
|
openInNewTab: user.openSearchInNewTab,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +54,16 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Select w="100%" data={searchEnginesData} {...form.getInputProps("defaultSearchEngineId")} />
|
<Select
|
||||||
|
label={t("user.field.defaultSearchEngine.label")}
|
||||||
|
w="100%"
|
||||||
|
data={searchEnginesData}
|
||||||
|
{...form.getInputProps("defaultSearchEngineId")}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label={t("user.field.openSearchInNewTab.label")}
|
||||||
|
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
|
||||||
|
/>
|
||||||
|
|
||||||
<Group justify="end">
|
<Group justify="end">
|
||||||
<Button type="submit" color="teal" loading={isPending}>
|
<Button type="submit" color="teal" loading={isPending}>
|
||||||
@@ -64,4 +75,4 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.changeDefaultSearchEngine>;
|
type FormType = z.infer<typeof validation.user.changeSearchPreferences>;
|
||||||
+1
-1
@@ -4,6 +4,7 @@ import { Button, Group, Radio, Stack } from "@mantine/core";
|
|||||||
import type { DayOfWeek } from "@mantine/dates";
|
import type { DayOfWeek } from "@mantine/dates";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import localeData from "dayjs/plugin/localeData";
|
import localeData from "dayjs/plugin/localeData";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -11,7 +12,6 @@ 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";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
dayjs.extend(localeData);
|
dayjs.extend(localeData);
|
||||||
|
|||||||
+5
-2
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Stack, Switch } from "@mantine/core";
|
import { Button, Group, Stack, Switch } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -8,7 +9,6 @@ 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";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface PingIconsEnabledProps {
|
interface PingIconsEnabledProps {
|
||||||
@@ -51,7 +51,10 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Switch {...form.getInputProps("pingIconsEnabled")} label={t("user.field.pingIconsEnabled.label")} />
|
<Switch
|
||||||
|
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
|
||||||
|
label={t("user.field.pingIconsEnabled.label")}
|
||||||
|
/>
|
||||||
|
|
||||||
<Group justify="end">
|
<Group justify="end">
|
||||||
<Button type="submit" color="teal" loading={isPending}>
|
<Button type="submit" color="teal" loading={isPending}>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"
|
|||||||
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
|
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { canAccessUserEditPage } from "../access";
|
import { canAccessUserEditPage } from "../access";
|
||||||
import { ChangeDefaultSearchEngineForm } from "./_components/_change-default-search-engine";
|
|
||||||
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
|
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
|
||||||
|
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
|
||||||
import { DeleteUserButton } from "./_components/_delete-user-button";
|
import { DeleteUserButton } from "./_components/_delete-user-button";
|
||||||
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
|
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
|
||||||
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
|
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
|
||||||
@@ -83,10 +83,12 @@ export default async function EditUserPage(props: Props) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack mb="lg">
|
{session?.user.id === user.id && (
|
||||||
<Title order={2}>{tGeneral("item.language")}</Title>
|
<Stack mb="lg">
|
||||||
<CurrentLanguageCombobox />
|
<Title order={2}>{tGeneral("item.language")}</Title>
|
||||||
</Stack>
|
<CurrentLanguageCombobox />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack mb="lg">
|
<Stack mb="lg">
|
||||||
<Title order={2}>{tGeneral("item.board.title")}</Title>
|
<Title order={2}>{tGeneral("item.board.title")}</Title>
|
||||||
@@ -100,8 +102,8 @@ export default async function EditUserPage(props: Props) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack mb="lg">
|
<Stack mb="lg">
|
||||||
<Title order={2}>{tGeneral("item.defaultSearchEngine")}</Title>
|
<Title order={2}>{tGeneral("item.search")}</Title>
|
||||||
<ChangeDefaultSearchEngineForm user={user} searchEnginesData={searchEngines} />
|
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack mb="lg">
|
<Stack mb="lg">
|
||||||
|
|||||||
+2
-1
@@ -17,6 +17,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useListState } from "@mantine/hooks";
|
import { useListState } from "@mantine/hooks";
|
||||||
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
||||||
@@ -26,7 +27,7 @@ import { useModalAction } from "@homarr/modals";
|
|||||||
import { showErrorNotification } from "@homarr/notifications";
|
import { showErrorNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
|
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
import { createCustomErrorParams } from "@homarr/validation/form";
|
import { createCustomErrorParams } from "@homarr/validation/form";
|
||||||
|
|
||||||
import { GroupSelectModal } from "~/components/access/group-select-modal";
|
import { GroupSelectModal } from "~/components/access/group-select-modal";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
|
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -8,7 +9,6 @@ import { auth } from "@homarr/auth/next";
|
|||||||
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||||
import { getI18n } from "@homarr/translation/server";
|
import { getI18n } from "@homarr/translation/server";
|
||||||
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
|
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { ManageContainer } from "~/components/manage/manage-container";
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ const handler = auth(async (req) => {
|
|||||||
req,
|
req,
|
||||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||||
onError({ error, path, type }) {
|
onError({ error, path, type }) {
|
||||||
logger.error(`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}`);
|
logger.error(
|
||||||
|
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import { createId } from "@homarr/db/client";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
|
||||||
|
import type { Board, DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||||
|
import { getFirstEmptyPosition } from "./empty-position";
|
||||||
|
|
||||||
|
export interface CreateItemInput {
|
||||||
|
kind: WidgetKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createItemCallback =
|
||||||
|
({ kind }: CreateItemInput) =>
|
||||||
|
(previous: Board): Board => {
|
||||||
|
const firstSection = previous.sections
|
||||||
|
.filter((section): section is EmptySection => section.kind === "empty")
|
||||||
|
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
|
||||||
|
.at(0);
|
||||||
|
|
||||||
|
if (!firstSection) return previous;
|
||||||
|
|
||||||
|
const dynamicSectionsOfFirstSection = previous.sections.filter(
|
||||||
|
(section): section is DynamicSection => section.kind === "dynamic" && section.parentSectionId === firstSection.id,
|
||||||
|
);
|
||||||
|
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
|
||||||
|
const emptyPosition = getFirstEmptyPosition(elements, previous.columnCount);
|
||||||
|
|
||||||
|
if (!emptyPosition) {
|
||||||
|
console.error("Your board is full");
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
id: createId(),
|
||||||
|
kind,
|
||||||
|
options: {},
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
...emptyPosition,
|
||||||
|
integrationIds: [],
|
||||||
|
advancedOptions: {
|
||||||
|
customCssClasses: [],
|
||||||
|
},
|
||||||
|
} satisfies Modify<
|
||||||
|
Item,
|
||||||
|
{
|
||||||
|
kind: WidgetKind;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
sections: previous.sections.map((section) => {
|
||||||
|
// Return same section if item is not in it
|
||||||
|
if (section.id !== firstSection.id) return section;
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
items: section.items.concat(widget),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createId } from "@homarr/db/client";
|
||||||
|
|
||||||
|
import type { Board, DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
|
||||||
|
import { getFirstEmptyPosition } from "./empty-position";
|
||||||
|
|
||||||
|
export interface DuplicateItemInput {
|
||||||
|
itemId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const duplicateItemCallback =
|
||||||
|
({ itemId }: DuplicateItemInput) =>
|
||||||
|
(previous: Board): Board => {
|
||||||
|
const itemToDuplicate = previous.sections
|
||||||
|
.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })))
|
||||||
|
.find((item) => item.id === itemId);
|
||||||
|
if (!itemToDuplicate) return previous;
|
||||||
|
|
||||||
|
const currentSection = previous.sections.find((section) => section.id === itemToDuplicate.sectionId);
|
||||||
|
if (!currentSection) return previous;
|
||||||
|
|
||||||
|
const dynamicSectionsOfCurrentSection = previous.sections.filter(
|
||||||
|
(section): section is DynamicSection =>
|
||||||
|
section.kind === "dynamic" && section.parentSectionId === currentSection.id,
|
||||||
|
);
|
||||||
|
const elements = [...currentSection.items, ...dynamicSectionsOfCurrentSection];
|
||||||
|
let sectionId = currentSection.id;
|
||||||
|
let emptyPosition = getFirstEmptyPosition(
|
||||||
|
elements,
|
||||||
|
currentSection.kind === "dynamic" ? currentSection.width : previous.columnCount,
|
||||||
|
currentSection.kind === "dynamic" ? currentSection.height : undefined,
|
||||||
|
{
|
||||||
|
width: itemToDuplicate.width,
|
||||||
|
height: itemToDuplicate.height,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!emptyPosition) {
|
||||||
|
const firstSection = previous.sections
|
||||||
|
.filter((section): section is EmptySection => section.kind === "empty")
|
||||||
|
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
|
||||||
|
.at(0);
|
||||||
|
|
||||||
|
if (!firstSection) return previous;
|
||||||
|
|
||||||
|
const dynamicSectionsOfFirstSection = previous.sections.filter(
|
||||||
|
(section): section is DynamicSection =>
|
||||||
|
section.kind === "dynamic" && section.parentSectionId === firstSection.id,
|
||||||
|
);
|
||||||
|
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
|
||||||
|
emptyPosition = getFirstEmptyPosition(elements, previous.columnCount, undefined, {
|
||||||
|
width: itemToDuplicate.width,
|
||||||
|
height: itemToDuplicate.height,
|
||||||
|
});
|
||||||
|
if (!emptyPosition) {
|
||||||
|
console.error("Your board is full");
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionId = firstSection.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = structuredClone(itemToDuplicate);
|
||||||
|
widget.id = createId();
|
||||||
|
widget.xOffset = emptyPosition.xOffset;
|
||||||
|
widget.yOffset = emptyPosition.yOffset;
|
||||||
|
widget.sectionId = sectionId;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
...previous,
|
||||||
|
sections: previous.sections.map((section) => {
|
||||||
|
// Return same section if item is not in it
|
||||||
|
if (section.id !== sectionId) return section;
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
items: section.items.concat(widget),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
|
|
||||||
|
export const getFirstEmptyPosition = (
|
||||||
|
elements: Pick<Item, "yOffset" | "xOffset" | "width" | "height">[],
|
||||||
|
columnCount: number,
|
||||||
|
rowCount = 9999,
|
||||||
|
size: { width: number; height: number } = { width: 1, height: 1 },
|
||||||
|
) => {
|
||||||
|
for (let yOffset = 0; yOffset < rowCount + 1 - size.height; yOffset++) {
|
||||||
|
for (let xOffset = 0; xOffset < columnCount; xOffset++) {
|
||||||
|
const isOccupied = elements.some(
|
||||||
|
(element) =>
|
||||||
|
element.yOffset < yOffset + size.height &&
|
||||||
|
element.yOffset + element.height > yOffset &&
|
||||||
|
element.xOffset < xOffset + size.width &&
|
||||||
|
element.xOffset + element.width > xOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOccupied) {
|
||||||
|
return { xOffset, yOffset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Board } from "~/app/[locale]/boards/_types";
|
||||||
|
import { createItemCallback } from "../create-item";
|
||||||
|
import * as emptyPosition from "../empty-position";
|
||||||
|
import { createDynamicSection, createEmptySection, createItem } from "./shared";
|
||||||
|
|
||||||
|
describe("item actions create-item", () => {
|
||||||
|
test("should add it to first section", () => {
|
||||||
|
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||||
|
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||||
|
const input = {
|
||||||
|
sections: [createEmptySection("1", 2), createEmptySection("2", 0), createEmptySection("3", 1)],
|
||||||
|
columnCount: 4,
|
||||||
|
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||||
|
|
||||||
|
const result = createItemCallback({
|
||||||
|
kind: "clock",
|
||||||
|
})(input as unknown as Board);
|
||||||
|
|
||||||
|
const firstSection = result.sections.find((section) => section.id === "2");
|
||||||
|
const item = firstSection?.items.at(0);
|
||||||
|
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
|
||||||
|
expect(spy).toHaveBeenCalledWith([], input.columnCount);
|
||||||
|
});
|
||||||
|
test("should correctly pass dynamic section and items to getFirstEmptyPosition", () => {
|
||||||
|
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||||
|
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||||
|
const firstSection = createEmptySection("2", 0);
|
||||||
|
const expectedItem = createItem({ id: "12", xOffset: 1, yOffset: 2, width: 3, height: 2 });
|
||||||
|
firstSection.items.push(expectedItem);
|
||||||
|
const dynamicSectionInFirst = createDynamicSection({
|
||||||
|
id: "4",
|
||||||
|
parentSectionId: "2",
|
||||||
|
yOffset: 0,
|
||||||
|
xOffset: 0,
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
sections: [
|
||||||
|
createEmptySection("1", 2),
|
||||||
|
firstSection,
|
||||||
|
createEmptySection("3", 1),
|
||||||
|
dynamicSectionInFirst,
|
||||||
|
createDynamicSection({ id: "5", parentSectionId: "3", yOffset: 1 }),
|
||||||
|
],
|
||||||
|
columnCount: 4,
|
||||||
|
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||||
|
|
||||||
|
const result = createItemCallback({
|
||||||
|
kind: "clock",
|
||||||
|
})(input as unknown as Board);
|
||||||
|
|
||||||
|
const firstSectionResult = result.sections.find((section) => section.id === "2");
|
||||||
|
const item = firstSectionResult?.items.find((item) => item.id !== "12");
|
||||||
|
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
|
||||||
|
expect(spy).toHaveBeenCalledWith([expectedItem, dynamicSectionInFirst], input.columnCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Board } from "~/app/[locale]/boards/_types";
|
||||||
|
import { duplicateItemCallback } from "../duplicate-item";
|
||||||
|
import * as emptyPosition from "../empty-position";
|
||||||
|
import { createEmptySection, createItem } from "./shared";
|
||||||
|
|
||||||
|
describe("item actions duplicate-item", () => {
|
||||||
|
test("should copy it in the same section", () => {
|
||||||
|
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||||
|
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||||
|
const currentSection = createEmptySection("2", 1);
|
||||||
|
const currentItem = createItem({
|
||||||
|
id: "1",
|
||||||
|
xOffset: 1,
|
||||||
|
yOffset: 3,
|
||||||
|
width: 3,
|
||||||
|
height: 2,
|
||||||
|
kind: "minecraftServerStatus",
|
||||||
|
integrationIds: ["1"],
|
||||||
|
options: { address: "localhost" },
|
||||||
|
advancedOptions: { customCssClasses: ["test"] },
|
||||||
|
});
|
||||||
|
const otherItem = createItem({
|
||||||
|
id: "2",
|
||||||
|
});
|
||||||
|
currentSection.items.push(currentItem, otherItem);
|
||||||
|
const input = {
|
||||||
|
columnCount: 10,
|
||||||
|
sections: [createEmptySection("1", 0), currentSection, createEmptySection("3", 2)],
|
||||||
|
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||||
|
|
||||||
|
const result = duplicateItemCallback({ itemId: currentItem.id })(input as unknown as Board);
|
||||||
|
|
||||||
|
const section = result.sections.find((section) => section.id === "2");
|
||||||
|
expect(section?.items.length).toBe(3);
|
||||||
|
const duplicatedItem = section?.items.find((item) => item.id !== currentItem.id && item.id !== otherItem.id);
|
||||||
|
|
||||||
|
expect(duplicatedItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: "minecraftServerStatus",
|
||||||
|
xOffset: 5,
|
||||||
|
yOffset: 5,
|
||||||
|
width: 3,
|
||||||
|
height: 2,
|
||||||
|
integrationIds: ["1"],
|
||||||
|
options: { address: "localhost" },
|
||||||
|
advancedOptions: { customCssClasses: ["test"] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
|
import { getFirstEmptyPosition } from "../empty-position";
|
||||||
|
|
||||||
|
describe("get first empty position", () => {
|
||||||
|
test.each([
|
||||||
|
[[[" ", " ", " ", " "]], [1, 1], 0, 0],
|
||||||
|
[[["a", " ", " ", " "]], [1, 1], 1, 0],
|
||||||
|
[[[" ", "a", " ", " "]], [1, 1], 0, 0],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", " "],
|
||||||
|
["a", "a", " ", " "],
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[[["a", "a", "a", "a"]], [1, 1], 0, 1],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", "a", "a"],
|
||||||
|
["a", "a", "a", "a"],
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", "b", "b"],
|
||||||
|
["a", "a", " ", "b", "b"],
|
||||||
|
],
|
||||||
|
[1, 2],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", " ", "b", "b"],
|
||||||
|
["a", "a", " ", " ", "b", "b"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", "d", "b", "b"],
|
||||||
|
["a", "a", "c", "e", "b", "b"],
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", " ", "b", "b"],
|
||||||
|
["a", "a", " ", "e", "b", "b"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
])("should return the first empty position", (layout, size, expectedX, expectedY) => {
|
||||||
|
const elements = createElementsFromLayout(layout);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const result = getFirstEmptyPosition(elements, layout[0]!.length, undefined, { width: size[0]!, height: size[1]! });
|
||||||
|
|
||||||
|
expect(result).toEqual({ xOffset: expectedX, yOffset: expectedY });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[[[" ", " "]], [1, 1], 0, 0, 1],
|
||||||
|
[[["a", " "]], [1, 1], 1, 0, 1],
|
||||||
|
[[["a", "a"]], [1, 1], undefined, undefined, 1],
|
||||||
|
[[["a", "a"]], [1, 1], 0, 1, 2],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "b", " ", " "],
|
||||||
|
["a", "c", " ", "d"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "b", " ", " "],
|
||||||
|
["a", "c", " ", "d"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
],
|
||||||
|
[[["a", "b"]], [2, 1], 0, 1, 2],
|
||||||
|
])("should return the first empty position with limited rows", (layout, size, expectedX, expectedY, rowCount) => {
|
||||||
|
const elements = createElementsFromLayout(layout);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const result = getFirstEmptyPosition(elements, layout[0]!.length, rowCount, { width: size[0]!, height: size[1]! });
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedX !== undefined ? { xOffset: expectedX, yOffset: expectedY } : undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const createElementsFromLayout = (layout: string[][]) => {
|
||||||
|
const elements: (Pick<Item, "xOffset" | "yOffset" | "width" | "height"> & { char: string })[] = [];
|
||||||
|
for (let yOffset = 0; yOffset < layout.length; yOffset++) {
|
||||||
|
const row = layout[yOffset];
|
||||||
|
if (!row) continue;
|
||||||
|
for (let xOffset = 0; xOffset < row.length; xOffset++) {
|
||||||
|
const item = row[xOffset];
|
||||||
|
if (item === " " || !item) continue;
|
||||||
|
|
||||||
|
const existing = elements.find((element) => element.char === item);
|
||||||
|
if (existing) {
|
||||||
|
existing.height = yOffset - existing.yOffset + 1;
|
||||||
|
existing.width = xOffset - existing.xOffset + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push({
|
||||||
|
yOffset,
|
||||||
|
xOffset,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
char: item,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||||
|
|
||||||
|
export const createEmptySection = (id: string, yOffset: number): EmptySection => ({
|
||||||
|
id,
|
||||||
|
kind: "empty",
|
||||||
|
yOffset,
|
||||||
|
xOffset: 0,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createDynamicSection = (section: Omit<Partial<DynamicSection>, "kind">): DynamicSection => ({
|
||||||
|
id: section.id ?? "0",
|
||||||
|
kind: "dynamic",
|
||||||
|
parentSectionId: section.parentSectionId ?? "0",
|
||||||
|
height: section.height ?? 1,
|
||||||
|
width: section.width ?? 1,
|
||||||
|
yOffset: section.yOffset ?? 0,
|
||||||
|
xOffset: section.xOffset ?? 0,
|
||||||
|
items: section.items ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createItem = (item: Partial<Item>): Item => ({
|
||||||
|
id: item.id ?? "0",
|
||||||
|
width: item.width ?? 1,
|
||||||
|
height: item.height ?? 1,
|
||||||
|
yOffset: item.yOffset ?? 0,
|
||||||
|
xOffset: item.xOffset ?? 0,
|
||||||
|
kind: item.kind ?? "clock",
|
||||||
|
integrationIds: item.integrationIds ?? [],
|
||||||
|
options: item.options ?? {},
|
||||||
|
advancedOptions: item.advancedOptions ?? { customCssClasses: [] },
|
||||||
|
});
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import type { Modify } from "@homarr/common/types";
|
|
||||||
import { createId } from "@homarr/db/client";
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||||
|
|
||||||
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
||||||
|
import type { CreateItemInput } from "./actions/create-item";
|
||||||
|
import { createItemCallback } from "./actions/create-item";
|
||||||
|
import type { DuplicateItemInput } from "./actions/duplicate-item";
|
||||||
|
import { duplicateItemCallback } from "./actions/duplicate-item";
|
||||||
|
|
||||||
interface MoveAndResizeItem {
|
interface MoveAndResizeItem {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -42,87 +43,19 @@ interface UpdateItemIntegrations {
|
|||||||
newIntegrations: string[];
|
newIntegrations: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateItem {
|
|
||||||
kind: WidgetKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DuplicateItem {
|
|
||||||
itemId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useItemActions = () => {
|
export const useItemActions = () => {
|
||||||
const { updateBoard } = useUpdateBoard();
|
const { updateBoard } = useUpdateBoard();
|
||||||
|
|
||||||
const createItem = useCallback(
|
const createItem = useCallback(
|
||||||
({ kind }: CreateItem) => {
|
(input: CreateItemInput) => {
|
||||||
updateBoard((previous) => {
|
updateBoard(createItemCallback(input));
|
||||||
const lastSection = previous.sections
|
|
||||||
.filter((section): section is EmptySection => section.kind === "empty")
|
|
||||||
.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset)[0];
|
|
||||||
|
|
||||||
if (!lastSection) return previous;
|
|
||||||
|
|
||||||
const widget = {
|
|
||||||
id: createId(),
|
|
||||||
kind,
|
|
||||||
options: {},
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
integrationIds: [],
|
|
||||||
advancedOptions: {
|
|
||||||
customCssClasses: [],
|
|
||||||
},
|
|
||||||
} satisfies Modify<
|
|
||||||
Omit<Item, "yOffset" | "xOffset">,
|
|
||||||
{
|
|
||||||
kind: WidgetKind;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...previous,
|
|
||||||
sections: previous.sections.map((section) => {
|
|
||||||
// Return same section if item is not in it
|
|
||||||
if (section.id !== lastSection.id) return section;
|
|
||||||
return {
|
|
||||||
...section,
|
|
||||||
items: section.items.concat(widget as unknown as Item),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[updateBoard],
|
[updateBoard],
|
||||||
);
|
);
|
||||||
|
|
||||||
const duplicateItem = useCallback(
|
const duplicateItem = useCallback(
|
||||||
({ itemId }: DuplicateItem) => {
|
({ itemId }: DuplicateItemInput) => {
|
||||||
updateBoard((previous) => {
|
updateBoard(duplicateItemCallback({ itemId }));
|
||||||
const itemToDuplicate = previous.sections
|
|
||||||
.flatMap((section) => section.items)
|
|
||||||
.find((item) => item.id === itemId);
|
|
||||||
|
|
||||||
if (!itemToDuplicate) return previous;
|
|
||||||
|
|
||||||
const newItem = {
|
|
||||||
...itemToDuplicate,
|
|
||||||
id: createId(),
|
|
||||||
yOffset: undefined,
|
|
||||||
xOffset: undefined,
|
|
||||||
} satisfies Modify<Item, { yOffset?: number; xOffset?: number }>;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...previous,
|
|
||||||
sections: previous.sections.map((section) => {
|
|
||||||
// Return same section if item is not in it
|
|
||||||
if (!section.items.some((item) => item.id === itemId)) return section;
|
|
||||||
return {
|
|
||||||
...section,
|
|
||||||
items: section.items.concat(newItem as unknown as Item),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[updateBoard],
|
[updateBoard],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,12 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
|||||||
>
|
>
|
||||||
<Throw
|
<Throw
|
||||||
error={new NoIntegrationSelectedError()}
|
error={new NoIntegrationSelectedError()}
|
||||||
when={widgetSupportsIntegrations && item.integrationIds.length === 0}
|
when={
|
||||||
|
widgetSupportsIntegrations &&
|
||||||
|
item.integrationIds.length === 0 &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<BoardItemMenu offset={4} item={newItem} />
|
<BoardItemMenu offset={4} item={newItem} />
|
||||||
<Comp
|
<Comp
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
|
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { GridStack } from "@homarr/gridstack";
|
import type { GridStack } from "@homarr/gridstack";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import type { Item } from "~/app/[locale]/boards/_types";
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
|
|||||||
@@ -2,19 +2,32 @@ import { Card, Collapse, Group, Stack, Title, UnstyledButton } from "@mantine/co
|
|||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|
||||||
import type { CategorySection } from "~/app/[locale]/boards/_types";
|
import type { CategorySection } from "~/app/[locale]/boards/_types";
|
||||||
|
import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
|
||||||
import { CategoryMenu } from "./category/category-menu";
|
import { CategoryMenu } from "./category/category-menu";
|
||||||
import { GridStack } from "./gridstack/gridstack";
|
import { GridStack } from "./gridstack/gridstack";
|
||||||
|
import classes from "./item.module.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: CategorySection;
|
section: CategorySection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BoardCategorySection = ({ section }: Props) => {
|
export const BoardCategorySection = ({ section }: Props) => {
|
||||||
const [opened, { toggle }] = useDisclosure(false);
|
const { mutate } = clientApi.section.changeCollapsed.useMutation();
|
||||||
|
const board = useRequiredBoard();
|
||||||
|
const [opened, { toggle }] = useDisclosure(section.collapsed, {
|
||||||
|
onOpen() {
|
||||||
|
mutate({ sectionId: section.id, collapsed: true });
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
mutate({ sectionId: section.id, collapsed: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card withBorder p={0}>
|
<Card style={{ "--opacity": board.opacity / 100 }} withBorder p={0} className={classes.itemCard}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group wrap="nowrap" gap="sm">
|
<Group wrap="nowrap" gap="sm">
|
||||||
<UnstyledButton w="100%" p="sm" onClick={toggle}>
|
<UnstyledButton w="100%" p="sm" onClick={toggle}>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const createSections = (categoryCount: number) => {
|
|||||||
name: `Category ${index}`,
|
name: `Category ${index}`,
|
||||||
yOffset: index,
|
yOffset: index,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
|
collapsed: false,
|
||||||
items: [],
|
items: [],
|
||||||
})) satisfies Section[];
|
})) satisfies Section[];
|
||||||
};
|
};
|
||||||
|
|||||||
+1
@@ -118,6 +118,7 @@ const createSections = (initialYOffsets: number[]) => {
|
|||||||
id: yOffset.toString(),
|
id: yOffset.toString(),
|
||||||
kind: index % 2 === 0 ? "empty" : "category",
|
kind: index % 2 === 0 ? "empty" : "category",
|
||||||
name: "Category",
|
name: "Category",
|
||||||
|
collapsed: false,
|
||||||
yOffset,
|
yOffset,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
items: [createItem({ id: yOffset.toString() })],
|
items: [createItem({ id: yOffset.toString() })],
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const useCategoryActions = () => {
|
|||||||
kind: "category",
|
kind: "category",
|
||||||
yOffset,
|
yOffset,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
|
collapsed: false,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,6 +90,7 @@ export const useCategoryActions = () => {
|
|||||||
kind: "category",
|
kind: "category",
|
||||||
yOffset: lastYOffset + 1,
|
yOffset: lastYOffset + 1,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
|
collapsed: false,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -12,11 +12,7 @@ export const env = createEnv({
|
|||||||
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
|
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
|
||||||
* built with invalid env vars.
|
* built with invalid env vars.
|
||||||
*/
|
*/
|
||||||
server: {
|
server: {},
|
||||||
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
|
|
||||||
DOCKER_HOSTNAMES: z.string().optional(),
|
|
||||||
DOCKER_PORTS: z.string().optional(),
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Specify your client-side environment variables schema here.
|
* Specify your client-side environment variables schema here.
|
||||||
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
|
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
|
||||||
@@ -30,9 +26,8 @@ export const env = createEnv({
|
|||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
PORT: process.env.PORT,
|
PORT: process.env.PORT,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
|
|
||||||
DOCKER_PORTS: process.env.DOCKER_PORTS,
|
|
||||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||||
},
|
},
|
||||||
skipValidation: shouldSkipEnvValidation(),
|
skipValidation: shouldSkipEnvValidation(),
|
||||||
|
emptyStringAsUndefined: true,
|
||||||
});
|
});
|
||||||
@@ -38,15 +38,15 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"undici": "7.2.3"
|
"undici": "7.3.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": "^22.10.7",
|
"@types/node": "^22.12.0",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.19.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"@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/ws": "^8.5.13",
|
"@types/ws": "^8.5.14",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.19.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ const handler = applyWSSHandler({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Enable heartbeat messages to keep connection open (disabled by default)
|
||||||
|
keepAlive: {
|
||||||
|
enabled: true,
|
||||||
|
// server ping message interval in milliseconds
|
||||||
|
pingMs: 30000,
|
||||||
|
// connection is terminated if pong message is not received in this many milliseconds
|
||||||
|
pongWaitMs: 5000,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on("connection", (websocket, incomingMessage) => {
|
wss.on("connection", (websocket, incomingMessage) => {
|
||||||
|
|||||||
+3
-10
@@ -24,10 +24,10 @@
|
|||||||
<strong>Demo ✨</strong>
|
<strong>Demo ✨</strong>
|
||||||
</a>
|
</a>
|
||||||
•
|
•
|
||||||
<a href="https://homarr.dev/docs/getting-started/installation/">
|
<a href="https://homarr.dev/docs/category/installation-1/">
|
||||||
<strong>Install 💻</strong>
|
<strong>Install 💻</strong>
|
||||||
</a> •
|
</a> •
|
||||||
<a href="https://translate.homarr.dev/">
|
<a href="https://crowdin.com/project/homarr_labs">
|
||||||
<strong>Translations 🈺</strong>
|
<strong>Translations 🈺</strong>
|
||||||
</a> •
|
</a> •
|
||||||
<a href="https://discord.com/invite/aCsmEV5RgA">
|
<a href="https://discord.com/invite/aCsmEV5RgA">
|
||||||
@@ -35,13 +35,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> This is a beta version of Homarr. You can download the old but stable version at https://github.com/ajnart/homarr/. Running this version is at your own risk.
|
|
||||||
|
|
||||||
Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -100,7 +93,7 @@ Homarr has a [built-in collection of widgets and integrations](https://homarr.de
|
|||||||
Since we are updating Homarr very frequently, we recommend reading our official installation guides:
|
Since we are updating Homarr very frequently, we recommend reading our official installation guides:
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<a href="https://homarr.dev/docs/getting-started/installation/">
|
<a href="https://homarr.dev/docs/category/installation-1/">
|
||||||
Please click here for official installation instructions
|
Please click here for official installation instructions
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ describe("Onboarding", () => {
|
|||||||
environment: {
|
environment: {
|
||||||
AUTH_PROVIDERS: "ldap",
|
AUTH_PROVIDERS: "ldap",
|
||||||
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
|
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
|
||||||
AUTH_LDAP_BASE: "",
|
AUTH_LDAP_BASE: "not-used",
|
||||||
AUTH_LDAP_BIND_DN: "",
|
AUTH_LDAP_BIND_DN: "not-used",
|
||||||
AUTH_LDAP_BIND_PASSWORD: "",
|
AUTH_LDAP_BIND_PASSWORD: "not-used",
|
||||||
},
|
},
|
||||||
mounts: {
|
mounts: {
|
||||||
"/appdata": localMountPath,
|
"/appdata": localMountPath,
|
||||||
|
|||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.0.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -38,29 +38,29 @@
|
|||||||
"@semantic-release/github": "^11.0.1",
|
"@semantic-release/github": "^11.0.1",
|
||||||
"@semantic-release/npm": "^12.0.1",
|
"@semantic-release/npm": "^12.0.1",
|
||||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||||
"@turbo/gen": "^2.3.3",
|
"@turbo/gen": "^2.3.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "^2.1.8",
|
"@vitest/coverage-v8": "^3.0.4",
|
||||||
"@vitest/ui": "^2.1.8",
|
"@vitest/ui": "^3.0.4",
|
||||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"semantic-release": "^24.2.1",
|
"semantic-release": "^24.2.1",
|
||||||
"testcontainers": "^10.16.0",
|
"testcontainers": "^10.17.2",
|
||||||
"turbo": "^2.3.3",
|
"turbo": "^2.3.4",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^3.0.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.4",
|
"packageManager": "pnpm@9.15.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.13.0"
|
"node": ">=22.13.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"allowNonAppliedPatches": true,
|
"allowNonAppliedPatches": true,
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"proxmox-api>undici": "7.2.3"
|
"proxmox-api>undici": "7.3.0"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||||
|
|||||||
@@ -32,7 +32,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",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.19.0",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
"@homarr/icons": "workspace:^0.1.0",
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
@@ -42,20 +43,20 @@
|
|||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
"@trpc/server": "next",
|
"@trpc/server": "next",
|
||||||
"dockerode": "4.0.2",
|
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "15.1.4",
|
"next": "15.1.6",
|
||||||
|
"pretty-print-error": "^1.1.2",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"trpc-to-openapi": "^2.1.2"
|
"trpc-to-openapi": "^2.1.2",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"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/dockerode": "^3.3.34",
|
"eslint": "^9.19.0",
|
||||||
"eslint": "^9.18.0",
|
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
|
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
|
||||||
@@ -9,7 +10,6 @@ import type { Database } from "@homarr/db";
|
|||||||
import { and, eq, inArray } from "@homarr/db";
|
import { and, eq, inArray } from "@homarr/db";
|
||||||
import { integrations } from "@homarr/db/schema";
|
import { integrations } from "@homarr/db/schema";
|
||||||
import type { IntegrationKind } from "@homarr/definitions";
|
import type { IntegrationKind } from "@homarr/definitions";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { publicProcedure } from "../trpc";
|
import { publicProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -98,56 +98,59 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
|||||||
action: IntegrationAction,
|
action: IntegrationAction,
|
||||||
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
|
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
|
||||||
) => {
|
) => {
|
||||||
return publicProcedure
|
return publicProcedure.input(z.object({ integrationIds: z.array(z.string()) })).use(async ({ ctx, input, next }) => {
|
||||||
.input(z.object({ integrationIds: z.array(z.string()).min(1) }))
|
const dbIntegrations =
|
||||||
.use(async ({ ctx, input, next }) => {
|
input.integrationIds.length >= 1
|
||||||
const dbIntegrations = await ctx.db.query.integrations.findMany({
|
? await ctx.db.query.integrations.findMany({
|
||||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||||
with: {
|
|
||||||
secrets: true,
|
|
||||||
items: {
|
|
||||||
with: {
|
with: {
|
||||||
item: {
|
secrets: true,
|
||||||
|
items: {
|
||||||
with: {
|
with: {
|
||||||
section: {
|
item: {
|
||||||
columns: {
|
with: {
|
||||||
boardId: true,
|
section: {
|
||||||
|
columns: {
|
||||||
|
boardId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
userPermissions: true,
|
||||||
|
groupPermissions: true,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
userPermissions: true,
|
: [];
|
||||||
groupPermissions: true,
|
|
||||||
},
|
const offset = input.integrationIds.length - dbIntegrations.length;
|
||||||
|
if (offset !== 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const offset = input.integrationIds.length - dbIntegrations.length;
|
if (dbIntegrations.length >= 1) {
|
||||||
if (offset !== 0) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
|
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
|
||||||
|
}
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
integrations: dbIntegrations.map(
|
integrations: dbIntegrations.map(
|
||||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
kind: kind as TKind,
|
kind: kind as TKind,
|
||||||
decryptedSecrets: secrets.map((secret) => ({
|
decryptedSecrets: secrets.map((secret) => ({
|
||||||
...secret,
|
...secret,
|
||||||
value: decryptSecret(secret.value),
|
value: decryptSecret(secret.value),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { publicProcedure } from "../trpc";
|
import { publicProcedure } from "../trpc";
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { logRouter } from "./router/log";
|
|||||||
import { mediaRouter } from "./router/medias/media-router";
|
import { mediaRouter } from "./router/medias/media-router";
|
||||||
import { onboardRouter } from "./router/onboard/onboard-router";
|
import { onboardRouter } from "./router/onboard/onboard-router";
|
||||||
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
|
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
|
||||||
|
import { sectionRouter } from "./router/section/section-router";
|
||||||
import { serverSettingsRouter } from "./router/serverSettings";
|
import { serverSettingsRouter } from "./router/serverSettings";
|
||||||
import { updateCheckerRouter } from "./router/update-checker";
|
import { updateCheckerRouter } from "./router/update-checker";
|
||||||
import { userRouter } from "./router/user";
|
import { userRouter } from "./router/user";
|
||||||
@@ -27,6 +28,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
invite: inviteRouter,
|
invite: inviteRouter,
|
||||||
integration: integrationRouter,
|
integration: integrationRouter,
|
||||||
board: boardRouter,
|
board: boardRouter,
|
||||||
|
section: sectionRouter,
|
||||||
app: innerAppRouter,
|
app: innerAppRouter,
|
||||||
searchEngine: searchEngineRouter,
|
searchEngine: searchEngineRouter,
|
||||||
widget: widgetRouter,
|
widget: widgetRouter,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { asc, createId, eq, inArray, like } from "@homarr/db";
|
import { asc, createId, eq, inArray, like } from "@homarr/db";
|
||||||
import { apps } from "@homarr/db/schema";
|
import { apps } from "@homarr/db/schema";
|
||||||
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
||||||
import { getIconForName } from "@homarr/icons";
|
import { getIconForName } from "@homarr/icons";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { convertIntersectionToZodObject } from "../schema-merger";
|
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
@@ -102,7 +103,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return app;
|
return app;
|
||||||
}),
|
}),
|
||||||
byIds: protectedProcedure.input(z.array(z.string())).query(async ({ ctx, input }) => {
|
byIds: publicProcedure.input(z.array(z.string())).query(async ({ ctx, input }) => {
|
||||||
return await ctx.db.query.apps.findMany({
|
return await ctx.db.query.apps.findMany({
|
||||||
where: inArray(apps.id, input),
|
where: inArray(apps.id, input),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||||
import type { DeviceType } from "@homarr/common/server";
|
import type { DeviceType } from "@homarr/common/server";
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
integrationItems,
|
integrationItems,
|
||||||
integrationUserPermissions,
|
integrationUserPermissions,
|
||||||
items,
|
items,
|
||||||
|
sectionCollapseStates,
|
||||||
sections,
|
sections,
|
||||||
users,
|
users,
|
||||||
} from "@homarr/db/schema";
|
} from "@homarr/db/schema";
|
||||||
@@ -25,7 +27,7 @@ import { importOldmarrAsync } from "@homarr/old-import";
|
|||||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||||
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||||
import { createSectionSchema, sharedItemSchema, validation, z, zodUnionFromArray } from "@homarr/validation";
|
import { createSectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
|
||||||
|
|
||||||
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";
|
||||||
@@ -1024,6 +1026,9 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
|||||||
},
|
},
|
||||||
sections: {
|
sections: {
|
||||||
with: {
|
with: {
|
||||||
|
collapseStates: {
|
||||||
|
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||||
|
},
|
||||||
items: {
|
items: {
|
||||||
with: {
|
with: {
|
||||||
integrations: {
|
integrations: {
|
||||||
@@ -1058,9 +1063,10 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...otherBoardProperties,
|
...otherBoardProperties,
|
||||||
sections: sections.map((section) =>
|
sections: sections.map(({ collapseStates, ...section }) =>
|
||||||
parseSection({
|
parseSection({
|
||||||
...section,
|
...section,
|
||||||
|
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||||
items: section.items.map(({ integrations: itemIntegrations, ...item }) => ({
|
items: section.items.map(({ integrations: itemIntegrations, ...item }) => ({
|
||||||
...item,
|
...item,
|
||||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import type Docker from "dockerode";
|
import { z } from "zod";
|
||||||
import type { Container } from "dockerode";
|
|
||||||
|
|
||||||
import { db, like, or } from "@homarr/db";
|
import { db, like, or } from "@homarr/db";
|
||||||
import { icons } from "@homarr/db/schema";
|
import { icons } from "@homarr/db/schema";
|
||||||
import type { DockerContainerState } from "@homarr/definitions";
|
import { DockerSingleton } from "@homarr/docker";
|
||||||
|
import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@homarr/docker";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { createCacheChannel } from "@homarr/redis";
|
import { createCacheChannel } from "@homarr/redis";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||||
import { DockerSingleton } from "./docker-singleton";
|
|
||||||
|
|
||||||
const dockerCache = createCacheChannel<{
|
const dockerCache = createCacheChannel<{
|
||||||
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[];
|
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[];
|
||||||
}>("docker-containers", 5 * 60 * 1000);
|
}>("docker-containers", 5 * 60 * 1000);
|
||||||
|
|
||||||
export const dockerRouter = createTRPCRouter({
|
export const dockerRouter = createTRPCRouter({
|
||||||
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||||
const result = await dockerCache
|
const result = await dockerCache
|
||||||
.consumeAsync(async () => {
|
.consumeAsync(async () => {
|
||||||
const dockerInstances = DockerSingleton.getInstance();
|
const dockerInstances = DockerSingleton.getInstances();
|
||||||
const containers = await Promise.all(
|
const containers = await Promise.all(
|
||||||
// Return all the containers of all the instances into only one item
|
// Return all the containers of all the instances into only one item
|
||||||
dockerInstances.map(({ instance, host: key }) =>
|
dockerInstances.map(({ instance, host: key }) =>
|
||||||
@@ -33,8 +31,7 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
).then((containers) => containers.flat());
|
).then((containers) => containers.flat());
|
||||||
|
|
||||||
const extractImage = (container: Docker.ContainerInfo) =>
|
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||||
container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
|
||||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||||
const dbIcons =
|
const dbIcons =
|
||||||
likeQueries.length >= 1
|
likeQueries.length >= 1
|
||||||
@@ -151,7 +148,7 @@ const getContainerOrDefaultAsync = async (instance: Docker, id: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getContainerOrThrowAsync = async (id: string) => {
|
const getContainerOrThrowAsync = async (id: string) => {
|
||||||
const dockerInstances = DockerSingleton.getInstance();
|
const dockerInstances = DockerSingleton.getInstances();
|
||||||
const containers = await Promise.all(dockerInstances.map(({ instance }) => getContainerOrDefaultAsync(instance, id)));
|
const containers = await Promise.all(dockerInstances.map(({ instance }) => getContainerOrDefaultAsync(instance, id)));
|
||||||
const foundContainer = containers.find((container) => container) ?? null;
|
const foundContainer = containers.find((container) => container) ?? null;
|
||||||
|
|
||||||
@@ -168,21 +165,21 @@ const getContainerOrThrowAsync = async (id: string) => {
|
|||||||
interface DockerContainer {
|
interface DockerContainer {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
state: DockerContainerState;
|
state: ContainerState;
|
||||||
image: string;
|
image: string;
|
||||||
ports: Docker.Port[];
|
ports: Port[];
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeContainers(
|
function sanitizeContainers(
|
||||||
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[],
|
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[],
|
||||||
): DockerContainer[] {
|
): DockerContainer[] {
|
||||||
return containers.map((container) => {
|
return containers.map((container) => {
|
||||||
return {
|
return {
|
||||||
name: container.Names[0]?.split("/")[1] ?? "Unknown",
|
name: container.Names[0]?.split("/")[1] ?? "Unknown",
|
||||||
id: container.Id,
|
id: container.Id,
|
||||||
instance: container.instance,
|
instance: container.instance,
|
||||||
state: container.State as DockerContainerState,
|
state: container.State as ContainerState,
|
||||||
image: container.Image,
|
image: container.Image,
|
||||||
ports: container.Ports,
|
ports: container.Ports,
|
||||||
iconUrl: container.iconUrl,
|
iconUrl: container.iconUrl,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import Docker from "dockerode";
|
|
||||||
|
|
||||||
interface DockerInstance {
|
|
||||||
host: string;
|
|
||||||
instance: Docker;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DockerSingleton {
|
|
||||||
private static instances: DockerInstance[];
|
|
||||||
|
|
||||||
private createInstances() {
|
|
||||||
const instances: DockerInstance[] = [];
|
|
||||||
const hostVariable = process.env.DOCKER_HOST;
|
|
||||||
const portVariable = process.env.DOCKER_PORT;
|
|
||||||
if (hostVariable === undefined || portVariable === undefined) {
|
|
||||||
instances.push({ host: "socket", instance: new Docker() });
|
|
||||||
return instances;
|
|
||||||
}
|
|
||||||
const hosts = hostVariable.split(",");
|
|
||||||
const ports = portVariable.split(",");
|
|
||||||
|
|
||||||
if (hosts.length !== ports.length) {
|
|
||||||
throw new Error("The number of hosts and ports must match");
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts.forEach((host, i) => {
|
|
||||||
instances.push({
|
|
||||||
host: `${host}:${ports[i]}`,
|
|
||||||
instance: new Docker({
|
|
||||||
host,
|
|
||||||
port: parseInt(ports[i] ?? "", 10),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
return instances;
|
|
||||||
});
|
|
||||||
return instances;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static findInstance(key: string): DockerInstance | undefined {
|
|
||||||
return this.instances.find((instance) => instance.host === key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getInstance(): DockerInstance[] {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (!DockerSingleton.instances) {
|
|
||||||
DockerSingleton.instances = new DockerSingleton().createInstances();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instances;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
||||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||||
import { everyoneGroup } from "@homarr/definitions";
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
||||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse";
|
import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse";
|
||||||
import {
|
import {
|
||||||
ensureValidTokenOrThrow,
|
ensureValidTokenOrThrow,
|
||||||
importInitialOldmarrAsync,
|
importInitialOldmarrAsync,
|
||||||
importInitialOldmarrInputSchema,
|
importInitialOldmarrInputSchema,
|
||||||
} from "@homarr/old-import/import";
|
} from "@homarr/old-import/import";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, onboardingProcedure } from "../../trpc";
|
import { createTRPCRouter, onboardingProcedure } from "../../trpc";
|
||||||
import { nextOnboardingStepAsync } from "../onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "../onboard/onboard-queries";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||||
@@ -23,7 +24,7 @@ import {
|
|||||||
integrationSecretKindObject,
|
integrationSecretKindObject,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { integrationCreator } from "@homarr/integrations";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
@@ -205,7 +206,10 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.attemptSearchEngineCreation) {
|
if (
|
||||||
|
input.attemptSearchEngineCreation &&
|
||||||
|
integrationDefs[input.kind].category.flatMap((category) => category).includes("search")
|
||||||
|
) {
|
||||||
const icon = getIconUrl(input.kind);
|
const icon = getIconUrl(input.kind);
|
||||||
await ctx.db.insert(searchEngines).values({
|
await ctx.db.insert(searchEngines).values({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { formatError } from "pretty-print-error";
|
||||||
|
|
||||||
import { decryptSecret } from "@homarr/common/server";
|
import { decryptSecret } from "@homarr/common/server";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||||
import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations";
|
import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
type FormIntegration = Integration & {
|
type FormIntegration = Integration & {
|
||||||
secrets: {
|
secrets: {
|
||||||
@@ -28,11 +31,22 @@ export const testConnectionAsync = async (
|
|||||||
source: "form" as const,
|
source: "form" as const,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const decryptedDbSecrets = dbSecrets.map((secret) => ({
|
const decryptedDbSecrets = dbSecrets
|
||||||
...secret,
|
.map((secret) => {
|
||||||
value: decryptSecret(secret.value),
|
try {
|
||||||
source: "db" as const,
|
return {
|
||||||
}));
|
...secret,
|
||||||
|
value: decryptSecret(secret.value),
|
||||||
|
source: "db" as const,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"\n${formatError(error)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((secret) => secret !== null);
|
||||||
|
|
||||||
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
|
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
|
||||||
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { asc, createId, eq } from "@homarr/db";
|
import { asc, createId, eq } from "@homarr/db";
|
||||||
import { invites } from "@homarr/db/schema";
|
import { invites } from "@homarr/db/schema";
|
||||||
import { selectInviteSchema } from "@homarr/db/validationSchemas";
|
import { selectInviteSchema } from "@homarr/db/validationSchemas";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { fetchWithTimeout } from "@homarr/common";
|
import { fetchWithTimeout } from "@homarr/common";
|
||||||
import type { z } from "@homarr/validation";
|
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { InferInsertModel } from "@homarr/db";
|
import type { InferInsertModel } from "@homarr/db";
|
||||||
import { and, createId, desc, eq, like } from "@homarr/db";
|
import { and, createId, desc, eq, like } from "@homarr/db";
|
||||||
import { iconRepositories, icons, medias } from "@homarr/db/schema";
|
import { iconRepositories, icons, medias } from "@homarr/db/schema";
|
||||||
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
|
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { onboarding } from "@homarr/db/schema";
|
import { onboarding } from "@homarr/db/schema";
|
||||||
import { onboardingSteps } from "@homarr/definitions";
|
import { onboardingSteps } from "@homarr/definitions";
|
||||||
import { z, zodEnumFromArray } from "@homarr/validation";
|
import { zodEnumFromArray } from "@homarr/validation";
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";
|
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { asc, createId, eq, like, sql } from "@homarr/db";
|
import { asc, createId, eq, like, sql } from "@homarr/db";
|
||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import { searchEngines, users } from "@homarr/db/schema";
|
import { searchEngines, users } from "@homarr/db/schema";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { integrationCreator } from "@homarr/integrations";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { and, eq } from "@homarr/db";
|
||||||
|
import { sectionCollapseStates, sections } from "@homarr/db/schema";
|
||||||
|
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
export const sectionRouter = createTRPCRouter({
|
||||||
|
changeCollapsed: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
sectionId: z.string(),
|
||||||
|
collapsed: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const section = await ctx.db.query.sections.findFirst({
|
||||||
|
where: and(eq(sections.id, input.sectionId), eq(sections.kind, "category")),
|
||||||
|
with: {
|
||||||
|
collapseStates: {
|
||||||
|
where: eq(sectionCollapseStates.userId, ctx.session.user.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: `Section not found id=${input.sectionId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.collapseStates.length === 0) {
|
||||||
|
await ctx.db.insert(sectionCollapseStates).values({
|
||||||
|
sectionId: section.id,
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
collapsed: input.collapsed,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db
|
||||||
|
.update(sectionCollapseStates)
|
||||||
|
.set({
|
||||||
|
collapsed: input.collapsed,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(eq(sectionCollapseStates.sectionId, section.id), eq(sectionCollapseStates.userId, ctx.session.user.id)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import type { ServerSettings } from "@homarr/server-settings";
|
import type { ServerSettings } from "@homarr/server-settings";
|
||||||
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||||
|
|||||||
@@ -812,7 +812,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
expect(integration).toBeUndefined();
|
expect(integration).toBeUndefined();
|
||||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||||
});
|
});
|
||||||
it.each([[{ kind: "empty" as const }], [{ kind: "category" as const, name: "My first category" }]])(
|
it.each([[{ kind: "empty" as const }], [{ kind: "category" as const, collapsed: false, name: "My first category" }]])(
|
||||||
"should add section when present in input",
|
"should add section when present in input",
|
||||||
async (partialSection) => {
|
async (partialSection) => {
|
||||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||||
@@ -1023,6 +1023,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
yOffset: 1,
|
yOffset: 1,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
name: "Test",
|
name: "Test",
|
||||||
|
collapsed: true,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1031,6 +1032,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
name: "After",
|
name: "After",
|
||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
|
collapsed: false,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
@@ -8,7 +9,7 @@ import { selectUserSchema } from "@homarr/db/validationSchemas";
|
|||||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { convertIntersectionToZodObject } from "../schema-merger";
|
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||||
|
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
initUser: onboardingProcedure
|
initUser: onboardingProcedure
|
||||||
@@ -214,6 +216,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
firstDayOfWeek: true,
|
firstDayOfWeek: true,
|
||||||
pingIconsEnabled: true,
|
pingIconsEnabled: true,
|
||||||
defaultSearchEngineId: true,
|
defaultSearchEngineId: true,
|
||||||
|
openSearchInNewTab: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
|
||||||
@@ -238,6 +241,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
firstDayOfWeek: true,
|
firstDayOfWeek: true,
|
||||||
pingIconsEnabled: true,
|
pingIconsEnabled: true,
|
||||||
defaultSearchEngineId: true,
|
defaultSearchEngineId: true,
|
||||||
|
openSearchInNewTab: true,
|
||||||
},
|
},
|
||||||
where: eq(users.id, input.userId),
|
where: eq(users.id, input.userId),
|
||||||
});
|
});
|
||||||
@@ -404,7 +408,13 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.userId), "view");
|
// Only allow user to select boards they have access to
|
||||||
|
if (input.homeBoardId) {
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.homeBoardId), "view");
|
||||||
|
}
|
||||||
|
if (input.mobileHomeBoardId) {
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.mobileHomeBoardId), "view");
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
@@ -416,40 +426,32 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
changeDefaultSearchEngine: protectedProcedure
|
changeDefaultSearchEngine: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
convertIntersectionToZodObject(validation.user.changeDefaultSearchEngine.and(z.object({ userId: z.string() }))),
|
convertIntersectionToZodObject(
|
||||||
|
validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/users/changeSearchEngine", tags: ["users"], protect: true } })
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "PATCH",
|
||||||
|
path: "/api/users/changeSearchEngine",
|
||||||
|
tags: ["users"],
|
||||||
|
protect: true,
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const user = ctx.session.user;
|
await changeSearchPreferencesAsync(ctx.db, ctx.session, {
|
||||||
// Only admins can change other users passwords
|
...input,
|
||||||
if (!user.permissions.includes("admin") && user.id !== input.userId) {
|
openInNewTab: undefined,
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "User not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbUser = await ctx.db.query.users.findFirst({
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
where: eq(users.id, input.userId),
|
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
if (!dbUser) {
|
changeSearchPreferences: protectedProcedure
|
||||||
throw new TRPCError({
|
.input(convertIntersectionToZodObject(changeSearchPreferencesInputSchema))
|
||||||
code: "NOT_FOUND",
|
.output(z.void())
|
||||||
message: "User not found",
|
.meta({ openapi: { method: "PATCH", path: "/api/users/search-preferences", tags: ["users"], protect: true } })
|
||||||
});
|
.mutation(async ({ input, ctx }) => {
|
||||||
}
|
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
|
||||||
|
|
||||||
await ctx.db
|
|
||||||
.update(users)
|
|
||||||
.set({
|
|
||||||
defaultSearchEngineId: input.defaultSearchEngineId,
|
|
||||||
})
|
|
||||||
.where(eq(users.id, input.userId));
|
|
||||||
}),
|
}),
|
||||||
changeColorScheme: protectedProcedure
|
changeColorScheme: protectedProcedure
|
||||||
.input(validation.user.changeColorScheme)
|
.input(validation.user.changeColorScheme)
|
||||||
@@ -463,21 +465,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, ctx.session.user.id));
|
.where(eq(users.id, ctx.session.user.id));
|
||||||
}),
|
}),
|
||||||
getPingIconsEnabledOrDefault: publicProcedure.query(async ({ ctx }) => {
|
|
||||||
if (!ctx.session?.user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await ctx.db.query.users.findFirst({
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
pingIconsEnabled: true,
|
|
||||||
},
|
|
||||||
where: eq(users.id, ctx.session.user.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return user?.pingIconsEnabled ?? false;
|
|
||||||
}),
|
|
||||||
changePingIconsEnabled: protectedProcedure
|
changePingIconsEnabled: protectedProcedure
|
||||||
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
|
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -496,21 +483,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, ctx.session.user.id));
|
.where(eq(users.id, ctx.session.user.id));
|
||||||
}),
|
}),
|
||||||
getFirstDayOfWeekForUserOrDefault: publicProcedure.input(z.undefined()).query(async ({ ctx }) => {
|
|
||||||
if (!ctx.session?.user) {
|
|
||||||
return 1 as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await ctx.db.query.users.findFirst({
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
firstDayOfWeek: true,
|
|
||||||
},
|
|
||||||
where: eq(users.id, ctx.session.user.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return user?.firstDayOfWeek ?? (1 as const);
|
|
||||||
}),
|
|
||||||
changeFirstDayOfWeek: protectedProcedure
|
changeFirstDayOfWeek: protectedProcedure
|
||||||
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
|
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { Session } from "@homarr/auth";
|
||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import { eq } from "@homarr/db";
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { users } from "@homarr/db/schema";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
|
export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and(
|
||||||
|
z.object({ userId: z.string() }),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const changeSearchPreferencesAsync = async (
|
||||||
|
db: Database,
|
||||||
|
session: Session,
|
||||||
|
input: Modify<z.infer<typeof changeSearchPreferencesInputSchema>, { openInNewTab: boolean | undefined }>,
|
||||||
|
) => {
|
||||||
|
const user = session.user;
|
||||||
|
// Only admins can change other users passwords
|
||||||
|
if (!user.permissions.includes("admin") && user.id !== input.userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbUser = await db.query.users.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: eq(users.id, input.userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
defaultSearchEngineId: input.defaultSearchEngineId,
|
||||||
|
openSearchInNewTab: input.openInNewTab,
|
||||||
|
})
|
||||||
|
.where(eq(users.id, input.userId));
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { sendPingRequestAsync } from "@homarr/ping";
|
import { sendPingRequestAsync } from "@homarr/ping";
|
||||||
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||||
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { Modify } from "@homarr/common/types";
|
import type { Modify } from "@homarr/common/types";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
@@ -7,7 +8,6 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
|||||||
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
||||||
import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations";
|
import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations";
|
||||||
import { downloadClientRequestHandler } from "@homarr/request-handler/downloads";
|
import { downloadClientRequestHandler } from "@homarr/request-handler/downloads";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import type { IntegrationAction } from "../../middlewares/integration";
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator, MediaRequestStatus } from "@homarr/integrations";
|
import { integrationCreator, MediaRequestStatus } from "@homarr/integrations";
|
||||||
import type { MediaRequest } from "@homarr/integrations/types";
|
import type { MediaRequest } from "@homarr/integrations/types";
|
||||||
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
||||||
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,37 @@
|
|||||||
import type { RssFeed } from "@homarr/cron-jobs";
|
import { z } from "zod";
|
||||||
import { createItemChannel } from "@homarr/redis";
|
|
||||||
|
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||||
|
|
||||||
import { createOneItemMiddleware } from "../../middlewares/item";
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
export const rssFeedRouter = createTRPCRouter({
|
export const rssFeedRouter = createTRPCRouter({
|
||||||
getFeeds: publicProcedure.unstable_concat(createOneItemMiddleware("rssFeed")).query(async ({ input }) => {
|
getFeeds: publicProcedure
|
||||||
const channel = createItemChannel<RssFeed[]>(input.itemId);
|
.input(
|
||||||
return await channel.getAsync();
|
z.object({
|
||||||
}),
|
urls: z.array(z.string()),
|
||||||
|
maximumAmountPosts: z.number(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const rssFeeds = await Promise.all(
|
||||||
|
input.urls.map(async (url) => {
|
||||||
|
const innerHandler = rssFeedsRequestHandler.handler({
|
||||||
|
url,
|
||||||
|
count: input.maximumAmountPosts,
|
||||||
|
});
|
||||||
|
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return rssFeeds
|
||||||
|
.flatMap((rssFeed) => rssFeed.data.entries)
|
||||||
|
.slice(0, input.maximumAmountPosts)
|
||||||
|
.sort((entryA, entryB) => {
|
||||||
|
return entryA.published && entryB.published
|
||||||
|
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
|
||||||
|
: 0;
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { integrationCreator } from "@homarr/integrations";
|
||||||
import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state";
|
import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state";
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import type { IntegrationAction } from "../../middlewares/integration";
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AnyZodObject, ZodIntersection, ZodObject } from "@homarr/validation";
|
import { z } from "zod";
|
||||||
import { z } from "@homarr/validation";
|
import type { AnyZodObject, ZodIntersection, ZodObject } from "zod";
|
||||||
|
|
||||||
export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<AnyZodObject, AnyZodObject>>(
|
export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<AnyZodObject, AnyZodObject>>(
|
||||||
intersection: TIntersection,
|
intersection: TIntersection,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import type { OpenApiMeta } from "trpc-to-openapi";
|
import type { OpenApiMeta } from "trpc-to-openapi";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { FlattenError } from "@homarr/common";
|
import { FlattenError } from "@homarr/common";
|
||||||
@@ -16,7 +17,6 @@ import { userAgent } from "@homarr/common/server";
|
|||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
|
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { ZodError } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
|
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import { formatError } from "pretty-print-error";
|
||||||
|
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { createAdapter } from "./adapter";
|
import { createAdapter } from "./adapter";
|
||||||
import { createSessionCallback } from "./callbacks";
|
import { createSessionCallback } from "./callbacks";
|
||||||
@@ -26,15 +28,16 @@ export const createConfiguration = (
|
|||||||
const adapter = createAdapter(db, provider);
|
const adapter = createAdapter(db, provider);
|
||||||
return NextAuth({
|
return NextAuth({
|
||||||
logger: {
|
logger: {
|
||||||
error: (code, ...message) => {
|
error: (error) => {
|
||||||
// Remove the big error message for failed login attempts
|
// Remove the big error message for failed login attempts
|
||||||
// as it is not useful for the user.
|
// as it is not useful for the user.
|
||||||
if (code.name === "CredentialsSignin") {
|
if (error.name === "CredentialsSignin") {
|
||||||
console.warn("The login attempt of a user was not successful.");
|
logger.warn("The login attempt of a user was not successful.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(code, ...message);
|
logger.error(formatError(error));
|
||||||
|
logger.error(formatError(error.cause));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
|
|||||||
@@ -86,4 +86,5 @@ export const env = createEnv({
|
|||||||
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: process.env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE,
|
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: process.env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE,
|
||||||
},
|
},
|
||||||
skipValidation,
|
skipValidation,
|
||||||
|
emptyStringAsUndefined: true,
|
||||||
});
|
});
|
||||||
@@ -30,14 +30,16 @@
|
|||||||
"@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/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "7.3.1",
|
"ldapts": "7.3.1",
|
||||||
"next": "15.1.4",
|
"next": "15.1.6",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
|
"pretty-print-error": "^1.1.2",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.0.0",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -45,7 +47,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/cookies": "0.9.0",
|
"@types/cookies": "0.9.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.19.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 100 of 247 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user