mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add notification warning alert to Tasks page (#20900)
## Problem Users may not realize that task notifications are disabled by default. To improve awareness, we show a warning alert on the Tasks page when all task notifications are disabled. **Alert visibility logic:** - Shows when **all** task notification templates (Task Working, Task Idle, Task Completed, Task Failed) are disabled - Can be dismissed by the user, which stores the dismissal in the user preferences API - If the user later enables any task notification in Account Settings, the dismissal state is cleared so the alert will show again if they disable all notifications in the future <img width="2980" height="1588" alt="Screenshot 2025-11-25 at 17 48 17" src="https://github.com/user-attachments/assets/316bf097-d9d2-4489-bc16-2987ba45f45c" /> ## Changes - Added a warning alert to the Tasks page when all task notifications are disabled - Introduced new `/users/{user}/preferences` endpoint to manage user preferences (stored in `user_configs` table) - Alert is dismissible and stores the dismissal state via the new user preferences API endpoint - Enabling any task notification in Account Settings clears the dismissal state via the preferences API - Added comprehensive Storybook stories for both TasksPage and NotificationsPage to test all alert visibility states and interactions Closes: https://github.com/coder/internal/issues/1089
This commit is contained in:
Generated
+94
@@ -8387,6 +8387,84 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/preferences": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get user preference settings",
|
||||
"operationId": "get-user-preference-settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UserPreferenceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Update user preference settings",
|
||||
"operationId": "update-user-preference-settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "New preference settings",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateUserPreferenceSettingsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UserPreferenceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/profile": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -19254,6 +19332,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateUserPreferenceSettingsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_notification_alert_dismissed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateUserProfileRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -19639,6 +19725,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserPreferenceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_notification_alert_dismissed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserQuietHoursScheduleConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+84
@@ -7418,6 +7418,74 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/preferences": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Users"],
|
||||
"summary": "Get user preference settings",
|
||||
"operationId": "get-user-preference-settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UserPreferenceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Users"],
|
||||
"summary": "Update user preference settings",
|
||||
"operationId": "update-user-preference-settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "New preference settings",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateUserPreferenceSettingsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UserPreferenceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/profile": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -17660,6 +17728,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateUserPreferenceSettingsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_notification_alert_dismissed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateUserProfileRequest": {
|
||||
"type": "object",
|
||||
"required": ["username"],
|
||||
@@ -18020,6 +18096,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserPreferenceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_notification_alert_dismissed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserQuietHoursScheduleConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1336,6 +1336,8 @@ func New(options *Options) *API {
|
||||
})
|
||||
r.Get("/appearance", api.userAppearanceSettings)
|
||||
r.Put("/appearance", api.putUserAppearanceSettings)
|
||||
r.Get("/preferences", api.userPreferenceSettings)
|
||||
r.Put("/preferences", api.putUserPreferenceSettings)
|
||||
r.Route("/password", func(r chi.Router) {
|
||||
r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute))
|
||||
r.Put("/", api.putUserPassword)
|
||||
|
||||
@@ -3431,6 +3431,17 @@ func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserS
|
||||
return q.db.GetUserStatusCounts(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
user, err := q.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionReadPersonal, user); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return q.db.GetUserTaskNotificationAlertDismissed(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
u, err := q.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -5464,6 +5475,17 @@ 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) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg database.UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
|
||||
user, err := q.db.GetUserByID(ctx, arg.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, user); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return q.db.UpdateUserTaskNotificationAlertDismissed(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 {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -1477,6 +1478,21 @@ func (s *MethodTestSuite) TestUser() {
|
||||
dbm.EXPECT().UpdateUserTerminalFont(gomock.Any(), arg).Return(uc, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(u, policy.ActionUpdatePersonal).Returns(uc)
|
||||
}))
|
||||
s.Run("GetUserTaskNotificationAlertDismissed", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
dbm.EXPECT().GetUserByID(gomock.Any(), u.ID).Return(u, nil).AnyTimes()
|
||||
dbm.EXPECT().GetUserTaskNotificationAlertDismissed(gomock.Any(), u.ID).Return(false, nil).AnyTimes()
|
||||
check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns(false)
|
||||
}))
|
||||
s.Run("UpdateUserTaskNotificationAlertDismissed", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
user := testutil.Fake(s.T(), faker, database.User{})
|
||||
userConfig := database.UserConfig{UserID: user.ID, Key: "task_notification_alert_dismissed", Value: "false"}
|
||||
userConfigValue, _ := strconv.ParseBool(userConfig.Value)
|
||||
arg := database.UpdateUserTaskNotificationAlertDismissedParams{UserID: user.ID, TaskNotificationAlertDismissed: userConfigValue}
|
||||
dbm.EXPECT().GetUserByID(gomock.Any(), user.ID).Return(user, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateUserTaskNotificationAlertDismissed(gomock.Any(), arg).Return(false, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(user, policy.ActionUpdatePersonal).Returns(userConfigValue)
|
||||
}))
|
||||
s.Run("UpdateUserStatus", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
arg := database.UpdateUserStatusParams{ID: u.ID, Status: u.Status, UpdatedAt: u.UpdatedAt}
|
||||
|
||||
@@ -1845,6 +1845,13 @@ func (m queryMetricsStore) GetUserStatusCounts(ctx context.Context, arg database
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserTaskNotificationAlertDismissed(ctx, userID)
|
||||
m.queryLatencies.WithLabelValues("GetUserTaskNotificationAlertDismissed").Observe(time.Since(start).Seconds())
|
||||
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)
|
||||
@@ -3350,6 +3357,13 @@ func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.Up
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg database.UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserTaskNotificationAlertDismissed(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateUserTaskNotificationAlertDismissed").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserTerminalFont(ctx, arg)
|
||||
|
||||
@@ -3942,6 +3942,21 @@ func (mr *MockStoreMockRecorder) GetUserStatusCounts(ctx, arg any) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusCounts", reflect.TypeOf((*MockStore)(nil).GetUserStatusCounts), ctx, arg)
|
||||
}
|
||||
|
||||
// GetUserTaskNotificationAlertDismissed mocks base method.
|
||||
func (m *MockStore) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserTaskNotificationAlertDismissed", ctx, userID)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserTaskNotificationAlertDismissed indicates an expected call of GetUserTaskNotificationAlertDismissed.
|
||||
func (mr *MockStoreMockRecorder) GetUserTaskNotificationAlertDismissed(ctx, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserTaskNotificationAlertDismissed", reflect.TypeOf((*MockStore)(nil).GetUserTaskNotificationAlertDismissed), ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserTerminalFont mocks base method.
|
||||
func (m *MockStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7174,6 +7189,21 @@ func (mr *MockStoreMockRecorder) UpdateUserStatus(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserTaskNotificationAlertDismissed mocks base method.
|
||||
func (m *MockStore) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg database.UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserTaskNotificationAlertDismissed", ctx, arg)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateUserTaskNotificationAlertDismissed indicates an expected call of UpdateUserTaskNotificationAlertDismissed.
|
||||
func (mr *MockStoreMockRecorder) UpdateUserTaskNotificationAlertDismissed(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserTaskNotificationAlertDismissed", reflect.TypeOf((*MockStore)(nil).UpdateUserTaskNotificationAlertDismissed), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserTerminalFont mocks base method.
|
||||
func (m *MockStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -447,6 +447,7 @@ 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)
|
||||
GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, 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)
|
||||
@@ -714,6 +715,7 @@ type sqlcQuerier interface {
|
||||
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
||||
UpdateUserSecret(ctx context.Context, arg UpdateUserSecretParams) (UserSecret, error)
|
||||
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
|
||||
UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error)
|
||||
UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error)
|
||||
UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error)
|
||||
UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error
|
||||
|
||||
@@ -16324,6 +16324,23 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int6
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getUserTaskNotificationAlertDismissed = `-- name: GetUserTaskNotificationAlertDismissed :one
|
||||
SELECT
|
||||
value::boolean as task_notification_alert_dismissed
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = $1
|
||||
AND key = 'preference_task_notification_alert_dismissed'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserTaskNotificationAlertDismissed, userID)
|
||||
var task_notification_alert_dismissed bool
|
||||
err := row.Scan(&task_notification_alert_dismissed)
|
||||
return task_notification_alert_dismissed, err
|
||||
}
|
||||
|
||||
const getUserTerminalFont = `-- name: GetUserTerminalFont :one
|
||||
SELECT
|
||||
value as terminal_font
|
||||
@@ -17076,6 +17093,33 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserTaskNotificationAlertDismissed = `-- name: UpdateUserTaskNotificationAlertDismissed :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
($1, 'preference_task_notification_alert_dismissed', ($2::boolean)::text)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = $2
|
||||
WHERE user_configs.user_id = $1
|
||||
AND user_configs.key = 'preference_task_notification_alert_dismissed'
|
||||
RETURNING value::boolean AS task_notification_alert_dismissed
|
||||
`
|
||||
|
||||
type UpdateUserTaskNotificationAlertDismissedParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
TaskNotificationAlertDismissed bool `db:"task_notification_alert_dismissed" json:"task_notification_alert_dismissed"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserTaskNotificationAlertDismissed, arg.UserID, arg.TaskNotificationAlertDismissed)
|
||||
var task_notification_alert_dismissed bool
|
||||
err := row.Scan(&task_notification_alert_dismissed)
|
||||
return task_notification_alert_dismissed, err
|
||||
}
|
||||
|
||||
const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
|
||||
@@ -168,6 +168,29 @@ WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'terminal_font'
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUserTaskNotificationAlertDismissed :one
|
||||
SELECT
|
||||
value::boolean as task_notification_alert_dismissed
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
AND key = 'preference_task_notification_alert_dismissed';
|
||||
|
||||
-- name: UpdateUserTaskNotificationAlertDismissed :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
(@user_id, 'preference_task_notification_alert_dismissed', (@task_notification_alert_dismissed::boolean)::text)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = @task_notification_alert_dismissed
|
||||
WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'preference_task_notification_alert_dismissed'
|
||||
RETURNING value::boolean AS task_notification_alert_dismissed;
|
||||
|
||||
-- name: UpdateUserRoles :one
|
||||
UPDATE
|
||||
users
|
||||
|
||||
@@ -1077,6 +1077,74 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get user preference settings
|
||||
// @ID get-user-preference-settings
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Param user path string true "User ID, name, or me"
|
||||
// @Success 200 {object} codersdk.UserPreferenceSettings
|
||||
// @Router /users/{user}/preferences [get]
|
||||
func (api *API) userPreferenceSettings(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
taskAlertDismissed, err := api.Database.GetUserTaskNotificationAlertDismissed(ctx, user.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Error reading user preference settings.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserPreferenceSettings{
|
||||
TaskNotificationAlertDismissed: taskAlertDismissed,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Update user preference settings
|
||||
// @ID update-user-preference-settings
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Param user path string true "User ID, name, or me"
|
||||
// @Param request body codersdk.UpdateUserPreferenceSettingsRequest true "New preference settings"
|
||||
// @Success 200 {object} codersdk.UserPreferenceSettings
|
||||
// @Router /users/{user}/preferences [put]
|
||||
func (api *API) putUserPreferenceSettings(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
var params codersdk.UpdateUserPreferenceSettingsRequest
|
||||
if !httpapi.Read(ctx, rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedTaskAlertDismissed, err := api.Database.UpdateUserTaskNotificationAlertDismissed(ctx, database.UpdateUserTaskNotificationAlertDismissedParams{
|
||||
UserID: user.ID,
|
||||
TaskNotificationAlertDismissed: params.TaskNotificationAlertDismissed,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating user task notification alert dismissed.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserPreferenceSettings{
|
||||
TaskNotificationAlertDismissed: updatedTaskAlertDismissed,
|
||||
})
|
||||
}
|
||||
|
||||
func isValidFontName(font codersdk.TerminalFontName) bool {
|
||||
return slices.Contains(codersdk.TerminalFontNames, font)
|
||||
}
|
||||
|
||||
+78
-9
@@ -2192,16 +2192,16 @@ func TestUserTerminalFont(t *testing.T) {
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
// given
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, "me")
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)
|
||||
|
||||
// when
|
||||
updated, err := client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
ThemePreference: "light",
|
||||
TerminalFont: "fira-code",
|
||||
})
|
||||
@@ -2218,16 +2218,16 @@ func TestUserTerminalFont(t *testing.T) {
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
// given
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, "me")
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)
|
||||
|
||||
// when
|
||||
_, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
_, err = client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
ThemePreference: "light",
|
||||
TerminalFont: "foobar",
|
||||
})
|
||||
@@ -2243,16 +2243,16 @@ func TestUserTerminalFont(t *testing.T) {
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
// given
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, "me")
|
||||
initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)
|
||||
|
||||
// when
|
||||
_, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
_, err = client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
|
||||
ThemePreference: "light",
|
||||
TerminalFont: "",
|
||||
})
|
||||
@@ -2262,6 +2262,75 @@ func TestUserTerminalFont(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserTaskNotificationAlertDismissed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("defaults to false", 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.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
// When: getting user preference settings for a user
|
||||
settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the task notification alert dismissed should default to false
|
||||
require.False(t, settings.TaskNotificationAlertDismissed)
|
||||
})
|
||||
|
||||
t.Run("update to true", 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.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
// When: user dismisses the task notification alert
|
||||
updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
TaskNotificationAlertDismissed: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the setting is updated to true
|
||||
require.True(t, updated.TaskNotificationAlertDismissed)
|
||||
})
|
||||
|
||||
t.Run("update to false", 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.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
// Given: user has dismissed the task notification alert
|
||||
_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
TaskNotificationAlertDismissed: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: the task notification alert dismissal is cleared
|
||||
// (e.g., when user enables a task notification in the UI settings)
|
||||
updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
TaskNotificationAlertDismissed: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the setting is updated to false
|
||||
require.False(t, updated.TaskNotificationAlertDismissed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacesByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user