mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add canAssignMultipleUsers field to IncidentRole and update related components for multi-user assignment
This commit is contained in:
@@ -493,4 +493,38 @@ export default class IncidentRole extends BaseModel {
|
||||
default: true,
|
||||
})
|
||||
public isDeleteable?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateIncidentRole,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadIncidentRole,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditIncidentRole,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Can Assign Multiple Users",
|
||||
description:
|
||||
"Can multiple users be assigned to this role? If false, only one user can be assigned.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public canAssignMultipleUsers?: boolean = undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1769802715014 implements MigrationInterface {
|
||||
public name = "MigrationName1769802715014";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentRole" ADD "canAssignMultipleUsers" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentRole" DROP COLUMN "canAssignMultipleUsers"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -247,6 +247,7 @@ import { MigrationName1769723982900 } from "./1769723982900-MigrationName";
|
||||
import { MigrationName1769772215532 } from "./1769772215532-MigrationName";
|
||||
import { MigrationName1769774527481 } from "./1769774527481-MigrationName";
|
||||
import { MigrationName1769780297584 } from "./1769780297584-MigrationName";
|
||||
import { MigrationName1769802715014 } from "./1769802715014-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -498,4 +499,5 @@ export default [
|
||||
MigrationName1769772215532,
|
||||
MigrationName1769774527481,
|
||||
MigrationName1769780297584,
|
||||
MigrationName1769802715014,
|
||||
];
|
||||
|
||||
@@ -1006,6 +1006,7 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
observer.color = Gray500;
|
||||
observer.roleIcon = IconProp.Activity;
|
||||
observer.projectId = createdItem.id!;
|
||||
observer.canAssignMultipleUsers = true;
|
||||
|
||||
observer = await IncidentRoleService.create({
|
||||
data: observer,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface MemberRole {
|
||||
color: Color;
|
||||
isPrimaryRole?: boolean;
|
||||
icon?: IconProp;
|
||||
canAssignMultipleUsers?: boolean;
|
||||
}
|
||||
|
||||
export interface AssignedMember {
|
||||
@@ -108,28 +109,39 @@ const MemberRoleAssignment: FunctionComponent<ComponentProps> = (
|
||||
[props.onUnassignMember],
|
||||
);
|
||||
|
||||
// Get member for a specific role (only one per role)
|
||||
const getMemberForRole: (roleId: ObjectID) => AssignedMember | null =
|
||||
// Get members for a specific role
|
||||
const getMembersForRole: (roleId: ObjectID) => Array<AssignedMember> =
|
||||
useCallback(
|
||||
(roleId: ObjectID): AssignedMember | null => {
|
||||
return (
|
||||
props.assignedMembers.find((member: AssignedMember) => {
|
||||
return member.roleId.toString() === roleId.toString();
|
||||
}) || null
|
||||
);
|
||||
(roleId: ObjectID): Array<AssignedMember> => {
|
||||
return props.assignedMembers.filter((member: AssignedMember) => {
|
||||
return member.roleId.toString() === roleId.toString();
|
||||
});
|
||||
},
|
||||
[props.assignedMembers],
|
||||
);
|
||||
|
||||
const getUserDropdownOptions: () => Array<DropdownOption> =
|
||||
useCallback((): Array<DropdownOption> => {
|
||||
return props.availableUsers.map((user: AvailableUser) => {
|
||||
return {
|
||||
value: user.id.toString(),
|
||||
label: user.name || user.email,
|
||||
};
|
||||
});
|
||||
}, [props.availableUsers]);
|
||||
// Get available users for a role (excluding already assigned users for that role)
|
||||
const getAvailableUsersForRole: (roleId: ObjectID) => Array<DropdownOption> =
|
||||
useCallback(
|
||||
(roleId: ObjectID): Array<DropdownOption> => {
|
||||
const assignedUserIds: Set<string> = new Set(
|
||||
getMembersForRole(roleId).map((m: AssignedMember) => {
|
||||
return m.userId.toString();
|
||||
}),
|
||||
);
|
||||
return props.availableUsers
|
||||
.filter((user: AvailableUser) => {
|
||||
return !assignedUserIds.has(user.id.toString());
|
||||
})
|
||||
.map((user: AvailableUser) => {
|
||||
return {
|
||||
value: user.id.toString(),
|
||||
label: user.name || user.email,
|
||||
};
|
||||
});
|
||||
},
|
||||
[props.availableUsers, getMembersForRole],
|
||||
);
|
||||
|
||||
const cardTitle: string = props.title || "Team Members";
|
||||
const cardDescription: string =
|
||||
@@ -159,6 +171,65 @@ const MemberRoleAssignment: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
// Render a single member row
|
||||
const renderMemberRow: (
|
||||
member: AssignedMember,
|
||||
isLastInGroup?: boolean,
|
||||
) => ReactElement = (
|
||||
member: AssignedMember,
|
||||
isLastInGroup?: boolean,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div
|
||||
key={member.memberId.toString()}
|
||||
className={`flex items-center justify-between py-2 ${!isLastInGroup ? "border-b border-gray-100" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{member.userProfilePictureUrl ? (
|
||||
<Image
|
||||
imageUrl={Route.fromString(member.userProfilePictureUrl)}
|
||||
alt={member.userName}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center">
|
||||
<span className="text-xs font-medium text-white">
|
||||
{member.userName?.charAt(0)?.toUpperCase() ||
|
||||
member.userEmail?.charAt(0)?.toUpperCase() ||
|
||||
"?"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{member.userName || member.userEmail}
|
||||
</p>
|
||||
{member.userName && member.userEmail && (
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{member.userEmail}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowConfirmDelete(member);
|
||||
}}
|
||||
disabled={isUnassigning?.toString() === member.memberId.toString()}
|
||||
className="inline-flex items-center p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors disabled:opacity-50"
|
||||
title="Remove"
|
||||
>
|
||||
{isUnassigning?.toString() === member.memberId.toString() ? (
|
||||
<Icon icon={IconProp.Refresh} className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Icon icon={IconProp.Close} className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
@@ -202,232 +273,180 @@ const MemberRoleAssignment: FunctionComponent<ComponentProps> = (
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden border border-gray-200 rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-1/3"
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider"
|
||||
>
|
||||
Assigned Member
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-right text-xs font-semibold text-gray-600 uppercase tracking-wider w-24"
|
||||
>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{props.roles.map((role: MemberRole) => {
|
||||
const member: AssignedMember | null = getMemberForRole(
|
||||
role.id,
|
||||
);
|
||||
const isDropdownActive: boolean =
|
||||
activeRoleDropdown?.toString() === role.id.toString();
|
||||
const availableUsers: Array<DropdownOption> =
|
||||
getUserDropdownOptions();
|
||||
<div className="space-y-4">
|
||||
{props.roles.map((role: MemberRole) => {
|
||||
const members: Array<AssignedMember> = getMembersForRole(
|
||||
role.id,
|
||||
);
|
||||
const isDropdownActive: boolean =
|
||||
activeRoleDropdown?.toString() === role.id.toString();
|
||||
const availableUsers: Array<DropdownOption> =
|
||||
getAvailableUsersForRole(role.id);
|
||||
const canAddMore: boolean =
|
||||
role.canAssignMultipleUsers || members.length === 0;
|
||||
|
||||
return (
|
||||
<tr key={role.id.toString()} className="hover:bg-gray-50">
|
||||
{/* Role Column */}
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: role.color
|
||||
? `${role.color.toString()}20`
|
||||
: "#f3f4f6",
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={role.icon || IconProp.User}
|
||||
className="w-4 h-4"
|
||||
style={{
|
||||
color: role.color?.toString() || "#6b7280",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{role.name}
|
||||
</span>
|
||||
{role.isPrimaryRole && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
|
||||
Primary
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Member Column */}
|
||||
<td className="px-4 py-4">
|
||||
{isDropdownActive ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Dropdown
|
||||
options={availableUsers}
|
||||
placeholder="Select member..."
|
||||
onChange={(
|
||||
value:
|
||||
| DropdownValue
|
||||
| Array<DropdownValue>
|
||||
| null,
|
||||
) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const option:
|
||||
| DropdownOption
|
||||
| undefined = availableUsers.find(
|
||||
(opt: DropdownOption) => {
|
||||
return (
|
||||
opt.value.toString() ===
|
||||
value.toString()
|
||||
);
|
||||
},
|
||||
);
|
||||
setSelectedUserOption(option || null);
|
||||
} else {
|
||||
setSelectedUserOption(null);
|
||||
}
|
||||
}}
|
||||
value={selectedUserOption || undefined}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (selectedUserOption) {
|
||||
handleAssign(
|
||||
new ObjectID(
|
||||
selectedUserOption.value.toString(),
|
||||
),
|
||||
role.id,
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedUserOption || isAssigning}
|
||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 border border-transparent rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isAssigning ? (
|
||||
<Icon
|
||||
icon={IconProp.Refresh}
|
||||
className="w-3.5 h-3.5 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Save"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveRoleDropdown(null);
|
||||
setSelectedUserOption(null);
|
||||
}}
|
||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : member ? (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Avatar */}
|
||||
{member.userProfilePictureUrl ? (
|
||||
<Image
|
||||
imageUrl={Route.fromString(
|
||||
member.userProfilePictureUrl,
|
||||
)}
|
||||
alt={member.userName}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center">
|
||||
<span className="text-xs font-medium text-white">
|
||||
{member.userName?.charAt(0)?.toUpperCase() ||
|
||||
member.userEmail
|
||||
?.charAt(0)
|
||||
?.toUpperCase() ||
|
||||
"?"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* User Info */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{member.userName || member.userEmail}
|
||||
</p>
|
||||
{member.userName && member.userEmail && (
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{member.userEmail}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 italic">
|
||||
Not assigned
|
||||
return (
|
||||
<div
|
||||
key={role.id.toString()}
|
||||
className="border border-gray-200 rounded-lg overflow-hidden"
|
||||
>
|
||||
{/* Role Header */}
|
||||
<div
|
||||
className="px-4 py-3 flex items-center justify-between"
|
||||
style={{
|
||||
backgroundColor: role.color
|
||||
? `${role.color.toString()}10`
|
||||
: "#f9fafb",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: role.color
|
||||
? `${role.color.toString()}25`
|
||||
: "#e5e7eb",
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={role.icon || IconProp.User}
|
||||
className="w-4.5 h-4.5"
|
||||
style={{
|
||||
color: role.color?.toString() || "#6b7280",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{role.name}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{role.isPrimaryRole && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
|
||||
Primary
|
||||
</span>
|
||||
)}
|
||||
{role.canAssignMultipleUsers && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700">
|
||||
Multiple
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{members.length}{" "}
|
||||
{members.length === 1 ? "member" : "members"}{" "}
|
||||
assigned
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Column */}
|
||||
<td className="px-4 py-4 text-right">
|
||||
{!isDropdownActive && (
|
||||
<>
|
||||
{member ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowConfirmDelete(member);
|
||||
}}
|
||||
disabled={
|
||||
isUnassigning?.toString() ===
|
||||
member.memberId.toString()
|
||||
}
|
||||
className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-red-600 bg-red-50 border border-red-200 rounded-md hover:bg-red-100 hover:border-red-300 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isUnassigning?.toString() ===
|
||||
member.memberId.toString() ? (
|
||||
<Icon
|
||||
icon={IconProp.Refresh}
|
||||
className="w-3.5 h-3.5 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Remove"
|
||||
)}
|
||||
</button>
|
||||
) : availableUsers.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveRoleDropdown(role.id);
|
||||
}}
|
||||
className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-indigo-600 bg-indigo-50 border border-indigo-200 rounded-md hover:bg-indigo-100 hover:border-indigo-300 transition-colors"
|
||||
>
|
||||
Assign
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">
|
||||
No users
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Add Button */}
|
||||
{!isDropdownActive &&
|
||||
canAddMore &&
|
||||
availableUsers.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveRoleDropdown(role.id);
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 hover:border-gray-400 transition-all shadow-sm"
|
||||
>
|
||||
<Icon icon={IconProp.Add} className="w-3.5 h-3.5" />
|
||||
{members.length === 0 ? "Assign" : "Add"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown for adding member */}
|
||||
{isDropdownActive && (
|
||||
<div className="px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 max-w-sm">
|
||||
<Dropdown
|
||||
options={availableUsers}
|
||||
placeholder="Select member..."
|
||||
onChange={(
|
||||
value:
|
||||
| DropdownValue
|
||||
| Array<DropdownValue>
|
||||
| null,
|
||||
) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const option: DropdownOption | undefined =
|
||||
availableUsers.find(
|
||||
(opt: DropdownOption) => {
|
||||
return (
|
||||
opt.value.toString() ===
|
||||
value.toString()
|
||||
);
|
||||
},
|
||||
);
|
||||
setSelectedUserOption(option || null);
|
||||
} else {
|
||||
setSelectedUserOption(null);
|
||||
}
|
||||
}}
|
||||
value={selectedUserOption || undefined}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (selectedUserOption) {
|
||||
handleAssign(
|
||||
new ObjectID(
|
||||
selectedUserOption.value.toString(),
|
||||
),
|
||||
role.id,
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedUserOption || isAssigning}
|
||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 border border-transparent rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isAssigning ? (
|
||||
<Icon
|
||||
icon={IconProp.Refresh}
|
||||
className="w-3.5 h-3.5 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Save"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveRoleDropdown(null);
|
||||
setSelectedUserOption(null);
|
||||
}}
|
||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Members List */}
|
||||
<div className="px-4 py-2 bg-white">
|
||||
{members.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 italic py-2">
|
||||
Not assigned
|
||||
</p>
|
||||
) : (
|
||||
members.map(
|
||||
(member: AssignedMember, index: number) => {
|
||||
return renderMemberRow(
|
||||
member,
|
||||
index === members.length - 1,
|
||||
);
|
||||
},
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,7 @@ const IncidentMemberRoleAssignment: FunctionComponent<ComponentProps> = (
|
||||
color: true,
|
||||
isPrimaryRole: true,
|
||||
roleIcon: true,
|
||||
canAssignMultipleUsers: true,
|
||||
},
|
||||
sort: {
|
||||
isPrimaryRole: SortOrder.Descending,
|
||||
@@ -118,6 +119,7 @@ const IncidentMemberRoleAssignment: FunctionComponent<ComponentProps> = (
|
||||
color: role.color || Color.fromString("#6b7280"),
|
||||
isPrimaryRole: role.isPrimaryRole || false,
|
||||
icon: role.roleIcon || undefined,
|
||||
canAssignMultipleUsers: role.canAssignMultipleUsers || false,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import LowercaseDomains from "./LowercaseDomains";
|
||||
import AddAttributeKeysColumnToTelemetryTables from "./AddAttributeKeysColumnToTelemetryTables";
|
||||
import AddDefaultIncidentRolesToExistingProjects from "./AddDefaultIncidentRolesToExistingProjects";
|
||||
import AddDefaultIconsToIncidentRoles from "./AddDefaultIconsToIncidentRoles";
|
||||
import UpdateObserverRoleToAllowMultipleUsers from "./UpdateObserverRoleToAllowMultipleUsers";
|
||||
|
||||
// This is the order in which the migrations will be run. Add new migrations to the end of the array.
|
||||
|
||||
@@ -114,6 +115,7 @@ const DataMigrations: Array<DataMigrationBase> = [
|
||||
new AddAttributeKeysColumnToTelemetryTables(),
|
||||
new AddDefaultIncidentRolesToExistingProjects(),
|
||||
new AddDefaultIconsToIncidentRoles(),
|
||||
new UpdateObserverRoleToAllowMultipleUsers(),
|
||||
];
|
||||
|
||||
export default DataMigrations;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import DataMigrationBase from "./DataMigrationBase";
|
||||
import { LIMIT_INFINITY } from "Common/Types/Database/LimitMax";
|
||||
import IncidentRoleService from "Common/Server/Services/IncidentRoleService";
|
||||
import IncidentRole from "Common/Models/DatabaseModels/IncidentRole";
|
||||
|
||||
export default class UpdateObserverRoleToAllowMultipleUsers extends DataMigrationBase {
|
||||
public constructor() {
|
||||
super("UpdateObserverRoleToAllowMultipleUsers");
|
||||
}
|
||||
|
||||
public override async migrate(): Promise<void> {
|
||||
// Get all Observer roles across all projects
|
||||
const observerRoles: Array<IncidentRole> = await IncidentRoleService.findBy(
|
||||
{
|
||||
query: {
|
||||
name: "Observer",
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
canAssignMultipleUsers: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_INFINITY,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
for (const role of observerRoles) {
|
||||
// Update the role to allow multiple users
|
||||
if (!role.canAssignMultipleUsers) {
|
||||
await IncidentRoleService.updateOneById({
|
||||
id: role.id!,
|
||||
data: {
|
||||
canAssignMultipleUsers: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async rollback(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user