mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add a theme picker (#11140)
This commit is contained in:
Generated
+63
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+55
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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, ¶ms) {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user