Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions client/src/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Per-user color scheme preference (issue #365). NULL means "follow the
-- system/browser color scheme", which is the default for all existing users.

ALTER TABLE user_management_service.user_interface_settings
ADD COLUMN IF NOT EXISTS theme_preference character varying(16);

ALTER TABLE user_management_service.user_interface_settings
ADD CONSTRAINT user_interface_settings_theme_preference_check
CHECK (
theme_preference IS NULL
OR theme_preference IN ('SYSTEM', 'LIGHT', 'DARK')
);
51 changes: 51 additions & 0 deletions server/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions server/graphql/modules/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,9 @@ const Org: GQLOrgResolvers = {
// set by the org's admin. We can always add that ability later, but we're
// leaving this empty for now.
mrtChartConfigurations: [],
// Theme is a personal accessibility preference with no org-level
// default.
themePreference: null,
};
},
async isDemoOrg(org, _, context) {
Expand Down
39 changes: 39 additions & 0 deletions server/graphql/modules/user.resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,45 @@ describe('user resolvers', () => {
});
});

describe('Mutation.setThemePreference', () => {
function makeCtx(user: { id: string; orgId: string } | null) {
const upsertUserInterfaceSettings = jest.fn(async () => []);
const ctx = {
getUser: () => user,
services: { UserManagementService: { upsertUserInterfaceSettings } },
};
return { ctx, upsertUserInterfaceSettings };
}

const Mutation = resolvers.Mutation as {
setThemePreference: (
parent: unknown,
args: { themePreference: 'SYSTEM' | 'LIGHT' | 'DARK' },
ctx: unknown,
) => Promise<unknown>;
};

it('throws unauthenticatedError when there is no signed-in user', async () => {
const { ctx, upsertUserInterfaceSettings } = makeCtx(null);
await expect(
Mutation.setThemePreference({}, { themePreference: 'DARK' }, ctx),
).rejects.toThrow('User required.');
expect(upsertUserInterfaceSettings).not.toHaveBeenCalled();
});

it('persists the preference for the signed-in user', async () => {
const { ctx, upsertUserInterfaceSettings } = makeCtx({
id: 'user-1',
orgId: 'org-1',
});
await Mutation.setThemePreference({}, { themePreference: 'DARK' }, ctx);
expect(upsertUserInterfaceSettings).toHaveBeenCalledWith({
userId: 'user-1',
userInterfaceSettings: { themePreference: 'DARK' },
});
});
});

describe('User.readMeJWT does not leak org secrets to non-admins', () => {
const TEST_JWT_SECRET = 'test-readme-jwt-secret';
let originalSecret: string | undefined;
Expand Down
36 changes: 36 additions & 0 deletions server/graphql/modules/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ const typeDefs = /* GraphQL */ `
moderatorSafetySepia: Boolean!
moderatorSafetyBlurLevel: Int!
mrtChartConfigurations: [ManualReviewChartSettings!]!
"""
Null means the user has never chosen a theme; the client falls back to
its default (currently light).
"""
themePreference: ThemePreference
}

"""
The user's color scheme preference. SYSTEM means "follow the operating
system / browser color scheme".
"""
enum ThemePreference {
SYSTEM
LIGHT
DARK
}

input ModeratorSafetySettingsInput {
Expand Down Expand Up @@ -157,6 +172,9 @@ const typeDefs = /* GraphQL */ `
setMrtChartConfigurationSettings(
mrtChartConfigurationSettings: ManualReviewChartConfigurationsInput!
): SetMrtChartConfigurationSettingsSuccessResponse
setThemePreference(
themePreference: ThemePreference!
): SetThemePreferenceSuccessResponse
}

union AddFavoriteRuleResponse = AddFavoriteRuleSuccessResponse
Expand All @@ -177,6 +195,10 @@ const typeDefs = /* GraphQL */ `
_: Boolean
}

type SetThemePreferenceSuccessResponse {
_: Boolean
}

type AddFavoriteMRTQueueSuccessResponse {
_: Boolean
}
Expand Down Expand Up @@ -297,6 +319,20 @@ const Mutation: GQLMutationResolvers = {
return gqlSuccessResult({}, 'SetModeratorSafetySettingsSuccessResponse');
},

async setThemePreference(_, params, context) {
const user = context.getUser();
if (user == null) {
throw unauthenticatedError('User required.');
}
await context.services.UserManagementService.upsertUserInterfaceSettings({
userId: user.id,
userInterfaceSettings: {
themePreference: params.themePreference,
},
});
return gqlSuccessResult({}, 'SetThemePreferenceSuccessResponse');
},

async setMrtChartConfigurationSettings(_, params, context) {
const user = context.getUser();
if (user == null) {
Expand Down
4 changes: 4 additions & 0 deletions server/services/userManagementService/dbTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type MrtChartConfig = {
)
);

export type ThemePreference = 'SYSTEM' | 'LIGHT' | 'DARK';

export type UserManagementPg = {
'user_management_service.user_interface_settings': {
user_id: string;
Expand All @@ -29,6 +31,8 @@ export type UserManagementPg = {
moderator_safety_sepia: boolean | null;
moderator_safety_blur_level: number | null;
mrt_chart_configurations: MrtChartConfig[] | null;
// NULL means "follow the system color scheme" (see issue #365)
theme_preference: ThemePreference | null;
};
// We use ColumnType in this table because all the moderator_safety columns
// are non-null and have default values, so we can provide null values on
Expand Down
Loading
Loading