mirror of https://github.com/coder/coder.git
feat: add display_name field to groups (#8740)
* feat: add display_name field to groups This is a non-unique human friendly group name for display purposes. This means a display name can be used instead of using an environment var to remap groups with OIDC names to Coder names. Now groups can retain the OIDC name for mapping, and use a display name for display purposes.
This commit is contained in:
parent
6ea32e4e80
commit
4c1e63aae8
|
@ -7263,6 +7263,9 @@ const docTemplate = `{
|
|||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -8247,6 +8250,9 @@ const docTemplate = `{
|
|||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
|
|
@ -6471,6 +6471,9 @@
|
|||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -7403,6 +7406,9 @@
|
|||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
|
|
@ -3402,6 +3402,7 @@ func (q *FakeQuerier) InsertAllUsersGroup(ctx context.Context, orgID uuid.UUID)
|
|||
return q.InsertGroup(ctx, database.InsertGroupParams{
|
||||
ID: orgID,
|
||||
Name: database.AllUsersGroup,
|
||||
DisplayName: "",
|
||||
OrganizationID: orgID,
|
||||
})
|
||||
}
|
||||
|
@ -3521,6 +3522,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar
|
|||
group := database.Group{
|
||||
ID: arg.ID,
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
AvatarURL: arg.AvatarURL,
|
||||
QuotaAllowance: arg.QuotaAllowance,
|
||||
|
@ -4329,6 +4331,7 @@ func (q *FakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGrou
|
|||
|
||||
for i, group := range q.groups {
|
||||
if group.ID == arg.ID {
|
||||
group.DisplayName = arg.DisplayName
|
||||
group.Name = arg.Name
|
||||
group.AvatarURL = arg.AvatarURL
|
||||
group.QuotaAllowance = arg.QuotaAllowance
|
||||
|
|
|
@ -279,9 +279,11 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat
|
|||
}
|
||||
|
||||
func Group(t testing.TB, db database.Store, orig database.Group) database.Group {
|
||||
name := takeFirst(orig.Name, namesgenerator.GetRandomName(1))
|
||||
group, err := db.InsertGroup(genCtx, database.InsertGroupParams{
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
|
||||
Name: name,
|
||||
DisplayName: takeFirst(orig.DisplayName, name),
|
||||
OrganizationID: takeFirst(orig.OrganizationID, uuid.New()),
|
||||
AvatarURL: takeFirst(orig.AvatarURL, "https://logo.example.com"),
|
||||
QuotaAllowance: takeFirst(orig.QuotaAllowance, 0),
|
||||
|
|
|
@ -298,9 +298,12 @@ CREATE TABLE groups (
|
|||
name text NOT NULL,
|
||||
organization_id uuid NOT NULL,
|
||||
avatar_url text DEFAULT ''::text NOT NULL,
|
||||
quota_allowance integer DEFAULT 0 NOT NULL
|
||||
quota_allowance integer DEFAULT 0 NOT NULL,
|
||||
display_name text DEFAULT ''::text NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.';
|
||||
|
||||
CREATE TABLE licenses (
|
||||
id integer NOT NULL,
|
||||
uploaded_at timestamp with time zone NOT NULL,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
BEGIN;
|
||||
|
||||
ALTER TABLE groups
|
||||
DROP COLUMN display_name;
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,8 @@
|
|||
BEGIN;
|
||||
|
||||
ALTER TABLE groups
|
||||
ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
||||
|
||||
COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.';
|
||||
|
||||
COMMIT;
|
|
@ -1496,6 +1496,8 @@ type Group struct {
|
|||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
||||
QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"`
|
||||
// Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
}
|
||||
|
||||
type GroupMember struct {
|
||||
|
|
|
@ -1180,7 +1180,7 @@ func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
|
|||
|
||||
const getGroupByID = `-- name: GetGroupByID :one
|
||||
SELECT
|
||||
id, name, organization_id, avatar_url, quota_allowance
|
||||
id, name, organization_id, avatar_url, quota_allowance, display_name
|
||||
FROM
|
||||
groups
|
||||
WHERE
|
||||
|
@ -1198,13 +1198,14 @@ func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, err
|
|||
&i.OrganizationID,
|
||||
&i.AvatarURL,
|
||||
&i.QuotaAllowance,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one
|
||||
SELECT
|
||||
id, name, organization_id, avatar_url, quota_allowance
|
||||
id, name, organization_id, avatar_url, quota_allowance, display_name
|
||||
FROM
|
||||
groups
|
||||
WHERE
|
||||
|
@ -1229,13 +1230,14 @@ func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrg
|
|||
&i.OrganizationID,
|
||||
&i.AvatarURL,
|
||||
&i.QuotaAllowance,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many
|
||||
SELECT
|
||||
id, name, organization_id, avatar_url, quota_allowance
|
||||
id, name, organization_id, avatar_url, quota_allowance, display_name
|
||||
FROM
|
||||
groups
|
||||
WHERE
|
||||
|
@ -1259,6 +1261,7 @@ func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organization
|
|||
&i.OrganizationID,
|
||||
&i.AvatarURL,
|
||||
&i.QuotaAllowance,
|
||||
&i.DisplayName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1280,7 +1283,7 @@ INSERT INTO groups (
|
|||
organization_id
|
||||
)
|
||||
VALUES
|
||||
($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance
|
||||
($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name
|
||||
`
|
||||
|
||||
// We use the organization_id as the id
|
||||
|
@ -1295,6 +1298,7 @@ func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uui
|
|||
&i.OrganizationID,
|
||||
&i.AvatarURL,
|
||||
&i.QuotaAllowance,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -1303,17 +1307,19 @@ const insertGroup = `-- name: InsertGroup :one
|
|||
INSERT INTO groups (
|
||||
id,
|
||||
name,
|
||||
display_name,
|
||||
organization_id,
|
||||
avatar_url,
|
||||
quota_allowance
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5) RETURNING id, name, organization_id, avatar_url, quota_allowance
|
||||
($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name
|
||||
`
|
||||
|
||||
type InsertGroupParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
||||
QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"`
|
||||
|
@ -1323,6 +1329,7 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr
|
|||
row := q.db.QueryRowContext(ctx, insertGroup,
|
||||
arg.ID,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.OrganizationID,
|
||||
arg.AvatarURL,
|
||||
arg.QuotaAllowance,
|
||||
|
@ -1334,6 +1341,7 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr
|
|||
&i.OrganizationID,
|
||||
&i.AvatarURL,
|
||||
&i.QuotaAllowance,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -1343,15 +1351,17 @@ UPDATE
|
|||
groups
|
||||
SET
|
||||
name = $1,
|
||||
avatar_url = $2,
|
||||
quota_allowance = $3
|
||||
display_name = $2,
|
||||
avatar_url = $3,
|
||||
quota_allowance = $4
|
||||
WHERE
|
||||
id = $4
|
||||
RETURNING id, name, organization_id, avatar_url, quota_allowance
|
||||
id = $5
|
||||
RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name
|
||||
`
|
||||
|
||||
type UpdateGroupByIDParams struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
||||
QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
|
@ -1360,6 +1370,7 @@ type UpdateGroupByIDParams struct {
|
|||
func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateGroupByID,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.AvatarURL,
|
||||
arg.QuotaAllowance,
|
||||
arg.ID,
|
||||
|
@ -1371,6 +1382,7 @@ func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDPar
|
|||
&i.OrganizationID,
|
||||
&i.AvatarURL,
|
||||
&i.QuotaAllowance,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -34,12 +34,13 @@ AND
|
|||
INSERT INTO groups (
|
||||
id,
|
||||
name,
|
||||
display_name,
|
||||
organization_id,
|
||||
avatar_url,
|
||||
quota_allowance
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6) RETURNING *;
|
||||
|
||||
-- We use the organization_id as the id
|
||||
-- for simplicity since all users is
|
||||
|
@ -57,11 +58,12 @@ VALUES
|
|||
UPDATE
|
||||
groups
|
||||
SET
|
||||
name = $1,
|
||||
avatar_url = $2,
|
||||
quota_allowance = $3
|
||||
name = @name,
|
||||
display_name = @display_name,
|
||||
avatar_url = @avatar_url,
|
||||
quota_allowance = @quota_allowance
|
||||
WHERE
|
||||
id = $4
|
||||
id = @id
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteGroupByID :exec
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
type CreateGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
QuotaAllowance int `json:"quota_allowance"`
|
||||
}
|
||||
|
@ -19,6 +20,7 @@ type CreateGroupRequest struct {
|
|||
type Group struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||
Members []User `json:"members"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
|
@ -98,6 +100,7 @@ type PatchGroupRequest struct {
|
|||
AddUsers []string `json:"add_users"`
|
||||
RemoveUsers []string `json:"remove_users"`
|
||||
Name string `json:"name"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
QuotaAllowance *int `json:"quota_allowance"`
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ We track the following resources:
|
|||
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr></tbody></table> |
|
||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr></tbody></table> |
|
||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>inactivity_ttl</td><td>true</td></tr><tr><td>locked_ttl</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>restart_requirement_days_of_week</td><td>true</td></tr><tr><td>restart_requirement_weeks</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
|
|
|
@ -174,6 +174,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -234,6 +235,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -294,6 +296,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -429,6 +432,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
|
|||
[
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -470,6 +474,7 @@ Status Code **200**
|
|||
| --------------------- | ---------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string | false | | |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» members` | array | false | | |
|
||||
| `»» avatar_url` | string(uri) | false | | |
|
||||
|
@ -521,6 +526,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"quota_allowance": 0
|
||||
}
|
||||
|
@ -540,6 +546,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -601,6 +608,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -1171,6 +1179,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
|
|||
"groups": [
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -1234,6 +1243,7 @@ Status Code **200**
|
|||
| `[array item]` | array | false | | |
|
||||
| `» groups` | array | false | | |
|
||||
| `»» avatar_url` | string | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» id` | string(uuid) | false | | |
|
||||
| `»» members` | array | false | | |
|
||||
| `»»» avatar_url` | string(uri) | false | | |
|
||||
|
|
|
@ -765,6 +765,7 @@
|
|||
"groups": [
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -1418,6 +1419,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"quota_allowance": 0
|
||||
}
|
||||
|
@ -1428,6 +1430,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------------- | ------- | -------- | ------------ | ----------- |
|
||||
| `avatar_url` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `quota_allowance` | integer | false | | |
|
||||
|
||||
|
@ -2930,6 +2933,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
```json
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
|
@ -2961,6 +2965,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------------- | --------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `avatar_url` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `members` | array of [codersdk.User](#codersdkuser) | false | | |
|
||||
| `name` | string | false | | |
|
||||
|
|
|
@ -20,3 +20,12 @@ coder groups create [flags] <name>
|
|||
| Environment | <code>$CODER_AVATAR_URL</code> |
|
||||
|
||||
Set an avatar for a group.
|
||||
|
||||
### --display-name
|
||||
|
||||
| | |
|
||||
| ----------- | -------------------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_DISPLAY_NAME</code> |
|
||||
|
||||
Optional human friendly name for the group.
|
||||
|
|
|
@ -28,6 +28,15 @@ Add users to the group. Accepts emails or IDs.
|
|||
|
||||
Update the group avatar.
|
||||
|
||||
### --display-name
|
||||
|
||||
| | |
|
||||
| ----------- | -------------------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_DISPLAY_NAME</code> |
|
||||
|
||||
Optional human friendly name for the group.
|
||||
|
||||
### -n, --name
|
||||
|
||||
| | |
|
||||
|
|
|
@ -14,12 +14,12 @@ coder groups list [flags]
|
|||
|
||||
### -c, --column
|
||||
|
||||
| | |
|
||||
| ------- | ---------------------------------------------------- |
|
||||
| Type | <code>string-array</code> |
|
||||
| Default | <code>name,organization id,members,avatar url</code> |
|
||||
| | |
|
||||
| ------- | ----------------------------------------------------------------- |
|
||||
| Type | <code>string-array</code> |
|
||||
| Default | <code>name,display name,organization id,members,avatar url</code> |
|
||||
|
||||
Columns to display in table output. Available columns: name, organization id, members, avatar url.
|
||||
Columns to display in table output. Available columns: name, display name, organization id, members, avatar url.
|
||||
|
||||
### -o, --output
|
||||
|
||||
|
|
|
@ -151,6 +151,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||
&database.AuditableGroup{}: {
|
||||
"id": ActionTrack,
|
||||
"name": ActionTrack,
|
||||
"display_name": ActionTrack,
|
||||
"organization_id": ActionIgnore, // Never changes.
|
||||
"avatar_url": ActionTrack,
|
||||
"quota_allowance": ActionTrack,
|
||||
|
|
|
@ -12,7 +12,11 @@ import (
|
|||
)
|
||||
|
||||
func (r *RootCmd) groupCreate() *clibase.Cmd {
|
||||
var avatarURL string
|
||||
var (
|
||||
avatarURL string
|
||||
displayName string
|
||||
)
|
||||
|
||||
client := new(codersdk.Client)
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "create <name>",
|
||||
|
@ -30,8 +34,9 @@ func (r *RootCmd) groupCreate() *clibase.Cmd {
|
|||
}
|
||||
|
||||
group, err := client.CreateGroup(ctx, org.ID, codersdk.CreateGroupRequest{
|
||||
Name: inv.Args[0],
|
||||
AvatarURL: avatarURL,
|
||||
Name: inv.Args[0],
|
||||
DisplayName: displayName,
|
||||
AvatarURL: avatarURL,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create group: %w", err)
|
||||
|
@ -50,6 +55,12 @@ func (r *RootCmd) groupCreate() *clibase.Cmd {
|
|||
Env: "CODER_AVATAR_URL",
|
||||
Value: clibase.StringOf(&avatarURL),
|
||||
},
|
||||
{
|
||||
Flag: "display-name",
|
||||
Description: `Optional human friendly name for the group.`,
|
||||
Env: "CODER_DISPLAY_NAME",
|
||||
Value: clibase.StringOf(&displayName),
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -15,10 +15,11 @@ import (
|
|||
|
||||
func (r *RootCmd) groupEdit() *clibase.Cmd {
|
||||
var (
|
||||
avatarURL string
|
||||
name string
|
||||
addUsers []string
|
||||
rmUsers []string
|
||||
avatarURL string
|
||||
name string
|
||||
displayName string
|
||||
addUsers []string
|
||||
rmUsers []string
|
||||
)
|
||||
client := new(codersdk.Client)
|
||||
cmd := &clibase.Cmd{
|
||||
|
@ -52,6 +53,10 @@ func (r *RootCmd) groupEdit() *clibase.Cmd {
|
|||
req.AvatarURL = &avatarURL
|
||||
}
|
||||
|
||||
if inv.ParsedFlags().Lookup("display-name").Changed {
|
||||
req.DisplayName = &displayName
|
||||
}
|
||||
|
||||
userRes, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get users: %w", err)
|
||||
|
@ -90,6 +95,12 @@ func (r *RootCmd) groupEdit() *clibase.Cmd {
|
|||
Description: "Update the group avatar.",
|
||||
Value: clibase.StringOf(&avatarURL),
|
||||
},
|
||||
{
|
||||
Flag: "display-name",
|
||||
Description: `Optional human friendly name for the group.`,
|
||||
Env: "CODER_DISPLAY_NAME",
|
||||
Value: clibase.StringOf(&displayName),
|
||||
},
|
||||
{
|
||||
Flag: "add-users",
|
||||
FlagShorthand: "a",
|
||||
|
|
|
@ -67,6 +67,7 @@ type groupTableRow struct {
|
|||
|
||||
// For table output:
|
||||
Name string `json:"-" table:"name,default_sort"`
|
||||
DisplayName string `json:"-" table:"display_name"`
|
||||
OrganizationID uuid.UUID `json:"-" table:"organization_id"`
|
||||
Members []string `json:"-" table:"members"`
|
||||
AvatarURL string `json:"-" table:"avatar_url"`
|
||||
|
@ -81,6 +82,7 @@ func groupsToRows(groups ...codersdk.Group) []groupTableRow {
|
|||
}
|
||||
rows = append(rows, groupTableRow{
|
||||
Name: group.Name,
|
||||
DisplayName: group.DisplayName,
|
||||
OrganizationID: group.OrganizationID,
|
||||
AvatarURL: group.AvatarURL,
|
||||
Members: members,
|
||||
|
|
|
@ -6,5 +6,8 @@ Create a user group
|
|||
-u, --avatar-url string, $CODER_AVATAR_URL
|
||||
Set an avatar for a group.
|
||||
|
||||
--display-name string, $CODER_DISPLAY_NAME
|
||||
Optional human friendly name for the group.
|
||||
|
||||
---
|
||||
Run `coder --help` for a list of global options.
|
||||
|
|
|
@ -9,6 +9,9 @@ Edit a user group
|
|||
-u, --avatar-url string
|
||||
Update the group avatar.
|
||||
|
||||
--display-name string, $CODER_DISPLAY_NAME
|
||||
Optional human friendly name for the group.
|
||||
|
||||
-n, --name string
|
||||
Update the group name.
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ Usage: coder groups list [flags]
|
|||
List user groups
|
||||
|
||||
[1mOptions[0m
|
||||
-c, --column string-array (default: name,organization id,members,avatar url)
|
||||
Columns to display in table output. Available columns: name,
|
||||
organization id, members, avatar url.
|
||||
-c, --column string-array (default: name,display name,organization id,members,avatar url)
|
||||
Columns to display in table output. Available columns: name, display
|
||||
name, organization id, members, avatar url.
|
||||
|
||||
-o, --output string (default: table)
|
||||
Output format. Available formats: table, json.
|
||||
|
|
|
@ -56,6 +56,7 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request)
|
|||
group, err := api.Database.InsertGroup(ctx, database.InsertGroupParams{
|
||||
ID: uuid.New(),
|
||||
Name: req.Name,
|
||||
DisplayName: req.DisplayName,
|
||||
OrganizationID: org.ID,
|
||||
AvatarURL: req.AvatarURL,
|
||||
QuotaAllowance: int32(req.QuotaAllowance),
|
||||
|
@ -177,6 +178,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
|
|||
ID: group.ID,
|
||||
AvatarURL: group.AvatarURL,
|
||||
Name: group.Name,
|
||||
DisplayName: group.DisplayName,
|
||||
QuotaAllowance: group.QuotaAllowance,
|
||||
}
|
||||
|
||||
|
@ -190,6 +192,9 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
|
|||
if req.QuotaAllowance != nil {
|
||||
updateGroupParams.QuotaAllowance = int32(*req.QuotaAllowance)
|
||||
}
|
||||
if req.DisplayName != nil {
|
||||
updateGroupParams.DisplayName = *req.DisplayName
|
||||
}
|
||||
|
||||
group, err = tx.UpdateGroupByID(ctx, updateGroupParams)
|
||||
if err != nil {
|
||||
|
@ -395,9 +400,11 @@ func convertGroup(g database.Group, users []database.User) codersdk.Group {
|
|||
for _, user := range users {
|
||||
orgs[user.ID] = []uuid.UUID{g.OrganizationID}
|
||||
}
|
||||
|
||||
return codersdk.Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
DisplayName: g.DisplayName,
|
||||
OrganizationID: g.OrganizationID,
|
||||
AvatarURL: g.AvatarURL,
|
||||
QuotaAllowance: int(g.QuotaAllowance),
|
||||
|
|
|
@ -37,6 +37,7 @@ func TestCreateGroup(t *testing.T) {
|
|||
require.Equal(t, "hi", group.Name)
|
||||
require.Equal(t, "https://example.com", group.AvatarURL)
|
||||
require.Empty(t, group.Members)
|
||||
require.Empty(t, group.DisplayName)
|
||||
require.NotEqual(t, uuid.Nil.String(), group.ID.String())
|
||||
})
|
||||
|
||||
|
@ -124,11 +125,45 @@ func TestPatchGroup(t *testing.T) {
|
|||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
const displayName = "foobar"
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "hi",
|
||||
AvatarURL: "https://example.com",
|
||||
QuotaAllowance: 10,
|
||||
DisplayName: "",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 10, group.QuotaAllowance)
|
||||
|
||||
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
|
||||
Name: "bye",
|
||||
AvatarURL: ptr.Ref("https://google.com"),
|
||||
QuotaAllowance: ptr.Ref(20),
|
||||
DisplayName: ptr.Ref(displayName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, displayName, group.DisplayName)
|
||||
require.Equal(t, "bye", group.Name)
|
||||
require.Equal(t, "https://google.com", group.AvatarURL)
|
||||
require.Equal(t, 20, group.QuotaAllowance)
|
||||
})
|
||||
|
||||
t.Run("DisplayNameUnchanged", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
const displayName = "foobar"
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "hi",
|
||||
AvatarURL: "https://example.com",
|
||||
QuotaAllowance: 10,
|
||||
DisplayName: displayName,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 10, group.QuotaAllowance)
|
||||
|
@ -139,6 +174,7 @@ func TestPatchGroup(t *testing.T) {
|
|||
QuotaAllowance: ptr.Ref(20),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, displayName, group.DisplayName)
|
||||
require.Equal(t, "bye", group.Name)
|
||||
require.Equal(t, "https://google.com", group.AvatarURL)
|
||||
require.Equal(t, 20, group.QuotaAllowance)
|
||||
|
|
|
@ -174,6 +174,7 @@ export interface CreateFirstUserResponse {
|
|||
// From codersdk/groups.go
|
||||
export interface CreateGroupRequest {
|
||||
readonly name: string
|
||||
readonly display_name: string
|
||||
readonly avatar_url: string
|
||||
readonly quota_allowance: number
|
||||
}
|
||||
|
@ -506,6 +507,7 @@ export interface GitSSHKey {
|
|||
export interface Group {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly display_name: string
|
||||
readonly organization_id: string
|
||||
readonly members: User[]
|
||||
readonly avatar_url: string
|
||||
|
@ -666,6 +668,7 @@ export interface PatchGroupRequest {
|
|||
readonly add_users: string[]
|
||||
readonly remove_users: string[]
|
||||
readonly name: string
|
||||
readonly display_name?: string
|
||||
readonly avatar_url?: string
|
||||
readonly quota_allowance?: number
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export const UserOrGroupAutocomplete: React.FC<
|
|||
}}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option) =>
|
||||
isGroup(option) ? option.name : option.email
|
||||
isGroup(option) ? option.display_name || option.name : option.email
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
const isOptionGroup = isGroup(option)
|
||||
|
@ -79,7 +79,11 @@ export const UserOrGroupAutocomplete: React.FC<
|
|||
return (
|
||||
<Box component="li" {...props}>
|
||||
<AvatarData
|
||||
title={isOptionGroup ? option.name : option.username}
|
||||
title={
|
||||
isOptionGroup
|
||||
? option.display_name || option.name
|
||||
: option.username
|
||||
}
|
||||
subtitle={isOptionGroup ? getGroupSubtitle(option) : option.email}
|
||||
src={option.avatar_url}
|
||||
/>
|
||||
|
|
|
@ -29,6 +29,7 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
|
|||
const form = useFormik<CreateGroupRequest>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
display_name: "",
|
||||
avatar_url: "",
|
||||
quota_allowance: 0,
|
||||
},
|
||||
|
@ -51,6 +52,17 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
|
|||
fullWidth
|
||||
label="Name"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"display_name",
|
||||
"Optional: keep empty to default to the name.",
|
||||
)}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoComplete="display_name"
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Display Name"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("avatar_url")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
|
|
|
@ -33,6 +33,7 @@ import { pageTitle } from "utils/page"
|
|||
import { groupMachine } from "xServices/groups/groupXService"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
|
||||
|
||||
const AddGroupMember: React.FC<{
|
||||
isLoading: boolean
|
||||
|
@ -101,7 +102,9 @@ export const GroupPage: React.FC = () => {
|
|||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(group?.name ?? "Loading...")}</title>
|
||||
<title>
|
||||
{pageTitle((group?.display_name || group?.name) ?? "Loading...")}
|
||||
</title>
|
||||
</Helmet>
|
||||
<ChooseOne>
|
||||
<Cond condition={isLoading}>
|
||||
|
@ -127,13 +130,18 @@ export const GroupPage: React.FC = () => {
|
|||
</Maybe>
|
||||
}
|
||||
>
|
||||
<PageHeaderTitle>{group?.name}</PageHeaderTitle>
|
||||
<PageHeaderTitle>
|
||||
{group?.display_name || group?.name}
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
{group?.members.length} members
|
||||
{/* Show the name if it differs from the display name. */}
|
||||
{group?.display_name && group?.display_name !== group?.name
|
||||
? group?.name
|
||||
: ""}{" "}
|
||||
</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack spacing={2.5}>
|
||||
<Stack spacing={1}>
|
||||
<Maybe condition={canUpdateGroup}>
|
||||
<AddGroupMember
|
||||
isLoading={state.matches("addingMember")}
|
||||
|
@ -146,6 +154,13 @@ export const GroupPage: React.FC = () => {
|
|||
}}
|
||||
/>
|
||||
</Maybe>
|
||||
<PaginationStatus
|
||||
isLoading={Boolean(isLoading)}
|
||||
showing={group?.members.length ?? 0}
|
||||
total={group?.members.length ?? 0}
|
||||
label="members"
|
||||
/>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
|
|
@ -25,6 +25,13 @@ WithGroups.args = {
|
|||
isTemplateRBACEnabled: true,
|
||||
}
|
||||
|
||||
export const WithDisplayGroup = Template.bind({})
|
||||
WithGroups.args = {
|
||||
groups: [{ ...MockGroup, name: "front-end" }],
|
||||
canCreateGroup: true,
|
||||
isTemplateRBACEnabled: true,
|
||||
}
|
||||
|
||||
export const EmptyGroup = Template.bind({})
|
||||
EmptyGroup.args = {
|
||||
groups: [],
|
||||
|
|
|
@ -137,11 +137,11 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
|
|||
<AvatarData
|
||||
avatar={
|
||||
<GroupAvatar
|
||||
name={group.name}
|
||||
name={group.display_name || group.name}
|
||||
avatarURL={group.avatar_url}
|
||||
/>
|
||||
}
|
||||
title={group.name}
|
||||
title={group.display_name || group.name}
|
||||
subtitle={`${group.members.length} members`}
|
||||
/>
|
||||
</TableCell>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Stack } from "components/Stack/Stack"
|
|||
|
||||
type FormData = {
|
||||
name: string
|
||||
display_name: string
|
||||
avatar_url: string
|
||||
quota_allowance: number
|
||||
}
|
||||
|
@ -34,6 +35,7 @@ const UpdateGroupForm: FC<{
|
|||
const form = useFormik<FormData>({
|
||||
initialValues: {
|
||||
name: group.name,
|
||||
display_name: group.display_name,
|
||||
avatar_url: group.avatar_url,
|
||||
quota_allowance: group.quota_allowance,
|
||||
},
|
||||
|
@ -55,7 +57,17 @@ const UpdateGroupForm: FC<{
|
|||
fullWidth
|
||||
label="Name"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"display_name",
|
||||
"Optional: keep empty to default to the name.",
|
||||
)}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoComplete="display_name"
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Display Name"
|
||||
/>
|
||||
<LazyIconField
|
||||
{...getFieldHelpers("avatar_url")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
|
@ -63,7 +75,6 @@ const UpdateGroupForm: FC<{
|
|||
label={t("form.fields.icon")}
|
||||
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"quota_allowance",
|
||||
|
|
|
@ -253,11 +253,11 @@ export const TemplatePermissionsPageView: FC<
|
|||
<AvatarData
|
||||
avatar={
|
||||
<GroupAvatar
|
||||
name={group.name}
|
||||
name={group.display_name || group.name}
|
||||
avatarURL={group.avatar_url}
|
||||
/>
|
||||
}
|
||||
title={group.name}
|
||||
title={group.display_name || group.name}
|
||||
subtitle={getGroupSubtitle(group)}
|
||||
/>
|
||||
</TableCell>
|
||||
|
|
|
@ -1665,6 +1665,7 @@ export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
|
|||
export const MockGroup: TypesGen.Group = {
|
||||
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
||||
name: "Front-End",
|
||||
display_name: "Front-End",
|
||||
avatar_url: "https://example.com",
|
||||
organization_id: MockOrganization.id,
|
||||
members: [MockUser, MockUser2],
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Group } from "api/typesGenerated"
|
|||
export const everyOneGroup = (organizationId: string): Group => ({
|
||||
id: organizationId,
|
||||
name: "Everyone",
|
||||
display_name: "",
|
||||
organization_id: organizationId,
|
||||
members: [],
|
||||
avatar_url: "",
|
||||
|
|
|
@ -23,7 +23,12 @@ export const editGroupMachine = createMachine(
|
|||
},
|
||||
events: {} as {
|
||||
type: "UPDATE"
|
||||
data: { name: string; avatar_url: string; quota_allowance: number }
|
||||
data: {
|
||||
display_name: string
|
||||
name: string
|
||||
avatar_url: string
|
||||
quota_allowance: number
|
||||
}
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import("./editGroupXService.typegen").Typegen0,
|
||||
|
|
|
@ -183,6 +183,7 @@ export const groupMachine = createMachine(
|
|||
|
||||
return patchGroup(group.id, {
|
||||
name: "",
|
||||
display_name: "",
|
||||
add_users: [userId],
|
||||
remove_users: [],
|
||||
})
|
||||
|
@ -194,6 +195,7 @@ export const groupMachine = createMachine(
|
|||
|
||||
return patchGroup(group.id, {
|
||||
name: "",
|
||||
display_name: "",
|
||||
add_users: [],
|
||||
remove_users: [userId],
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue