feat: board settings (#137)

* refactor: improve user feedback for general board settings section

* wip: add board settings for background and colors, move danger zone to own file, refactor code

* feat: add shade selector

* feat: add slider for opacity

* fix: issue with invalid hex values for color preview

* refactor: add shared mutation hook for saving partial board settings with invalidate query

* fix: add cleanup for not applied changes to logo and page title

* feat: add layout settings

* feat: add empty custom css section to board settings

* refactor: improve layout of board logo on mobile

* feat: add theme provider for board colors

* refactor: add auto contrast for better contrast of buttons with low primary shade

* feat: add background for boards

* feat: add opacity for boards

* feat: add rename board

* feat: add visibility and delete of board settings

* fix: issue that wrong data is updated with update board method

* refactor: improve danger zone button placement for mobile

* fix: board not revalidated when already in boards layout

* refactor: improve board color preview

* refactor: change save button color to teal, add placeholders for general board settings

* chore: update initial migration

* refactor: remove unnecessary div

* chore: address pull request feedback

* fix: ci issues

* fix: deepsource issues

* chore: address pull request feedback

* fix: formatting issue

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2024-03-03 16:01:32 +01:00
committed by GitHub
parent 2a83df3485
commit bb02163e25
49 changed files with 1620 additions and 406 deletions

View File

@@ -79,6 +79,24 @@ export const boardRouter = createTRPCRouter({
});
});
}),
rename: publicProcedure
.input(validation.board.rename)
.mutation(async ({ ctx, input }) => {
await noBoardWithSimilarName(ctx.db, input.name, [input.id]);
await ctx.db
.update(boards)
.set({ name: input.name })
.where(eq(boards.id, input.id));
}),
changeVisibility: publicProcedure
.input(validation.board.changeVisibility)
.mutation(async ({ ctx, input }) => {
await ctx.db
.update(boards)
.set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id));
}),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
@@ -92,11 +110,11 @@ export const boardRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
return await getFullBoardWithWhere(ctx.db, eq(boards.name, input.name));
}),
saveGeneralSettings: publicProcedure
.input(validation.board.saveGeneralSettings)
savePartialSettings: publicProcedure
.input(validation.board.savePartialSettings)
.mutation(async ({ ctx, input }) => {
const board = await ctx.db.query.boards.findFirst({
where: eq(boards.id, input.boardId),
where: eq(boards.id, input.id),
});
if (!board) {
@@ -109,12 +127,30 @@ export const boardRouter = createTRPCRouter({
await ctx.db
.update(boards)
.set({
// general settings
pageTitle: input.pageTitle,
metaTitle: input.metaTitle,
logoImageUrl: input.logoImageUrl,
faviconImageUrl: input.faviconImageUrl,
// background settings
backgroundImageUrl: input.backgroundImageUrl,
backgroundImageAttachment: input.backgroundImageAttachment,
backgroundImageRepeat: input.backgroundImageRepeat,
backgroundImageSize: input.backgroundImageSize,
// color settings
primaryColor: input.primaryColor,
secondaryColor: input.secondaryColor,
opacity: input.opacity,
// custom css
customCss: input.customCss,
// layout settings
columnCount: input.columnCount,
})
.where(eq(boards.id, input.boardId));
.where(eq(boards.id, input.id));
}),
save: publicProcedure
.input(validation.board.save)
@@ -122,7 +158,7 @@ export const boardRouter = createTRPCRouter({
await ctx.db.transaction(async (tx) => {
const dbBoard = await getFullBoardWithWhere(
tx,
eq(boards.id, input.boardId),
eq(boards.id, input.id),
);
const addedSections = filterAddedItems(
@@ -276,6 +312,32 @@ export const boardRouter = createTRPCRouter({
}),
});
const noBoardWithSimilarName = async (
db: Database,
name: string,
ignoredIds: string[] = [],
) => {
const boards = await db.query.boards.findMany({
columns: {
id: true,
name: true,
},
});
const board = boards.find(
(board) =>
board.name.toLowerCase() === name.toLowerCase() &&
!ignoredIds.includes(board.id),
);
if (board) {
throw new TRPCError({
code: "CONFLICT",
message: "Board with similar name already exists",
});
}
};
const getFullBoardWithWhere = async (db: Database, where: SQL<unknown>) => {
const board = await db.query.boards.findFirst({
where,

View File

@@ -66,7 +66,7 @@ describe("byName should return board by name", () => {
it("should throw error when not present");
});
describe("saveGeneralSettings should save general settings", () => {
describe("savePartialSettings should save general settings", () => {
it("should save general settings", async () => {
const db = createDb();
const caller = boardRouter.createCaller({ db, session: null });
@@ -78,12 +78,12 @@ describe("saveGeneralSettings should save general settings", () => {
const { boardId } = await createFullBoardAsync(db, "default");
await caller.saveGeneralSettings({
await caller.savePartialSettings({
pageTitle: newPageTitle,
metaTitle: newMetaTitle,
logoImageUrl: newLogoImageUrl,
faviconImageUrl: newFaviconImageUrl,
boardId,
id: boardId,
});
});
@@ -92,12 +92,12 @@ describe("saveGeneralSettings should save general settings", () => {
const caller = boardRouter.createCaller({ db, session: null });
const act = async () =>
await caller.saveGeneralSettings({
await caller.savePartialSettings({
pageTitle: "newPageTitle",
metaTitle: "newMetaTitle",
logoImageUrl: "http://logo.image/url.png",
faviconImageUrl: "http://favicon.image/url.png",
boardId: "nonExistentBoardId",
id: "nonExistentBoardId",
});
await expect(act()).rejects.toThrowError("Board not found");
@@ -112,7 +112,7 @@ describe("save should save full board", () => {
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
await caller.save({
boardId,
id: boardId,
sections: [
{
id: createId(),
@@ -149,7 +149,7 @@ describe("save should save full board", () => {
);
await caller.save({
boardId,
id: boardId,
sections: [
{
id: sectionId,
@@ -208,7 +208,7 @@ describe("save should save full board", () => {
await db.insert(integrations).values(anotherIntegration);
await caller.save({
boardId,
id: boardId,
sections: [
{
id: sectionId,
@@ -269,7 +269,7 @@ describe("save should save full board", () => {
const newSectionId = createId();
await caller.save({
boardId,
id: boardId,
sections: [
{
id: newSectionId,
@@ -319,7 +319,7 @@ describe("save should save full board", () => {
const newItemId = createId();
await caller.save({
boardId,
id: boardId,
sections: [
{
id: sectionId,
@@ -392,7 +392,7 @@ describe("save should save full board", () => {
await db.insert(integrations).values(integration);
await caller.save({
boardId,
id: boardId,
sections: [
{
id: sectionId,
@@ -459,7 +459,7 @@ describe("save should save full board", () => {
});
await caller.save({
boardId,
id: boardId,
sections: [
{
id: sectionId,
@@ -512,7 +512,7 @@ describe("save should save full board", () => {
);
await caller.save({
boardId,
id: boardId,
sections: [
{
id: sectionId,
@@ -569,7 +569,7 @@ describe("save should save full board", () => {
const act = async () =>
await caller.save({
boardId: "nonExistentBoardId",
id: "nonExistentBoardId",
sections: [],
});