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:
@@ -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,
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user