Compare commits

...
71 Commits
Author SHA1 Message Date
Releases Homarr d7822d2b98 chore(release): 1.6.0 [skip ci] 2025-02-14 19:13:36 +00:00
homarr-releases[bot] 51bea03099 chore(release): automatic release v1.6.0 2025-02-14 19:12:54 +00:00
homarr-renovate[bot] 8ee39eb6db fix(deps): update dependency sass to ^1.85.0 (#2326)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-14 02:17:04 +00:00
homarr-renovate[bot] 4074d189e3 chore(deps): update pnpm to v10.4.0 (#2325)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-14 00:54:38 +00:00
homarr-renovate[bot] 7a7f40a140 chore(deps): update dependency @types/node to ^22.13.4 (#2324)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-13 22:45:58 +00:00
homarr-renovate[bot] 1f9442f7f4 chore(deps): update dependency @types/node to ^22.13.3 (#2323)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-13 22:17:18 +00:00
Meier Lukas a38afb05d3 fix: add mysql pool with keep-alive to not close the connection (#2282) 2025-02-13 18:41:21 +01:00
homarr-renovate[bot] fae1aacf20 chore(deps): update node.js to v22.14.0 (#2320)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-13 18:40:31 +01:00
homarr-renovate[bot] d508fd466d chore(deps): update dependency @types/node to ^22.13.2 (#2316)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-13 04:54:32 +00:00
homarr-renovate[bot] 4d3cc1d806 fix(deps): update dependency @ctrl/qbittorrent to ^9.4.0 (#2315)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-13 02:18:40 +00:00
homarr-crowdin[bot] e1fa92b91e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-13 00:45:21 +00:00
homarr-renovate[bot] 10454656a2 fix(deps): update turbo monorepo to ^2.4.2 (#2313)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-12 22:36:27 +00:00
homarr-crowdin[bot] ee08725d53 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-12 00:44:59 +00:00
homarr-renovate[bot] 379675a901 fix(deps): update dependency zod to ^3.24.2 (#2311)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 23:18:01 +00:00
homarr-renovate[bot] 8649faca79 fix(deps): update dependency @ctrl/qbittorrent to ^9.3.0 (#2310)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 18:54:19 +00:00
homarr-renovate[bot] 91260bf0d4 chore(deps): update node.js to v22.14.0 (#2307)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 17:45:14 +00:00
homarr-renovate[bot] 51038cc03c chore(deps): update dependency eslint to ^9.20.1 (#2309)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 17:36:09 +00:00
homarr-renovate[bot] fc43da59f8 fix(deps): update dependency jotai to ^2.12.0 (#2306)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 06:21:49 +00:00
homarr-renovate[bot] 2399967868 fix(deps): update nextjs monorepo to ^15.1.7 (#2302)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 03:02:53 +00:00
homarr-renovate[bot] 480dae09c4 chore(deps): update pnpm to v10.3.0 (#2301)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-11 00:54:23 +00:00
homarr-crowdin[bot] 2d2ba2e088 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-11 00:45:04 +00:00
Meier Lukas 0502e47770 fix: add error handling for available updates (#2289) 2025-02-10 20:25:03 +01:00
homarr-crowdin[bot] 3c877be44a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-10 19:22:24 +00:00
homarr-renovate[bot] 8371f46d34 fix(deps): update dependency typescript-eslint to ^8.24.0 (#2297)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-10 17:34:03 +00:00
homarr-renovate[bot] 2b9f834d96 fix(deps): update turbo monorepo to ^2.4.1 (#2296)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-10 17:33:30 +00:00
homarr-renovate[bot] 8ec0080fca fix(deps): update dependency drizzle-orm to ^0.39.3 (#2295)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-10 16:54:54 +00:00
Meier Lukas b7fd2470ee fix(users): edit profile username not lowercase (#2279) 2025-02-09 19:09:48 +01:00
Meier Lukas 749b0c6ee9 feat(lang): add catalan language (#2283) 2025-02-09 19:09:38 +01:00
homarr-renovate[bot] 25b7419d7b chore(deps): update dependency semantic-release to ^24.2.2 (#2286)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-09 15:42:54 +00:00
homarr-renovate[bot] 7edaa48fb9 fix(deps): update dependency @extractus/feed-extractor to v7.1.4 (#2281)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-09 03:23:10 +00:00
homarr-crowdin[bot] 627064b632 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-09 00:48:17 +00:00
homarr-renovate[bot] 42a68140b1 fix(deps): update mantine monorepo to ^7.16.3 (#2275)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-08 11:28:49 +00:00
homarr-renovate[bot]andMeier Lukas f6a5c04017 chore(deps): update pnpm to v10 (#2239)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-02-08 11:16:07 +01:00
Toine Rademacher (toineenzo) 2e150d548f chore(integrations): change icon links to svg and new repository (#2008) 2025-02-08 10:40:15 +01:00
Yossi HillaliandMeier Lukas 584c660906 fix(calendar): info popover overlap (#2268)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-02-08 10:37:26 +01:00
homarr-crowdin[bot] 7abc4e4e79 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-02-08 00:44:43 +00:00
homarr-renovate[bot] 0e553a5f8f chore(deps): update dependency eslint to ^9.20.0 (#2273)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-02-07 21:55:01 +00:00
Meier Lukas dff6cb9d31 feat(settings): add simple-ping settings (#2118) 2025-02-07 22:10:35 +01:00
Meier Lukas c04c42dc8a chore(issue-template): add missing homarr version 1.5.0 2025-02-07 20:20:22 +01:00
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
200 changed files with 11609 additions and 2591 deletions

No files matched your search

+2
View File
@@ -31,6 +31,8 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.5.0
- 1.4.0
- 1.3.1 - 1.3.1
- 1.3.0 - 1.3.0
- 1.2.0 - 1.2.0
+1 -1
View File
@@ -1 +1 @@
22.13.1 22.14.0
+51
View File
@@ -1,3 +1,54 @@
## [1.6.0](https://github.com/homarr-labs/homarr/compare/v1.5.0...v1.6.0) (2025-02-14)
### Features
* **lang:** add catalan language ([#2283](https://github.com/homarr-labs/homarr/issues/2283)) ([749b0c6](https://github.com/homarr-labs/homarr/commit/749b0c6ee9dc7802ae10393b90e19bf3c43bf9e0))
* **settings:** add simple-ping settings ([#2118](https://github.com/homarr-labs/homarr/issues/2118)) ([dff6cb9](https://github.com/homarr-labs/homarr/commit/dff6cb9d31c3884bf811774a7c90dad27ac19691))
### Bug Fixes
* add error handling for available updates ([#2289](https://github.com/homarr-labs/homarr/issues/2289)) ([0502e47](https://github.com/homarr-labs/homarr/commit/0502e477707932db96b25372ddab8263572b389c))
* add mysql pool with keep-alive to not close the connection ([#2282](https://github.com/homarr-labs/homarr/issues/2282)) ([a38afb0](https://github.com/homarr-labs/homarr/commit/a38afb05d35cf2bdba378be3744f3a8b6c9d38f6))
* **calendar:** info popover overlap ([#2268](https://github.com/homarr-labs/homarr/issues/2268)) ([584c660](https://github.com/homarr-labs/homarr/commit/584c660906f5ae7e8ab1c88ce9cfbbbbdcdb5145))
* **deps:** update dependency @ctrl/qbittorrent to ^9.3.0 ([#2310](https://github.com/homarr-labs/homarr/issues/2310)) ([8649fac](https://github.com/homarr-labs/homarr/commit/8649faca7929daff426607f104057fce26edd049))
* **deps:** update dependency @ctrl/qbittorrent to ^9.4.0 ([#2315](https://github.com/homarr-labs/homarr/issues/2315)) ([4d3cc1d](https://github.com/homarr-labs/homarr/commit/4d3cc1d80698e659a669de7d4bd0d185ad68605e))
* **deps:** update dependency @extractus/feed-extractor to v7.1.4 ([#2281](https://github.com/homarr-labs/homarr/issues/2281)) ([7edaa48](https://github.com/homarr-labs/homarr/commit/7edaa48fb9bcf48e8b734beec5a8bad003124233))
* **deps:** update dependency drizzle-orm to ^0.39.3 ([#2295](https://github.com/homarr-labs/homarr/issues/2295)) ([8ec0080](https://github.com/homarr-labs/homarr/commit/8ec0080fca17c530cdd0576ba101299a0f769463))
* **deps:** update dependency jotai to ^2.12.0 ([#2306](https://github.com/homarr-labs/homarr/issues/2306)) ([fc43da5](https://github.com/homarr-labs/homarr/commit/fc43da59f845934da428bcafd4ec46551ace8a81))
* **deps:** update dependency sass to ^1.85.0 ([#2326](https://github.com/homarr-labs/homarr/issues/2326)) ([8ee39eb](https://github.com/homarr-labs/homarr/commit/8ee39eb6db89fdeaec0fae7988ffa97280c2f5ab))
* **deps:** update dependency typescript-eslint to ^8.24.0 ([#2297](https://github.com/homarr-labs/homarr/issues/2297)) ([8371f46](https://github.com/homarr-labs/homarr/commit/8371f46d34ad36fd25f23d91804d3f95439d7012))
* **deps:** update dependency zod to ^3.24.2 ([#2311](https://github.com/homarr-labs/homarr/issues/2311)) ([379675a](https://github.com/homarr-labs/homarr/commit/379675a901637a1b878595a80d0776d05311218d))
* **deps:** update mantine monorepo to ^7.16.3 ([#2275](https://github.com/homarr-labs/homarr/issues/2275)) ([42a6814](https://github.com/homarr-labs/homarr/commit/42a68140b1493cc171977d996eea16d876dce60c))
* **deps:** update nextjs monorepo to ^15.1.7 ([#2302](https://github.com/homarr-labs/homarr/issues/2302)) ([2399967](https://github.com/homarr-labs/homarr/commit/2399967868652436ee6827bce8883c1b4a00af0e))
* **deps:** update turbo monorepo to ^2.4.1 ([#2296](https://github.com/homarr-labs/homarr/issues/2296)) ([2b9f834](https://github.com/homarr-labs/homarr/commit/2b9f834d9645aaf5f5aa1b8a5f1e48300389ac8b))
* **deps:** update turbo monorepo to ^2.4.2 ([#2313](https://github.com/homarr-labs/homarr/issues/2313)) ([1045465](https://github.com/homarr-labs/homarr/commit/10454656a286c320a8d3b5ff12a138025d871b20))
* **users:** edit profile username not lowercase ([#2279](https://github.com/homarr-labs/homarr/issues/2279)) ([b7fd247](https://github.com/homarr-labs/homarr/commit/b7fd2470ee0d85821f79a34e839e62fbab6989c6))
## [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) ## [1.4.0](https://github.com/homarr-labs/homarr/compare/v1.3.1...v1.4.0) (2025-01-31)
### Features ### Features
+5 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.13.1-alpine AS base FROM node:22.14.0-alpine AS base
FROM base AS builder FROM base AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@@ -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
+14 -13
View File
@@ -18,6 +18,7 @@
"@homarr/analytics": "workspace:^0.1.0", "@homarr/analytics": "workspace:^0.1.0",
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
"@homarr/boards": "workspace:^0.1.0",
"@homarr/certificates": "workspace:^0.1.0", "@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0", "@homarr/cron-job-status": "workspace:^0.1.0",
@@ -42,15 +43,15 @@
"@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.2", "@mantine/colors-generator": "^7.16.3",
"@mantine/core": "^7.16.2", "@mantine/core": "^7.16.3",
"@mantine/dropzone": "^7.16.2", "@mantine/dropzone": "^7.16.3",
"@mantine/hooks": "^7.16.2", "@mantine/hooks": "^7.16.3",
"@mantine/modals": "^7.16.2", "@mantine/modals": "^7.16.3",
"@mantine/tiptap": "^7.16.2", "@mantine/tiptap": "^7.16.3",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@t3-oss/env-nextjs": "^0.12.0", "@t3-oss/env-nextjs": "^0.12.0",
"@tabler/icons-react": "^3.29.0", "@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.0", "@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-query-next-experimental": "^5.66.0", "@tanstack/react-query-next-experimental": "^5.66.0",
@@ -67,33 +68,33 @@
"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.2", "jotai": "^2.12.0",
"mantine-react-table": "2.0.0-beta.8", "mantine-react-table": "2.0.0-beta.8",
"next": "15.1.6", "next": "15.1.7",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"react": "19.0.0", "react": "19.0.0",
"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.85.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.18.3", "swagger-ui-react": "^5.18.3",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.1" "zod": "^3.24.2"
}, },
"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.1", "@types/chroma-js": "3.1.1",
"@types/node": "^22.12.0", "@types/node": "^22.13.4",
"@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": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.19.0", "eslint": "^9.20.1",
"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"
@@ -5,12 +5,13 @@ import { Box, LoadingOverlay, Stack } from "@mantine/core";
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 { useRequiredBoard } from "@homarr/boards/context";
import { BoardCategorySection } from "~/components/board/sections/category-section"; import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section"; import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardBackgroundVideo } from "~/components/layout/background"; import { BoardBackgroundVideo } from "~/components/layout/background";
import { fullHeightWithoutHeaderAndFooter } from "~/constants"; import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { useIsBoardReady, useRequiredBoard } from "./_context"; import { useIsBoardReady } from "./_ready-context";
let boardName: string | null = null; let boardName: string | null = null;
@@ -1,118 +0,0 @@
"use client";
import type { Dispatch, PropsWithChildren, SetStateAction } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { updateBoardName } from "./_client";
const BoardContext = createContext<{
board: RouterOutputs["board"]["getHomeBoard"];
isReady: boolean;
markAsReady: (id: string) => void;
isEditMode: boolean;
setEditMode: Dispatch<SetStateAction<boolean>>;
} | null>(null);
export const BoardProvider = ({
children,
initialBoard,
}: PropsWithChildren<{
initialBoard: RouterOutputs["board"]["getBoardByName"];
}>) => {
const pathname = usePathname();
const utils = clientApi.useUtils();
const [readySections, setReadySections] = useState<string[]>([]);
const [isEditMode, setEditMode] = useState(false);
const { data } = clientApi.board.getBoardByName.useQuery(
{ name: initialBoard.name },
{
initialData: initialBoard,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
);
// Update the board name so it can be used within updateBoard method
updateBoardName(initialBoard.name);
// Invalidate the board when the pathname changes
// This allows to refetch the board when it might have changed - e.g. if someone else added an item
useEffect(() => {
return () => {
setReadySections([]);
void utils.board.getBoardByName.invalidate({ name: initialBoard.name });
};
}, [pathname, utils, initialBoard.name]);
useEffect(() => {
setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
return (
<BoardContext.Provider
value={{
board: data,
isReady: data.sections.length === readySections.length,
markAsReady,
isEditMode,
setEditMode,
}}
>
{children}
</BoardContext.Provider>
);
};
export const useMarkSectionAsReady = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return context.markAsReady;
};
export const useIsBoardReady = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return context.isReady;
};
export const useRequiredBoard = () => {
const optionalBoard = useOptionalBoard();
if (!optionalBoard) {
throw new Error("Board is required");
}
return optionalBoard;
};
export const useOptionalBoard = () => {
const context = useContext(BoardContext);
return context?.board;
};
export const useEditMode = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return [context.isEditMode, context.setEditMode] as const;
};
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useRequiredBoard } from "./_context"; import { useRequiredBoard } from "@homarr/boards/context";
export const CustomCss = () => { export const CustomCss = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
@@ -20,6 +20,8 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -32,7 +34,6 @@ import { CategoryEditModal } from "~/components/board/sections/category/category
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions"; import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
import { HeaderButton } from "~/components/layout/header/button"; import { HeaderButton } from "~/components/layout/header/button";
import { env } from "~/env"; import { env } from "~/env";
import { useEditMode, useRequiredBoard } from "./_context";
export const BoardContentHeaderActions = () => { export const BoardContentHeaderActions = () => {
const [isEditMode] = useEditMode(); const [isEditMode] = useEditMode();
@@ -119,7 +120,7 @@ const AddMenu = () => {
}; };
const EditModeMenu = () => { const EditModeMenu = () => {
const [isEditMode, setEditMode] = useEditMode(); const [isEditMode, { open, close }] = useEditMode();
const board = useRequiredBoard(); const board = useRequiredBoard();
const utils = clientApi.useUtils(); const utils = clientApi.useUtils();
const t = useScopedI18n("board.action.edit"); const t = useScopedI18n("board.action.edit");
@@ -131,7 +132,7 @@ const EditModeMenu = () => {
}); });
void utils.board.getBoardByName.invalidate({ name: board.name }); void utils.board.getBoardByName.invalidate({ name: board.name });
void revalidatePathActionAsync(`/boards/${board.name}`); void revalidatePathActionAsync(`/boards/${board.name}`);
setEditMode(false); close();
}, },
onError() { onError() {
showErrorNotification({ showErrorNotification({
@@ -143,8 +144,8 @@ const EditModeMenu = () => {
const toggle = useCallback(() => { const toggle = useCallback(() => {
if (isEditMode) return saveBoard(board); if (isEditMode) return saveBoard(board);
setEditMode(true); open();
}, [board, isEditMode, saveBoard, setEditMode]); }, [board, isEditMode, saveBoard, open]);
useHotkeys([["mod+e", toggle]]); useHotkeys([["mod+e", toggle]]);
usePreventLeaveWithDirty(isEditMode); usePreventLeaveWithDirty(isEditMode);
@@ -0,0 +1,67 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
const BoardReadyContext = createContext<{
isReady: boolean;
markAsReady: (id: string) => void;
} | null>(null);
export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
const pathname = usePathname();
const utils = clientApi.useUtils();
const board = useRequiredBoard();
const [readySections, setReadySections] = useState<string[]>([]);
// Reset sections required for ready state
useEffect(() => {
return () => {
setReadySections([]);
};
}, [pathname, utils]);
useEffect(() => {
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [board.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
return (
<BoardReadyContext.Provider
value={{
isReady: board.sections.length === readySections.length,
markAsReady,
}}
>
{children}
</BoardReadyContext.Provider>
);
};
export const useMarkSectionAsReady = () => {
const context = useContext(BoardReadyContext);
if (!context) {
throw new Error("BoardReadyProvider is required");
}
return context.markAsReady;
};
export const useIsBoardReady = () => {
const context = useContext(BoardReadyContext);
if (!context) {
throw new Error("BoardReadyProvider is required");
}
return context.isReady;
};
@@ -4,10 +4,10 @@ import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core"; import type { MantineColorsTuple } from "@mantine/core";
import { createTheme, darken, lighten, MantineProvider } from "@mantine/core"; import { createTheme, darken, lighten, MantineProvider } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions"; import type { ColorScheme } from "@homarr/definitions";
import { useColorSchemeManager } from "../../_client-providers/mantine"; import { useColorSchemeManager } from "../../_client-providers/mantine";
import { useRequiredBoard } from "./_context";
export const BoardMantineProvider = ({ export const BoardMantineProvider = ({
children, children,
@@ -0,0 +1,48 @@
"use client";
import { Button, Group, Stack, Switch } from "@mantine/core";
import { useForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
interface Props {
board: Board;
}
export const BehaviorSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useForm({
initialValues: {
disableStatus: board.disableStatus,
},
});
return (
<form
onSubmit={form.onSubmit((values) => {
savePartialSettings({
id: board.id,
...values,
});
})}
>
<Stack>
<Switch
label={t("board.field.disableStatus.label")}
description={t("board.field.disableStatus.description")}
{...form.getInputProps("disableStatus", { type: "checkbox" })}
/>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
@@ -5,11 +5,11 @@ import { useRouter } from "next/navigation";
import { Button, Divider, Group, Stack, Text } from "@mantine/core"; import { Button, Divider, Group, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { BoardRenameModal } from "~/components/board/modals/board-rename-modal"; import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { useRequiredBoard } from "../../(content)/_context";
import classes from "./danger.module.css"; import classes from "./danger.module.css";
export const DangerZoneSettingsContent = ({ hideVisibility }: { hideVisibility: boolean }) => { export const DangerZoneSettingsContent = ({ hideVisibility }: { hideVisibility: boolean }) => {
@@ -5,13 +5,13 @@ import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks"; import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
import { IconAlertTriangle } from "@tabler/icons-react"; import { IconAlertTriangle } from "@tabler/icons-react";
import { useUpdateBoard } from "@homarr/boards/updater";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { validation } from "@homarr/validation";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
import { useUpdateBoard } from "../../(content)/_client";
import { useSavePartialSettingsMutation } from "./_shared"; import { useSavePartialSettingsMutation } from "./_shared";
interface Props { interface Props {
@@ -4,6 +4,7 @@ import { AccordionControl, AccordionItem, AccordionPanel, Container, Stack, Text
import { import {
IconAlertTriangle, IconAlertTriangle,
IconBrush, IconBrush,
IconClick,
IconFileTypeCss, IconFileTypeCss,
IconLayout, IconLayout,
IconPhoto, IconPhoto,
@@ -23,6 +24,7 @@ import type { TablerIcon } from "@homarr/ui";
import { getBoardPermissionsAsync } from "~/components/board/permissions/server"; import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion"; import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
import { BackgroundSettingsContent } from "./_background"; import { BackgroundSettingsContent } from "./_background";
import { BehaviorSettingsContent } from "./_behavior";
import { BoardAccessSettings } from "./_board-access"; import { BoardAccessSettings } from "./_board-access";
import { ColorSettingsContent } from "./_colors"; import { ColorSettingsContent } from "./_colors";
import { CustomCssSettingsContent } from "./_customCss"; import { CustomCssSettingsContent } from "./_customCss";
@@ -95,6 +97,9 @@ export default async function BoardSettingsPage(props: Props) {
<AccordionItemFor value="customCss" icon={IconFileTypeCss}> <AccordionItemFor value="customCss" icon={IconFileTypeCss}>
<CustomCssSettingsContent board={board} /> <CustomCssSettingsContent board={board} />
</AccordionItemFor> </AccordionItemFor>
<AccordionItemFor value="behavior" icon={IconClick}>
<BehaviorSettingsContent board={board} />
</AccordionItemFor>
{hasFullAccess && ( {hasFullAccess && (
<> <>
<AccordionItemFor value="access" icon={IconUser}> <AccordionItemFor value="access" icon={IconUser}>
@@ -2,8 +2,9 @@
import { IconLayoutBoard } from "@tabler/icons-react"; import { IconLayoutBoard } from "@tabler/icons-react";
import { useRequiredBoard } from "@homarr/boards/context";
import { HeaderButton } from "~/components/layout/header/button"; import { HeaderButton } from "~/components/layout/header/button";
import { useRequiredBoard } from "./(content)/_context";
export const BoardOtherHeaderActions = () => { export const BoardOtherHeaderActions = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
@@ -3,6 +3,8 @@ import { notFound } from "next/navigation";
import { AppShellMain } from "@mantine/core"; import { AppShellMain } from "@mantine/core";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { BoardProvider } from "@homarr/boards/context";
import { EditModeProvider } from "@homarr/boards/edit-mode";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { MainHeader } from "~/components/layout/header"; import { MainHeader } from "~/components/layout/header";
@@ -10,9 +12,9 @@ import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
import { ClientShell } from "~/components/layout/shell"; import { ClientShell } from "~/components/layout/shell";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme"; import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import type { Board } from "./_types"; import type { Board } from "./_types";
import { BoardProvider } from "./(content)/_context";
import type { Params } from "./(content)/_creator"; import type { Params } from "./(content)/_creator";
import { CustomCss } from "./(content)/_custom-css"; import { CustomCss } from "./(content)/_custom-css";
import { BoardReadyProvider } from "./(content)/_ready-context";
import { BoardMantineProvider } from "./(content)/_theme"; import { BoardMantineProvider } from "./(content)/_theme";
interface CreateBoardLayoutProps<TParams extends Params> { interface CreateBoardLayoutProps<TParams extends Params> {
@@ -42,17 +44,21 @@ export const createBoardLayout = <TParams extends Params>({
return ( return (
<BoardProvider initialBoard={initialBoard}> <BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider defaultColorScheme={colorScheme}> <BoardReadyProvider>
<CustomCss /> <EditModeProvider>
<ClientShell hasNavigation={false}> <BoardMantineProvider defaultColorScheme={colorScheme}>
<MainHeader <CustomCss />
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />} <ClientShell hasNavigation={false}>
actions={headerActions} <MainHeader
hasNavigation={false} logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
/> actions={headerActions}
<AppShellMain>{children}</AppShellMain> hasNavigation={false}
</ClientShell> />
</BoardMantineProvider> <AppShellMain>{children}</AppShellMain>
</ClientShell>
</BoardMantineProvider>
</EditModeProvider>
</BoardReadyProvider>
</BoardProvider> </BoardProvider>
); );
}; };
+2
View File
@@ -94,6 +94,8 @@ export default async function Layout(props: {
board: { board: {
homeBoardId: serverSettings.board.homeBoardId, homeBoardId: serverSettings.board.homeBoardId,
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId, mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus,
}, },
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId }, search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
}} }}
@@ -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,11 +1,11 @@
"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 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 { validation } from "@homarr/validation"; import { validation } from "@homarr/validation";
@@ -14,14 +14,21 @@ 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, {
@@ -33,11 +40,23 @@ export const AppForm = (props: AppFormProps) => {
}, },
}); });
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>
@@ -8,8 +8,7 @@ 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 } from "@homarr/validation";
import { AppForm } from "../../_form"; import { AppForm } from "../../_form";
@@ -19,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");
@@ -34,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"),
}); });
}, },
}); });
@@ -50,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}
@@ -7,44 +7,55 @@ 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 } 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}
/>
); );
}; };
@@ -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")}
@@ -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>
@@ -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 ? (
@@ -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",
}, },
]; ];
@@ -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">
@@ -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
@@ -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,6 +1,6 @@
"use client"; "use client";
import { Group, Text } from "@mantine/core"; import { Group, Switch, Text } from "@mantine/core";
import { IconLayoutDashboard } from "@tabler/icons-react"; import { IconLayoutDashboard } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -56,6 +56,18 @@ export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSett
)} )}
{...form.getInputProps("mobileHomeBoardId")} {...form.getInputProps("mobileHomeBoardId")}
/> />
<Text fw={500}>{tBoard("status.title")}</Text>
<Switch
{...form.getInputProps("enableStatusByDefault", { type: "checkbox" })}
label={tBoard("status.enableStatusByDefault.label")}
description={tBoard("status.enableStatusByDefault.description")}
/>
<Switch
{...form.getInputProps("forceDisableStatus", { type: "checkbox" })}
label={tBoard("status.forceDisableStatus.label")}
description={tBoard("status.forceDisableStatus.description")}
/>
</> </>
)} )}
</CommonSettingsForm> </CommonSettingsForm>
@@ -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>
@@ -9,6 +9,7 @@ import { ErrorBoundary } from "react-error-boundary";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals"; import { useModalAction } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications"; import { showSuccessNotification } from "@homarr/notifications";
import { useSettings } from "@homarr/settings";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { BoardItemAdvancedOptions } from "@homarr/validation";
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
@@ -29,6 +30,7 @@ interface WidgetPreviewPageContentProps {
} }
export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPreviewPageContentProps) => { export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPreviewPageContentProps) => {
const settings = useSettings();
const t = useScopedI18n("widgetPreview"); const t = useScopedI18n("widgetPreview");
const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal); const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal);
const { openModal: openPreviewDimensionsModal } = useModalAction(PreviewDimensionsModal); const { openModal: openPreviewDimensionsModal } = useModalAction(PreviewDimensionsModal);
@@ -43,7 +45,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
integrationIds: string[]; integrationIds: string[];
advancedOptions: BoardItemAdvancedOptions; advancedOptions: BoardItemAdvancedOptions;
}>({ }>({
options: reduceWidgetOptionsWithDefaultValues(kind, {}), options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}),
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
customCssClasses: [], customCssClasses: [],
@@ -63,8 +65,9 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind), (currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
), ),
integrationSupport: "supportedIntegrations" in currentDefinition, integrationSupport: "supportedIntegrations" in currentDefinition,
settings,
}); });
}, [currentDefinition, integrationData, kind, openWidgetEditModal, state]); }, [currentDefinition, integrationData, kind, openWidgetEditModal, settings, state]);
const Comp = loadWidgetDynamic(kind); const Comp = loadWidgetDynamic(kind);
@@ -1,9 +1,9 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useUpdateBoard } from "@homarr/boards/updater";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { BoardItemAdvancedOptions } from "@homarr/validation";
import type { Item } from "~/app/[locale]/boards/_types"; import type { Item } from "~/app/[locale]/boards/_types";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
import type { CreateItemInput } from "./actions/create-item"; import type { CreateItemInput } from "./actions/create-item";
import { createItemCallback } from "./actions/create-item"; import { createItemCallback } from "./actions/create-item";
import type { DuplicateItemInput } from "./actions/duplicate-item"; import type { DuplicateItemInput } from "./actions/duplicate-item";
@@ -5,11 +5,13 @@ import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors"; import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useSettings } from "@homarr/settings";
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetError } from "@homarr/widgets/errors"; import { WidgetError } from "@homarr/widgets/errors";
import type { Item } from "~/app/[locale]/boards/_types"; import type { Item } from "~/app/[locale]/boards/_types";
import { useEditMode, useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
import classes from "../sections/item.module.css"; import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions"; import { useItemActions } from "./item-actions";
import { BoardItemMenu } from "./item-menu"; import { BoardItemMenu } from "./item-menu";
@@ -53,11 +55,12 @@ interface InnerContentProps {
} }
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const settings = useSettings();
const board = useRequiredBoard(); const board = useRequiredBoard();
const [isEditMode] = useEditMode(); const [isEditMode] = useEditMode();
const Comp = loadWidgetDynamic(item.kind); const Comp = loadWidgetDynamic(item.kind);
const { definition } = widgetImports[item.kind]; const { definition } = widgetImports[item.kind];
const options = reduceWidgetOptionsWithDefaultValues(item.kind, item.options); const options = reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options);
const newItem = { ...item, options }; const newItem = { ...item, options };
const { updateItemOptions } = useItemActions(); const { updateItemOptions } = useItemActions();
const updateOptions = ({ newOptions }: { newOptions: Record<string, unknown> }) => const updateOptions = ({ newOptions }: { newOptions: Record<string, unknown> }) =>
@@ -3,13 +3,14 @@ import { ActionIcon, Menu } from "@mantine/core";
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react"; import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { widgetImports } from "@homarr/widgets"; import { widgetImports } from "@homarr/widgets";
import { WidgetEditModal } from "@homarr/widgets/modals"; import { WidgetEditModal } from "@homarr/widgets/modals";
import type { Item } from "~/app/[locale]/boards/_types"; import type { Item } from "~/app/[locale]/boards/_types";
import { useEditMode } from "~/app/[locale]/boards/(content)/_context";
import { useSectionContext } from "../sections/section-context"; import { useSectionContext } from "../sections/section-context";
import { useItemActions } from "./item-actions"; import { useItemActions } from "./item-actions";
import { ItemMoveModal } from "./item-move-modal"; import { ItemMoveModal } from "./item-move-modal";
@@ -35,6 +36,7 @@ export const BoardItemMenu = ({
const { data: integrationData, isPending } = clientApi.integration.all.useQuery(); const { data: integrationData, isPending } = clientApi.integration.all.useQuery();
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const { gridstack } = useSectionContext().refs; const { gridstack } = useSectionContext().refs;
const settings = useSettings();
// Reset error boundary on next render if item has been edited // Reset error boundary on next render if item has been edited
useEffect(() => { useEffect(() => {
@@ -75,6 +77,7 @@ export const BoardItemMenu = ({
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind), (currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
), ),
integrationSupport: "supportedIntegrations" in currentDefinition, integrationSupport: "supportedIntegrations" in currentDefinition,
settings,
}); });
}; };
@@ -3,9 +3,9 @@ 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 { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
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"; import classes from "./item.module.css";
@@ -1,9 +1,9 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useUpdateBoard } from "@homarr/boards/updater";
import { createId } from "@homarr/db/client"; import { createId } from "@homarr/db/client";
import type { CategorySection, EmptySection } from "~/app/[locale]/boards/_types"; import type { CategorySection, EmptySection } from "~/app/[locale]/boards/_types";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
import type { MoveCategoryInput } from "./actions/move-category"; import type { MoveCategoryInput } from "./actions/move-category";
import { moveCategoryCallback } from "./actions/move-category"; import { moveCategoryCallback } from "./actions/move-category";
import type { RemoveCategoryInput } from "./actions/remove-category"; import type { RemoveCategoryInput } from "./actions/remove-category";
@@ -1,12 +1,15 @@
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 { useSettings } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client"; 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 +100,29 @@ export const useCategoryMenuActions = (category: CategorySection) => {
); );
}, [category, openModal, renameCategory, t]); }, [category, openModal, renameCategory, t]);
const settings = useSettings();
const openAllInNewTabs = useCallback(async () => {
const appIds = filterByItemKind(category.items, settings, "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, settings]);
return { return {
addCategoryAbove, addCategoryAbove,
addCategoryBelow, addCategoryBelow,
@@ -104,5 +130,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,11 +13,12 @@ import {
IconTrash, IconTrash,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useEditMode } from "@homarr/boards/edit-mode";
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";
import type { CategorySection } from "~/app/[locale]/boards/_types"; import type { CategorySection } from "~/app/[locale]/boards/_types";
import { useEditMode } from "~/app/[locale]/boards/(content)/_context";
import { useCategoryMenuActions } from "./category-menu-actions"; import { useCategoryMenuActions } from "./category-menu-actions";
interface Props { interface Props {
@@ -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,23 @@
import type { WidgetKind } from "@homarr/definitions";
import type { SettingsContextProps } from "@homarr/settings";
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[],
settings: SettingsContextProps,
kind: TKind,
) => {
return items
.filter((item) => item.kind === kind)
.map((item) => ({
...item,
options: reduceWidgetOptionsWithDefaultValues(
kind,
settings,
item.options,
) as WidgetComponentProps<TKind>["options"],
}));
};
@@ -1,7 +1,8 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useRequiredBoard } from "@homarr/boards/context";
import type { DynamicSection, Item, Section } from "~/app/[locale]/boards/_types"; import type { DynamicSection, Item, Section } from "~/app/[locale]/boards/_types";
import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
import { BoardItemContent } from "../items/item-content"; import { BoardItemContent } from "../items/item-content";
import { BoardDynamicSection } from "./dynamic-section"; import { BoardDynamicSection } from "./dynamic-section";
import { GridStackItem } from "./gridstack/gridstack-item"; import { GridStackItem } from "./gridstack/gridstack-item";
@@ -1,7 +1,8 @@
import { Box, Card } from "@mantine/core"; import { Box, Card } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context";
import type { DynamicSection } from "~/app/[locale]/boards/_types"; import type { DynamicSection } from "~/app/[locale]/boards/_types";
import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
import { BoardDynamicSectionMenu } from "./dynamic/dynamic-menu"; import { BoardDynamicSectionMenu } from "./dynamic/dynamic-menu";
import { GridStack } from "./gridstack/gridstack"; import { GridStack } from "./gridstack/gridstack";
import classes from "./item.module.css"; import classes from "./item.module.css";
@@ -1,9 +1,9 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useUpdateBoard } from "@homarr/boards/updater";
import { createId } from "@homarr/db/client"; import { createId } from "@homarr/db/client";
import type { DynamicSection, EmptySection } from "~/app/[locale]/boards/_types"; import type { DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
interface RemoveDynamicSection { interface RemoveDynamicSection {
id: string; id: string;
@@ -1,11 +1,11 @@
import { ActionIcon, Menu } from "@mantine/core"; import { ActionIcon, Menu } from "@mantine/core";
import { IconDotsVertical, IconTrash } from "@tabler/icons-react"; import { IconDotsVertical, IconTrash } from "@tabler/icons-react";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal } from "@homarr/modals"; import { useConfirmModal } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { DynamicSection } from "~/app/[locale]/boards/_types"; import type { DynamicSection } from "~/app/[locale]/boards/_types";
import { useEditMode } from "~/app/[locale]/boards/(content)/_context";
import { useDynamicSectionActions } from "./dynamic-actions"; import { useDynamicSectionActions } from "./dynamic-actions";
export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSection }) => { export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSection }) => {
@@ -1,7 +1,8 @@
import combineClasses from "clsx"; import combineClasses from "clsx";
import { useEditMode } from "@homarr/boards/edit-mode";
import type { EmptySection } from "~/app/[locale]/boards/_types"; import type { EmptySection } from "~/app/[locale]/boards/_types";
import { useEditMode } from "~/app/[locale]/boards/(content)/_context";
import { GridStack } from "./gridstack/gridstack"; import { GridStack } from "./gridstack/gridstack";
import { useSectionItems } from "./use-section-items"; import { useSectionItems } from "./use-section-items";
@@ -2,10 +2,12 @@ import type { RefObject } from "react";
import { createRef, useCallback, useEffect, useRef } from "react"; import { createRef, useCallback, useEffect, useRef } from "react";
import { useElementSize } from "@mantine/hooks"; import { useElementSize } from "@mantine/hooks";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import type { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode } from "@homarr/gridstack"; import type { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode } from "@homarr/gridstack";
import type { Section } from "~/app/[locale]/boards/_types"; import type { Section } from "~/app/[locale]/boards/_types";
import { useEditMode, useMarkSectionAsReady, useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; import { useMarkSectionAsReady } from "~/app/[locale]/boards/(content)/_ready-context";
import { useItemActions } from "../../items/item-actions"; import { useItemActions } from "../../items/item-actions";
import { useSectionActions } from "../section-actions"; import { useSectionActions } from "../section-actions";
import { initializeGridstack } from "./init-gridstack"; import { initializeGridstack } from "./init-gridstack";
@@ -1,6 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client"; import { useUpdateBoard } from "@homarr/boards/updater";
interface MoveAndResizeInnerSection { interface MoveAndResizeInnerSection {
innerSectionId: string; innerSectionId: string;
@@ -1,5 +1,6 @@
import { useRequiredBoard } from "@homarr/boards/context";
import type { Section } from "~/app/[locale]/boards/_types"; import type { Section } from "~/app/[locale]/boards/_types";
import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
export const useSectionItems = (section: Section) => { export const useSectionItems = (section: Section) => {
const board = useRequiredBoard(); const board = useRequiredBoard();
@@ -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);
}); });
}} }}
> >
@@ -1,7 +1,7 @@
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import type { AppShellProps } from "@mantine/core"; import type { AppShellProps } from "@mantine/core";
import { useOptionalBoard } from "~/app/[locale]/boards/(content)/_context"; import { useOptionalBoard } from "@homarr/boards/context";
const supportedVideoFormats = ["mp4", "webm", "ogg"]; const supportedVideoFormats = ["mp4", "webm", "ogg"];
const isVideo = (url: string) => supportedVideoFormats.some((format) => url.toLowerCase().endsWith(`.${format}`)); const isVideo = (url: string) => supportedVideoFormats.some((format) => url.toLowerCase().endsWith(`.${format}`));
@@ -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,6 +1,7 @@
"use client"; "use client";
import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; import { useRequiredBoard } from "@homarr/boards/context";
import { homarrLogoPath, homarrPageTitle } from "./homarr-logo"; import { homarrLogoPath, homarrPageTitle } from "./homarr-logo";
import type { LogoWithTitleProps } from "./logo"; import type { LogoWithTitleProps } from "./logo";
import { Logo, LogoWithTitle } from "./logo"; import { Logo, LogoWithTitle } from "./logo";
@@ -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
@@ -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.12.0", "@types/node": "^22.13.4",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.19.0", "eslint": "^9.20.1",
"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.19.0", "eslint": "^9.20.1",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
+27 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.4.0", "version": "1.6.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -38,32 +38,48 @@
"@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.2",
"@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.2",
"testcontainers": "^10.17.2", "testcontainers": "^10.18.0",
"turbo": "^2.3.4", "turbo": "^2.4.2",
"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@10.4.0",
"engines": { "engines": {
"node": ">=22.13.1" "node": ">=22.14.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml",
"bcrypt",
"better-sqlite3",
"cpu-features",
"esbuild",
"sharp",
"ssh2",
"tree-sitter",
"tree-sitter-json"
],
"allowNonAppliedPatches": true, "allowNonAppliedPatches": true,
"overrides": { "overrides": {
"proxmox-api>undici": "7.3.0" "proxmox-api>undici": "7.3.0"
}, },
"patchedDependencies": { "patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch" "pretty-print-error": "patches/pretty-print-error.patch"
} },
"ignoredBuiltDependencies": [
"@scarf/scarf",
"core-js-pure",
"protobufjs"
]
} }
} }
+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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+4 -4
View File
@@ -44,19 +44,19 @@
"@trpc/react-query": "next", "@trpc/react-query": "next",
"@trpc/server": "next", "@trpc/server": "next",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.1.6", "next": "15.1.7",
"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",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.1.2", "trpc-to-openapi": "^2.1.3",
"zod": "^3.24.1" "zod": "^3.24.2"
}, },
"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.19.0", "eslint": "^9.20.1",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
+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
View File
@@ -484,6 +484,9 @@ export const boardRouter = createTRPCRouter({
// layout settings // layout settings
columnCount: input.columnCount, columnCount: input.columnCount,
// Behavior settings
disableStatus: input.disableStatus,
}) })
.where(eq(boards.id, input.id)); .where(eq(boards.id, input.id));
}), }),
@@ -195,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));
}), }),
}); });
@@ -247,7 +247,7 @@ describe("create should create a new integration", () => {
expect(dbSearchEngine!.short).toBe("j"); expect(dbSearchEngine!.short).toBe("j");
expect(dbSearchEngine!.name).toBe(input.name); expect(dbSearchEngine!.name).toBe(input.name);
expect(dbSearchEngine!.iconUrl).toBe( expect(dbSearchEngine!.iconUrl).toBe(
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/jellyseerr.png", "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
); );
}); });
+2 -2
View File
@@ -237,7 +237,7 @@ describe("editProfile shoud update user", () => {
expect(user).toHaveLength(1); expect(user).toHaveLength(1);
expect(user[0]).containSubset({ expect(user[0]).containSubset({
id: defaultOwnerId, id: defaultOwnerId,
name: "ABC", name: "abc",
email: "abc@gmail.com", email: "abc@gmail.com",
emailVerified, emailVerified,
}); });
@@ -272,7 +272,7 @@ describe("editProfile shoud update user", () => {
expect(user).toHaveLength(1); expect(user).toHaveLength(1);
expect(user[0]).containSubset({ expect(user[0]).containSubset({
id: defaultOwnerId, id: defaultOwnerId,
name: "ABC", name: "abc",
email: "myNewEmail@gmail.com", email: "myNewEmail@gmail.com",
emailVerified: null, emailVerified: null,
}); });
+11 -3
View File
@@ -1,11 +1,19 @@
import { formatError } from "pretty-print-error";
import { logger } from "@homarr/log";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
export const updateCheckerRouter = createTRPCRouter({ export const updateCheckerRouter = createTRPCRouter({
getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => { getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
const handler = updateCheckerRequestHandler.handler({}); try {
const data = await handler.getCachedOrUpdatedDataAsync({}); const handler = updateCheckerRequestHandler.handler({});
return data.data.availableUpdates; const data = await handler.getCachedOrUpdatedDataAsync({});
return data.data.availableUpdates;
} catch (error) {
logger.error(`Failed to get available updates\n${formatError(error)}`);
return undefined; // We return undefined to not show the indicator in the UI
}
}), }),
}); });
+2 -4
View File
@@ -523,12 +523,10 @@ const createUserAsync = async (db: Database, input: Omit<z.infer<typeof validati
const salt = await createSaltAsync(); const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt); const hashedPassword = await hashPasswordAsync(input.password, salt);
const username = input.username.toLowerCase();
const userId = createId(); const userId = createId();
await db.insert(users).values({ await db.insert(users).values({
id: userId, id: userId,
name: username, name: input.username,
email: input.email, email: input.email,
password: hashedPassword, password: hashedPassword,
salt, salt,
@@ -543,7 +541,7 @@ const checkUsernameAlreadyTakenAndThrowAsync = async (
ignoreId?: string, ignoreId?: string,
) => { ) => {
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
where: and(eq(users.name, username.toLowerCase()), eq(users.provider, provider)), where: and(eq(users.name, username), eq(users.provider, provider)),
}); });
if (!user) return; if (!user) return;
+2
View File
@@ -10,6 +10,7 @@ import { mediaServerRouter } from "./media-server";
import { mediaTranscodingRouter } from "./media-transcoding"; import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft"; import { minecraftRouter } from "./minecraft";
import { notebookRouter } from "./notebook"; import { notebookRouter } from "./notebook";
import { optionsRouter } from "./options";
import { rssFeedRouter } from "./rssFeed"; import { rssFeedRouter } from "./rssFeed";
import { smartHomeRouter } from "./smart-home"; import { smartHomeRouter } from "./smart-home";
import { weatherRouter } from "./weather"; import { weatherRouter } from "./weather";
@@ -29,4 +30,5 @@ export const widgetRouter = createTRPCRouter({
healthMonitoring: healthMonitoringRouter, healthMonitoring: healthMonitoringRouter,
mediaTranscoding: mediaTranscodingRouter, mediaTranscoding: mediaTranscodingRouter,
minecraft: minecraftRouter, minecraft: minecraftRouter,
options: optionsRouter,
}); });
@@ -0,0 +1,19 @@
import { getServerSettingsAsync } from "@homarr/db/queries";
import type { WidgetOptionsSettings } from "../../../../widgets/src";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const optionsRouter = createTRPCRouter({
getWidgetOptionSettings: publicProcedure.query(async ({ ctx }): Promise<WidgetOptionsSettings> => {
const serverSettings = await getServerSettingsAsync(ctx.db);
return {
server: {
board: {
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus,
},
},
};
}),
});
+3 -3
View File
@@ -34,12 +34,12 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "7.3.1", "ldapts": "7.3.1",
"next": "15.1.6", "next": "15.1.7",
"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" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,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.19.0", "eslint": "^9.20.1",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
+9
View File
@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@homarr/boards",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./updater": "./src/updater.ts",
"./edit-mode": "./src/edit-mode.tsx"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.20.1",
"typescript": "^5.7.3"
}
}
+70
View File
@@ -0,0 +1,70 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useContext, useEffect } from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { updateBoardName } from "./updater";
const BoardContext = createContext<{
board: RouterOutputs["board"]["getHomeBoard"];
} | null>(null);
export const BoardProvider = ({
children,
initialBoard,
}: PropsWithChildren<{
initialBoard: RouterOutputs["board"]["getBoardByName"];
}>) => {
const { data } = clientApi.board.getBoardByName.useQuery(
{ name: initialBoard.name },
{
initialData: initialBoard,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
);
// Update the board name so it can be used within updateBoard method
updateBoardName(initialBoard.name);
const pathname = usePathname();
const utils = clientApi.useUtils();
// Invalidate the board when the pathname changes
// This allows to refetch the board when it might have changed - e.g. if someone else added an item
useEffect(() => {
return () => {
void utils.board.getBoardByName.invalidate({ name: initialBoard.name });
};
}, [pathname, utils, initialBoard.name]);
return (
<BoardContext.Provider
value={{
board: data,
}}
>
{children}
</BoardContext.Provider>
);
};
export const useRequiredBoard = () => {
const optionalBoard = useOptionalBoard();
if (!optionalBoard) {
throw new Error("Board is required");
}
return optionalBoard;
};
export const useOptionalBoard = () => {
const context = useContext(BoardContext);
return context?.board ?? null;
};
+23
View File
@@ -0,0 +1,23 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useContext } from "react";
import { useDisclosure } from "@mantine/hooks";
const EditModeContext = createContext<ReturnType<typeof useDisclosure> | null>(null);
export const EditModeProvider = ({ children }: PropsWithChildren) => {
const editModeDisclosure = useDisclosure(false);
return <EditModeContext.Provider value={editModeDisclosure}>{children}</EditModeContext.Provider>;
};
export const useEditMode = () => {
const context = useContext(EditModeContext);
if (!context) {
throw new Error("EditMode is required");
}
return context;
};
+34
View File
@@ -0,0 +1,34 @@
"use client";
import { useCallback } from "react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
let boardName: string | null = null;
export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
const updateBoard = useCallback(
(updaterWithoutUndefined: UpdateCallback) => {
if (!boardName) {
throw new Error("Board name is not set");
}
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
previous ? updaterWithoutUndefined(previous) : previous,
);
},
[utils],
);
return {
updateBoard,
};
};
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
+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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+1 -1
View File
@@ -33,7 +33,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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+3 -3
View File
@@ -29,17 +29,17 @@
"dependencies": { "dependencies": {
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.1.6", "next": "15.1.7",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"undici": "7.3.0", "undici": "7.3.0",
"zod": "^3.24.1" "zod": "^3.24.2"
}, },
"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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+1 -1
View File
@@ -30,7 +30,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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+1 -1
View File
@@ -32,7 +32,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/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"eslint": "^9.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+2 -2
View File
@@ -38,13 +38,13 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"semver-parser": "^4.1.7" "semver-parser": "^4.1.8"
}, },
"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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
+9
View File
@@ -1,4 +1,6 @@
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { sendPingRequestAsync } from "@homarr/ping"; import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { pingChannel, pingUrlChannel } from "@homarr/redis";
@@ -13,6 +15,13 @@ const resetPreviousUrlsAsync = async () => {
export const pingJob = createCronJob("ping", EVERY_MINUTE, { export const pingJob = createCronJob("ping", EVERY_MINUTE, {
beforeStart: resetPreviousUrlsAsync, beforeStart: resetPreviousUrlsAsync,
}).withCallback(async () => { }).withCallback(async () => {
const boardSettings = await getServerSettingByKeyAsync(db, "board");
if (boardSettings.forceDisableStatus) {
logger.debug("Simple ping is disabled by server settings");
return;
}
const urls = await pingUrlChannel.getAllAsync(); const urls = await pingUrlChannel.getAllAsync();
await Promise.allSettled([...new Set(urls)].map(pingAsync)); await Promise.allSettled([...new Set(urls)].map(pingAsync));
+8 -3
View File
@@ -1,9 +1,11 @@
import type { Database as BetterSqlite3Connection } from "better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import type { Logger } from "drizzle-orm"; import type { Logger } from "drizzle-orm";
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3"; import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
import type { MySql2Database } from "drizzle-orm/mysql2"; import type { MySql2Database } from "drizzle-orm/mysql2";
import { drizzle as drizzleMysql } from "drizzle-orm/mysql2"; import { drizzle as drizzleMysql } from "drizzle-orm/mysql2";
import type { Pool as MysqlConnectionPool } from "mysql2";
import mysql from "mysql2"; import mysql from "mysql2";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
@@ -29,7 +31,7 @@ const init = () => {
} }
}; };
export let connection: Database.Database | mysql.Connection; export let connection: BetterSqlite3Connection | MysqlConnectionPool;
export let database: HomarrDatabase; export let database: HomarrDatabase;
class WinstonDrizzleLogger implements Logger { class WinstonDrizzleLogger implements Logger {
@@ -49,14 +51,17 @@ const initBetterSqlite = () => {
const initMySQL2 = () => { const initMySQL2 = () => {
if (!env.DB_HOST) { if (!env.DB_HOST) {
connection = mysql.createConnection({ uri: env.DB_URL }); connection = mysql.createPool({ uri: env.DB_URL, maxIdle: 0, idleTimeout: 60000, enableKeepAlive: true });
} else { } else {
connection = mysql.createConnection({ connection = mysql.createPool({
host: env.DB_HOST, host: env.DB_HOST,
database: env.DB_NAME, database: env.DB_NAME,
port: env.DB_PORT, port: env.DB_PORT,
user: env.DB_USER, user: env.DB_USER,
password: env.DB_PASSWORD, password: env.DB_PASSWORD,
maxIdle: 0,
idleTimeout: 60000,
enableKeepAlive: true,
}); });
} }
@@ -0,0 +1,2 @@
-- Custom SQL migration file, put your code below! --
-- This file is empty as there was a bug in the migration script of sqlite, missing on delete actions. See https://github.com/homarr-labs/homarr/pull/2211 --
@@ -0,0 +1 @@
ALTER TABLE `board` ADD `disable_status` boolean DEFAULT false NOT NULL;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -162,6 +162,20 @@
"when": 1737927618711, "when": 1737927618711,
"tag": "0022_famous_otto_octavius", "tag": "0022_famous_otto_octavius",
"breakpoints": true "breakpoints": true
},
{
"idx": 23,
"version": "5",
"when": 1738687012272,
"tag": "0023_fix_on_delete_actions",
"breakpoints": true
},
{
"idx": 24,
"version": "5",
"when": 1738961147412,
"tag": "0024_mean_vin_gonzales",
"breakpoints": true
} }
] ]
} }
@@ -0,0 +1,39 @@
-- Custom SQL migration file, put your code below! --
COMMIT TRANSACTION;
--> statement-breakpoint
PRAGMA foreign_keys = OFF;
--> statement-breakpoint
BEGIN TRANSACTION;
--> statement-breakpoint
CREATE TABLE `__new_user` (
`id` text PRIMARY KEY NOT NULL,
`name` text,
`email` text,
`email_verified` integer,
`image` text,
`password` text,
`salt` text,
`provider` text DEFAULT 'credentials' NOT NULL,
`home_board_id` text,
`mobile_home_board_id` text,
`default_search_engine_id` text,
`open_search_in_new_tab` integer DEFAULT true NOT NULL,
`color_scheme` text DEFAULT 'dark' NOT NULL,
`first_day_of_week` integer DEFAULT 1 NOT NULL,
`ping_icons_enabled` integer DEFAULT false NOT NULL,
FOREIGN KEY (`home_board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`mobile_home_board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`default_search_engine_id`) REFERENCES `search_engine`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
INSERT INTO `__new_user`("id", "name", "email", "email_verified", "image", "password", "salt", "provider", "home_board_id", "mobile_home_board_id", "default_search_engine_id", "open_search_in_new_tab", "color_scheme", "first_day_of_week", "ping_icons_enabled") SELECT "id", "name", "email", "email_verified", "image", "password", "salt", "provider", "home_board_id", "mobile_home_board_id", "default_search_engine_id", "open_search_in_new_tab", "color_scheme", "first_day_of_week", "ping_icons_enabled" FROM `user`;
--> statement-breakpoint
DROP TABLE `user`;
--> statement-breakpoint
ALTER TABLE `__new_user` RENAME TO `user`;
--> statement-breakpoint
COMMIT TRANSACTION;
--> statement-breakpoint
PRAGMA foreign_keys = ON;
--> statement-breakpoint
BEGIN TRANSACTION;
@@ -0,0 +1 @@
ALTER TABLE `board` ADD `disable_status` integer DEFAULT false NOT NULL;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -162,6 +162,20 @@
"when": 1737927609085, "when": 1737927609085,
"tag": "0022_modern_sunfire", "tag": "0022_modern_sunfire",
"breakpoints": true "breakpoints": true
},
{
"idx": 23,
"version": "6",
"when": 1738686324915,
"tag": "0023_fix_on_delete_actions",
"breakpoints": true
},
{
"idx": 24,
"version": "6",
"when": 1738961178990,
"tag": "0024_bitter_scrambler",
"breakpoints": true
} }
] ]
} }
+3 -3
View File
@@ -44,11 +44,11 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@t3-oss/env-nextjs": "^0.12.0", "@t3-oss/env-nextjs": "^0.12.0",
"@testcontainers/mysql": "^10.17.2", "@testcontainers/mysql": "^10.18.0",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"drizzle-kit": "^0.30.4", "drizzle-kit": "^0.30.4",
"drizzle-orm": "^0.39.1", "drizzle-orm": "^0.39.3",
"drizzle-zod": "^0.7.0", "drizzle-zod": "^0.7.0",
"mysql2": "3.12.0" "mysql2": "3.12.0"
}, },
@@ -58,7 +58,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.12", "@types/better-sqlite3": "7.6.12",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.19.0", "eslint": "^9.20.1",
"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
View File
@@ -272,6 +272,7 @@ export const boards = mysqlTable("board", {
opacity: int().default(100).notNull(), opacity: int().default(100).notNull(),
customCss: text(), customCss: text(),
columnCount: int().default(10).notNull(), columnCount: int().default(10).notNull(),
disableStatus: boolean().default(false).notNull(),
}); });
export const boardUserPermissions = mysqlTable( export const boardUserPermissions = mysqlTable(
+1
View File
@@ -258,6 +258,7 @@ export const boards = sqliteTable("board", {
opacity: int().default(100).notNull(), opacity: int().default(100).notNull(),
customCss: text(), customCss: text(),
columnCount: int().default(10).notNull(), columnCount: int().default(10).notNull(),
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
}); });
export const boardUserPermissions = sqliteTable( export const boardUserPermissions = sqliteTable(
+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.19.0", "eslint": "^9.20.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff. Show more