feat: Custom accent color (#4897)
* types * Working, but messy * Add InputColor component * types * Show default theme values when not customized * Support custom theme on team sign-in page * Payload validation * Custom theme on shared documents * Improve theme validation * Team -> Workspace in settings
This commit is contained in:
@@ -107,7 +107,7 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
|
||||
if (preferences) {
|
||||
for (const value of Object.values(TeamPreference)) {
|
||||
if (has(preferences, value)) {
|
||||
team.setPreference(value, Boolean(preferences[value]));
|
||||
team.setPreference(value, preferences[value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
AllowNull,
|
||||
AfterUpdate,
|
||||
} from "sequelize-typescript";
|
||||
import { CollectionPermission, TeamPreference } from "@shared/types";
|
||||
import {
|
||||
CollectionPermission,
|
||||
TeamPreference,
|
||||
TeamPreferences,
|
||||
} from "@shared/types";
|
||||
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
|
||||
import env from "@server/env";
|
||||
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
|
||||
@@ -39,8 +43,6 @@ import NotContainsUrl from "./validators/NotContainsUrl";
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
|
||||
export type TeamPreferences = Record<string, unknown>;
|
||||
|
||||
@Scopes(() => ({
|
||||
withDomains: {
|
||||
include: [{ model: TeamDomain }],
|
||||
@@ -184,7 +186,10 @@ class Team extends ParanoidModel {
|
||||
* @param value Sets the preference value
|
||||
* @returns The current team preferences
|
||||
*/
|
||||
public setPreference = (preference: TeamPreference, value: boolean) => {
|
||||
public setPreference = <T extends keyof TeamPreferences>(
|
||||
preference: T,
|
||||
value: TeamPreferences[T]
|
||||
) => {
|
||||
if (!this.preferences) {
|
||||
this.preferences = {};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import presentNotificationSetting from "./notificationSetting";
|
||||
import presentPin from "./pin";
|
||||
import presentPolicies from "./policy";
|
||||
import presentProviderConfig from "./providerConfig";
|
||||
import presentPublicTeam from "./publicTeam";
|
||||
import presentRevision from "./revision";
|
||||
import presentSearchQuery from "./searchQuery";
|
||||
import presentShare from "./share";
|
||||
@@ -39,6 +40,7 @@ export {
|
||||
presentIntegration,
|
||||
presentMembership,
|
||||
presentNotificationSetting,
|
||||
presentPublicTeam,
|
||||
presentPin,
|
||||
presentPolicies,
|
||||
presentProviderConfig,
|
||||
|
||||
9
server/presenters/publicTeam.ts
Normal file
9
server/presenters/publicTeam.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Team } from "@server/models";
|
||||
|
||||
export default function presentPublicTeam(team: Team) {
|
||||
return {
|
||||
name: team.name,
|
||||
avatarUrl: team.avatarUrl,
|
||||
customTheme: team.preferences?.customTheme,
|
||||
};
|
||||
}
|
||||
@@ -31,6 +31,7 @@ router.post("auth.config", async (ctx: APIContext) => {
|
||||
ctx.body = {
|
||||
data: {
|
||||
name: team.name,
|
||||
customTheme: team.getPreference(TeamPreference.CustomTheme),
|
||||
logo: team.getPreference(TeamPreference.PublicBranding)
|
||||
? team.avatarUrl
|
||||
: undefined,
|
||||
@@ -56,6 +57,7 @@ router.post("auth.config", async (ctx: APIContext) => {
|
||||
ctx.body = {
|
||||
data: {
|
||||
name: team.name,
|
||||
customTheme: team.getPreference(TeamPreference.CustomTheme),
|
||||
logo: team.getPreference(TeamPreference.PublicBranding)
|
||||
? team.avatarUrl
|
||||
: undefined,
|
||||
@@ -82,6 +84,7 @@ router.post("auth.config", async (ctx: APIContext) => {
|
||||
ctx.body = {
|
||||
data: {
|
||||
name: team.name,
|
||||
customTheme: team.getPreference(TeamPreference.CustomTheme),
|
||||
logo: team.getPreference(TeamPreference.PublicBranding)
|
||||
? team.avatarUrl
|
||||
: undefined,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "fs-extra";
|
||||
import invariant from "invariant";
|
||||
import Router from "koa-router";
|
||||
import { pick } from "lodash";
|
||||
import mime from "mime-types";
|
||||
import { Op, ScopeOptions, WhereOptions } from "sequelize";
|
||||
import { TeamPreference } from "@shared/types";
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
presentCollection,
|
||||
presentDocument,
|
||||
presentPolicies,
|
||||
presentPublicTeam,
|
||||
} from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
|
||||
@@ -419,7 +419,7 @@ router.post(
|
||||
? {
|
||||
document: serializedDocument,
|
||||
team: team?.getPreference(TeamPreference.PublicBranding)
|
||||
? pick(team, ["avatarUrl", "name"])
|
||||
? presentPublicTeam(team)
|
||||
: undefined,
|
||||
sharedTree:
|
||||
share && share.includeChildDocuments
|
||||
|
||||
@@ -29,7 +29,7 @@ import searches from "./searches";
|
||||
import shares from "./shares";
|
||||
import stars from "./stars";
|
||||
import subscriptions from "./subscriptions";
|
||||
import team from "./team";
|
||||
import teams from "./teams";
|
||||
import users from "./users";
|
||||
import views from "./views";
|
||||
|
||||
@@ -74,7 +74,7 @@ router.use("/", searches.routes());
|
||||
router.use("/", shares.routes());
|
||||
router.use("/", stars.routes());
|
||||
router.use("/", subscriptions.routes());
|
||||
router.use("/", team.routes());
|
||||
router.use("/", teams.routes());
|
||||
router.use("/", integrations.routes());
|
||||
router.use("/", notificationSettings.routes());
|
||||
router.use("/", attachments.routes());
|
||||
|
||||
1
server/routes/api/teams/index.ts
Normal file
1
server/routes/api/teams/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./teams";
|
||||
55
server/routes/api/teams/schema.ts
Normal file
55
server/routes/api/teams/schema.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { z } from "zod";
|
||||
import { UserRole } from "@server/models/User";
|
||||
import BaseSchema from "@server/routes/api/BaseSchema";
|
||||
|
||||
export const TeamsUpdateSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
/** Team name */
|
||||
name: z.string().optional(),
|
||||
/** Avatar URL */
|
||||
avatarUrl: z.string().optional(),
|
||||
/** The subdomain to access the team */
|
||||
subdomain: z.string().optional(),
|
||||
/** Whether public sharing is enabled */
|
||||
sharing: z.boolean().optional(),
|
||||
/** Whether siginin with email is enabled */
|
||||
guestSignin: z.boolean().optional(),
|
||||
/** Whether third-party document embeds are enabled */
|
||||
documentEmbeds: z.boolean().optional(),
|
||||
/** Whether team members are able to create new collections */
|
||||
memberCollectionCreate: z.boolean().optional(),
|
||||
/** Whether collaborative editing is enabled */
|
||||
collaborativeEditing: z.boolean().optional(),
|
||||
/** The default landing collection for the team */
|
||||
defaultCollectionId: z.string().uuid().nullish(),
|
||||
/** The default user role */
|
||||
defaultUserRole: z
|
||||
.string()
|
||||
.refine((val) => Object.values(UserRole).includes(val as UserRole))
|
||||
.optional(),
|
||||
/** Whether new users must be invited to join the team */
|
||||
inviteRequired: z.boolean().optional(),
|
||||
/** Domains allowed to sign-in with SSO */
|
||||
allowedDomains: z.array(z.string()).optional(),
|
||||
/** Team preferences */
|
||||
preferences: z
|
||||
.object({
|
||||
/** Whether documents have a separate edit mode instead of seamless editing. */
|
||||
seamlessEdit: z.boolean().optional(),
|
||||
/** Whether to use team logo across the app for branding. */
|
||||
publicBranding: z.boolean().optional(),
|
||||
/** Whether viewers should see download options. */
|
||||
viewersCanExport: z.boolean().optional(),
|
||||
/** The custom theme for the team. */
|
||||
customTheme: z
|
||||
.object({
|
||||
accent: z.string().min(4).max(7).regex(/^#/).optional(),
|
||||
accentText: z.string().min(4).max(7).regex(/^#/).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TeamsUpdateSchemaReq = z.infer<typeof TeamsUpdateSchema>;
|
||||
@@ -5,12 +5,13 @@ import teamUpdater from "@server/commands/teamUpdater";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { rateLimiter } from "@server/middlewares/rateLimiter";
|
||||
import validate from "@server/middlewares/validate";
|
||||
import { Event, Team, TeamDomain, User } from "@server/models";
|
||||
import { authorize } from "@server/policies";
|
||||
import { presentTeam, presentPolicies } from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
|
||||
import { assertUuid } from "@server/validation";
|
||||
import * as T from "./schema";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -18,49 +19,16 @@ router.post(
|
||||
"team.update",
|
||||
auth(),
|
||||
rateLimiter(RateLimiterStrategy.TenPerHour),
|
||||
async (ctx: APIContext) => {
|
||||
const {
|
||||
name,
|
||||
avatarUrl,
|
||||
subdomain,
|
||||
sharing,
|
||||
guestSignin,
|
||||
documentEmbeds,
|
||||
memberCollectionCreate,
|
||||
collaborativeEditing,
|
||||
defaultCollectionId,
|
||||
defaultUserRole,
|
||||
inviteRequired,
|
||||
allowedDomains,
|
||||
preferences,
|
||||
} = ctx.request.body;
|
||||
|
||||
validate(T.TeamsUpdateSchema),
|
||||
async (ctx: APIContext<T.TeamsUpdateSchemaReq>) => {
|
||||
const { user } = ctx.state.auth;
|
||||
const team = await Team.findByPk(user.teamId, {
|
||||
include: [{ model: TeamDomain }],
|
||||
});
|
||||
authorize(user, "update", team);
|
||||
|
||||
if (defaultCollectionId !== undefined && defaultCollectionId !== null) {
|
||||
assertUuid(defaultCollectionId, "defaultCollectionId must be uuid");
|
||||
}
|
||||
|
||||
const updatedTeam = await teamUpdater({
|
||||
params: {
|
||||
name,
|
||||
avatarUrl,
|
||||
subdomain,
|
||||
sharing,
|
||||
guestSignin,
|
||||
documentEmbeds,
|
||||
memberCollectionCreate,
|
||||
collaborativeEditing,
|
||||
defaultCollectionId,
|
||||
defaultUserRole,
|
||||
inviteRequired,
|
||||
allowedDomains,
|
||||
preferences,
|
||||
},
|
||||
params: ctx.input.body,
|
||||
user,
|
||||
team,
|
||||
ip: ctx.request.ip,
|
||||
Reference in New Issue
Block a user