feat: Unified icon picker (#7038)

This commit is contained in:
Hemachandar
2024-06-23 19:01:18 +05:30
committed by GitHub
parent 56d90e6bc3
commit 6fd3a0fa8a
83 changed files with 2302 additions and 852 deletions

View File

@@ -1,5 +1,4 @@
import { CollectionPermission } from "@shared/types";
import { colorPalette } from "@shared/utils/collections";
import { Document, UserMembership, GroupPermission } from "@server/models";
import {
buildUser,
@@ -182,6 +181,23 @@ describe("#collections.move", () => {
expect(body.success).toBe(true);
});
it("should allow setting an emoji as icon", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
const collection = await buildCollection({ teamId: team.id });
const res = await server.post("/api/collections.move", {
body: {
token: admin.getJwtToken(),
id: collection.id,
index: "P",
icon: "😁",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toBe(true);
});
it("should return error when icon is not valid", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
@@ -1150,7 +1166,6 @@ describe("#collections.create", () => {
expect(body.data.name).toBe("Test");
expect(body.data.sort.field).toBe("index");
expect(body.data.sort.direction).toBe("asc");
expect(colorPalette.includes(body.data.color)).toBeTruthy();
expect(body.policies.length).toBe(1);
expect(body.policies[0].abilities.read).toBeTruthy();
});

View File

@@ -1,21 +1,13 @@
import emojiRegex from "emoji-regex";
import isUndefined from "lodash/isUndefined";
import { z } from "zod";
import { randomElement } from "@shared/random";
import { CollectionPermission, FileOperationFormat } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections";
import { Collection } from "@server/models";
import { zodEnumFromObjectKeys } from "@server/utils/zod";
import { ValidateColor, ValidateIndex } from "@server/validation";
import { BaseSchema, ProsemirrorSchema } from "../schema";
function zodEnumFromObjectKeys<
TI extends Record<string, any>,
R extends string = TI extends Record<infer R, any> ? R : never
>(input: TI): z.ZodEnum<[R, ...R[]]> {
const [firstKey, ...otherKeys] = Object.keys(input) as [R, ...R[]];
return z.enum([firstKey, ...otherKeys]);
}
const BaseIdSchema = z.object({
/** Id of the collection to be updated */
id: z.string(),
@@ -27,7 +19,7 @@ export const CollectionsCreateSchema = BaseSchema.extend({
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.default(randomElement(colorPalette)),
.nullish(),
description: z.string().nullish(),
data: ProsemirrorSchema.nullish(),
permission: z
@@ -35,7 +27,12 @@ export const CollectionsCreateSchema = BaseSchema.extend({
.nullish()
.transform((val) => (isUndefined(val) ? null : val)),
sharing: z.boolean().default(true),
icon: zodEnumFromObjectKeys(IconLibrary.mapping).optional(),
icon: z
.union([
z.string().regex(emojiRegex()),
zodEnumFromObjectKeys(IconLibrary.mapping),
])
.optional(),
sort: z
.object({
field: z.union([z.literal("title"), z.literal("index")]),
@@ -174,7 +171,12 @@ export const CollectionsUpdateSchema = BaseSchema.extend({
name: z.string().optional(),
description: z.string().nullish(),
data: ProsemirrorSchema.nullish(),
icon: zodEnumFromObjectKeys(IconLibrary.mapping).nullish(),
icon: z
.union([
z.string().regex(emojiRegex()),
zodEnumFromObjectKeys(IconLibrary.mapping),
])
.nullish(),
permission: z.nativeEnum(CollectionPermission).nullish(),
color: z
.string()

View File

@@ -2786,7 +2786,7 @@ describe("#documents.create", () => {
expect(body.message).toEqual("parentDocumentId: Invalid uuid");
});
it("should create as a new document", async () => {
it("should create as a new document with emoji", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({
@@ -2809,6 +2809,34 @@ describe("#documents.create", () => {
expect(newDocument!.parentDocumentId).toBe(null);
expect(newDocument!.collectionId).toBe(collection.id);
expect(newDocument!.emoji).toBe("🚢");
expect(newDocument!.icon).toBe("🚢");
expect(body.policies[0].abilities.update).toEqual(true);
});
it("should create as a new document with icon", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({
userId: user.id,
teamId: team.id,
});
const res = await server.post("/api/documents.create", {
body: {
token: user.getJwtToken(),
collectionId: collection.id,
icon: "🚢",
title: "new document",
text: "hello",
publish: true,
},
});
const body = await res.json();
const newDocument = await Document.findByPk(body.data.id);
expect(res.status).toEqual(200);
expect(newDocument!.parentDocumentId).toBe(null);
expect(newDocument!.collectionId).toBe(collection.id);
expect(newDocument!.emoji).toBe("🚢");
expect(newDocument!.icon).toBe("🚢");
expect(body.policies[0].abilities.update).toEqual(true);
});
@@ -3094,7 +3122,7 @@ describe("#documents.update", () => {
expect(res.status).toEqual(403);
});
it("should fail to update an invalid emoji value", async () => {
it("should fail to update an invalid icon value", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
@@ -3105,13 +3133,13 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
emoji: ":)",
icon: ":)",
},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("emoji: Invalid");
expect(body.message).toBe("icon: Invalid");
});
it("should successfully update the emoji", async () => {
@@ -3124,12 +3152,34 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
emoji: "😂",
emoji: "🚢",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.emoji).toBe("😂");
expect(body.data.emoji).toBe("🚢");
expect(body.data.icon).toBe("🚢");
expect(body.data.color).toBeNull;
});
it("should successfully update the icon", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
const res = await server.post("/api/documents.update", {
body: {
token: user.getJwtToken(),
id: document.id,
icon: "beaker",
color: "#FFDDEE",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.icon).toBe("beaker");
expect(body.data.color).toBe("#FFDDEE");
});
it("should not add template to collection structure when publishing", async () => {

View File

@@ -944,6 +944,8 @@ router.post(
createdById: user.id,
template: true,
emoji: original.emoji,
icon: original.icon,
color: original.color,
title: original.title,
text: original.text,
content: original.content,
@@ -1041,6 +1043,7 @@ router.post(
document,
user,
...input,
icon: input.icon ?? input.emoji,
publish,
collectionId,
insightsEnabled,
@@ -1382,6 +1385,8 @@ router.post(
title,
text,
emoji,
icon,
color,
publish,
collectionId,
parentDocumentId,
@@ -1445,7 +1450,8 @@ router.post(
const document = await documentCreator({
title,
text,
emoji,
icon: icon ?? emoji,
color,
createdAt,
publish,
collectionId: collection?.id,

View File

@@ -4,8 +4,11 @@ import isEmpty from "lodash/isEmpty";
import isUUID from "validator/lib/isUUID";
import { z } from "zod";
import { DocumentPermission, StatusFilter } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { UrlHelper } from "@shared/utils/UrlHelper";
import { BaseSchema } from "@server/routes/api/schema";
import { zodEnumFromObjectKeys } from "@server/utils/zod";
import { ValidateColor } from "@server/validation";
const DocumentsSortParamsSchema = z.object({
/** Specifies the attributes by which documents will be sorted in the list */
@@ -223,6 +226,20 @@ export const DocumentsUpdateSchema = BaseSchema.extend({
/** Emoji displayed alongside doc title */
emoji: z.string().regex(emojiRegex()).nullish(),
/** Icon displayed alongside doc title */
icon: z
.union([
z.string().regex(emojiRegex()),
zodEnumFromObjectKeys(IconLibrary.mapping),
])
.nullish(),
/** Icon color */
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.nullish(),
/** Boolean to denote if the doc should occupy full width */
fullWidth: z.boolean().optional(),
@@ -319,7 +336,21 @@ export const DocumentsCreateSchema = BaseSchema.extend({
text: z.string().default(""),
/** Emoji displayed alongside doc title */
emoji: z.string().regex(emojiRegex()).optional(),
emoji: z.string().regex(emojiRegex()).nullish(),
/** Icon displayed alongside doc title */
icon: z
.union([
z.string().regex(emojiRegex()),
zodEnumFromObjectKeys(IconLibrary.mapping),
])
.optional(),
/** Icon color */
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.nullish(),
/** Boolean to denote if the doc should be published */
publish: z.boolean().optional(),