feat(groups): add home board settings (#2321)
This commit is contained in:
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||
import { and, createId, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db";
|
||||
import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import {
|
||||
boardGroupPermissions,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
boardUserPermissions,
|
||||
groupMembers,
|
||||
groupPermissions,
|
||||
groups,
|
||||
integrationGroupPermissions,
|
||||
integrationItems,
|
||||
integrationUserPermissions,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
users,
|
||||
} from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||
import { everyoneGroup, getPermissionsWithChildren, getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||
import { importOldmarrAsync } from "@homarr/old-import";
|
||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||
@@ -57,6 +58,37 @@ export const boardRouter = createTRPCRouter({
|
||||
where: eq(boards.isPublic, true),
|
||||
});
|
||||
}),
|
||||
getBoardsForGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(z.object({ groupId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const dbEveryoneAndCurrentGroup = await ctx.db.query.groups.findMany({
|
||||
where: or(eq(groups.name, everyoneGroup), eq(groups.id, input.groupId)),
|
||||
with: {
|
||||
boardPermissions: true,
|
||||
permissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
const distinctPermissions = new Set(
|
||||
dbEveryoneAndCurrentGroup.flatMap((group) => group.permissions.map(({ permission }) => permission)),
|
||||
);
|
||||
const canViewAllBoards = getPermissionsWithChildren([...distinctPermissions]).includes("board-view-all");
|
||||
|
||||
const boardIds = dbEveryoneAndCurrentGroup.flatMap((group) =>
|
||||
group.boardPermissions.map(({ boardId }) => boardId),
|
||||
);
|
||||
const boardWhere = canViewAllBoards ? undefined : or(eq(boards.isPublic, true), inArray(boards.id, boardIds));
|
||||
|
||||
return await ctx.db.query.boards.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
logoImageUrl: true,
|
||||
},
|
||||
where: boardWhere,
|
||||
});
|
||||
}),
|
||||
getAllBoards: publicProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
|
||||
@@ -89,6 +121,7 @@ export const boardRouter = createTRPCRouter({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
logoImageUrl: true,
|
||||
isPublic: true,
|
||||
},
|
||||
with: {
|
||||
@@ -975,9 +1008,13 @@ export const boardRouter = createTRPCRouter({
|
||||
* For an example of a user with deviceType = 'mobile' it would go through the following order:
|
||||
* 1. user.mobileHomeBoardId
|
||||
* 2. user.homeBoardId
|
||||
* 3. serverSettings.mobileHomeBoardId
|
||||
* 4. serverSettings.homeBoardId
|
||||
* 5. show NOT_FOUND error
|
||||
* 3. group.mobileHomeBoardId of the lowest positions group
|
||||
* 4. group.homeBoardId of the lowest positions group
|
||||
* 5. everyoneGroup.mobileHomeBoardId
|
||||
* 6. everyoneGroup.homeBoardId
|
||||
* 7. serverSettings.mobileHomeBoardId
|
||||
* 8. serverSettings.homeBoardId
|
||||
* 9. show NOT_FOUND error
|
||||
*/
|
||||
const getHomeIdBoardAsync = async (
|
||||
db: Database,
|
||||
@@ -985,12 +1022,46 @@ const getHomeIdBoardAsync = async (
|
||||
deviceType: DeviceType,
|
||||
) => {
|
||||
const settingKey = deviceType === "mobile" ? "mobileHomeBoardId" : "homeBoardId";
|
||||
if (user?.[settingKey] || user?.homeBoardId) {
|
||||
return user[settingKey] ?? user.homeBoardId;
|
||||
} else {
|
||||
|
||||
if (!user) {
|
||||
const boardSettings = await getServerSettingByKeyAsync(db, "board");
|
||||
return boardSettings[settingKey] ?? boardSettings.homeBoardId;
|
||||
}
|
||||
|
||||
if (user[settingKey]) return user[settingKey];
|
||||
if (user.homeBoardId) return user.homeBoardId;
|
||||
|
||||
const lowestGroupExceptEveryone = await db
|
||||
.select({
|
||||
homeBoardId: groups.homeBoardId,
|
||||
mobileHomeBoardId: groups.mobileHomeBoardId,
|
||||
})
|
||||
.from(groups)
|
||||
.leftJoin(groupMembers, eq(groups.id, groupMembers.groupId))
|
||||
.where(
|
||||
and(
|
||||
eq(groupMembers.userId, user.id),
|
||||
not(eq(groups.name, everyoneGroup)),
|
||||
not(isNull(groups[settingKey])),
|
||||
not(isNull(groups.homeBoardId)),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(groups.position))
|
||||
.limit(1)
|
||||
.then((result) => result[0]);
|
||||
|
||||
if (lowestGroupExceptEveryone?.[settingKey]) return lowestGroupExceptEveryone[settingKey];
|
||||
if (lowestGroupExceptEveryone?.homeBoardId) return lowestGroupExceptEveryone.homeBoardId;
|
||||
|
||||
const dbEveryoneGroup = await db.query.groups.findFirst({
|
||||
where: eq(groups.name, everyoneGroup),
|
||||
});
|
||||
|
||||
if (dbEveryoneGroup?.[settingKey]) return dbEveryoneGroup[settingKey];
|
||||
if (dbEveryoneGroup?.homeBoardId) return dbEveryoneGroup.homeBoardId;
|
||||
|
||||
const boardSettings = await getServerSettingByKeyAsync(db, "board");
|
||||
return boardSettings[settingKey] ?? boardSettings.homeBoardId;
|
||||
};
|
||||
|
||||
const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
||||
import { and, createId, eq, handleTransactionsAsync, like, not, sql } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { validation } from "@homarr/validation";
|
||||
@@ -12,6 +13,30 @@ import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||
|
||||
export const groupRouter = createTRPCRouter({
|
||||
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
|
||||
const dbGroups = await ctx.db.query.groups.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return dbGroups.map((group) => ({
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
}));
|
||||
}),
|
||||
|
||||
getPaginated: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.paginated)
|
||||
@@ -153,10 +178,13 @@ export const groupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||
|
||||
const maxPosition = await getMaxGroupPositionAsync(ctx.db);
|
||||
|
||||
const groupId = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: input.name,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
|
||||
await ctx.db.insert(groupPermissions).values({
|
||||
@@ -172,10 +200,13 @@ export const groupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||
|
||||
const maxPosition = await getMaxGroupPositionAsync(ctx.db);
|
||||
|
||||
const id = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id,
|
||||
name: input.name,
|
||||
position: maxPosition + 1,
|
||||
ownerId: ctx.session.user.id,
|
||||
});
|
||||
|
||||
@@ -197,6 +228,43 @@ export const groupRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(groups.id, input.id));
|
||||
}),
|
||||
savePartialSettings: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePartialSettings)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
homeBoardId: input.settings.homeBoardId,
|
||||
mobileHomeBoardId: input.settings.mobileHomeBoardId,
|
||||
})
|
||||
.where(eq(groups.id, input.id));
|
||||
}),
|
||||
savePositions: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePositions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
handleAsync: async (db, schema) => {
|
||||
await db.transaction(async (trx) => {
|
||||
for (const { id, position } of positions) {
|
||||
await trx.update(schema.groups).set({ position }).where(eq(groups.id, id));
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync: (db) => {
|
||||
db.transaction((trx) => {
|
||||
for (const { id, position } of positions) {
|
||||
trx.update(groups).set({ position }).where(eq(groups.id, id)).run();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
savePermissions: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePermissions)
|
||||
|
||||
@@ -205,6 +205,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "group1",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
await db.insert(groupMembers).values({
|
||||
@@ -1166,6 +1167,7 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "group1",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
await db.insert(boardGroupPermissions).values({
|
||||
@@ -1260,6 +1262,7 @@ describe("saveGroupBoardPermissions should save group board permissions", () =>
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "group1",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
const boardId = createId();
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("paginated should return a list of groups with pagination", () => {
|
||||
[1, 2, 3, 4, 5].map((number) => ({
|
||||
id: number.toString(),
|
||||
name: `Group ${number}`,
|
||||
position: number,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -66,6 +67,7 @@ describe("paginated should return a list of groups with pagination", () => {
|
||||
[1, 2, 3, 4, 5].map((number) => ({
|
||||
id: number.toString(),
|
||||
name: `Group ${number}`,
|
||||
position: number,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -89,6 +91,7 @@ describe("paginated should return a list of groups with pagination", () => {
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupMembers).values({
|
||||
groupId,
|
||||
@@ -123,6 +126,7 @@ describe("paginated should return a list of groups with pagination", () => {
|
||||
["first", "second", "third", "forth", "fifth"].map((key, index) => ({
|
||||
id: index.toString(),
|
||||
name: key,
|
||||
position: index + 1,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -163,10 +167,12 @@ describe("byId should return group by id including members and permissions", ()
|
||||
{
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: "Another group",
|
||||
position: 2,
|
||||
},
|
||||
]);
|
||||
await db.insert(groupMembers).values({
|
||||
@@ -202,6 +208,7 @@ describe("byId should return group by id including members and permissions", ()
|
||||
await db.insert(groups).values({
|
||||
id: "2",
|
||||
name: "Group",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -278,6 +285,7 @@ describe("create should create group in database", () => {
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: similarName,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -314,10 +322,12 @@ describe("update should update name with value that is no duplicate", () => {
|
||||
{
|
||||
id: groupId,
|
||||
name: initialValue,
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: "Third",
|
||||
position: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -347,10 +357,12 @@ describe("update should update name with value that is no duplicate", () => {
|
||||
{
|
||||
id: groupId,
|
||||
name: "Something",
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: initialDuplicate,
|
||||
position: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -373,6 +385,7 @@ describe("update should update name with value that is no duplicate", () => {
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: "something",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -413,6 +426,7 @@ describe("savePermissions should save permissions for group", () => {
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupPermissions).values({
|
||||
groupId,
|
||||
@@ -442,6 +456,7 @@ describe("savePermissions should save permissions for group", () => {
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: "Group",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -494,6 +509,7 @@ describe("transferOwnership should transfer ownership of group", () => {
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
ownerId: defaultOwnerId,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -518,6 +534,7 @@ describe("transferOwnership should transfer ownership of group", () => {
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: "Group",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -559,10 +576,12 @@ describe("deleteGroup should delete group", () => {
|
||||
{
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: "Another group",
|
||||
position: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -586,6 +605,7 @@ describe("deleteGroup should delete group", () => {
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: "Group",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -638,6 +658,7 @@ describe("addMember should add member to group", () => {
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
ownerId: defaultOwnerId,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -715,6 +736,7 @@ describe("addMember should add member to group", () => {
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
ownerId: defaultOwnerId,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -753,6 +775,7 @@ describe("removeMember should remove member from group", () => {
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
ownerId: defaultOwnerId,
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupMembers).values({
|
||||
groupId,
|
||||
@@ -833,6 +856,7 @@ describe("removeMember should remove member from group", () => {
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
ownerId: defaultOwnerId,
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupMembers).values({
|
||||
groupId,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq, like } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { boards, groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema";
|
||||
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||
@@ -31,12 +32,14 @@ export const userRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
throwIfCredentialsDisabled();
|
||||
|
||||
const maxPosition = await getMaxGroupPositionAsync(ctx.db);
|
||||
const userId = await createUserAsync(ctx.db, input);
|
||||
const groupId = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: credentialsAdminGroup,
|
||||
ownerId: userId,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
await ctx.db.insert(groupPermissions).values({
|
||||
groupId,
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("hasQueryAccessToIntegrationsAsync should check if the user has query a
|
||||
},
|
||||
];
|
||||
await db.insert(boards).values(createMockBoard({ id: "1" }));
|
||||
await db.insert(groups).values({ id: "1", name: "" });
|
||||
await db.insert(groups).values({ id: "1", name: "", position: 1 });
|
||||
await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" });
|
||||
await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "1", permission: "view" });
|
||||
|
||||
@@ -325,7 +325,7 @@ describe("hasQueryAccessToIntegrationsAsync should check if the user has query a
|
||||
},
|
||||
];
|
||||
await db.insert(boards).values(createMockBoard({ id: "1" }));
|
||||
await db.insert(groups).values({ id: "1", name: "" });
|
||||
await db.insert(groups).values({ id: "1", name: "", position: 1 });
|
||||
await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" });
|
||||
await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "1", permission: "view" });
|
||||
|
||||
@@ -379,7 +379,7 @@ describe("hasQueryAccessToIntegrationsAsync should check if the user has query a
|
||||
];
|
||||
await db.insert(boards).values(createMockBoard({ id: "1" }));
|
||||
await db.insert(boards).values(createMockBoard({ id: "2" }));
|
||||
await db.insert(groups).values({ id: "1", name: "" });
|
||||
await db.insert(groups).values({ id: "1", name: "", position: 1 });
|
||||
await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" });
|
||||
await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "2", permission: "view" });
|
||||
await db.insert(boardUserPermissions).values({ userId: session.user.id, boardId: "1", permission: "view" });
|
||||
|
||||
@@ -301,6 +301,7 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "homarr_example",
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
@@ -25,6 +25,7 @@ describe("getCurrentUserPermissions", () => {
|
||||
await db.insert(groups).values({
|
||||
id: "2",
|
||||
name: "test",
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupPermissions).values({
|
||||
groupId: "2",
|
||||
@@ -51,6 +52,7 @@ describe("getCurrentUserPermissions", () => {
|
||||
await db.insert(groups).values({
|
||||
id: "2",
|
||||
name: "test",
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupPermissions).values({
|
||||
groupId: "2",
|
||||
@@ -81,6 +83,7 @@ describe("getCurrentUserPermissions", () => {
|
||||
await db.insert(groups).values({
|
||||
id: mockId,
|
||||
name: "test",
|
||||
position: 1,
|
||||
});
|
||||
await db.insert(groupMembers).values({
|
||||
userId: mockId,
|
||||
|
||||
@@ -259,4 +259,5 @@ const createGroupAsync = async (db: Database, name = "test") =>
|
||||
await db.insert(groups).values({
|
||||
id: "1",
|
||||
name,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
ALTER TABLE `group` ADD `home_board_id` varchar(64);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `group` ADD `mobile_home_board_id` varchar(64);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `group` ADD `position` smallint;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `temp_group` (
|
||||
`id` varchar(64) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`position` smallint NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `temp_group`(`id`, `name`, `position`) SELECT `id`, `name`, ROW_NUMBER() OVER(ORDER BY `name`) FROM `group` WHERE `name` != 'everyone';
|
||||
--> statement-breakpoint
|
||||
UPDATE `group` SET `position`=(SELECT `position` FROM `temp_group` WHERE `temp_group`.`id`=`group`.`id`);
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `temp_group`;
|
||||
--> statement-breakpoint
|
||||
UPDATE `group` SET `position` = -1 WHERE `name` = 'everyone';
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `group` MODIFY `position` smallint NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `group` ADD CONSTRAINT `group_home_board_id_board_id_fk` FOREIGN KEY (`home_board_id`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `group` ADD CONSTRAINT `group_mobile_home_board_id_board_id_fk` FOREIGN KEY (`mobile_home_board_id`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action;
|
||||
1811
packages/db/migrations/mysql/meta/0025_snapshot.json
Normal file
1811
packages/db/migrations/mysql/meta/0025_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,13 @@
|
||||
"when": 1738961147412,
|
||||
"tag": "0024_mean_vin_gonzales",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "5",
|
||||
"when": 1739469710187,
|
||||
"tag": "0025_add-group-home-board-settings",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const seedEveryoneGroupAsync = async (db: Database) => {
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: everyoneGroup,
|
||||
position: -1,
|
||||
});
|
||||
console.log("Created group 'everyone' through seed");
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
COMMIT TRANSACTION;
|
||||
--> statement-breakpoint
|
||||
PRAGMA foreign_keys = OFF;
|
||||
--> statement-breakpoint
|
||||
BEGIN TRANSACTION;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `__new_group` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`owner_id` text,
|
||||
`home_board_id` text,
|
||||
`mobile_home_board_id` text,
|
||||
`position` integer NOT NULL,
|
||||
FOREIGN KEY (`owner_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set 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
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_group`("id", "name", "owner_id", "position") SELECT "id", "name", "owner_id", -1 FROM `group` WHERE "name" = 'everyone';
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_group`("id", "name", "owner_id", "position") SELECT "id", "name", "owner_id", ROW_NUMBER() OVER(ORDER BY "name") FROM `group` WHERE "name" != 'everyone';
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `group`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `__new_group` RENAME TO `group`;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `group_name_unique` ON `group` (`name`);
|
||||
--> statement-breakpoint
|
||||
COMMIT TRANSACTION;
|
||||
--> statement-breakpoint
|
||||
PRAGMA foreign_keys = ON;
|
||||
--> statement-breakpoint
|
||||
BEGIN TRANSACTION;
|
||||
1736
packages/db/migrations/sqlite/meta/0025_snapshot.json
Normal file
1736
packages/db/migrations/sqlite/meta/0025_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,13 @@
|
||||
"when": 1738961178990,
|
||||
"tag": "0024_bitter_scrambler",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "6",
|
||||
"when": 1739468826756,
|
||||
"tag": "0025_add-group-home-board-settings",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
11
packages/db/queries/group.ts
Normal file
11
packages/db/queries/group.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { max } from "drizzle-orm";
|
||||
|
||||
import type { HomarrDatabase } from "../driver";
|
||||
import { groups } from "../schema";
|
||||
|
||||
export const getMaxGroupPositionAsync = async (db: HomarrDatabase) => {
|
||||
return await db
|
||||
.select({ value: max(groups.position) })
|
||||
.from(groups)
|
||||
.then((result) => result[0]?.value ?? 1);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./item";
|
||||
export * from "./server-setting";
|
||||
export * from "./group";
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
int,
|
||||
mysqlTable,
|
||||
primaryKey,
|
||||
smallint,
|
||||
text,
|
||||
timestamp,
|
||||
tinyint,
|
||||
@@ -150,6 +151,13 @@ export const groups = mysqlTable("group", {
|
||||
ownerId: varchar({ length: 64 }).references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
homeBoardId: varchar({ length: 64 }).references(() => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
mobileHomeBoardId: varchar({ length: 64 }).references(() => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
position: smallint().notNull(),
|
||||
});
|
||||
|
||||
export const groupPermissions = mysqlTable("groupPermission", {
|
||||
@@ -499,6 +507,16 @@ export const groupRelations = relations(groups, ({ one, many }) => ({
|
||||
fields: [groups.ownerId],
|
||||
references: [users.id],
|
||||
}),
|
||||
homeBoard: one(boards, {
|
||||
fields: [groups.homeBoardId],
|
||||
references: [boards.id],
|
||||
relationName: "groupRelations__board__homeBoardId",
|
||||
}),
|
||||
mobileHomeBoard: one(boards, {
|
||||
fields: [groups.mobileHomeBoardId],
|
||||
references: [boards.id],
|
||||
relationName: "groupRelations__board__mobileHomeBoardId",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const groupPermissionRelations = relations(groupPermissions, ({ one }) => ({
|
||||
@@ -574,6 +592,12 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
||||
}),
|
||||
userPermissions: many(boardUserPermissions),
|
||||
groupPermissions: many(boardGroupPermissions),
|
||||
groupHomes: many(groups, {
|
||||
relationName: "groupRelations__board__homeBoardId",
|
||||
}),
|
||||
mobileHomeBoard: many(groups, {
|
||||
relationName: "groupRelations__board__mobileHomeBoardId",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const sectionRelations = relations(sections, ({ many, one }) => ({
|
||||
|
||||
@@ -133,6 +133,13 @@ export const groups = sqliteTable("group", {
|
||||
ownerId: text().references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
homeBoardId: text().references(() => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
mobileHomeBoardId: text().references(() => boards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
position: int().notNull(),
|
||||
});
|
||||
|
||||
export const groupPermissions = sqliteTable("groupPermission", {
|
||||
@@ -486,6 +493,16 @@ export const groupRelations = relations(groups, ({ one, many }) => ({
|
||||
fields: [groups.ownerId],
|
||||
references: [users.id],
|
||||
}),
|
||||
homeBoard: one(boards, {
|
||||
fields: [groups.homeBoardId],
|
||||
references: [boards.id],
|
||||
relationName: "groupRelations__board__homeBoardId",
|
||||
}),
|
||||
mobileHomeBoard: one(boards, {
|
||||
fields: [groups.mobileHomeBoardId],
|
||||
references: [boards.id],
|
||||
relationName: "groupRelations__board__mobileHomeBoardId",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const groupPermissionRelations = relations(groupPermissions, ({ one }) => ({
|
||||
@@ -561,6 +578,12 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
||||
}),
|
||||
userPermissions: many(boardUserPermissions),
|
||||
groupPermissions: many(boardGroupPermissions),
|
||||
groupHomes: many(groups, {
|
||||
relationName: "groupRelations__board__homeBoardId",
|
||||
}),
|
||||
mobileHomeBoard: many(groups, {
|
||||
relationName: "groupRelations__board__mobileHomeBoardId",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const sectionRelations = relations(sections, ({ many, one }) => ({
|
||||
|
||||
@@ -36,6 +36,7 @@ export const createUserInsertCollection = (
|
||||
insertCollection.groups.push({
|
||||
id: adminGroupId,
|
||||
name: credentialsAdminGroup,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
insertCollection.groupPermissions.push({
|
||||
|
||||
@@ -305,7 +305,15 @@
|
||||
"search": "Find a group",
|
||||
"field": {
|
||||
"name": "Name",
|
||||
"members": "Members"
|
||||
"members": "Members",
|
||||
"homeBoard": {
|
||||
"label": "Home board",
|
||||
"description": "Only boards accessible to the group can be selected"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": "Mobile board",
|
||||
"description": "Only boards accessible to the group can be selected"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"admin": {
|
||||
@@ -501,7 +509,35 @@
|
||||
"select": {
|
||||
"label": "Select group",
|
||||
"notFound": "No group found"
|
||||
},
|
||||
"settings": {
|
||||
"board": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Settings saved",
|
||||
"message": "Board settings saved successfully"
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to save settings",
|
||||
"message": "Unable to save board settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"changePosition": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": "Position changed successfully"
|
||||
},
|
||||
"error": {
|
||||
"message": "Unable to change position"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultGroup": {
|
||||
"name": "Default group",
|
||||
"description": "{name} - All signed in users"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
@@ -888,6 +924,7 @@
|
||||
},
|
||||
"dangerZone": "Danger zone",
|
||||
"noResults": "No results found",
|
||||
"unsavedChanges": "You have unsaved changes!",
|
||||
"preview": {
|
||||
"show": "Show preview",
|
||||
"hide": "Hide preview"
|
||||
@@ -2414,6 +2451,13 @@
|
||||
"ownerOfGroup": "Owner of this group",
|
||||
"ownerOfGroupDeleted": "The owner of this group was deleted. It currently has no owner."
|
||||
},
|
||||
"setting": {
|
||||
"title": "Settings",
|
||||
"alert": "Group settings are prioritized by the order of groups in the list. The top settings overwrite the bottom settings.",
|
||||
"board": {
|
||||
"title": "Boards"
|
||||
}
|
||||
},
|
||||
"members": {
|
||||
"title": "Members",
|
||||
"search": "Find a member",
|
||||
|
||||
@@ -4,6 +4,7 @@ export { SearchInput } from "./search-input";
|
||||
export * from "./select-with-description";
|
||||
export * from "./select-with-description-and-badge";
|
||||
export { SelectWithCustomItems } from "./select-with-custom-items";
|
||||
export type { SelectWithCustomItemsProps } from "./select-with-custom-items";
|
||||
export { TablePagination } from "./table-pagination";
|
||||
export { TextMultiSelect } from "./text-multi-select";
|
||||
export { UserAvatar } from "./user-avatar";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { SelectProps } from "@mantine/core";
|
||||
import { Combobox, Input, InputBase, useCombobox } from "@mantine/core";
|
||||
import { Combobox, ComboboxClearButton, Input, InputBase, useCombobox } from "@mantine/core";
|
||||
import { useUncontrolled } from "@mantine/hooks";
|
||||
|
||||
interface BaseSelectItem {
|
||||
@@ -11,7 +11,7 @@ interface BaseSelectItem {
|
||||
}
|
||||
|
||||
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
|
||||
extends Pick<SelectProps, "label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder"> {
|
||||
extends Pick<SelectProps, "label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder" | "clearable"> {
|
||||
data: TSelectItem[];
|
||||
description?: string;
|
||||
withAsterisk?: boolean;
|
||||
@@ -32,6 +32,7 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
|
||||
placeholder,
|
||||
SelectOption,
|
||||
w,
|
||||
clearable,
|
||||
...props
|
||||
}: Props<TSelectItem>) => {
|
||||
const combobox = useCombobox({
|
||||
@@ -65,6 +66,8 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
|
||||
[setValue, data, combobox],
|
||||
);
|
||||
|
||||
const _clearable = clearable && Boolean(_value);
|
||||
|
||||
return (
|
||||
<Combobox store={combobox} withinPortal={false} onOptionSubmit={onOptionSubmit}>
|
||||
<Combobox.Target>
|
||||
@@ -73,9 +76,11 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
|
||||
component="button"
|
||||
type="button"
|
||||
pointer
|
||||
rightSection={<Combobox.Chevron />}
|
||||
__clearSection={<ComboboxClearButton onClear={() => setValue(null, null)} />}
|
||||
__clearable={_clearable}
|
||||
__defaultRightSection={<Combobox.Chevron />}
|
||||
onClick={toggle}
|
||||
rightSectionPointerEvents="none"
|
||||
rightSectionPointerEvents={_clearable ? "all" : "none"}
|
||||
multiline
|
||||
w={w}
|
||||
>
|
||||
|
||||
@@ -18,11 +18,25 @@ const createSchema = z.object({
|
||||
|
||||
const updateSchema = createSchema.merge(byIdSchema);
|
||||
|
||||
const settingsSchema = z.object({
|
||||
homeBoardId: z.string().nullable(),
|
||||
mobileHomeBoardId: z.string().nullable(),
|
||||
});
|
||||
|
||||
const savePartialSettingsSchema = z.object({
|
||||
id: z.string(),
|
||||
settings: settingsSchema.partial(),
|
||||
});
|
||||
|
||||
const savePermissionsSchema = z.object({
|
||||
groupId: z.string(),
|
||||
permissions: z.array(zodEnumFromArray(groupPermissionKeys)),
|
||||
});
|
||||
|
||||
const savePositionsSchema = z.object({
|
||||
positions: z.array(z.string()),
|
||||
});
|
||||
|
||||
const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() });
|
||||
|
||||
export const groupSchemas = {
|
||||
@@ -30,4 +44,7 @@ export const groupSchemas = {
|
||||
update: updateSchema,
|
||||
savePermissions: savePermissionsSchema,
|
||||
groupUser: groupUserSchema,
|
||||
savePartialSettings: savePartialSettingsSchema,
|
||||
settings: settingsSchema,
|
||||
savePositions: savePositionsSchema,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user