Compare commits

...
90 Commits
Author SHA1 Message Date
Releases Homarr 60323b2651 chore(release): 1.5.0 [skip ci] 2025-02-07 19:13:19 +00:00
homarr-releases[bot] 93235cab6f chore(release): automatic release v1.5.0 2025-02-07 19:12:36 +00:00
homarr-renovate[bot] c849109df5 fix(deps): update dependency @tabler/icons-react to ^3.30.0 (#2266)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-07 14:46:12 +00:00
homarr-renovate[bot] 0da5a1b0fb fix(deps): update dependency ioredis to v5.5.0 (#2264)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-07 10:54:55 +00:00
homarr-renovate[bot] 7a8e671205 fix(deps): update testcontainers-node monorepo to ^10.18.0 (#2259)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-07 00:56:39 +00:00
homarr-renovate[bot] 3cbb656ee9 fix(deps): update dependency octokit to ^4.1.1 (#2262)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-07 00:56:14 +00:00
homarr-crowdin[bot] 55a493c475 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-07 00:44:51 +00:00
homarr-renovate[bot] 7bbbaf343b fix(deps): update dependency sass to ^1.84.0 (#2257)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-06 00:56:25 +00:00
homarr-crowdin[bot] 09b6323246 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-06 00:45:07 +00:00
homarr-renovate[bot] 3d0b319a88 fix(deps): update dependency drizzle-orm to ^0.39.2 (#2255)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-05 21:53:21 +00:00
ManuelandMeier Lukas 26468a4137 fix: foreign key reference default search engine (#2211)
* fix: foreign key reference default search engine

* fix: add sqlite migration to fix on delete action

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-02-05 11:36:42 +01:00
homarr-crowdin[bot] 62c83b79b4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-05 00:44:34 +00:00
Manuel da09826393 fix: make system health temp optional (#2247) 2025-02-04 19:10:12 +01:00
ManuelandMeier Lukas 8058b6207a feat: add api key delete (#2210)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-02-04 17:14:15 +01:00
homarr-renovate[bot] c623a420ca fix(deps): update dependency trpc-to-openapi to ^2.1.3 (#2244)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-04 06:55:03 +00:00
homarr-renovate[bot] d5e36835f6 chore(deps): update dependency @types/node to ^22.13.1 (#2243)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-04 00:54:41 +00:00
homarr-crowdin[bot] 1f12a893ff chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-04 00:44:13 +00:00
Manuel bb67bf5fa2 feat: open all apps in category (#2212) 2025-02-03 21:28:01 +01:00
Jack WellerandMeier Lukas be603badfe feat: custom time/date format in clock widget (#2180)
* feat: custom time/date format in clock widget

* feat: add customFormat variables for old-imports

* fix: added missing comma for old-import

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-02-03 19:33:29 +01:00
homarr-crowdin[bot]andMeier Lukas 0d97cc50c2 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-02-03 18:24:58 +00:00
homarr-renovate[bot] 14a4b94911 chore(deps): update vitest monorepo to ^3.0.5 (#2227)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-03 18:23:27 +00:00
homarr-renovate[bot] 83044d23dd fix(deps): update dependency typescript-eslint to ^8.23.0 (#2232)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-03 18:22:17 +00:00
homarr-renovate[bot] 12456af101 chore(deps): update pnpm to v9.15.5 (#2224)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-03 18:19:36 +00:00
Meier Lukas 8c656febeb ci: fix corepack issue (#2237) 2025-02-03 19:10:49 +01:00
homarr-crowdin[bot] 2598ade925 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-02 00:46:10 +00:00
homarr-renovate[bot] 46cfb2383b fix(deps): update dependency semver-parser to ^4.1.8 (#2216)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-01 08:35:16 +00:00
homarr-renovate[bot] a385337268 fix(deps): update dependency jotai to ^2.11.3 (#2215)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-01 03:00:53 +00:00
homarr-crowdin[bot] bc79c6abab chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-01 00:47:18 +00:00
ManuelandMeier Lukas 5f36d8b125 feat: improve consistency and design (#1867)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-31 23:32:08 +01:00
homarr-renovate[bot] 6a9b65f219 chore(deps): update dependency @types/node to ^22.13.0 (#2208)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 20:18:06 +00:00
homarr-renovate[bot] 6ce7bc0ee2 fix(deps): update turbo monorepo to ^2.4.0 (#2207)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 19:40:11 +00:00
Meier Lukas 36c246cca8 chore(issue-template): add missing homarr version 1.4.0 2025-01-31 20:38:03 +01:00
Releases Homarr 3506f875e3 chore(release): 1.4.0 [skip ci] 2025-01-31 19:13:04 +00:00
homarr-releases[bot] e30e4b8cb7 chore(release): automatic release v1.4.0 2025-01-31 19:12:20 +00:00
homarr-renovate[bot] 9662e1abba fix(deps): update tiptap monorepo to v2.11.5 (#2203)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 09:33:25 +00:00
homarr-renovate[bot] e44c3a7bc8 fix(deps): update tanstack-query monorepo to ^5.66.0 (#2202)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 09:16:43 +00:00
homarr-renovate[bot] c287e9965f fix(deps): update dependency jotai to ^2.11.2 (#2201)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 02:14:23 +00:00
homarr-crowdin[bot] bea0124e3f chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-31 00:44:11 +00:00
Manuel d4154d259f fix: respect certificates for app ping (#2198) 2025-01-30 23:12:45 +01:00
Meier Lukas 77746278a3 fix(docker): missing production env variable (#2188) 2025-01-30 22:14:52 +01:00
Meier Lukas c75d4f0f58 fix: negative curve radius system health (#2197) 2025-01-30 22:14:26 +01:00
Meier Lukas 96fd5cc35e fix: use hostname for suspense query url (#2187) 2025-01-30 22:14:14 +01:00
Meier Lukas bd0b7c9518 fix: board crashes with empty string favicon (#2192) 2025-01-30 20:15:05 +01:00
homarr-renovate[bot] c05c51856b fix(deps): update tiptap monorepo to v2.11.4 (#2195)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-30 16:18:52 +00:00
homarr-crowdin[bot] dac1646ecc chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-30 00:43:11 +00:00
Yossi Hillali c19a4009f3 feat: weather widget - add wind speed, option to disable decimals for temperature & redesign dropdown forecast (#2099) 2025-01-29 23:02:13 +02:00
homarr-renovate[bot] 5657384d2a chore(deps): update dependency @types/swagger-ui-react to v5 (#2181)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 21:30:45 +01:00
homarr-renovate[bot] 4c72a34daf fix(deps): update dependency drizzle-orm to ^0.39.1 (#2186)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 16:39:45 +00:00
homarr-renovate[bot] de48ef21e9 chore(deps): update dependency @types/chroma-js to v3.1.1 (#2185)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 16:18:59 +00:00
homarr-renovate[bot] 37ccfab89a fix(deps): update dependency drizzle-kit to ^0.30.4 (#2183)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 15:43:18 +00:00
homarr-crowdin[bot] 07d60fe13e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-29 00:44:09 +00:00
Meier Lukas bbd1b0c5d0 chore: remove node-options from example env 2025-01-28 22:26:44 +01:00
Meier Lukas c130e92661 chore(issue-template): add missing homarr version 1.3.1 2025-01-28 22:24:58 +01:00
Releases Homarr 0de853ff37 chore(release): 1.3.1 [skip ci] 2025-01-28 21:20:02 +00:00
homarr-releases[bot] a438a23f4e chore(release): automatic release v1.3.1 2025-01-28 21:19:19 +00:00
Manuel 8dd7fe297c fix: automatic search engine creation (#2172) 2025-01-28 22:18:31 +01:00
Manuel 780efab80c fix: app url line clamp (#2170) 2025-01-28 20:33:04 +01:00
homarr-renovate[bot] a4e9abcda1 fix(deps): update dependency swagger-ui-react to ^5.18.3 (#2164)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-28 13:00:40 +00:00
homarr-renovate[bot] 55b1afbb21 fix(deps): update tanstack-query monorepo to ^5.65.1 (#2161)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-28 09:17:32 +00:00
homarr-renovate[bot] b447fc9fe4 chore(deps): update dependency @types/node to ^22.12.0 (#2159)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-28 02:15:44 +00:00
homarr-crowdin[bot] 51ebaba399 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-28 00:44:31 +00:00
homarr-renovate[bot]andMeier Lukas 9d4aea2c41 fix(deps): update dependency typescript-eslint to ^8.22.0 (#2149)
* fix(deps): update dependency typescript-eslint to ^8.22.0

* fix: lint issues

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-27 22:32:23 +01:00
Meier Lukas 4dcdac6621 chore(issue-template): add missing homarr version 1.3.0 2025-01-27 21:22:18 +01:00
Releases Homarr 84bc242834 chore(release): 1.3.0 [skip ci] 2025-01-27 20:09:27 +00:00
homarr-releases[bot] f840f4c600 chore(release): automatic release v1.3.0 2025-01-27 20:08:24 +00:00
Manuel 5c8d7fcc15 feat: log body integration test failure (#2151) 2025-01-27 21:06:27 +01:00
Meier Lukas 93dc3ae97e fix: unable to update integration when encryption key changed (#2133) 2025-01-27 20:36:03 +01:00
Meier Lukas 7cb0aa70f1 feat(category): save collapse state for signed in users (#2134) 2025-01-27 20:34:50 +01:00
homarr-renovate[bot] 5c219a8b59 fix(deps): update testcontainers-node monorepo to ^10.17.2 (#2148)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:54:15 +00:00
homarr-renovate[bot] 50970e431d fix(deps): update tanstack-query monorepo to ^5.65.0 (#2146)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:33:14 +00:00
homarr-renovate[bot] af67bf26d6 fix(deps): update dependency drizzle-zod to ^0.7.0 (#2143)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:29:42 +00:00
homarr-renovate[bot] 0d5487366c fix(deps): update dependency drizzle-kit to ^0.30.3 (#2142)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:28:44 +00:00
homarr-renovate[bot] f0daef237c fix(deps): update dependency drizzle-orm to ^0.39.0 (#2144)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 11:53:35 +00:00
homarr-crowdin[bot] 54a62c49cf chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-27 00:45:54 +00:00
Meier Lukas 92f70f5a03 feat(user): add search in new tab preference (#2125) 2025-01-26 22:37:48 +01:00
Meier Lukas c43a2f0488 refactor: move zod import from validation package to zod (#2111)
* refactor: move zod import from validation package to zod

* refactor: move missing zod imports
2025-01-26 22:16:27 +01:00
Meier Lukas 1146c59a41 feat: add opacity for categories (#2132) 2025-01-26 21:26:42 +01:00
Meier Lukas af1f8eaf76 fix: websocket is not keep alive (#2131) 2025-01-26 21:26:21 +01:00
Meier Lukas dd2ca13004 fix: multi-text-input not saving on blur (#2128) 2025-01-26 21:26:08 +01:00
Meier Lukas b38de29a88 fix: app url not required (#2130) 2025-01-26 21:25:57 +01:00
Meier Lukas 2bb44e1666 fix: bookmarks show unauthorized when not logged in (#2123) 2025-01-26 21:25:04 +01:00
Meier Lukas 57b523fa80 fix: split up proxmox secrets are incorrectly migrated (#2124) 2025-01-26 21:24:50 +01:00
homarr-renovate[bot] ecbd2d14da chore(deps): update dependency @types/swagger-ui-react to ^4.19.0 (#2127)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-26 13:08:59 +00:00
homarr-renovate[bot] bbac4128eb fix(deps): update mantine monorepo to ^7.16.2 (#2129)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-26 12:56:37 +00:00
homarr-crowdin[bot]andMeier Lukas 5aaca9261f chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-26 10:58:49 +00:00
homarr-renovate[bot]andMeier Lukas 963d77463a fix(deps): update dependency @t3-oss/env-nextjs to ^0.12.0 (#2117)
* fix(deps): update dependency @t3-oss/env-nextjs to ^0.12.0

* fix: inconsistent tanstack versions

* fix: broken lock file

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-26 11:50:53 +01:00
Spillebulle b28274fa85 chore(lang): fix typo in english translation (#2112) 2025-01-25 16:34:56 +01:00
homarr-crowdin[bot] 3e85f4e44d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-25 00:42:53 +00:00
Meier Lukas 614f8077e1 chore(issue-template): add missing homarr version 1.2.0 2025-01-24 20:22:11 +01:00
homarr-renovate[bot] b36b5c1eb1 chore(deps): update dependency eslint to ^9.19.0 (#2104)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-24 20:19:35 +01:00
235 changed files with 19642 additions and 3619 deletions

No files matched your search

+1 -4
View File
@@ -32,7 +32,4 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# If it is used, please use the full path to the directory where the certificates are stored. # If it is used, please use the full path to the directory where the certificates are stored.
# LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES' # LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES'
TURBO_TELEMETRY_DISABLED=1 TURBO_TELEMETRY_DISABLED=1
# Configure logging to use winston logger
NODE_OPTIONS='-r @homarr/log/override'
+4
View File
@@ -31,6 +31,10 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.4.0
- 1.3.1
- 1.3.0
- 1.2.0
- 1.1.0 - 1.1.0
- 1.0.1 - 1.0.1
- 1.0.0 - 1.0.0
+80
View File
@@ -1,3 +1,83 @@
## [1.5.0](https://github.com/homarr-labs/homarr/compare/v1.4.0...v1.5.0) (2025-02-07)
### Features
* add api key delete ([#2210](https://github.com/homarr-labs/homarr/issues/2210)) ([8058b62](https://github.com/homarr-labs/homarr/commit/8058b6207ad4be1d646182b67b810c397097c054))
* custom time/date format in clock widget ([#2180](https://github.com/homarr-labs/homarr/issues/2180)) ([be603ba](https://github.com/homarr-labs/homarr/commit/be603badfed63ad7583a5265bef3b9e85f6b6b61))
* improve consistency and design ([#1867](https://github.com/homarr-labs/homarr/issues/1867)) ([5f36d8b](https://github.com/homarr-labs/homarr/commit/5f36d8b12528fd2c8c3da783b266d614203ec46a))
* open all apps in category ([#2212](https://github.com/homarr-labs/homarr/issues/2212)) ([bb67bf5](https://github.com/homarr-labs/homarr/commit/bb67bf5fa2968d91ad3522ebae960a6880f85987))
### Bug Fixes
* **deps:** update dependency @tabler/icons-react to ^3.30.0 ([#2266](https://github.com/homarr-labs/homarr/issues/2266)) ([c849109](https://github.com/homarr-labs/homarr/commit/c849109df5b622aca8cf7b392b94685581bf6b32))
* **deps:** update dependency drizzle-orm to ^0.39.2 ([#2255](https://github.com/homarr-labs/homarr/issues/2255)) ([3d0b319](https://github.com/homarr-labs/homarr/commit/3d0b319a88bdb8d955d7942553d92988bf5b2c28))
* **deps:** update dependency ioredis to v5.5.0 ([#2264](https://github.com/homarr-labs/homarr/issues/2264)) ([0da5a1b](https://github.com/homarr-labs/homarr/commit/0da5a1b0fb86ff2074f12a5e028fcc09ff94a4a5))
* **deps:** update dependency jotai to ^2.11.3 ([#2215](https://github.com/homarr-labs/homarr/issues/2215)) ([a385337](https://github.com/homarr-labs/homarr/commit/a3853372687aed543fad2dd89bfb13871af0f2c1))
* **deps:** update dependency octokit to ^4.1.1 ([#2262](https://github.com/homarr-labs/homarr/issues/2262)) ([3cbb656](https://github.com/homarr-labs/homarr/commit/3cbb656ee9b540d2dd4c7db37946efacbbfdc522))
* **deps:** update dependency sass to ^1.84.0 ([#2257](https://github.com/homarr-labs/homarr/issues/2257)) ([7bbbaf3](https://github.com/homarr-labs/homarr/commit/7bbbaf343befaaa3d03a91e932f683c5aec4b41e))
* **deps:** update dependency semver-parser to ^4.1.8 ([#2216](https://github.com/homarr-labs/homarr/issues/2216)) ([46cfb23](https://github.com/homarr-labs/homarr/commit/46cfb2383b6628472d3972da2166884330bf6d24))
* **deps:** update dependency trpc-to-openapi to ^2.1.3 ([#2244](https://github.com/homarr-labs/homarr/issues/2244)) ([c623a42](https://github.com/homarr-labs/homarr/commit/c623a420cadce677e39b99fceeb3f3a391f879ea))
* **deps:** update dependency typescript-eslint to ^8.23.0 ([#2232](https://github.com/homarr-labs/homarr/issues/2232)) ([83044d2](https://github.com/homarr-labs/homarr/commit/83044d23dd7accb1b3a21cdde1e034969a494ccb))
* **deps:** update testcontainers-node monorepo to ^10.18.0 ([#2259](https://github.com/homarr-labs/homarr/issues/2259)) ([7a8e671](https://github.com/homarr-labs/homarr/commit/7a8e67120576b333bfcf73171b54ccfeeda0d44a))
* **deps:** update turbo monorepo to ^2.4.0 ([#2207](https://github.com/homarr-labs/homarr/issues/2207)) ([6ce7bc0](https://github.com/homarr-labs/homarr/commit/6ce7bc0ee2dcd5150d72fdbc2070c05ba836ebd5))
* foreign key reference default search engine ([#2211](https://github.com/homarr-labs/homarr/issues/2211)) ([26468a4](https://github.com/homarr-labs/homarr/commit/26468a413795afa7cc3444058bfc73435fb7f2a0))
* make system health temp optional ([#2247](https://github.com/homarr-labs/homarr/issues/2247)) ([da09826](https://github.com/homarr-labs/homarr/commit/da098263930c9c9611a1505d52d3816bd2a49e7e))
## [1.4.0](https://github.com/homarr-labs/homarr/compare/v1.3.1...v1.4.0) (2025-01-31)
### Features
* weather widget - add wind speed, option to disable decimals for temperature & redesign dropdown forecast ([#2099](https://github.com/homarr-labs/homarr/issues/2099)) ([c19a400](https://github.com/homarr-labs/homarr/commit/c19a4009f37ec2aba8d4edd2d19819605ecb9575))
### Bug Fixes
* board crashes with empty string favicon ([#2192](https://github.com/homarr-labs/homarr/issues/2192)) ([bd0b7c9](https://github.com/homarr-labs/homarr/commit/bd0b7c9518af88e0a329ea6286cb5ef97098782a))
* **deps:** update dependency drizzle-kit to ^0.30.4 ([#2183](https://github.com/homarr-labs/homarr/issues/2183)) ([37ccfab](https://github.com/homarr-labs/homarr/commit/37ccfab89a7843c65d6f354bef2b0537b20ea757))
* **deps:** update dependency drizzle-orm to ^0.39.1 ([#2186](https://github.com/homarr-labs/homarr/issues/2186)) ([4c72a34](https://github.com/homarr-labs/homarr/commit/4c72a34daf06c5e00fc4ece6d990148c1cd6d87f))
* **deps:** update dependency jotai to ^2.11.2 ([#2201](https://github.com/homarr-labs/homarr/issues/2201)) ([c287e99](https://github.com/homarr-labs/homarr/commit/c287e9965f008b6ac6f3bac56057b94c412a3779))
* **deps:** update tanstack-query monorepo to ^5.66.0 ([#2202](https://github.com/homarr-labs/homarr/issues/2202)) ([e44c3a7](https://github.com/homarr-labs/homarr/commit/e44c3a7bc8be5b67d78096d5daa53ad23ccdea5d))
* **deps:** update tiptap monorepo to v2.11.4 ([#2195](https://github.com/homarr-labs/homarr/issues/2195)) ([c05c518](https://github.com/homarr-labs/homarr/commit/c05c51856baff5222b0abda56e1c11d9bc9bd6f9))
* **deps:** update tiptap monorepo to v2.11.5 ([#2203](https://github.com/homarr-labs/homarr/issues/2203)) ([9662e1a](https://github.com/homarr-labs/homarr/commit/9662e1abba3e105fd549d25ee0a0efc48cf293a1))
* **docker:** missing production env variable ([#2188](https://github.com/homarr-labs/homarr/issues/2188)) ([7774627](https://github.com/homarr-labs/homarr/commit/77746278a36f2b9d39c780eada25195121cc0803))
* negative curve radius system health ([#2197](https://github.com/homarr-labs/homarr/issues/2197)) ([c75d4f0](https://github.com/homarr-labs/homarr/commit/c75d4f0f58136a0499a51494eb54c78c745f0b3f))
* respect certificates for app ping ([#2198](https://github.com/homarr-labs/homarr/issues/2198)) ([d4154d2](https://github.com/homarr-labs/homarr/commit/d4154d259fa50bfd754e7ea98b872df78864e014))
* use hostname for suspense query url ([#2187](https://github.com/homarr-labs/homarr/issues/2187)) ([96fd5cc](https://github.com/homarr-labs/homarr/commit/96fd5cc35e9ca85aba48670860e6bf2a4a2caaa9))
## [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) ## [1.2.0](https://github.com/homarr-labs/homarr/compare/v1.1.0...v1.2.0) (2025-01-24)
### Features ### Features
+6 -1
View File
@@ -10,6 +10,8 @@ RUN apk add --no-cache libc6-compat curl bash
RUN apk update RUN apk update
COPY . . COPY . .
# Install working version of corepack (See https://github.com/nodejs/corepack/issues/612)
RUN npm install -g corepack@0.31.0 && corepack --version
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
# Copy static data as it is not part of the build # Copy static data as it is not part of the build
@@ -17,6 +19,8 @@ COPY static-data ./static-data
ARG SKIP_ENV_VALIDATION='true' ARG SKIP_ENV_VALIDATION='true'
ARG CI='true' ARG CI='true'
ARG DISABLE_REDIS_LOGS='true' ARG DISABLE_REDIS_LOGS='true'
# Install working version of corepack (See https://github.com/nodejs/corepack/issues/612)
RUN npm install -g corepack@0.31.0 && corepack --version
RUN corepack enable pnpm && pnpm build RUN corepack enable pnpm && pnpm build
FROM base AS runner FROM base AS runner
@@ -63,6 +67,7 @@ ENV DB_URL='/appdata/db/db.sqlite'
ENV DB_DIALECT='sqlite' ENV DB_DIALECT='sqlite'
ENV DB_DRIVER='better-sqlite3' ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials' ENV AUTH_PROVIDERS='credentials'
ENV NODE_ENV='production'
ENTRYPOINT [ "/app/entrypoint.sh" ] ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"] CMD ["sh", "run.sh"]
+21 -19
View File
@@ -36,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.1", "@mantine/colors-generator": "^7.16.2",
"@mantine/core": "^7.16.1", "@mantine/core": "^7.16.2",
"@mantine/dropzone": "^7.16.1", "@mantine/dropzone": "^7.16.2",
"@mantine/hooks": "^7.16.1", "@mantine/hooks": "^7.16.2",
"@mantine/modals": "^7.16.1", "@mantine/modals": "^7.16.2",
"@mantine/tiptap": "^7.16.1", "@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.29.0", "@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.64.2", "@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.64.2", "@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-query-next-experimental": "5.64.2", "@tanstack/react-query-next-experimental": "^5.66.0",
"@trpc/client": "next", "@trpc/client": "next",
"@trpc/next": "next", "@trpc/next": "next",
"@trpc/react-query": "next", "@trpc/react-query": "next",
@@ -66,7 +67,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
"glob": "^11.0.1", "glob": "^11.0.1",
"jotai": "^2.11.1", "jotai": "^2.11.3",
"mantine-react-table": "2.0.0-beta.8", "mantine-react-table": "2.0.0-beta.8",
"next": "15.1.6", "next": "15.1.6",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
@@ -75,23 +76,24 @@
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.83.4", "sass": "^1.84.0",
"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.1",
"@types/node": "^22.10.10", "@types/node": "^22.13.1",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.0.8", "@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": "^5.18.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"
@@ -18,8 +18,8 @@ import superjson from "superjson";
import type { SuperJSONResult } from "superjson"; import type { SuperJSONResult } from "superjson";
import type { AppRouter } from "@homarr/api"; import type { AppRouter } from "@homarr/api";
import { clientApi, getTrpcUrl } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { createHeadersCallbackForSource } from "@homarr/api/shared"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { env } from "~/env"; import { env } from "~/env";
@@ -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[];
@@ -7,6 +7,7 @@ import "~/styles/gridstack.scss";
import { IntegrationProvider } from "@homarr/auth/client"; import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server"; import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
import { isNullOrWhitespace } from "@homarr/common";
import { getI18n } from "@homarr/translation/server"; import { getI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
@@ -48,11 +49,13 @@ 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: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : undefined,
apple: board.faviconImageUrl ? board.faviconImageUrl : undefined, apple: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : undefined,
}, },
appleWebApp: { appleWebApp: {
startupImage: { url: board.faviconImageUrl ? board.faviconImageUrl : "/logo/logo.png" }, startupImage: {
url: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : "/logo/logo.png",
},
}, },
}; };
} catch (error) { } catch (error) {
@@ -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 = () => {
+19
View File
@@ -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} />,
@@ -1,5 +1,5 @@
.bannerContainer { .bannerContainer {
border-radius: 8px; border-radius: 16px;
overflow: hidden; overflow: hidden;
@mixin dark { @mixin dark {
background: linear-gradient( background: linear-gradient(
@@ -1,12 +1,12 @@
"use client"; "use client";
import { useRef } from "react";
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 { 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";
@@ -14,30 +14,49 @@ import { IconPicker } from "~/components/icons/picker/icon-picker";
type FormType = z.infer<typeof validation.app.manage>; type FormType = z.infer<typeof validation.app.manage>;
interface AppFormProps { interface AppFormProps {
submitButtonTranslation: (t: TranslationFunction) => string; buttonLabels: {
submit: string;
submitAndCreateAnother?: string;
};
initialValues?: FormType; initialValues?: FormType;
handleSubmit: (values: FormType) => void; handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void;
isPending: boolean; isPending: boolean;
} }
export const AppForm = (props: AppFormProps) => { export const AppForm = ({
const { submitButtonTranslation, handleSubmit, initialValues, isPending } = props; buttonLabels,
handleSubmit: originalHandleSubmit,
initialValues,
isPending,
}: 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 ?? "",
}, },
}); });
const shouldCreateAnother = useRef(false);
const handleSubmit = (values: FormType) => {
const redirect = !shouldCreateAnother.current;
const afterSuccess = shouldCreateAnother.current
? () => {
form.reset();
shouldCreateAnother.current = false;
}
: undefined;
originalHandleSubmit(values, redirect, afterSuccess);
};
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack> <Stack>
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} /> <TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} /> <IconPicker {...form.getInputProps("iconUrl")} />
<Textarea {...form.getInputProps("description")} label={t("app.field.description.label")} /> <Textarea {...form.getInputProps("description")} label={t("app.field.description.label")} />
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} /> <TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
@@ -45,8 +64,19 @@ export const AppForm = (props: AppFormProps) => {
<Button variant="default" component={Link} href="/manage/apps"> <Button variant="default" component={Link} href="/manage/apps">
{t("common.action.backToOverview")} {t("common.action.backToOverview")}
</Button> </Button>
{buttonLabels.submitAndCreateAnother && (
<Button
type="submit"
onClick={() => {
shouldCreateAnother.current = true;
}}
loading={isPending}
>
{buttonLabels.submitAndCreateAnother}
</Button>
)}
<Button type="submit" loading={isPending}> <Button type="submit" loading={isPending}>
{submitButtonTranslation(t)} {buttonLabels.submit}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -2,14 +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 type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
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 { useI18n, useScopedI18n } from "@homarr/translation/client";
import { useScopedI18n } from "@homarr/translation/client"; import type { validation } from "@homarr/validation";
import type { validation, z } from "@homarr/validation";
import { AppForm } from "../../_form"; import { AppForm } from "../../_form";
@@ -18,14 +18,15 @@ interface AppEditFormProps {
} }
export const AppEditForm = ({ app }: AppEditFormProps) => { export const AppEditForm = ({ app }: AppEditFormProps) => {
const t = useScopedI18n("app.page.edit.notification"); const tScoped = useScopedI18n("app.page.edit.notification");
const t = useI18n();
const router = useRouter(); const router = useRouter();
const { mutate, isPending } = clientApi.app.update.useMutation({ const { mutate, isPending } = clientApi.app.update.useMutation({
onSuccess: () => { onSuccess: () => {
showSuccessNotification({ showSuccessNotification({
title: t("success.title"), title: tScoped("success.title"),
message: t("success.message"), message: tScoped("success.message"),
}); });
void revalidatePathActionAsync("/manage/apps").then(() => { void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps"); router.push("/manage/apps");
@@ -33,8 +34,8 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
}, },
onError: () => { onError: () => {
showErrorNotification({ showErrorNotification({
title: t("error.title"), title: tScoped("error.title"),
message: t("error.message"), message: tScoped("error.message"),
}); });
}, },
}); });
@@ -49,11 +50,11 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
[mutate, app.id], [mutate, app.id],
); );
const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.save"), []);
return ( return (
<AppForm <AppForm
submitButtonTranslation={submitButtonTranslation} buttonLabels={{
submit: t("common.action.save"),
}}
initialValues={app} initialValues={app}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
isPending={isPending} isPending={isPending}
@@ -2,48 +2,60 @@
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 { useI18n, useScopedI18n } from "@homarr/translation/client";
import { useScopedI18n } from "@homarr/translation/client"; import type { validation } from "@homarr/validation";
import type { validation, z } from "@homarr/validation";
import { AppForm } from "../_form"; import { AppForm } from "../_form";
export const AppNewForm = () => { export const AppNewForm = () => {
const t = useScopedI18n("app.page.create.notification"); const tScoped = useScopedI18n("app.page.create.notification");
const t = useI18n();
const router = useRouter(); const router = useRouter();
const { mutate, isPending } = clientApi.app.create.useMutation({ const { mutate, isPending } = clientApi.app.create.useMutation({
onSuccess: () => {
showSuccessNotification({
title: t("success.title"),
message: t("success.message"),
});
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
onError: () => { onError: () => {
showErrorNotification({ showErrorNotification({
title: t("error.title"), title: tScoped("error.title"),
message: t("error.message"), message: tScoped("error.message"),
}); });
}, },
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => { (values: z.infer<typeof validation.app.manage>, redirect: boolean, afterSuccess?: () => void) => {
mutate(values); mutate(values, {
}, onSuccess() {
[mutate], showSuccessNotification({
); title: tScoped("success.title"),
message: tScoped("success.message"),
});
afterSuccess?.();
const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.create"), []); if (!redirect) {
return;
}
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
});
},
[mutate, router, tScoped],
);
return ( return (
<AppForm submitButtonTranslation={submitButtonTranslation} handleSubmit={handleSubmit} isPending={isPending} /> <AppForm
buttonLabels={{
submit: t("common.action.create"),
submitAndCreateAnother: t("common.action.createAnother"),
}}
handleSubmit={handleSubmit}
isPending={isPending}
/>
); );
}; };
@@ -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";
@@ -45,7 +45,7 @@ export default async function AppsPage(props: AppsPageProps) {
<Stack> <Stack>
<Title>{t("page.list.title")}</Title> <Title>{t("page.list.title")}</Title>
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} /> <SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} flexExpand />
{session.user.permissions.includes("app-create") && ( {session.user.permissions.includes("app-create") && (
<MobileAffixButton component={Link} href="/manage/apps/new"> <MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")} {t("page.create.title")}
@@ -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>
)} )}
@@ -67,7 +67,7 @@ const BoardCard = async ({ board }: BoardCardProps) => {
const VisibilityIcon = board.isPublic ? IconWorld : IconLock; const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
return ( return (
<Card withBorder> <Card radius="lg" withBorder>
<CardSection p="sm" withBorder> <CardSection p="sm" withBorder>
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Group gap="sm"> <Group gap="sm">
@@ -106,15 +106,25 @@ const BoardCard = async ({ board }: BoardCardProps) => {
</Group> </Group>
</CardSection> </CardSection>
<CardSection p="sm"> <CardSection>
<Group wrap="nowrap"> <Group gap={0} wrap="nowrap">
<Button component={Link} href={`/boards/${board.name}`} variant="default" fullWidth> <Button
style={{ border: "none", borderRadius: 0 }}
component={Link}
href={`/boards/${board.name}`}
variant="default"
fullWidth
>
{t("action.open.label")} {t("action.open.label")}
</Button> </Button>
{isMenuVisible && ( {isMenuVisible && (
<Menu position="bottom-end"> <Menu position="bottom-end">
<MenuTarget> <MenuTarget>
<ActionIcon variant="default" size="lg"> <ActionIcon
style={{ borderTop: "none", borderBottom: "none", borderRight: "none", borderRadius: 0 }}
variant="default"
size="lg"
>
<IconDotsVertical size={16} stroke={1.5} /> <IconDotsVertical size={16} stroke={1.5} />
</ActionIcon> </ActionIcon>
</MenuTarget> </MenuTarget>
@@ -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;
} }
@@ -33,6 +33,7 @@ export const IntegrationCreateDropdownContent = () => {
value={search} value={search}
data-autofocus data-autofocus
onChange={handleSearch} onChange={handleSearch}
variant="filled"
/> />
{filteredKinds.length > 0 ? ( {filteredKinds.length > 0 ? (
@@ -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";
@@ -0,0 +1,36 @@
.root {
border-radius: var(--mantine-radius-lg);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
.item {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
border: 1px solid transparent;
position: relative;
z-index: 0;
transition: transform 150ms ease;
overflow: hidden;
&[data-first="true"] {
border-radius: var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0;
}
&[data-last="true"] {
border-radius: 0 0 var(--mantine-radius-lg) var(--mantine-radius-lg);
}
&[data-active] {
transform: scale(1.01);
z-index: 1;
background-color: var(--mantine-color-body);
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
box-shadow: var(--mantine-shadow-md);
border-radius: var(--mantine-radius-lg);
}
}
.chevron {
&[data-rotate] {
transform: rotate(180deg);
}
}
@@ -44,6 +44,7 @@ import { NoResults } from "~/components/no-results";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion"; import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { DeleteIntegrationActionButton } from "./_integration-buttons"; import { DeleteIntegrationActionButton } from "./_integration-buttons";
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown"; import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
import classes from "./page.module.css";
interface IntegrationsPageProps { interface IntegrationsPageProps {
searchParams: Promise<{ searchParams: Promise<{
@@ -133,7 +134,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
return <NoResults icon={IconPlugX} title={t("page.list.noResults.title")} />; return <NoResults icon={IconPlugX} title={t("page.list.noResults.title")} />;
} }
const grouppedIntegrations = integrations.reduce( const groupedIntegrations = integrations.reduce(
(acc, integration) => { (acc, integration) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!acc[integration.kind]) { if (!acc[integration.kind]) {
@@ -147,11 +148,13 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
{} as Record<IntegrationKind, RouterOutputs["integration"]["all"]>, {} as Record<IntegrationKind, RouterOutputs["integration"]["all"]>,
); );
const entries = objectEntries(groupedIntegrations);
return ( return (
<ActiveTabAccordion defaultValue={activeTab} variant="separated"> <ActiveTabAccordion defaultValue={activeTab} radius="lg" classNames={classes}>
{objectEntries(grouppedIntegrations).map(([kind, integrations]) => ( {entries.map(([kind, integrations], index) => (
<AccordionItem key={kind} value={kind}> <AccordionItem key={kind} value={kind} data-first={index === 0} data-last={index === entries.length - 1}>
<AccordionControl icon={<IntegrationAvatar size="sm" kind={kind} />}> <AccordionControl icon={<IntegrationAvatar size="sm" kind={kind} radius="sm" />}>
<Group> <Group>
<Text>{getIntegrationName(kind)}</Text> <Text>{getIntegrationName(kind)}</Text>
<CountBadge count={integrations.length} /> <CountBadge count={integrations.length} />
+30 -23
View File
@@ -1,26 +1,26 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { AppShellMain } from "@mantine/core"; import { AppShellMain } from "@mantine/core";
import { import {
IconAffiliateFilled,
IconBook2, IconBook2,
IconBox, IconBox,
IconBrandDiscord, IconBrandDiscord,
IconBrandDocker, IconBrandDocker,
IconBrandGithub, IconBrandGithub,
IconBrandTablerFilled,
IconCertificate, IconCertificate,
IconClipboardListFilled,
IconDirectionsFilled,
IconGitFork, IconGitFork,
IconHome, IconHelpSquareRoundedFilled,
IconInfoSmall, IconHomeFilled,
IconLayoutDashboard, IconLayoutDashboardFilled,
IconLogs,
IconMailForward, IconMailForward,
IconPhoto, IconPhotoFilled,
IconPlug, IconPointerFilled,
IconQuestionMark,
IconReport,
IconSearch, IconSearch,
IconSettings, IconSettingsFilled,
IconTool, IconUserFilled,
IconUser,
IconUsers, IconUsers,
IconUsersGroup, IconUsersGroup,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@@ -31,6 +31,7 @@ import { createDocumentationLink } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { MainHeader } from "~/components/layout/header"; import { MainHeader } from "~/components/layout/header";
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
import type { NavigationLink } from "~/components/layout/navigation"; import type { NavigationLink } from "~/components/layout/navigation";
import { MainNavigation } from "~/components/layout/navigation"; import { MainNavigation } from "~/components/layout/navigation";
import { ClientShell } from "~/components/layout/shell"; import { ClientShell } from "~/components/layout/shell";
@@ -41,11 +42,11 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
const navigationLinks: NavigationLink[] = [ const navigationLinks: NavigationLink[] = [
{ {
label: t("items.home"), label: t("items.home"),
icon: IconHome, icon: IconHomeFilled,
href: "/manage", href: "/manage",
}, },
{ {
icon: IconLayoutDashboard, icon: IconLayoutDashboardFilled,
href: "/manage/boards", href: "/manage/boards",
label: t("items.boards"), label: t("items.boards"),
}, },
@@ -54,9 +55,12 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
href: "/manage/apps", href: "/manage/apps",
label: t("items.apps"), label: t("items.apps"),
hidden: !session, hidden: !session,
iconProps: {
strokeWidth: 2.5,
},
}, },
{ {
icon: IconPlug, icon: IconAffiliateFilled,
href: "/manage/integrations", href: "/manage/integrations",
label: t("items.integrations"), label: t("items.integrations"),
hidden: !session, hidden: !session,
@@ -66,15 +70,18 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
href: "/manage/search-engines", href: "/manage/search-engines",
label: t("items.searchEngies"), label: t("items.searchEngies"),
hidden: !session, hidden: !session,
iconProps: {
strokeWidth: 2.5,
},
}, },
{ {
icon: IconPhoto, icon: IconPhotoFilled,
href: "/manage/medias", href: "/manage/medias",
label: t("items.medias"), label: t("items.medias"),
hidden: !session, hidden: !session,
}, },
{ {
icon: IconUser, icon: IconUserFilled,
label: t("items.users.label"), label: t("items.users.label"),
hidden: !session?.user.permissions.includes("admin"), hidden: !session?.user.permissions.includes("admin"),
items: [ items: [
@@ -98,7 +105,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
}, },
{ {
label: t("items.tools.label"), label: t("items.tools.label"),
icon: IconTool, icon: IconPointerFilled,
// As permissions always include there children permissions, we can check other-view-logs as admin includes it // As permissions always include there children permissions, we can check other-view-logs as admin includes it
hidden: !session?.user.permissions.includes("other-view-logs"), hidden: !session?.user.permissions.includes("other-view-logs"),
items: [ items: [
@@ -110,13 +117,13 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
}, },
{ {
label: t("items.tools.items.api"), label: t("items.tools.items.api"),
icon: IconPlug, icon: IconDirectionsFilled,
href: "/manage/tools/api", href: "/manage/tools/api",
hidden: !session?.user.permissions.includes("admin"), hidden: !session?.user.permissions.includes("admin"),
}, },
{ {
label: t("items.tools.items.logs"), label: t("items.tools.items.logs"),
icon: IconLogs, icon: IconBrandTablerFilled,
href: "/manage/tools/logs", href: "/manage/tools/logs",
hidden: !session?.user.permissions.includes("other-view-logs"), hidden: !session?.user.permissions.includes("other-view-logs"),
}, },
@@ -128,7 +135,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
}, },
{ {
label: t("items.tools.items.tasks"), label: t("items.tools.items.tasks"),
icon: IconReport, icon: IconClipboardListFilled,
href: "/manage/tools/tasks", href: "/manage/tools/tasks",
hidden: !session?.user.permissions.includes("admin"), hidden: !session?.user.permissions.includes("admin"),
}, },
@@ -137,12 +144,12 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{ {
label: t("items.settings"), label: t("items.settings"),
href: "/manage/settings", href: "/manage/settings",
icon: IconSettings, icon: IconSettingsFilled,
hidden: !session?.user.permissions.includes("admin"), hidden: !session?.user.permissions.includes("admin"),
}, },
{ {
label: t("items.help.label"), label: t("items.help.label"),
icon: IconQuestionMark, icon: IconHelpSquareRoundedFilled,
items: [ items: [
{ {
label: t("items.help.items.documentation"), label: t("items.help.items.documentation"),
@@ -172,7 +179,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
}, },
{ {
label: t("items.about"), label: t("items.about"),
icon: IconInfoSmall, icon: homarrLogoPath,
href: "/manage/about", href: "/manage/about",
}, },
]; ];
@@ -16,6 +16,7 @@ import {
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react"; 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";
@@ -25,7 +26,6 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { createLocalImageUrl } from "@homarr/icons/local"; 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";
@@ -61,7 +61,7 @@ export default async function GroupsListPage(props: MediaListPageProps) {
const { items: medias, totalCount } = await api.media.getPaginated(searchParams); const { items: medias, totalCount } = await api.media.getPaginated(searchParams);
return ( return (
<ManageContainer size="xl"> <ManageContainer>
<DynamicBreadcrumb /> <DynamicBreadcrumb />
<Stack> <Stack>
<Title>{t("media.plural")}</Title> <Title>{t("media.plural")}</Title>
+1 -1
View File
@@ -83,7 +83,7 @@ export default async function ManagementPage() {
{links.map( {links.map(
(link) => (link) =>
!link.hidden && ( !link.hidden && (
<Card component={Link} href={link.href} key={link.href} withBorder> <Card component={Link} href={link.href} key={link.href} radius="lg">
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
<Group wrap="nowrap"> <Group wrap="nowrap">
<Text size="2.4rem" fw="bolder"> <Text size="2.4rem" fw="bolder">
@@ -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";
@@ -58,7 +58,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
/> />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} /> <IconPicker {...form.getInputProps("iconUrl")} />
<Fieldset legend={t("search.engine.page.edit.configControl")}> <Fieldset legend={t("search.engine.page.edit.configControl")}>
<SegmentedControl <SegmentedControl
@@ -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";
@@ -45,7 +45,7 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
<Stack> <Stack>
<Title>{tEngine("page.list.title")}</Title> <Title>{tEngine("page.list.title")}</Title>
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<SearchInput placeholder={`${tEngine("search")}...`} defaultValue={searchParams.search} /> <SearchInput placeholder={`${tEngine("search")}...`} defaultValue={searchParams.search} flexExpand />
{session.user.permissions.includes("search-engine-create") && ( {session.user.permissions.includes("search-engine-create") && (
<MobileAffixButton component={Link} href="/manage/search-engines/new"> <MobileAffixButton component={Link} href="/manage/search-engines/new">
{tEngine("page.create.title")} {tEngine("page.create.title")}
@@ -1,14 +1,15 @@
"use client"; "use client";
import { useMemo } from "react"; import { useCallback, useMemo } from "react";
import { Button, Group, Stack, Text, Title } from "@mantine/core"; import { ActionIcon, Button, Group, Stack, Text, Title } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table"; import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable, useMantineReactTable } from "mantine-react-table"; import { MantineReactTable, useMantineReactTable } 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 { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui"; import { UserAvatar } from "@homarr/ui";
@@ -20,7 +21,8 @@ interface ApiKeysManagementProps {
export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => { export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
const { openModal } = useModalAction(CopyApiKeyModal); const { openModal } = useModalAction(CopyApiKeyModal);
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({ const { openConfirmModal } = useConfirmModal();
const { mutate: mutateCreate, isPending: isPendingCreate } = clientApi.apiKeys.create.useMutation({
async onSuccess(data) { async onSuccess(data) {
openModal({ openModal({
apiKey: data.apiKey, apiKey: data.apiKey,
@@ -28,7 +30,26 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
await revalidatePathActionAsync("/manage/tools/api"); await revalidatePathActionAsync("/manage/tools/api");
}, },
}); });
const { mutateAsync: mutateDeleteAsync, isPending: isPendingDelete } = clientApi.apiKeys.delete.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/api");
},
});
const t = useScopedI18n("management.page.tool.api.tab.apiKey"); const t = useScopedI18n("management.page.tool.api.tab.apiKey");
const handleDelete = useCallback(
(id: string) => {
openConfirmModal({
title: t("modal.delete.title"),
children: t("modal.delete.text"),
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await mutateDeleteAsync({ apiKeyId: id });
},
});
},
[t, openConfirmModal, mutateDeleteAsync],
);
const columns = useMemo<MRT_ColumnDef<RouterOutputs["apiKeys"]["getAll"][number]>[]>( const columns = useMemo<MRT_ColumnDef<RouterOutputs["apiKeys"]["getAll"][number]>[]>(
() => [ () => [
@@ -46,8 +67,18 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
</Group> </Group>
), ),
}, },
{
header: t("table.header.actions"),
Cell: ({ row }) => (
<Group gap="xs">
<ActionIcon onClick={() => handleDelete(row.original.id)} loading={isPendingDelete} c="red">
<IconTrash size="1rem" />
</ActionIcon>
</Group>
),
},
], ],
[t], [t, handleDelete, isPendingDelete],
); );
const table = useMantineReactTable({ const table = useMantineReactTable({
@@ -56,9 +87,9 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
renderTopToolbarCustomActions: () => ( renderTopToolbarCustomActions: () => (
<Button <Button
onClick={() => { onClick={() => {
mutate(); mutateCreate();
}} }}
loading={isPending} loading={isPendingCreate}
> >
{t("button.createApiToken")} {t("button.createApiToken")}
</Button> </Button>
@@ -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);
@@ -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 {
@@ -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>;
@@ -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);
@@ -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 {
@@ -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";
@@ -102,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">
@@ -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";
+2 -1
View File
@@ -1,6 +1,7 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter, createTRPCContext } from "@homarr/api"; import { appRouter, createTRPCContext } from "@homarr/api";
import { trpcPath } from "@homarr/api/shared";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
@@ -25,7 +26,7 @@ export function OPTIONS() {
const handler = auth(async (req) => { const handler = auth(async (req) => {
const response = await fetchRequestHandler({ const response = await fetchRequestHandler({
endpoint: "/api/trpc", endpoint: trpcPath,
router: appRouter, router: appRouter,
req, req,
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }), createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
@@ -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[];
}; };
@@ -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;
@@ -1,5 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { fetchApi } from "@homarr/api/client";
import { createId } from "@homarr/db/client"; import { createId } from "@homarr/db/client";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
@@ -7,6 +8,7 @@ import { useI18n } from "@homarr/translation/client";
import type { CategorySection } from "~/app/[locale]/boards/_types"; import type { CategorySection } from "~/app/[locale]/boards/_types";
import { useCategoryActions } from "./category-actions"; import { useCategoryActions } from "./category-actions";
import { CategoryEditModal } from "./category-edit-modal"; import { CategoryEditModal } from "./category-edit-modal";
import { filterByItemKind } from "./filter";
export const useCategoryMenuActions = (category: CategorySection) => { export const useCategoryMenuActions = (category: CategorySection) => {
const { openModal } = useModalAction(CategoryEditModal); const { openModal } = useModalAction(CategoryEditModal);
@@ -97,6 +99,28 @@ export const useCategoryMenuActions = (category: CategorySection) => {
); );
}, [category, openModal, renameCategory, t]); }, [category, openModal, renameCategory, t]);
const openAllInNewTabs = useCallback(async () => {
const appIds = filterByItemKind(category.items, "app").map((item) => {
return item.options.appId;
});
const apps = await fetchApi.app.byIds.query(appIds);
const appsWithUrls = apps.filter((app) => app.href && app.href.length > 0);
for (const app of appsWithUrls) {
const openedWindow = window.open(app.href ?? undefined);
if (openedWindow) {
continue;
}
openConfirmModal({
title: t("section.category.openAllInNewTabs.title"),
children: t("section.category.openAllInNewTabs.text"),
});
break;
}
}, [category, t, openConfirmModal]);
return { return {
addCategoryAbove, addCategoryAbove,
addCategoryBelow, addCategoryBelow,
@@ -104,5 +128,6 @@ export const useCategoryMenuActions = (category: CategorySection) => {
moveCategoryDown, moveCategoryDown,
remove, remove,
edit, edit,
openAllInNewTabs,
}; };
}; };
@@ -5,6 +5,7 @@ import { ActionIcon, Menu } from "@mantine/core";
import { import {
IconDotsVertical, IconDotsVertical,
IconEdit, IconEdit,
IconExternalLink,
IconRowInsertBottom, IconRowInsertBottom,
IconRowInsertTop, IconRowInsertTop,
IconTransitionBottom, IconTransitionBottom,
@@ -12,6 +13,7 @@ import {
IconTrash, IconTrash,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import type { MaybePromise } from "@homarr/common/types";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui"; import type { TablerIcon } from "@homarr/ui";
@@ -27,8 +29,6 @@ export const CategoryMenu = ({ category }: Props) => {
const actions = useActions(category); const actions = useActions(category);
const t = useScopedI18n("section.category"); const t = useScopedI18n("section.category");
if (actions.length === 0) return null;
return ( return (
<Menu withArrow> <Menu withArrow>
<Menu.Target> <Menu.Target>
@@ -37,18 +37,20 @@ export const CategoryMenu = ({ category }: Props) => {
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
{actions.map((action) => ( {actions.map((action) => {
<React.Fragment key={action.label}> return (
{"group" in action && <Menu.Label>{t(action.group)}</Menu.Label>} <React.Fragment key={action.label}>
<Menu.Item {"group" in action && <Menu.Label>{t(action.group)}</Menu.Label>}
leftSection={<action.icon size="1rem" />} <Menu.Item
onClick={action.onClick} leftSection={<action.icon size="1rem" />}
color={"color" in action ? action.color : undefined} onClick={action.onClick}
> color={"color" in action ? action.color : undefined}
{t(action.label)} >
</Menu.Item> {t(action.label)}
</React.Fragment> </Menu.Item>
))} </React.Fragment>
);
})}
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
); );
@@ -106,15 +108,21 @@ const useEditModeActions = (category: CategorySection) => {
] as const satisfies ActionDefinition[]; ] as const satisfies ActionDefinition[];
}; };
// TODO: once apps are added we can use this for the open many apps action const useNonEditModeActions = (category: CategorySection) => {
const useNonEditModeActions = (_category: CategorySection) => { const { openAllInNewTabs } = useCategoryMenuActions(category);
return [] as const satisfies ActionDefinition[]; return [
{
icon: IconExternalLink,
label: "action.openAllInNewTabs",
onClick: openAllInNewTabs,
},
] as const satisfies ActionDefinition[];
}; };
interface ActionDefinition { interface ActionDefinition {
icon: TablerIcon; icon: TablerIcon;
label: string; label: string;
onClick: () => void; onClick: () => MaybePromise<void>;
color?: string; color?: string;
group?: string; group?: string;
} }
@@ -0,0 +1,14 @@
import type { WidgetKind } from "@homarr/definitions";
import type { WidgetComponentProps } from "@homarr/widgets";
import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets";
import type { Item } from "~/app/[locale]/boards/_types";
export const filterByItemKind = <TKind extends WidgetKind>(items: Item[], kind: TKind) => {
return items
.filter((item) => item.kind === kind)
.map((item) => ({
...item,
options: reduceWidgetOptionsWithDefaultValues(kind, item.options) as WidgetComponentProps<TKind>["options"],
}));
};
@@ -1,5 +1,5 @@
import type { FocusEventHandler } from "react"; import type { FocusEventHandler } from "react";
import { startTransition, useState } from "react"; import { startTransition } from "react";
import { import {
ActionIcon, ActionIcon,
Box, Box,
@@ -17,7 +17,7 @@ import {
UnstyledButton, UnstyledButton,
useCombobox, useCombobox,
} from "@mantine/core"; } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue, useUncontrolled } from "@mantine/hooks";
import { IconUpload } from "@tabler/icons-react"; import { IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -28,17 +28,27 @@ import { UploadMedia } from "~/app/[locale]/manage/medias/_actions/upload-media"
import classes from "./icon-picker.module.css"; import classes from "./icon-picker.module.css";
interface IconPickerProps { interface IconPickerProps {
initialValue?: string; value?: string;
onChange: (iconUrl: string) => void; onChange: (iconUrl: string) => void;
error?: string | null; error?: string | null;
onFocus?: FocusEventHandler; onFocus?: FocusEventHandler;
onBlur?: FocusEventHandler; onBlur?: FocusEventHandler;
} }
export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: IconPickerProps) => { export const IconPicker = ({ value: propsValue, onChange, error, onFocus, onBlur }: IconPickerProps) => {
const [value, setValue] = useState<string>(initialValue ?? ""); const [value, setValue] = useUncontrolled({
const [search, setSearch] = useState(initialValue ?? ""); value: propsValue,
const [previewUrl, setPreviewUrl] = useState<string | null>(initialValue ?? null); onChange,
});
const [search, setSearch] = useUncontrolled({
value,
onChange: (value) => {
setValue(value);
},
});
const [previewUrl, setPreviewUrl] = useUncontrolled({
value: propsValue ?? null,
});
const { data: session } = useSession(); const { data: session } = useSession();
const tCommon = useScopedI18n("common"); const tCommon = useScopedI18n("common");
@@ -68,10 +78,9 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
onClick={() => { onClick={() => {
const value = item.url; const value = item.url;
startTransition(() => { startTransition(() => {
setValue(value);
setPreviewUrl(value); setPreviewUrl(value);
setSearch(value); setSearch(value);
onChange(value); setValue(value);
combobox.closeDropdown(); combobox.closeDropdown();
}); });
}} }}
@@ -128,7 +137,6 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
setSearch(event.currentTarget.value); setSearch(event.currentTarget.value);
setValue(event.currentTarget.value); setValue(event.currentTarget.value);
setPreviewUrl(null); setPreviewUrl(null);
onChange(event.currentTarget.value);
}} }}
onClick={() => combobox.openDropdown()} onClick={() => combobox.openDropdown()}
onFocus={(event) => { onFocus={(event) => {
@@ -154,7 +162,6 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
setValue(url); setValue(url);
setPreviewUrl(url); setPreviewUrl(url);
setSearch(url); setSearch(url);
onChange(url);
}); });
}} }}
> >
@@ -20,6 +20,7 @@ export const DesktopSearchInput = () => {
size="sm" size="sm"
leftSection={<IconSearch size={20} stroke={1.5} />} leftSection={<IconSearch size={20} stroke={1.5} />}
onClick={openSpotlight} onClick={openSpotlight}
radius="xl"
> >
{`${t("search.placeholder")}...`} {`${t("search.placeholder")}...`}
</TextInput> </TextInput>
@@ -1,7 +1,7 @@
import type { JSX } from "react"; import type { JSX } from "react";
import { AppShellNavbar, AppShellSection, ScrollArea } from "@mantine/core"; import { AppShellNavbar, AppShellSection, Image, ScrollArea } from "@mantine/core";
import type { TablerIcon } from "@homarr/ui"; import type { TablerIcon, TablerIconProps } from "@homarr/ui";
import type { ClientNavigationLink } from "./navigation-link"; import type { ClientNavigationLink } from "./navigation-link";
import { CommonNavLink } from "./navigation-link"; import { CommonNavLink } from "./navigation-link";
@@ -27,8 +27,13 @@ export const MainNavigation = ({ headerSection, footerSection, links }: MainNavi
return null; return null;
} }
const { icon: TablerIcon, ...props } = link; const { icon: TablerIcon, iconProps, ...props } = link;
const Icon = <TablerIcon size={20} stroke={1.5} />; const Icon =
typeof TablerIcon === "string" ? (
<Image src={TablerIcon} w={20} h={20} />
) : (
<TablerIcon size={20} stroke={1.5} {...iconProps} />
);
let clientLink: ClientNavigationLink; let clientLink: ClientNavigationLink;
if ("items" in props) { if ("items" in props) {
clientLink = { clientLink = {
@@ -38,7 +43,7 @@ export const MainNavigation = ({ headerSection, footerSection, links }: MainNavi
.map((item) => { .map((item) => {
return { return {
...item, ...item,
icon: <item.icon size={20} stroke={1.5} />, icon: <item.icon size={20} stroke={1.5} {...iconProps} />,
}; };
}), }),
} as ClientNavigationLink; } as ClientNavigationLink;
@@ -55,7 +60,8 @@ export const MainNavigation = ({ headerSection, footerSection, links }: MainNavi
interface CommonNavigationLinkProps { interface CommonNavigationLinkProps {
label: string; label: string;
icon: TablerIcon; icon: TablerIcon | string;
iconProps?: TablerIconProps;
hidden?: boolean; hidden?: boolean;
} }
+2 -2
View File
@@ -4,7 +4,7 @@ import { createTRPCClient, httpLink } from "@trpc/client";
import SuperJSON from "superjson"; import SuperJSON from "superjson";
import type { AppRouter } from "@homarr/api"; import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource } from "@homarr/api/shared"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { createI18nMiddleware } from "@homarr/translation/middleware"; import { createI18nMiddleware } from "@homarr/translation/middleware";
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
@@ -34,7 +34,7 @@ export const config = {
export const serverFetchApi = createTRPCClient<AppRouter>({ export const serverFetchApi = createTRPCClient<AppRouter>({
links: [ links: [
httpLink({ httpLink({
url: `http://${process.env.HOSTNAME ?? "localhost"}:3000/api/trpc`, url: getTrpcUrl(),
transformer: SuperJSON, transformer: SuperJSON,
headers: createHeadersCallbackForSource("server-fetch"), headers: createHeadersCallbackForSource("server-fetch"),
}), }),
+2 -2
View File
@@ -44,9 +44,9 @@
"@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.10", "@types/node": "^22.13.1",
"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"
+1 -1
View File
@@ -34,7 +34,7 @@
"@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.14", "@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"
} }
+8
View File
@@ -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) => {
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.2.0", "version": "1.5.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -38,22 +38,22 @@
"@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.4", "@turbo/gen": "^2.4.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.4", "@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.4", "@vitest/ui": "^3.0.5",
"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.17.1", "testcontainers": "^10.18.0",
"turbo": "^2.3.4", "turbo": "^2.4.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4" "vitest": "^3.0.5"
}, },
"packageManager": "pnpm@9.15.4", "packageManager": "pnpm@9.15.5",
"engines": { "engines": {
"node": ">=22.13.1" "node": ">=22.13.1"
}, },
+1 -1
View File
@@ -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"
} }
} }
+4 -2
View File
@@ -45,16 +45,18 @@
"@trpc/server": "next", "@trpc/server": "next",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.1.6", "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.3",
"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",
"eslint": "^9.18.0", "eslint": "^9.19.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
+1 -14
View File
@@ -5,7 +5,7 @@ import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson"; import SuperJSON from "superjson";
import type { AppRouter } from "."; import type { AppRouter } from ".";
import { createHeadersCallbackForSource } from "./shared"; import { createHeadersCallbackForSource, getTrpcUrl } from "./shared";
export const clientApi = createTRPCReact<AppRouter>(); export const clientApi = createTRPCReact<AppRouter>();
export const fetchApi = createTRPCClient<AppRouter>({ export const fetchApi = createTRPCClient<AppRouter>({
@@ -17,16 +17,3 @@ export const fetchApi = createTRPCClient<AppRouter>({
}), }),
], ],
}); });
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
/**
* Creates the full url for the trpc api endpoint
* @returns
*/
export function getTrpcUrl() {
return `${getBaseUrl()}/api/trpc`;
}
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+2
View File
@@ -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,
+9 -1
View File
@@ -1,6 +1,8 @@
import { z } from "zod";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { generateSecureRandomToken } from "@homarr/common/server"; import { generateSecureRandomToken } from "@homarr/common/server";
import { createId, db } from "@homarr/db"; import { createId, db, eq } from "@homarr/db";
import { apiKeys } from "@homarr/db/schema"; import { apiKeys } from "@homarr/db/schema";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
@@ -39,4 +41,10 @@ export const apiKeysRouter = createTRPCRouter({
apiKey: `${id}.${randomToken}`, apiKey: `${id}.${randomToken}`,
}; };
}), }),
delete: permissionRequiredProcedure
.requiresPermission("admin")
.input(z.object({ apiKeyId: z.string() }))
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(apiKeys).where(eq(apiKeys.id, input.apiKeyId)).limit(1);
}),
}); });
+3 -2
View File
@@ -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),
}); });
+8 -2
View File
@@ -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,4 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod";
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";
@@ -6,7 +7,6 @@ import { DockerSingleton } from "@homarr/docker";
import type { Container, ContainerInfo, ContainerState, Docker, Port } 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";
+2 -1
View File
@@ -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 -1
View File
@@ -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";
+2 -1
View File
@@ -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";
@@ -194,6 +195,12 @@ export const searchEngineRouter = createTRPCRouter({
.requiresPermission("search-engine-full-all") .requiresPermission("search-engine-full-all")
.input(validation.common.byId) .input(validation.common.byId)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db
.update(users)
.set({
defaultSearchEngineId: null,
})
.where(eq(users.defaultSearchEngineId, input.id));
await ctx.db.delete(searchEngines).where(eq(searchEngines.id, input.id)); await ctx.db.delete(searchEngines).where(eq(searchEngines.id, input.id));
}), }),
}); });
@@ -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)),
);
}),
});
+3 -1
View File
@@ -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";
+3 -1
View File
@@ -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: [],
}, },
], ],
+27 -61
View File
@@ -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),
}); });
@@ -422,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)
@@ -469,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 }) => {
@@ -502,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 -1
View File
@@ -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";
+2 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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";
+2 -1
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import { z } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
@@ -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";
+5 -1
View File
@@ -6,7 +6,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const weatherRouter = createTRPCRouter({ export const weatherRouter = createTRPCRouter({
atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => { atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => {
const res = await fetchWithTimeout( const res = await fetchWithTimeout(
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min&current_weather=true&timezone=auto`, `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
); );
const json: unknown = await res.json(); const json: unknown = await res.json();
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json); const weather = await validation.widget.weather.atLocationOutput.parseAsync(json);
@@ -18,6 +18,10 @@ export const weatherRouter = createTRPCRouter({
weatherCode: weather.daily.weathercode[index] ?? 404, weatherCode: weather.daily.weathercode[index] ?? 404,
maxTemp: weather.daily.temperature_2m_max[index], maxTemp: weather.daily.temperature_2m_max[index],
minTemp: weather.daily.temperature_2m_min[index], minTemp: weather.daily.temperature_2m_min[index],
sunrise: weather.daily.sunrise[index],
sunset: weather.daily.sunset[index],
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
}; };
}), }),
}; };
+2 -2
View File
@@ -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,
+15
View File
@@ -36,3 +36,18 @@ async function importCookiesAsync() {
.map(({ name, value }) => `${name}=${value}`) .map(({ name, value }) => `${name}=${value}`)
.join(";"); .join(";");
} }
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
return `http://${process.env.HOSTNAME ?? "localhost"}:3000`;
}
export const trpcPath = "/api/trpc";
/**
* Creates the full url for the trpc api endpoint
* @returns
*/
export function getTrpcUrl() {
return `${getBaseUrl()}${trpcPath}`;
}
+1 -1
View File
@@ -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";
+4 -3
View File
@@ -30,7 +30,7 @@
"@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",
@@ -38,7 +38,8 @@
"next-auth": "5.0.0-beta.25", "next-auth": "5.0.0-beta.25",
"pretty-print-error": "^1.1.2", "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",
@@ -46,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"
} }
@@ -1,10 +1,11 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import type { z } from "zod";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import { and, eq } from "@homarr/db"; import { and, eq } from "@homarr/db";
import { users } from "@homarr/db/schema"; import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import type { validation, z } from "@homarr/validation"; import type { validation } from "@homarr/validation";
export const authorizeWithBasicCredentialsAsync = async ( export const authorizeWithBasicCredentialsAsync = async (
db: Database, db: Database,
@@ -1,4 +1,5 @@
import { CredentialsSignin } from "@auth/core/errors"; import { CredentialsSignin } from "@auth/core/errors";
import { z } from "zod";
import { extractErrorMessage } from "@homarr/common"; import { extractErrorMessage } from "@homarr/common";
import type { Database, InferInsertModel } from "@homarr/db"; import type { Database, InferInsertModel } from "@homarr/db";
@@ -6,7 +7,6 @@ import { and, createId, eq } from "@homarr/db";
import { users } from "@homarr/db/schema"; import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import type { validation } from "@homarr/validation"; import type { validation } from "@homarr/validation";
import { z } from "@homarr/validation";
import { env } from "../../../env"; import { env } from "../../../env";
import { LdapClient } from "../ldap-client"; import { LdapClient } from "../ldap-client";
+1 -2
View File
@@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { z } from "zod";
import { z } from "@homarr/validation";
import { expireDateAfter, generateSessionToken } from "../session"; import { expireDateAfter, generateSessionToken } from "../session";
+1 -1
View File
@@ -29,7 +29,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"
} }
} }
Loaded 100 of 235 files, more files were not shown because too many files have changed in this diff. Show more