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