feat: Unified icon picker (#7038)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user