feat: add a theme picker (#11140)

This commit is contained in:
Kayla Washburn
2023-12-14 10:38:44 -07:00
committed by GitHub
parent 0cd4842d18
commit 133dc66143
63 changed files with 1283 additions and 317 deletions
+63
View File
@@ -3742,6 +3742,52 @@ const docTemplate = `{
}
}
},
"/users/{user}/appearance": {
"put": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update user appearance settings",
"operationId": "update-user-appearance-settings",
"parameters": [
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
},
{
"description": "New appearance settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateUserAppearanceSettingsRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.User"
}
}
}
}
},
"/users/{user}/convert-login": {
"post": {
"security": [
@@ -10672,6 +10718,9 @@ const docTemplate = `{
}
]
},
"theme_preference": {
"type": "string"
},
"username": {
"type": "string"
}
@@ -11009,6 +11058,17 @@ const docTemplate = `{
}
}
},
"codersdk.UpdateUserAppearanceSettingsRequest": {
"type": "object",
"required": [
"theme_preference"
],
"properties": {
"theme_preference": {
"type": "string"
}
}
},
"codersdk.UpdateUserPasswordRequest": {
"type": "object",
"required": [
@@ -11151,6 +11211,9 @@ const docTemplate = `{
}
]
},
"theme_preference": {
"type": "string"
},
"username": {
"type": "string"
}
+55
View File
@@ -3286,6 +3286,46 @@
}
}
},
"/users/{user}/appearance": {
"put": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Update user appearance settings",
"operationId": "update-user-appearance-settings",
"parameters": [
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
},
{
"description": "New appearance settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateUserAppearanceSettingsRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.User"
}
}
}
}
},
"/users/{user}/convert-login": {
"post": {
"security": [
@@ -9653,6 +9693,9 @@
}
]
},
"theme_preference": {
"type": "string"
},
"username": {
"type": "string"
}
@@ -9970,6 +10013,15 @@
}
}
},
"codersdk.UpdateUserAppearanceSettingsRequest": {
"type": "object",
"required": ["theme_preference"],
"properties": {
"theme_preference": {
"type": "string"
}
}
},
"codersdk.UpdateUserPasswordRequest": {
"type": "object",
"required": ["password"],
@@ -10098,6 +10150,9 @@
}
]
},
"theme_preference": {
"type": "string"
},
"username": {
"type": "string"
}
+1
View File
@@ -800,6 +800,7 @@ func New(options *Options) *API {
r.Put("/suspend", api.putSuspendUserAccount())
r.Put("/activate", api.putActivateUserAccount())
})
r.Put("/appearance", api.putUserAppearanceSettings)
r.Route("/password", func(r chi.Router) {
r.Put("/", api.putUserPassword)
})
+1
View File
@@ -122,6 +122,7 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
AvatarURL: user.AvatarURL,
LoginType: codersdk.LoginType(user.LoginType),
ThemePreference: user.ThemePreference,
}
for _, roleName := range user.RBACRoles {
+11
View File
@@ -2699,6 +2699,17 @@ func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg da
return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg)
}
func (q *querier) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.User, error) {
u, err := q.db.GetUserByID(ctx, arg.ID)
if err != nil {
return database.User{}, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, u.UserDataRBACObject()); err != nil {
return database.User{}, err
}
return q.db.UpdateUserAppearanceSettings(ctx, arg)
}
// UpdateUserDeletedByID
// Deprecated: Delete this function in favor of 'SoftDeleteUserByID'. Deletes are
// irreversible.
+8
View File
@@ -962,6 +962,14 @@ func (s *MethodTestSuite) TestUser() {
UpdatedAt: u.UpdatedAt,
}).Asserts(u.UserDataRBACObject(), rbac.ActionUpdate).Returns(u)
}))
s.Run("UpdateUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserAppearanceSettingsParams{
ID: u.ID,
ThemePreference: u.ThemePreference,
UpdatedAt: u.UpdatedAt,
}).Asserts(u.UserDataRBACObject(), rbac.ActionUpdate).Returns(u)
}))
s.Run("UpdateUserStatus", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserStatusParams{
+20
View File
@@ -6222,6 +6222,26 @@ func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg
return nil
}
func (q *FakeQuerier) UpdateUserAppearanceSettings(_ context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.User, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.User{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for index, user := range q.users {
if user.ID != arg.ID {
continue
}
user.ThemePreference = arg.ThemePreference
q.users[index] = user
return user, nil
}
return database.User{}, sql.ErrNoRows
}
func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error {
if err := validateDatabaseType(params); err != nil {
return err
+7
View File
@@ -1691,6 +1691,13 @@ func (m metricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, ar
return r0
}
func (m metricsStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.User, error) {
start := time.Now()
r0, r1 := m.s.UpdateUserAppearanceSettings(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateUserAppearanceSettings").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) UpdateUserDeletedByID(ctx context.Context, arg database.UpdateUserDeletedByIDParams) error {
start := time.Now()
err := m.s.UpdateUserDeletedByID(ctx, arg)
+15
View File
@@ -3559,6 +3559,21 @@ func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(arg0, arg1 i
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), arg0, arg1)
}
// UpdateUserAppearanceSettings mocks base method.
func (m *MockStore) UpdateUserAppearanceSettings(arg0 context.Context, arg1 database.UpdateUserAppearanceSettingsParams) (database.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserAppearanceSettings", arg0, arg1)
ret0, _ := ret[0].(database.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUserAppearanceSettings indicates an expected call of UpdateUserAppearanceSettings.
func (mr *MockStoreMockRecorder) UpdateUserAppearanceSettings(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).UpdateUserAppearanceSettings), arg0, arg1)
}
// UpdateUserDeletedByID mocks base method.
func (m *MockStore) UpdateUserDeletedByID(arg0 context.Context, arg1 database.UpdateUserDeletedByIDParams) error {
m.ctrl.T.Helper()
+1
View File
@@ -332,6 +332,7 @@ type sqlcQuerier interface {
UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error
UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error
UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error
UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error)
UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error)
+41 -1
View File
@@ -7414,6 +7414,45 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat
return items, nil
}
const updateUserAppearanceSettings = `-- name: UpdateUserAppearanceSettings :one
UPDATE
users
SET
theme_preference = $2,
updated_at = $3
WHERE
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference
`
type UpdateUserAppearanceSettingsParams struct {
ID uuid.UUID `db:"id" json:"id"`
ThemePreference string `db:"theme_preference" json:"theme_preference"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserAppearanceSettings, arg.ID, arg.ThemePreference, arg.UpdatedAt)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.ThemePreference,
)
return i, err
}
const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
UPDATE
users
@@ -7542,7 +7581,8 @@ SET
avatar_url = $4,
updated_at = $5
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference
`
type UpdateUserProfileParams struct {
+12 -2
View File
@@ -80,7 +80,18 @@ SET
avatar_url = $4,
updated_at = $5
WHERE
id = $1 RETURNING *;
id = $1
RETURNING *;
-- name: UpdateUserAppearanceSettings :one
UPDATE
users
SET
theme_preference = $2,
updated_at = $3
WHERE
id = $1
RETURNING *;
-- name: UpdateUserRoles :one
UPDATE
@@ -266,4 +277,3 @@ RETURNING id, email, last_seen_at;
-- AllUserIDs returns all UserIDs regardless of user status or deletion.
-- name: AllUserIDs :many
SELECT DISTINCT id FROM USERS;
+51 -8
View File
@@ -626,15 +626,12 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
isDifferentUser := existentUser.ID != user.ID
if err == nil && isDifferentUser {
responseErrors := []codersdk.ValidationError{}
if existentUser.Username == params.Username {
responseErrors = append(responseErrors, codersdk.ValidationError{
Field: "username",
Detail: "this value is already in use and should be unique",
})
}
responseErrors := []codersdk.ValidationError{{
Field: "username",
Detail: "This username is already in use.",
}}
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: "User already exists.",
Message: "A user with this username already exists.",
Validations: responseErrors,
})
return
@@ -764,6 +761,52 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
}
}
// @Summary Update user appearance settings
// @ID update-user-appearance-settings
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Users
// @Param user path string true "User ID, name, or me"
// @Param request body codersdk.UpdateUserAppearanceSettingsRequest true "New appearance settings"
// @Success 200 {object} codersdk.User
// @Router /users/{user}/appearance [put]
func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
user = httpmw.UserParam(r)
)
var params codersdk.UpdateUserAppearanceSettingsRequest
if !httpapi.Read(ctx, rw, r, &params) {
return
}
updatedUser, err := api.Database.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{
ID: user.ID,
ThemePreference: params.ThemePreference,
UpdatedAt: dbtime.Now(),
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating user.",
Detail: err.Error(),
})
return
}
organizationIDs, err := userOrganizationIDs(ctx, api, user)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user's organizations.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizationIDs))
}
// @Summary Update user password
// @ID update-user-password
// @Security CoderSessionToken