mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support multiple terminal fonts (#17257)
Fixes: https://github.com/coder/coder/issues/15024
This commit is contained in:
Generated
+20
@@ -15603,6 +15603,19 @@ const docTemplate = `{
|
||||
"TemplateVersionWarningUnsupportedWorkspaces"
|
||||
]
|
||||
},
|
||||
"codersdk.TerminalFontName": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"",
|
||||
"ibm-plex-mono",
|
||||
"fira-code"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TerminalFontUnknown",
|
||||
"TerminalFontIBMPlexMono",
|
||||
"TerminalFontFiraCode"
|
||||
]
|
||||
},
|
||||
"codersdk.TimingStage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -15776,9 +15789,13 @@ const docTemplate = `{
|
||||
"codersdk.UpdateUserAppearanceSettingsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"terminal_font",
|
||||
"theme_preference"
|
||||
],
|
||||
"properties": {
|
||||
"terminal_font": {
|
||||
"$ref": "#/definitions/codersdk.TerminalFontName"
|
||||
},
|
||||
"theme_preference": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -16070,6 +16087,9 @@ const docTemplate = `{
|
||||
"codersdk.UserAppearanceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"terminal_font": {
|
||||
"$ref": "#/definitions/codersdk.TerminalFontName"
|
||||
},
|
||||
"theme_preference": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
Generated
+16
-1
@@ -14188,6 +14188,15 @@
|
||||
"enum": ["UNSUPPORTED_WORKSPACES"],
|
||||
"x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"]
|
||||
},
|
||||
"codersdk.TerminalFontName": {
|
||||
"type": "string",
|
||||
"enum": ["", "ibm-plex-mono", "fira-code"],
|
||||
"x-enum-varnames": [
|
||||
"TerminalFontUnknown",
|
||||
"TerminalFontIBMPlexMono",
|
||||
"TerminalFontFiraCode"
|
||||
]
|
||||
},
|
||||
"codersdk.TimingStage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -14358,8 +14367,11 @@
|
||||
},
|
||||
"codersdk.UpdateUserAppearanceSettingsRequest": {
|
||||
"type": "object",
|
||||
"required": ["theme_preference"],
|
||||
"required": ["terminal_font", "theme_preference"],
|
||||
"properties": {
|
||||
"terminal_font": {
|
||||
"$ref": "#/definitions/codersdk.TerminalFontName"
|
||||
},
|
||||
"theme_preference": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -14625,6 +14637,9 @@
|
||||
"codersdk.UserAppearanceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"terminal_font": {
|
||||
"$ref": "#/definitions/codersdk.TerminalFontName"
|
||||
},
|
||||
"theme_preference": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -2721,17 +2721,6 @@ func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetU
|
||||
return q.db.GetUserActivityInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
u, err := q.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return q.db.GetUserAppearanceSettings(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetUserByEmailOrUsername)(ctx, arg)
|
||||
}
|
||||
@@ -2804,6 +2793,28 @@ func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserS
|
||||
return q.db.GetUserStatusCounts(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
u, err := q.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return q.db.GetUserTerminalFont(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
u, err := q.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return q.db.GetUserThemePreference(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
u, err := q.db.GetUserByID(ctx, params.OwnerID)
|
||||
if err != nil {
|
||||
@@ -4321,17 +4332,6 @@ func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg da
|
||||
return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) {
|
||||
u, err := q.db.GetUserByID(ctx, arg.UserID)
|
||||
if err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
return q.db.UpdateUserAppearanceSettings(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
|
||||
return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id)
|
||||
}
|
||||
@@ -4469,6 +4469,28 @@ func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserS
|
||||
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserStatus)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) {
|
||||
u, err := q.db.GetUserByID(ctx, arg.UserID)
|
||||
if err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
return q.db.UpdateUserTerminalFont(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) {
|
||||
u, err := q.db.GetUserByID(ctx, arg.UserID)
|
||||
if err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
return q.db.UpdateUserThemePreference(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1628,27 +1628,48 @@ func (s *MethodTestSuite) TestUser() {
|
||||
[]database.GetUserWorkspaceBuildParametersRow{},
|
||||
)
|
||||
}))
|
||||
s.Run("GetUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) {
|
||||
s.Run("GetUserThemePreference", s.Subtest(func(db database.Store, check *expects) {
|
||||
ctx := context.Background()
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
db.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{
|
||||
db.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{
|
||||
UserID: u.ID,
|
||||
ThemePreference: "light",
|
||||
})
|
||||
check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("light")
|
||||
}))
|
||||
s.Run("UpdateUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) {
|
||||
s.Run("UpdateUserThemePreference", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
uc := database.UserConfig{
|
||||
UserID: u.ID,
|
||||
Key: "theme_preference",
|
||||
Value: "dark",
|
||||
}
|
||||
check.Args(database.UpdateUserAppearanceSettingsParams{
|
||||
check.Args(database.UpdateUserThemePreferenceParams{
|
||||
UserID: u.ID,
|
||||
ThemePreference: uc.Value,
|
||||
}).Asserts(u, policy.ActionUpdatePersonal).Returns(uc)
|
||||
}))
|
||||
s.Run("GetUserTerminalFont", s.Subtest(func(db database.Store, check *expects) {
|
||||
ctx := context.Background()
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
db.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{
|
||||
UserID: u.ID,
|
||||
TerminalFont: "ibm-plex-mono",
|
||||
})
|
||||
check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("ibm-plex-mono")
|
||||
}))
|
||||
s.Run("UpdateUserTerminalFont", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
uc := database.UserConfig{
|
||||
UserID: u.ID,
|
||||
Key: "terminal_font",
|
||||
Value: "ibm-plex-mono",
|
||||
}
|
||||
check.Args(database.UpdateUserTerminalFontParams{
|
||||
UserID: u.ID,
|
||||
TerminalFont: uc.Value,
|
||||
}).Asserts(u, policy.ActionUpdatePersonal).Returns(uc)
|
||||
}))
|
||||
s.Run("UpdateUserStatus", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
check.Args(database.UpdateUserStatusParams{
|
||||
|
||||
@@ -6448,20 +6448,6 @@ func (q *FakeQuerier) GetUserActivityInsights(_ context.Context, arg database.Ge
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserAppearanceSettings(_ context.Context, userID uuid.UUID) (string, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, uc := range q.userConfigs {
|
||||
if uc.UserID != userID || uc.Key != "theme_preference" {
|
||||
continue
|
||||
}
|
||||
return uc.Value, nil
|
||||
}
|
||||
|
||||
return "", sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return database.User{}, err
|
||||
@@ -6674,6 +6660,34 @@ func (q *FakeQuerier) GetUserStatusCounts(_ context.Context, arg database.GetUse
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, uc := range q.userConfigs {
|
||||
if uc.UserID != userID || uc.Key != "terminal_font" {
|
||||
continue
|
||||
}
|
||||
return uc.Value, nil
|
||||
}
|
||||
|
||||
return "", sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserThemePreference(_ context.Context, userID uuid.UUID) (string, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, uc := range q.userConfigs {
|
||||
if uc.UserID != userID || uc.Key != "theme_preference" {
|
||||
continue
|
||||
}
|
||||
return uc.Value, nil
|
||||
}
|
||||
|
||||
return "", sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -11015,33 +11029,6 @@ func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateUserAppearanceSettings(_ context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i, uc := range q.userConfigs {
|
||||
if uc.UserID != arg.UserID || uc.Key != "theme_preference" {
|
||||
continue
|
||||
}
|
||||
uc.Value = arg.ThemePreference
|
||||
q.userConfigs[i] = uc
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
uc := database.UserConfig{
|
||||
UserID: arg.UserID,
|
||||
Key: "theme_preference",
|
||||
Value: arg.ThemePreference,
|
||||
}
|
||||
q.userConfigs = append(q.userConfigs, uc)
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, id uuid.UUID) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
@@ -11367,6 +11354,60 @@ func (q *FakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse
|
||||
return database.User{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i, uc := range q.userConfigs {
|
||||
if uc.UserID != arg.UserID || uc.Key != "terminal_font" {
|
||||
continue
|
||||
}
|
||||
uc.Value = arg.TerminalFont
|
||||
q.userConfigs[i] = uc
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
uc := database.UserConfig{
|
||||
UserID: arg.UserID,
|
||||
Key: "terminal_font",
|
||||
Value: arg.TerminalFont,
|
||||
}
|
||||
q.userConfigs = append(q.userConfigs, uc)
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateUserThemePreference(_ context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i, uc := range q.userConfigs {
|
||||
if uc.UserID != arg.UserID || uc.Key != "theme_preference" {
|
||||
continue
|
||||
}
|
||||
uc.Value = arg.ThemePreference
|
||||
q.userConfigs[i] = uc
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
uc := database.UserConfig{
|
||||
UserID: arg.UserID,
|
||||
Key: "theme_preference",
|
||||
Value: arg.ThemePreference,
|
||||
}
|
||||
q.userConfigs = append(q.userConfigs, uc)
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateVolumeResourceMonitor(_ context.Context, arg database.UpdateVolumeResourceMonitorParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
|
||||
@@ -1509,13 +1509,6 @@ func (m queryMetricsStore) GetUserActivityInsights(ctx context.Context, arg data
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserAppearanceSettings(ctx, userID)
|
||||
m.queryLatencies.WithLabelValues("GetUserAppearanceSettings").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
start := time.Now()
|
||||
user, err := m.s.GetUserByEmailOrUsername(ctx, arg)
|
||||
@@ -1579,6 +1572,20 @@ func (m queryMetricsStore) GetUserStatusCounts(ctx context.Context, arg database
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserTerminalFont(ctx, userID)
|
||||
m.queryLatencies.WithLabelValues("GetUserTerminalFont").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserThemePreference(ctx, userID)
|
||||
m.queryLatencies.WithLabelValues("GetUserThemePreference").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID)
|
||||
@@ -2734,13 +2741,6 @@ func (m queryMetricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Contex
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, 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 queryMetricsStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateUserDeletedByID(ctx, id)
|
||||
@@ -2832,6 +2832,20 @@ func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.Up
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserTerminalFont(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateUserTerminalFont").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserThemePreference(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateUserThemePreference").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg)
|
||||
|
||||
@@ -3154,21 +3154,6 @@ func (mr *MockStoreMockRecorder) GetUserActivityInsights(ctx, arg any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserActivityInsights", reflect.TypeOf((*MockStore)(nil).GetUserActivityInsights), ctx, arg)
|
||||
}
|
||||
|
||||
// GetUserAppearanceSettings mocks base method.
|
||||
func (m *MockStore) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserAppearanceSettings", ctx, userID)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserAppearanceSettings indicates an expected call of GetUserAppearanceSettings.
|
||||
func (mr *MockStoreMockRecorder) GetUserAppearanceSettings(ctx, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).GetUserAppearanceSettings), ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserByEmailOrUsername mocks base method.
|
||||
func (m *MockStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3304,6 +3289,36 @@ func (mr *MockStoreMockRecorder) GetUserStatusCounts(ctx, arg any) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusCounts", reflect.TypeOf((*MockStore)(nil).GetUserStatusCounts), ctx, arg)
|
||||
}
|
||||
|
||||
// GetUserTerminalFont mocks base method.
|
||||
func (m *MockStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserTerminalFont", ctx, userID)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserTerminalFont indicates an expected call of GetUserTerminalFont.
|
||||
func (mr *MockStoreMockRecorder) GetUserTerminalFont(ctx, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserTerminalFont", reflect.TypeOf((*MockStore)(nil).GetUserTerminalFont), ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserThemePreference mocks base method.
|
||||
func (m *MockStore) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserThemePreference", ctx, userID)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserThemePreference indicates an expected call of GetUserThemePreference.
|
||||
func (mr *MockStoreMockRecorder) GetUserThemePreference(ctx, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserThemePreference", reflect.TypeOf((*MockStore)(nil).GetUserThemePreference), ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserWorkspaceBuildParameters mocks base method.
|
||||
func (m *MockStore) GetUserWorkspaceBuildParameters(ctx context.Context, arg database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -5783,21 +5798,6 @@ func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(ctx, arg any
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserAppearanceSettings mocks base method.
|
||||
func (m *MockStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserAppearanceSettings", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateUserAppearanceSettings indicates an expected call of UpdateUserAppearanceSettings.
|
||||
func (mr *MockStoreMockRecorder) UpdateUserAppearanceSettings(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).UpdateUserAppearanceSettings), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserDeletedByID mocks base method.
|
||||
func (m *MockStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -5989,6 +5989,36 @@ func (mr *MockStoreMockRecorder) UpdateUserStatus(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserTerminalFont mocks base method.
|
||||
func (m *MockStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserTerminalFont", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateUserTerminalFont indicates an expected call of UpdateUserTerminalFont.
|
||||
func (mr *MockStoreMockRecorder) UpdateUserTerminalFont(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserTerminalFont", reflect.TypeOf((*MockStore)(nil).UpdateUserTerminalFont), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserThemePreference mocks base method.
|
||||
func (m *MockStore) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserThemePreference", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateUserThemePreference indicates an expected call of UpdateUserThemePreference.
|
||||
func (mr *MockStoreMockRecorder) UpdateUserThemePreference(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserThemePreference", reflect.TypeOf((*MockStore)(nil).UpdateUserThemePreference), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateVolumeResourceMonitor mocks base method.
|
||||
func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -344,7 +344,6 @@ type sqlcQuerier interface {
|
||||
// produces a bloated value if a user has used multiple templates
|
||||
// simultaneously.
|
||||
GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error)
|
||||
GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error)
|
||||
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
|
||||
GetUserCount(ctx context.Context, includeSystem bool) (int64, error)
|
||||
@@ -370,6 +369,8 @@ type sqlcQuerier interface {
|
||||
// We do not start counting from 0 at the start_time. We check the last status change before the start_time for each user. As such,
|
||||
// the result shows the total number of users in each status on any particular day.
|
||||
GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error)
|
||||
GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error)
|
||||
GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error)
|
||||
GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error)
|
||||
// This will never return deleted users.
|
||||
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
||||
@@ -571,7 +572,6 @@ 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) (UserConfig, error)
|
||||
UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error
|
||||
UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error
|
||||
UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error
|
||||
@@ -585,6 +585,8 @@ type sqlcQuerier interface {
|
||||
UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error)
|
||||
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
||||
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
|
||||
UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error)
|
||||
UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error)
|
||||
UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error
|
||||
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error)
|
||||
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
||||
|
||||
@@ -12196,23 +12196,6 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserAppearanceSettings = `-- name: GetUserAppearanceSettings :one
|
||||
SELECT
|
||||
value as theme_preference
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = $1
|
||||
AND key = 'theme_preference'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserAppearanceSettings, userID)
|
||||
var theme_preference string
|
||||
err := row.Scan(&theme_preference)
|
||||
return theme_preference, err
|
||||
}
|
||||
|
||||
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system
|
||||
@@ -12310,6 +12293,40 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int6
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getUserTerminalFont = `-- name: GetUserTerminalFont :one
|
||||
SELECT
|
||||
value as terminal_font
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = $1
|
||||
AND key = 'terminal_font'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserTerminalFont, userID)
|
||||
var terminal_font string
|
||||
err := row.Scan(&terminal_font)
|
||||
return terminal_font, err
|
||||
}
|
||||
|
||||
const getUserThemePreference = `-- name: GetUserThemePreference :one
|
||||
SELECT
|
||||
value as theme_preference
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = $1
|
||||
AND key = 'theme_preference'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserThemePreference, userID)
|
||||
var theme_preference string
|
||||
err := row.Scan(&theme_preference)
|
||||
return theme_preference, err
|
||||
}
|
||||
|
||||
const getUsers = `-- name: GetUsers :many
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, COUNT(*) OVER() AS count
|
||||
@@ -12673,33 +12690,6 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateUserAppearanceSettings = `-- name: UpdateUserAppearanceSettings :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
($1, 'theme_preference', $2)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = $2
|
||||
WHERE user_configs.user_id = $1
|
||||
AND user_configs.key = 'theme_preference'
|
||||
RETURNING user_id, key, value
|
||||
`
|
||||
|
||||
type UpdateUserAppearanceSettingsParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
ThemePreference string `db:"theme_preference" json:"theme_preference"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (UserConfig, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserAppearanceSettings, arg.UserID, arg.ThemePreference)
|
||||
var i UserConfig
|
||||
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
|
||||
UPDATE
|
||||
users
|
||||
@@ -13047,6 +13037,60 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
($1, 'terminal_font', $2)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = $2
|
||||
WHERE user_configs.user_id = $1
|
||||
AND user_configs.key = 'terminal_font'
|
||||
RETURNING user_id, key, value
|
||||
`
|
||||
|
||||
type UpdateUserTerminalFontParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
TerminalFont string `db:"terminal_font" json:"terminal_font"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserTerminalFont, arg.UserID, arg.TerminalFont)
|
||||
var i UserConfig
|
||||
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserThemePreference = `-- name: UpdateUserThemePreference :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
($1, 'theme_preference', $2)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = $2
|
||||
WHERE user_configs.user_id = $1
|
||||
AND user_configs.key = 'theme_preference'
|
||||
RETURNING user_id, key, value
|
||||
`
|
||||
|
||||
type UpdateUserThemePreferenceParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
ThemePreference string `db:"theme_preference" json:"theme_preference"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserThemePreference, arg.UserID, arg.ThemePreference)
|
||||
var i UserConfig
|
||||
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
|
||||
SELECT
|
||||
id, workspace_agent_id, created_at, workspace_folder, config_path, name
|
||||
|
||||
@@ -102,7 +102,7 @@ SET
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: GetUserAppearanceSettings :one
|
||||
-- name: GetUserThemePreference :one
|
||||
SELECT
|
||||
value as theme_preference
|
||||
FROM
|
||||
@@ -111,7 +111,7 @@ WHERE
|
||||
user_id = @user_id
|
||||
AND key = 'theme_preference';
|
||||
|
||||
-- name: UpdateUserAppearanceSettings :one
|
||||
-- name: UpdateUserThemePreference :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
@@ -125,6 +125,29 @@ WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'theme_preference'
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUserTerminalFont :one
|
||||
SELECT
|
||||
value as terminal_font
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
AND key = 'terminal_font';
|
||||
|
||||
-- name: UpdateUserTerminalFont :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
(@user_id, 'terminal_font', @terminal_font)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = @terminal_font
|
||||
WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'terminal_font'
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserRoles :one
|
||||
UPDATE
|
||||
users
|
||||
|
||||
+43
-4
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
@@ -976,7 +977,7 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request)
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
themePreference, err := api.Database.GetUserAppearanceSettings(ctx, user.ID)
|
||||
themePreference, err := api.Database.GetUserThemePreference(ctx, user.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -989,8 +990,22 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request)
|
||||
themePreference = ""
|
||||
}
|
||||
|
||||
terminalFont, err := api.Database.GetUserTerminalFont(ctx, user.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Error reading user settings.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
terminalFont = ""
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{
|
||||
ThemePreference: themePreference,
|
||||
TerminalFont: codersdk.TerminalFontName(terminalFont),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1015,23 +1030,47 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
updatedSettings, err := api.Database.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{
|
||||
if !isValidFontName(params.TerminalFont) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Unsupported font family.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updatedThemePreference, err := api.Database.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{
|
||||
UserID: user.ID,
|
||||
ThemePreference: params.ThemePreference,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating user.",
|
||||
Message: "Internal error updating user theme preference.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updatedTerminalFont, err := api.Database.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{
|
||||
UserID: user.ID,
|
||||
TerminalFont: string(params.TerminalFont),
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating user terminal font.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{
|
||||
ThemePreference: updatedSettings.Value,
|
||||
ThemePreference: updatedThemePreference.Value,
|
||||
TerminalFont: codersdk.TerminalFontName(updatedTerminalFont.Value),
|
||||
})
|
||||
}
|
||||
|
||||
func isValidFontName(font codersdk.TerminalFontName) bool {
|
||||
return slices.Contains(codersdk.TerminalFontNames, font)
|
||||
}
|
||||
|
||||
// @Summary Update user password
|
||||
// @ID update-user-password
|
||||
// @Security CoderSessionToken
|
||||
|
||||
@@ -1972,6 +1972,86 @@ func TestPostTokens(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserTerminalFont(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("valid font", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient := coderdtest.New(t, nil)
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// given
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)
|
||||
|
||||
// when
|
||||
updated, err := client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
ThemePreference: "light",
|
||||
TerminalFont: "fira-code",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// then
|
||||
require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)
|
||||
})
|
||||
|
||||
t.Run("unsupported font", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient := coderdtest.New(t, nil)
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// given
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)
|
||||
|
||||
// when
|
||||
_, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
ThemePreference: "light",
|
||||
TerminalFont: "foobar",
|
||||
})
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("undefined font is not ok", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient := coderdtest.New(t, nil)
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// given
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)
|
||||
|
||||
// when
|
||||
_, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
ThemePreference: "light",
|
||||
TerminalFont: "",
|
||||
})
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacesByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
|
||||
+35
-8
@@ -189,12 +189,25 @@ type ValidateUserPasswordResponse struct {
|
||||
Details string `json:"details"`
|
||||
}
|
||||
|
||||
// TerminalFontName is the name of supported terminal font
|
||||
type TerminalFontName string
|
||||
|
||||
var TerminalFontNames = []TerminalFontName{TerminalFontUnknown, TerminalFontIBMPlexMono, TerminalFontFiraCode}
|
||||
|
||||
const (
|
||||
TerminalFontUnknown TerminalFontName = ""
|
||||
TerminalFontIBMPlexMono TerminalFontName = "ibm-plex-mono"
|
||||
TerminalFontFiraCode TerminalFontName = "fira-code"
|
||||
)
|
||||
|
||||
type UserAppearanceSettings struct {
|
||||
ThemePreference string `json:"theme_preference"`
|
||||
ThemePreference string `json:"theme_preference"`
|
||||
TerminalFont TerminalFontName `json:"terminal_font"`
|
||||
}
|
||||
|
||||
type UpdateUserAppearanceSettingsRequest struct {
|
||||
ThemePreference string `json:"theme_preference" validate:"required"`
|
||||
ThemePreference string `json:"theme_preference" validate:"required"`
|
||||
TerminalFont TerminalFontName `json:"terminal_font" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateUserPasswordRequest struct {
|
||||
@@ -466,17 +479,31 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// UpdateUserAppearanceSettings updates the appearance settings for a user.
|
||||
func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (User, error) {
|
||||
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/appearance", user), req)
|
||||
// GetUserAppearanceSettings fetches the appearance settings for a user.
|
||||
func (c *Client) GetUserAppearanceSettings(ctx context.Context, user string) (UserAppearanceSettings, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/appearance", user), nil)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
return UserAppearanceSettings{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return User{}, ReadBodyAsError(res)
|
||||
return UserAppearanceSettings{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp User
|
||||
var resp UserAppearanceSettings
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// UpdateUserAppearanceSettings updates the appearance settings for a user.
|
||||
func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (UserAppearanceSettings, error) {
|
||||
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/appearance", user), req)
|
||||
if err != nil {
|
||||
return UserAppearanceSettings{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return UserAppearanceSettings{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp UserAppearanceSettings
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
|
||||
Generated
+26
-6
@@ -6717,6 +6717,22 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
|--------------------------|
|
||||
| `UNSUPPORTED_WORKSPACES` |
|
||||
|
||||
## codersdk.TerminalFontName
|
||||
|
||||
```json
|
||||
""
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value |
|
||||
|-----------------|
|
||||
| `` |
|
||||
| `ibm-plex-mono` |
|
||||
| `fira-code` |
|
||||
|
||||
## codersdk.TimingStage
|
||||
|
||||
```json
|
||||
@@ -6914,15 +6930,17 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal_font": "",
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------|----------|--------------|-------------|
|
||||
| `theme_preference` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------------------------------------------------------|----------|--------------|-------------|
|
||||
| `terminal_font` | [codersdk.TerminalFontName](#codersdkterminalfontname) | true | | |
|
||||
| `theme_preference` | string | true | | |
|
||||
|
||||
## codersdk.UpdateUserNotificationPreferences
|
||||
|
||||
@@ -7265,15 +7283,17 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal_font": "",
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------|----------|--------------|-------------|
|
||||
| `theme_preference` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------------------------------------------------------|----------|--------------|-------------|
|
||||
| `terminal_font` | [codersdk.TerminalFontName](#codersdkterminalfontname) | false | | |
|
||||
| `theme_preference` | string | false | | |
|
||||
|
||||
## codersdk.UserLatency
|
||||
|
||||
|
||||
Generated
+3
@@ -501,6 +501,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/appearance \
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal_font": "",
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
@@ -531,6 +532,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal_font": "",
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
@@ -548,6 +550,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal_font": "",
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@fastly/performance-observer-polyfill": "2.0.0",
|
||||
"@fontsource-variable/inter": "5.1.1",
|
||||
"@fontsource/fira-code": "5.2.5",
|
||||
"@fontsource/ibm-plex-mono": "5.1.1",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@mui/icons-material": "5.16.14",
|
||||
|
||||
Generated
+8
@@ -40,6 +40,9 @@ importers:
|
||||
'@fontsource-variable/inter':
|
||||
specifier: 5.1.1
|
||||
version: 5.1.1
|
||||
'@fontsource/fira-code':
|
||||
specifier: 5.2.5
|
||||
version: 5.2.5
|
||||
'@fontsource/ibm-plex-mono':
|
||||
specifier: 5.1.1
|
||||
version: 5.1.1
|
||||
@@ -1040,6 +1043,9 @@ packages:
|
||||
'@fontsource-variable/inter@5.1.1':
|
||||
resolution: {integrity: sha512-OpXFTmiH6tHkYijMvQTycFKBLK4X+SRV6tet1m4YOUH7SzIIlMqDja+ocDtiCA72UthBH/vF+3ZtlMr2rN/wIw==, tarball: https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.1.1.tgz}
|
||||
|
||||
'@fontsource/fira-code@5.2.5':
|
||||
resolution: {integrity: sha512-Rn9PJoyfRr5D6ukEhZpzhpD+rbX2rtoz9QjkOuGxqFxrL69fQvhadMUBxQIOuTF4sTTkPRSKlAEpPjTKaI12QA==, tarball: https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.5.tgz}
|
||||
|
||||
'@fontsource/ibm-plex-mono@5.1.1':
|
||||
resolution: {integrity: sha512-1aayqPe/ZkD3MlvqpmOHecfA3f2B8g+fAEkgvcCd3lkPP0pS1T0xG5Zmn2EsJQqr1JURtugPUH+5NqvKyfFZMQ==, tarball: https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.1.1.tgz}
|
||||
|
||||
@@ -7012,6 +7018,8 @@ snapshots:
|
||||
|
||||
'@fontsource-variable/inter@5.1.1': {}
|
||||
|
||||
'@fontsource/fira-code@5.2.5': {}
|
||||
|
||||
'@fontsource/ibm-plex-mono@5.1.1': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
|
||||
+12
-1
@@ -428,6 +428,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
var eg errgroup.Group
|
||||
var user database.User
|
||||
var themePreference string
|
||||
var terminalFont string
|
||||
orgIDs := []uuid.UUID{}
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
@@ -436,13 +437,22 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
})
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
themePreference, err = h.opts.Database.GetUserAppearanceSettings(ctx, apiKey.UserID)
|
||||
themePreference, err = h.opts.Database.GetUserThemePreference(ctx, apiKey.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
themePreference = ""
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
terminalFont, err = h.opts.Database.GetUserTerminalFont(ctx, apiKey.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
terminalFont = ""
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
eg.Go(func() error {
|
||||
memberIDs, err := h.opts.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{apiKey.UserID})
|
||||
if errors.Is(err, sql.ErrNoRows) || len(memberIDs) == 0 {
|
||||
@@ -471,6 +481,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
defer wg.Done()
|
||||
userAppearance, err := json.Marshal(codersdk.UserAppearanceSettings{
|
||||
ThemePreference: themePreference,
|
||||
TerminalFont: codersdk.TerminalFontName(terminalFont),
|
||||
})
|
||||
if err == nil {
|
||||
state.UserAppearance = html.EscapeString(string(userAppearance))
|
||||
|
||||
@@ -251,6 +251,7 @@ export const updateAppearanceSettings = (
|
||||
// more responsive.
|
||||
queryClient.setQueryData(myAppearanceKey, {
|
||||
theme_preference: patch.theme_preference,
|
||||
terminal_font: patch.terminal_font,
|
||||
});
|
||||
},
|
||||
onSuccess: async () =>
|
||||
|
||||
Generated
+11
@@ -2657,6 +2657,15 @@ export interface TemplateVersionsByTemplateRequest extends Pagination {
|
||||
readonly include_archived: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export type TerminalFontName = "fira-code" | "ibm-plex-mono" | "";
|
||||
|
||||
export const TerminalFontNames: TerminalFontName[] = [
|
||||
"fira-code",
|
||||
"ibm-plex-mono",
|
||||
"",
|
||||
];
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export type TimingStage =
|
||||
| "apply"
|
||||
@@ -2790,6 +2799,7 @@ export interface UpdateTemplateMeta {
|
||||
// From codersdk/users.go
|
||||
export interface UpdateUserAppearanceSettingsRequest {
|
||||
readonly theme_preference: string;
|
||||
readonly terminal_font: TerminalFontName;
|
||||
}
|
||||
|
||||
// From codersdk/notifications.go
|
||||
@@ -2906,6 +2916,7 @@ export interface UserActivityInsightsResponse {
|
||||
// From codersdk/users.go
|
||||
export interface UserAppearanceSettings {
|
||||
readonly theme_preference: string;
|
||||
readonly terminal_font: TerminalFontName;
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
MockEntitlements,
|
||||
MockExperiments,
|
||||
MockUser,
|
||||
MockUserAppearanceSettings,
|
||||
MockWorkspace,
|
||||
MockWorkspaceAgent,
|
||||
} from "testHelpers/entities";
|
||||
@@ -76,6 +77,7 @@ const meta = {
|
||||
key: getAuthorizationKey({ checks: permissionChecks }),
|
||||
data: { editWorkspaceProxies: true },
|
||||
},
|
||||
{ key: ["me", "appearance"], data: MockUserAppearanceSettings },
|
||||
],
|
||||
chromatic: { delay: 300 },
|
||||
},
|
||||
@@ -106,6 +108,38 @@ export const Starting: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const FontFiraCode: Story = {
|
||||
decorators: [withWebSocket],
|
||||
parameters: {
|
||||
...meta.parameters,
|
||||
webSocket: [
|
||||
{
|
||||
event: "message",
|
||||
// Copied and pasted this from browser
|
||||
data: "[H[2J[1m[32m➜ [36mcoder[C[34mgit:([31mbq/refactor-web-term-notifications[34m) [33m✗",
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
...meta.parameters.queries.filter(
|
||||
(q) =>
|
||||
!(
|
||||
Array.isArray(q.key) &&
|
||||
q.key[0] === "me" &&
|
||||
q.key[1] === "appearance"
|
||||
),
|
||||
),
|
||||
{
|
||||
key: ["me", "appearance"],
|
||||
data: {
|
||||
...MockUserAppearanceSettings,
|
||||
terminal_font: "fira-code",
|
||||
},
|
||||
},
|
||||
createWorkspaceWithAgent("ready"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Ready: Story = {
|
||||
decorators: [withWebSocket],
|
||||
parameters: {
|
||||
|
||||
@@ -7,18 +7,20 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
import { WebglAddon } from "@xterm/addon-webgl";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { deploymentConfig } from "api/queries/deployment";
|
||||
import { appearanceSettings } from "api/queries/users";
|
||||
import {
|
||||
workspaceByOwnerAndName,
|
||||
workspaceUsage,
|
||||
} from "api/queries/workspaces";
|
||||
import { useProxy } from "contexts/ProxyContext";
|
||||
import { ThemeOverride } from "contexts/ThemeProvider";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import { type FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useQuery } from "react-query";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import themes from "theme";
|
||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
||||
import { DEFAULT_TERMINAL_FONT, terminalFonts } from "theme/constants";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { openMaybePortForwardedURL } from "utils/portForward";
|
||||
import { terminalWebsocketUrl } from "utils/terminal";
|
||||
@@ -100,6 +102,13 @@ const TerminalPage: FC = () => {
|
||||
handleWebLinkRef.current = handleWebLink;
|
||||
}, [handleWebLink]);
|
||||
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const appearanceSettingsQuery = useQuery(
|
||||
appearanceSettings(metadata.userAppearance),
|
||||
);
|
||||
const currentTerminalFont =
|
||||
appearanceSettingsQuery.data?.terminal_font || DEFAULT_TERMINAL_FONT;
|
||||
|
||||
// Create the terminal!
|
||||
const fitAddonRef = useRef<FitAddon>();
|
||||
useEffect(() => {
|
||||
@@ -110,7 +119,7 @@ const TerminalPage: FC = () => {
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
disableStdin: false,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
fontFamily: terminalFonts[currentTerminalFont],
|
||||
fontSize: 16,
|
||||
theme: {
|
||||
background: theme.palette.background.default,
|
||||
@@ -150,7 +159,12 @@ const TerminalPage: FC = () => {
|
||||
window.removeEventListener("resize", listener);
|
||||
terminal.dispose();
|
||||
};
|
||||
}, [config.isLoading, renderer, theme.palette.background.default]);
|
||||
}, [
|
||||
config.isLoading,
|
||||
renderer,
|
||||
theme.palette.background.default,
|
||||
currentTerminalFont,
|
||||
]);
|
||||
|
||||
// Updates the reconnection token into the URL if necessary.
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,6 +18,6 @@ type Story = StoryObj<typeof AppearanceForm>;
|
||||
|
||||
export const Example: Story = {
|
||||
args: {
|
||||
initialValues: { theme_preference: "" },
|
||||
initialValues: { theme_preference: "", terminal_font: "" },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import type { Interpolation } from "@emotion/react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import type { UpdateUserAppearanceSettingsRequest } from "api/typesGenerated";
|
||||
import {
|
||||
type TerminalFontName,
|
||||
TerminalFontNames,
|
||||
type UpdateUserAppearanceSettingsRequest,
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { PreviewBadge } from "components/Badges/Badges";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { ThemeOverride } from "contexts/ThemeProvider";
|
||||
import type { FC } from "react";
|
||||
import themes, { DEFAULT_THEME, type Theme } from "theme";
|
||||
import { DEFAULT_TERMINAL_FONT, terminalFontLabels } from "theme/constants";
|
||||
import { Section } from "../Section";
|
||||
|
||||
export interface AppearanceFormProps {
|
||||
isUpdating?: boolean;
|
||||
@@ -22,43 +33,107 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({
|
||||
initialValues,
|
||||
}) => {
|
||||
const currentTheme = initialValues.theme_preference || DEFAULT_THEME;
|
||||
const currentTerminalFont =
|
||||
initialValues.terminal_font || DEFAULT_TERMINAL_FONT;
|
||||
|
||||
const onChangeTheme = async (theme: string) => {
|
||||
if (isUpdating) {
|
||||
return;
|
||||
}
|
||||
await onSubmit({
|
||||
theme_preference: theme,
|
||||
terminal_font: currentTerminalFont,
|
||||
});
|
||||
};
|
||||
|
||||
await onSubmit({ theme_preference: theme });
|
||||
const onChangeTerminalFont = async (terminalFont: TerminalFontName) => {
|
||||
if (isUpdating) {
|
||||
return;
|
||||
}
|
||||
await onSubmit({
|
||||
theme_preference: currentTheme,
|
||||
terminal_font: terminalFont,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
|
||||
<Stack direction="row" wrap="wrap">
|
||||
<AutoThemePreviewButton
|
||||
displayName="Auto"
|
||||
active={currentTheme === "auto"}
|
||||
themes={[themes.dark, themes.light]}
|
||||
onSelect={() => onChangeTheme("auto")}
|
||||
/>
|
||||
<ThemePreviewButton
|
||||
displayName="Dark"
|
||||
active={currentTheme === "dark"}
|
||||
theme={themes.dark}
|
||||
onSelect={() => onChangeTheme("dark")}
|
||||
/>
|
||||
<ThemePreviewButton
|
||||
displayName="Light"
|
||||
active={currentTheme === "light"}
|
||||
theme={themes.light}
|
||||
onSelect={() => onChangeTheme("light")}
|
||||
/>
|
||||
</Stack>
|
||||
<Section
|
||||
title={
|
||||
<Stack direction="row" alignItems="center">
|
||||
<span>Theme</span>
|
||||
{isUpdating && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
}
|
||||
layout="fluid"
|
||||
>
|
||||
<Stack direction="row" wrap="wrap">
|
||||
<AutoThemePreviewButton
|
||||
displayName="Auto"
|
||||
active={currentTheme === "auto"}
|
||||
themes={[themes.dark, themes.light]}
|
||||
onSelect={() => onChangeTheme("auto")}
|
||||
/>
|
||||
<ThemePreviewButton
|
||||
displayName="Dark"
|
||||
active={currentTheme === "dark"}
|
||||
theme={themes.dark}
|
||||
onSelect={() => onChangeTheme("dark")}
|
||||
/>
|
||||
<ThemePreviewButton
|
||||
displayName="Light"
|
||||
active={currentTheme === "light"}
|
||||
theme={themes.light}
|
||||
onSelect={() => onChangeTheme("light")}
|
||||
/>
|
||||
</Stack>
|
||||
</Section>
|
||||
<div css={{ marginBottom: 48 }}></div>
|
||||
<Section
|
||||
title={
|
||||
<Stack direction="row" alignItems="center">
|
||||
<span>Terminal Font</span>
|
||||
{isUpdating && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
}
|
||||
layout="fluid"
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
aria-labelledby="fonts-radio-buttons-group-label"
|
||||
defaultValue={currentTerminalFont}
|
||||
name="fonts-radio-buttons-group"
|
||||
onChange={(_, value) =>
|
||||
onChangeTerminalFont(toTerminalFontName(value))
|
||||
}
|
||||
>
|
||||
{TerminalFontNames.filter((name) => name !== "").map((name) => (
|
||||
<FormControlLabel
|
||||
key={name}
|
||||
value={name}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<div css={{ fontFamily: terminalFontLabels[name] }}>
|
||||
{terminalFontLabels[name]}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Section>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export function toTerminalFontName(value: string): TerminalFontName {
|
||||
return TerminalFontNames.includes(value as TerminalFontName)
|
||||
? (value as TerminalFontName)
|
||||
: "";
|
||||
}
|
||||
|
||||
interface AutoThemePreviewButtonProps extends Omit<ThemePreviewProps, "theme"> {
|
||||
themes: [Theme, Theme];
|
||||
onSelect?: () => void;
|
||||
|
||||
@@ -12,13 +12,14 @@ describe("appearance page", () => {
|
||||
jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
|
||||
...MockUser,
|
||||
theme_preference: "dark",
|
||||
terminal_font: "fira-code",
|
||||
});
|
||||
|
||||
const dark = await screen.findByText("Dark");
|
||||
await userEvent.click(dark);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.updateAppearanceSettings).toBeCalledTimes(0);
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("changes theme to light", async () => {
|
||||
@@ -26,6 +27,7 @@ describe("appearance page", () => {
|
||||
|
||||
jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
|
||||
...MockUser,
|
||||
terminal_font: "ibm-plex-mono",
|
||||
theme_preference: "light",
|
||||
});
|
||||
|
||||
@@ -33,9 +35,30 @@ describe("appearance page", () => {
|
||||
await userEvent.click(light);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.updateAppearanceSettings).toBeCalledTimes(1);
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1);
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledWith({
|
||||
terminal_font: "ibm-plex-mono",
|
||||
theme_preference: "light",
|
||||
});
|
||||
});
|
||||
|
||||
it("changes font to fira code", async () => {
|
||||
renderWithAuth(<AppearancePage />);
|
||||
|
||||
jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
|
||||
...MockUser,
|
||||
terminal_font: "fira-code",
|
||||
theme_preference: "dark",
|
||||
});
|
||||
|
||||
const ibmPlex = await screen.findByText("Fira Code");
|
||||
await userEvent.click(ibmPlex);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1);
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledWith({
|
||||
terminal_font: "fira-code",
|
||||
theme_preference: "dark",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { updateAppearanceSettings } from "api/queries/users";
|
||||
import { appearanceSettings } from "api/queries/users";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import type { FC } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Section } from "../Section";
|
||||
import { AppearanceForm } from "./AppearanceForm";
|
||||
|
||||
export const AppearancePage: FC = () => {
|
||||
@@ -31,26 +28,15 @@ export const AppearancePage: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
title={
|
||||
<Stack direction="row" alignItems="center">
|
||||
<span>Theme</span>
|
||||
{updateAppearanceSettingsMutation.isLoading && (
|
||||
<CircularProgress size={16} />
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
layout="fluid"
|
||||
>
|
||||
<AppearanceForm
|
||||
isUpdating={updateAppearanceSettingsMutation.isLoading}
|
||||
error={updateAppearanceSettingsMutation.error}
|
||||
initialValues={{
|
||||
theme_preference: appearanceSettingsQuery.data.theme_preference,
|
||||
}}
|
||||
onSubmit={updateAppearanceSettingsMutation.mutateAsync}
|
||||
/>
|
||||
</Section>
|
||||
<AppearanceForm
|
||||
isUpdating={updateAppearanceSettingsMutation.isLoading}
|
||||
error={updateAppearanceSettingsMutation.error}
|
||||
initialValues={{
|
||||
theme_preference: appearanceSettingsQuery.data.theme_preference,
|
||||
terminal_font: appearanceSettingsQuery.data.terminal_font,
|
||||
}}
|
||||
onSubmit={updateAppearanceSettingsMutation.mutateAsync}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -536,6 +536,7 @@ export const SuspendedMockUser: TypesGen.User = {
|
||||
|
||||
export const MockUserAppearanceSettings: TypesGen.UserAppearanceSettings = {
|
||||
theme_preference: "dark",
|
||||
terminal_font: "",
|
||||
};
|
||||
|
||||
export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = {
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { TerminalFontName } from "api/typesGenerated";
|
||||
|
||||
export const borderRadius = 8;
|
||||
export const MONOSPACE_FONT_FAMILY =
|
||||
"'IBM Plex Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace";
|
||||
export const BODY_FONT_FAMILY = `"Inter Variable", system-ui, sans-serif`;
|
||||
|
||||
export const terminalFonts: Record<TerminalFontName, string> = {
|
||||
"fira-code": MONOSPACE_FONT_FAMILY.replace("IBM Plex Mono", "Fira Code"),
|
||||
"ibm-plex-mono": MONOSPACE_FONT_FAMILY,
|
||||
|
||||
"": MONOSPACE_FONT_FAMILY,
|
||||
};
|
||||
export const terminalFontLabels: Record<TerminalFontName, string> = {
|
||||
"fira-code": "Fira Code",
|
||||
"ibm-plex-mono": "IBM Plex Mono",
|
||||
"": "", // needed for enum completeness, otherwise fails the build
|
||||
};
|
||||
export const DEFAULT_TERMINAL_FONT = "ibm-plex-mono";
|
||||
|
||||
export const navHeight = 62;
|
||||
export const containerWidth = 1380;
|
||||
export const containerWidthMedium = 1080;
|
||||
|
||||
@@ -3,3 +3,6 @@ import "@fontsource/ibm-plex-mono/400.css";
|
||||
import "@fontsource/ibm-plex-mono/600.css";
|
||||
// Main body copy font
|
||||
import "@fontsource-variable/inter";
|
||||
// Alternative font for Terminal
|
||||
import "@fontsource/fira-code/400.css";
|
||||
import "@fontsource/fira-code/600.css";
|
||||
|
||||
Reference in New Issue
Block a user