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:
Tom Moor
2023-02-19 10:43:03 -05:00
committed by GitHub
parent 7c05b7326a
commit 70beb7524f
45 changed files with 684 additions and 390 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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());

View File

@@ -0,0 +1 @@
export { default } from "./teams";

View 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>;

View File

@@ -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,