@oneuptime/common 10.4.14 → 10.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +49 -0
- package/Models/AnalyticsModels/AuditLog.ts +8 -0
- package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
- package/Models/AnalyticsModels/Log.ts +1 -0
- package/Models/AnalyticsModels/Metric.ts +10 -0
- package/Models/AnalyticsModels/MonitorLog.ts +1 -0
- package/Models/AnalyticsModels/Profile.ts +1 -0
- package/Models/AnalyticsModels/ProfileSample.ts +1 -0
- package/Models/AnalyticsModels/Span.ts +1 -0
- package/Models/DatabaseModels/AlertCustomField.ts +37 -0
- package/Models/DatabaseModels/IncidentCustomField.ts +37 -0
- package/Models/DatabaseModels/IncidentMember.ts +9 -0
- package/Models/DatabaseModels/MonitorCustomField.ts +37 -0
- package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +37 -0
- package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +37 -0
- package/Models/DatabaseModels/StatusPageCustomField.ts +37 -0
- package/Models/DatabaseModels/TableView.ts +40 -0
- package/Models/DatabaseModels/TeamMemberCustomField.ts +37 -0
- package/Server/API/BaseAnalyticsAPI.ts +128 -20
- package/Server/API/MetricAPI.ts +5 -138
- package/Server/API/StatusAPI.ts +103 -7
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.ts +13 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.ts +34 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.ts +67 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/AccessTokenService.ts +1 -1
- package/Server/Services/AnalyticsDatabaseService.ts +24 -4
- package/Server/Services/MetricService.ts +113 -0
- package/Server/Services/ProjectService.ts +21 -1
- package/Server/Utils/Response.ts +4 -1
- package/Server/Utils/UserPermission/UserPermission.ts +17 -1
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +2 -2
- package/Types/API/HTTPResponse.ts +16 -0
- package/Types/BaseDatabase/ListResult.ts +6 -0
- package/Types/CustomField/CustomFieldType.ts +2 -0
- package/Types/Date.ts +9 -1
- package/Types/ListData.ts +14 -0
- package/Types/Monitor/DnsMonitor/DnsMonitorResponse.ts +3 -0
- package/Types/Monitor/DnssecMonitor/DnssecMonitorResponse.ts +5 -0
- package/Types/Monitor/DomainMonitor/DomainMonitorResponse.ts +4 -0
- package/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse.ts +4 -0
- package/Types/Monitor/SnmpMonitor/SnmpMonitorResponse.ts +3 -0
- package/Types/Probe/ProbeAttempt.ts +9 -0
- package/Types/Probe/ProbeMonitorResponse.ts +3 -0
- package/UI/Components/BulkUpdate/BulkOwnerActions.tsx +504 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +64 -54
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +38 -0
- package/UI/Components/CustomFields/DropdownOptionsInput.tsx +150 -0
- package/UI/Components/Detail/Detail.tsx +78 -11
- package/UI/Components/List/List.tsx +6 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +74 -2
- package/UI/Components/ModelTable/TableView.tsx +70 -30
- package/UI/Components/Pagination/Pagination.tsx +75 -33
- package/UI/Components/Table/Table.tsx +6 -0
- package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +1 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +33 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/AuditLog.js +8 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +10 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -0
- package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js +11 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TableView.js +40 -0
- package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +38 -0
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
- package/build/dist/Server/API/BaseAnalyticsAPI.js +105 -18
- package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
- package/build/dist/Server/API/MetricAPI.js +5 -113
- package/build/dist/Server/API/MetricAPI.js.map +1 -1
- package/build/dist/Server/API/StatusAPI.js +75 -8
- package/build/dist/Server/API/StatusAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.js +27 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AccessTokenService.js +1 -1
- package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +22 -3
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +89 -0
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +19 -1
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +6 -5
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/UserPermission/UserPermission.js +13 -1
- package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
- package/build/dist/Tests/Server/Services/AnalyticsDatabaseService.test.js +2 -2
- package/build/dist/Tests/Server/Services/AnalyticsDatabaseService.test.js.map +1 -1
- package/build/dist/Types/API/HTTPResponse.js +15 -0
- package/build/dist/Types/API/HTTPResponse.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldType.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Date.js +10 -1
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/ListData.js +4 -0
- package/build/dist/Types/ListData.js.map +1 -1
- package/build/dist/Types/Probe/ProbeAttempt.js +2 -0
- package/build/dist/Types/Probe/ProbeAttempt.js.map +1 -0
- package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js +376 -0
- package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js.map +1 -0
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +32 -25
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +32 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js +84 -0
- package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +34 -3
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +1 -1
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +45 -5
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js +40 -19
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +62 -36
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +1 -1
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +1 -0
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import React, { ReactElement, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
4
|
+
import Team from "../../../Models/DatabaseModels/Team";
|
|
5
|
+
import TeamMember from "../../../Models/DatabaseModels/TeamMember";
|
|
6
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
7
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
8
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
9
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
10
|
+
import ListResult from "../../../Types/BaseDatabase/ListResult";
|
|
11
|
+
import Query from "../../../Types/BaseDatabase/Query";
|
|
12
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
13
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
14
|
+
import API from "../../Utils/API/API";
|
|
15
|
+
import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
|
|
16
|
+
import ProjectUtil from "../../Utils/Project";
|
|
17
|
+
import { ButtonStyleType } from "../Button/Button";
|
|
18
|
+
import { DropdownOption, DropdownOptionGroup } from "../Dropdown/Dropdown";
|
|
19
|
+
import BasicFormModal from "../FormModal/BasicFormModal";
|
|
20
|
+
import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
|
|
21
|
+
import {
|
|
22
|
+
BulkActionButtonSchema,
|
|
23
|
+
BulkActionFailed,
|
|
24
|
+
BulkActionOnClickProps,
|
|
25
|
+
} from "./BulkUpdateForm";
|
|
26
|
+
|
|
27
|
+
type OwnerJunctionModel = BaseModel;
|
|
28
|
+
|
|
29
|
+
export interface BulkOwnerActionsConfig {
|
|
30
|
+
ownerUserModelType: { new (): OwnerJunctionModel };
|
|
31
|
+
ownerTeamModelType: { new (): OwnerJunctionModel };
|
|
32
|
+
resourceIdField: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BulkOwnerActionsResult<T extends BaseModel> {
|
|
36
|
+
bulkActions: Array<BulkActionButtonSchema<T>>;
|
|
37
|
+
modals: ReactElement;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type BulkOwnerMode = "add" | "remove";
|
|
41
|
+
|
|
42
|
+
const USER_PREFIX: string = "user:";
|
|
43
|
+
const TEAM_PREFIX: string = "team:";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reusable hook that provides "Add Owner" and "Remove Owner" bulk actions
|
|
47
|
+
* for any ModelTable whose model has companion `<Resource>OwnerUser` and
|
|
48
|
+
* `<Resource>OwnerTeam` junction tables (e.g., `ServiceOwnerUser` +
|
|
49
|
+
* `ServiceOwnerTeam` with `serviceId` as the foreign key).
|
|
50
|
+
*
|
|
51
|
+
* Usage:
|
|
52
|
+
* const { bulkActions, modals } = useBulkOwnerActions<Service>({
|
|
53
|
+
* ownerUserModelType: ServiceOwnerUser,
|
|
54
|
+
* ownerTeamModelType: ServiceOwnerTeam,
|
|
55
|
+
* resourceIdField: "serviceId",
|
|
56
|
+
* });
|
|
57
|
+
* <ModelTable bulkActions={{ buttons: [...bulkActions, ...] }} />
|
|
58
|
+
* {modals}
|
|
59
|
+
*/
|
|
60
|
+
function useBulkOwnerActions<T extends BaseModel>(
|
|
61
|
+
config: BulkOwnerActionsConfig,
|
|
62
|
+
): BulkOwnerActionsResult<T> {
|
|
63
|
+
const [userOptions, setUserOptions] = useState<Array<DropdownOption>>([]);
|
|
64
|
+
const [teamOptions, setTeamOptions] = useState<Array<DropdownOption>>([]);
|
|
65
|
+
const [showAddModal, setShowAddModal] = useState<boolean>(false);
|
|
66
|
+
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false);
|
|
67
|
+
const [bulkActionProps, setBulkActionProps] =
|
|
68
|
+
useState<BulkActionOnClickProps<T> | null>(null);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
|
|
72
|
+
if (!projectId) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fetchOwners: () => Promise<void> = async (): Promise<void> => {
|
|
77
|
+
try {
|
|
78
|
+
const [teamMembersResult, teamsResult]: [
|
|
79
|
+
ListResult<TeamMember>,
|
|
80
|
+
ListResult<Team>,
|
|
81
|
+
] = await Promise.all([
|
|
82
|
+
ModelAPI.getList<TeamMember>({
|
|
83
|
+
modelType: TeamMember,
|
|
84
|
+
query: { projectId: projectId },
|
|
85
|
+
limit: LIMIT_PER_PROJECT,
|
|
86
|
+
skip: 0,
|
|
87
|
+
select: {
|
|
88
|
+
_id: true,
|
|
89
|
+
user: {
|
|
90
|
+
_id: true,
|
|
91
|
+
name: true,
|
|
92
|
+
email: true,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
sort: {},
|
|
96
|
+
}),
|
|
97
|
+
ModelAPI.getList<Team>({
|
|
98
|
+
modelType: Team,
|
|
99
|
+
query: { projectId: projectId },
|
|
100
|
+
limit: LIMIT_PER_PROJECT,
|
|
101
|
+
skip: 0,
|
|
102
|
+
select: {
|
|
103
|
+
_id: true,
|
|
104
|
+
name: true,
|
|
105
|
+
},
|
|
106
|
+
sort: {
|
|
107
|
+
name: SortOrder.Ascending,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const seenUserIds: Set<string> = new Set<string>();
|
|
113
|
+
const users: Array<DropdownOption> = [];
|
|
114
|
+
|
|
115
|
+
for (const member of teamMembersResult.data) {
|
|
116
|
+
const userId: string = member.user?._id?.toString() || "";
|
|
117
|
+
if (!userId || seenUserIds.has(userId)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
seenUserIds.add(userId);
|
|
121
|
+
users.push({
|
|
122
|
+
value: `${USER_PREFIX}${userId}`,
|
|
123
|
+
label:
|
|
124
|
+
member.user?.name?.toString() ||
|
|
125
|
+
member.user?.email?.toString() ||
|
|
126
|
+
"",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
users.sort((a: DropdownOption, b: DropdownOption) => {
|
|
131
|
+
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
setUserOptions(users);
|
|
135
|
+
|
|
136
|
+
setTeamOptions(
|
|
137
|
+
teamsResult.data.map((team: Team) => {
|
|
138
|
+
return {
|
|
139
|
+
value: `${TEAM_PREFIX}${team._id?.toString() || ""}`,
|
|
140
|
+
label: team.name?.toString() || "",
|
|
141
|
+
};
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
} catch {
|
|
145
|
+
// dropdowns will remain empty; modal will show no options
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
void fetchOwners();
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
const applyOwners: (
|
|
153
|
+
selectedKeys: Array<string>,
|
|
154
|
+
mode: BulkOwnerMode,
|
|
155
|
+
) => Promise<void> = async (
|
|
156
|
+
selectedKeys: Array<string>,
|
|
157
|
+
mode: BulkOwnerMode,
|
|
158
|
+
): Promise<void> => {
|
|
159
|
+
if (!bulkActionProps) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { items, onProgressInfo, onBulkActionStart, onBulkActionEnd } =
|
|
164
|
+
bulkActionProps;
|
|
165
|
+
|
|
166
|
+
// Close the form modal first so the progress modal is visible
|
|
167
|
+
setShowAddModal(false);
|
|
168
|
+
setShowRemoveModal(false);
|
|
169
|
+
|
|
170
|
+
const userIds: Array<string> = [];
|
|
171
|
+
const teamIds: Array<string> = [];
|
|
172
|
+
|
|
173
|
+
for (const key of selectedKeys) {
|
|
174
|
+
if (key.startsWith(USER_PREFIX)) {
|
|
175
|
+
const id: string = key.slice(USER_PREFIX.length);
|
|
176
|
+
if (id) {
|
|
177
|
+
userIds.push(id);
|
|
178
|
+
}
|
|
179
|
+
} else if (key.startsWith(TEAM_PREFIX)) {
|
|
180
|
+
const id: string = key.slice(TEAM_PREFIX.length);
|
|
181
|
+
if (id) {
|
|
182
|
+
teamIds.push(id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
|
|
188
|
+
|
|
189
|
+
onBulkActionStart();
|
|
190
|
+
|
|
191
|
+
const totalItems: Array<T> = [...items];
|
|
192
|
+
const inProgressItems: Array<T> = [...items];
|
|
193
|
+
const successItems: Array<T> = [];
|
|
194
|
+
const failedItems: Array<BulkActionFailed<T>> = [];
|
|
195
|
+
|
|
196
|
+
for (const item of totalItems) {
|
|
197
|
+
inProgressItems.splice(inProgressItems.indexOf(item), 1);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
if (!item.id) {
|
|
201
|
+
throw new BadDataException("Item ID not found");
|
|
202
|
+
}
|
|
203
|
+
if (!projectId) {
|
|
204
|
+
throw new BadDataException("Project not found");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (mode === "add") {
|
|
208
|
+
const fetchUsers: Promise<Array<OwnerJunctionModel>> =
|
|
209
|
+
userIds.length > 0
|
|
210
|
+
? ModelAPI.getList<OwnerJunctionModel>({
|
|
211
|
+
modelType: config.ownerUserModelType,
|
|
212
|
+
query: {
|
|
213
|
+
[config.resourceIdField]: item.id,
|
|
214
|
+
projectId: projectId,
|
|
215
|
+
} as Query<OwnerJunctionModel>,
|
|
216
|
+
limit: LIMIT_PER_PROJECT,
|
|
217
|
+
skip: 0,
|
|
218
|
+
select: { userId: true } as Record<string, unknown>,
|
|
219
|
+
sort: {},
|
|
220
|
+
}).then((r: ListResult<OwnerJunctionModel>) => {
|
|
221
|
+
return r.data;
|
|
222
|
+
})
|
|
223
|
+
: Promise.resolve([] as Array<OwnerJunctionModel>);
|
|
224
|
+
|
|
225
|
+
const fetchTeams: Promise<Array<OwnerJunctionModel>> =
|
|
226
|
+
teamIds.length > 0
|
|
227
|
+
? ModelAPI.getList<OwnerJunctionModel>({
|
|
228
|
+
modelType: config.ownerTeamModelType,
|
|
229
|
+
query: {
|
|
230
|
+
[config.resourceIdField]: item.id,
|
|
231
|
+
projectId: projectId,
|
|
232
|
+
} as Query<OwnerJunctionModel>,
|
|
233
|
+
limit: LIMIT_PER_PROJECT,
|
|
234
|
+
skip: 0,
|
|
235
|
+
select: { teamId: true } as Record<string, unknown>,
|
|
236
|
+
sort: {},
|
|
237
|
+
}).then((r: ListResult<OwnerJunctionModel>) => {
|
|
238
|
+
return r.data;
|
|
239
|
+
})
|
|
240
|
+
: Promise.resolve([] as Array<OwnerJunctionModel>);
|
|
241
|
+
|
|
242
|
+
const [existingUsers, existingTeams]: [
|
|
243
|
+
Array<OwnerJunctionModel>,
|
|
244
|
+
Array<OwnerJunctionModel>,
|
|
245
|
+
] = await Promise.all([fetchUsers, fetchTeams]);
|
|
246
|
+
|
|
247
|
+
const existingUserIds: Set<string> = new Set<string>(
|
|
248
|
+
existingUsers
|
|
249
|
+
.map((row: OwnerJunctionModel) => {
|
|
250
|
+
return (
|
|
251
|
+
(
|
|
252
|
+
row as unknown as { userId?: ObjectID }
|
|
253
|
+
).userId?.toString() || ""
|
|
254
|
+
);
|
|
255
|
+
})
|
|
256
|
+
.filter((id: string) => {
|
|
257
|
+
return id.length > 0;
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const existingTeamIds: Set<string> = new Set<string>(
|
|
262
|
+
existingTeams
|
|
263
|
+
.map((row: OwnerJunctionModel) => {
|
|
264
|
+
return (
|
|
265
|
+
(
|
|
266
|
+
row as unknown as { teamId?: ObjectID }
|
|
267
|
+
).teamId?.toString() || ""
|
|
268
|
+
);
|
|
269
|
+
})
|
|
270
|
+
.filter((id: string) => {
|
|
271
|
+
return id.length > 0;
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
for (const userId of userIds) {
|
|
276
|
+
if (existingUserIds.has(userId)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const ownerModel: OwnerJunctionModel =
|
|
280
|
+
new config.ownerUserModelType();
|
|
281
|
+
(ownerModel as unknown as Record<string, unknown>)["userId"] =
|
|
282
|
+
new ObjectID(userId);
|
|
283
|
+
(ownerModel as unknown as Record<string, unknown>)[
|
|
284
|
+
config.resourceIdField
|
|
285
|
+
] = item.id;
|
|
286
|
+
(ownerModel as unknown as Record<string, unknown>)["projectId"] =
|
|
287
|
+
projectId;
|
|
288
|
+
await ModelAPI.create<OwnerJunctionModel>({
|
|
289
|
+
model: ownerModel,
|
|
290
|
+
modelType: config.ownerUserModelType,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const teamId of teamIds) {
|
|
295
|
+
if (existingTeamIds.has(teamId)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const ownerModel: OwnerJunctionModel =
|
|
299
|
+
new config.ownerTeamModelType();
|
|
300
|
+
(ownerModel as unknown as Record<string, unknown>)["teamId"] =
|
|
301
|
+
new ObjectID(teamId);
|
|
302
|
+
(ownerModel as unknown as Record<string, unknown>)[
|
|
303
|
+
config.resourceIdField
|
|
304
|
+
] = item.id;
|
|
305
|
+
(ownerModel as unknown as Record<string, unknown>)["projectId"] =
|
|
306
|
+
projectId;
|
|
307
|
+
await ModelAPI.create<OwnerJunctionModel>({
|
|
308
|
+
model: ownerModel,
|
|
309
|
+
modelType: config.ownerTeamModelType,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
const fetchMatchingUsers: Promise<Array<OwnerJunctionModel>> =
|
|
314
|
+
userIds.length > 0
|
|
315
|
+
? ModelAPI.getList<OwnerJunctionModel>({
|
|
316
|
+
modelType: config.ownerUserModelType,
|
|
317
|
+
query: {
|
|
318
|
+
[config.resourceIdField]: item.id,
|
|
319
|
+
projectId: projectId,
|
|
320
|
+
userId: new Includes(
|
|
321
|
+
userIds.map((id: string) => {
|
|
322
|
+
return new ObjectID(id);
|
|
323
|
+
}),
|
|
324
|
+
),
|
|
325
|
+
} as Query<OwnerJunctionModel>,
|
|
326
|
+
limit: LIMIT_PER_PROJECT,
|
|
327
|
+
skip: 0,
|
|
328
|
+
select: { _id: true } as Record<string, unknown>,
|
|
329
|
+
sort: {},
|
|
330
|
+
}).then((r: ListResult<OwnerJunctionModel>) => {
|
|
331
|
+
return r.data;
|
|
332
|
+
})
|
|
333
|
+
: Promise.resolve([] as Array<OwnerJunctionModel>);
|
|
334
|
+
|
|
335
|
+
const fetchMatchingTeams: Promise<Array<OwnerJunctionModel>> =
|
|
336
|
+
teamIds.length > 0
|
|
337
|
+
? ModelAPI.getList<OwnerJunctionModel>({
|
|
338
|
+
modelType: config.ownerTeamModelType,
|
|
339
|
+
query: {
|
|
340
|
+
[config.resourceIdField]: item.id,
|
|
341
|
+
projectId: projectId,
|
|
342
|
+
teamId: new Includes(
|
|
343
|
+
teamIds.map((id: string) => {
|
|
344
|
+
return new ObjectID(id);
|
|
345
|
+
}),
|
|
346
|
+
),
|
|
347
|
+
} as Query<OwnerJunctionModel>,
|
|
348
|
+
limit: LIMIT_PER_PROJECT,
|
|
349
|
+
skip: 0,
|
|
350
|
+
select: { _id: true } as Record<string, unknown>,
|
|
351
|
+
sort: {},
|
|
352
|
+
}).then((r: ListResult<OwnerJunctionModel>) => {
|
|
353
|
+
return r.data;
|
|
354
|
+
})
|
|
355
|
+
: Promise.resolve([] as Array<OwnerJunctionModel>);
|
|
356
|
+
|
|
357
|
+
const [matchingUsers, matchingTeams]: [
|
|
358
|
+
Array<OwnerJunctionModel>,
|
|
359
|
+
Array<OwnerJunctionModel>,
|
|
360
|
+
] = await Promise.all([fetchMatchingUsers, fetchMatchingTeams]);
|
|
361
|
+
|
|
362
|
+
for (const row of matchingUsers) {
|
|
363
|
+
if (!row.id) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
await ModelAPI.deleteItem<OwnerJunctionModel>({
|
|
367
|
+
modelType: config.ownerUserModelType,
|
|
368
|
+
id: row.id,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
for (const row of matchingTeams) {
|
|
372
|
+
if (!row.id) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
await ModelAPI.deleteItem<OwnerJunctionModel>({
|
|
376
|
+
modelType: config.ownerTeamModelType,
|
|
377
|
+
id: row.id,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
successItems.push(item);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
failedItems.push({
|
|
385
|
+
item: item,
|
|
386
|
+
failedMessage: API.getFriendlyMessage(err),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
onProgressInfo({
|
|
391
|
+
totalItems: totalItems,
|
|
392
|
+
failed: failedItems,
|
|
393
|
+
successItems: successItems,
|
|
394
|
+
inProgressItems: inProgressItems,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
onBulkActionEnd();
|
|
399
|
+
setBulkActionProps(null);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const groupedOwnerOptions: Array<DropdownOption | DropdownOptionGroup> =
|
|
403
|
+
useMemo((): Array<DropdownOption | DropdownOptionGroup> => {
|
|
404
|
+
const groups: Array<DropdownOptionGroup> = [];
|
|
405
|
+
if (userOptions.length > 0) {
|
|
406
|
+
groups.push({ label: "People", options: userOptions });
|
|
407
|
+
}
|
|
408
|
+
if (teamOptions.length > 0) {
|
|
409
|
+
groups.push({ label: "Teams", options: teamOptions });
|
|
410
|
+
}
|
|
411
|
+
return groups;
|
|
412
|
+
}, [userOptions, teamOptions]);
|
|
413
|
+
|
|
414
|
+
const addOwnerAction: BulkActionButtonSchema<T> = {
|
|
415
|
+
title: "Add Owner",
|
|
416
|
+
buttonStyleType: ButtonStyleType.NORMAL,
|
|
417
|
+
icon: IconProp.UserPlus,
|
|
418
|
+
onClick: async (actionProps: BulkActionOnClickProps<T>): Promise<void> => {
|
|
419
|
+
setBulkActionProps(actionProps);
|
|
420
|
+
setShowAddModal(true);
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const removeOwnerAction: BulkActionButtonSchema<T> = {
|
|
425
|
+
title: "Remove Owner",
|
|
426
|
+
buttonStyleType: ButtonStyleType.NORMAL,
|
|
427
|
+
icon: IconProp.UserMinus,
|
|
428
|
+
onClick: async (actionProps: BulkActionOnClickProps<T>): Promise<void> => {
|
|
429
|
+
setBulkActionProps(actionProps);
|
|
430
|
+
setShowRemoveModal(true);
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const modals: ReactElement = (
|
|
435
|
+
<>
|
|
436
|
+
{showAddModal && (
|
|
437
|
+
<BasicFormModal
|
|
438
|
+
title="Add Owner"
|
|
439
|
+
description="Select users and/or teams to add as owners to the selected items. Existing owners will be preserved."
|
|
440
|
+
onClose={() => {
|
|
441
|
+
setShowAddModal(false);
|
|
442
|
+
setBulkActionProps(null);
|
|
443
|
+
}}
|
|
444
|
+
submitButtonText="Add Owner"
|
|
445
|
+
onSubmit={async (formData: { ownerKeys: Array<string> }) => {
|
|
446
|
+
await applyOwners(formData.ownerKeys || [], "add");
|
|
447
|
+
}}
|
|
448
|
+
formProps={{
|
|
449
|
+
fields: [
|
|
450
|
+
{
|
|
451
|
+
field: {
|
|
452
|
+
ownerKeys: true,
|
|
453
|
+
},
|
|
454
|
+
title: "Select Owners",
|
|
455
|
+
description:
|
|
456
|
+
"These users and teams will be added as owners to each selected item.",
|
|
457
|
+
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
|
458
|
+
required: true,
|
|
459
|
+
dropdownOptions: groupedOwnerOptions,
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
}}
|
|
463
|
+
/>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{showRemoveModal && (
|
|
467
|
+
<BasicFormModal
|
|
468
|
+
title="Remove Owner"
|
|
469
|
+
description="Select users and/or teams to remove from the selected items. Items that do not have any of these owners will be skipped."
|
|
470
|
+
onClose={() => {
|
|
471
|
+
setShowRemoveModal(false);
|
|
472
|
+
setBulkActionProps(null);
|
|
473
|
+
}}
|
|
474
|
+
submitButtonText="Remove Owner"
|
|
475
|
+
onSubmit={async (formData: { ownerKeys: Array<string> }) => {
|
|
476
|
+
await applyOwners(formData.ownerKeys || [], "remove");
|
|
477
|
+
}}
|
|
478
|
+
formProps={{
|
|
479
|
+
fields: [
|
|
480
|
+
{
|
|
481
|
+
field: {
|
|
482
|
+
ownerKeys: true,
|
|
483
|
+
},
|
|
484
|
+
title: "Select Owners",
|
|
485
|
+
description:
|
|
486
|
+
"These users and teams will be removed as owners from each selected item.",
|
|
487
|
+
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
|
488
|
+
required: true,
|
|
489
|
+
dropdownOptions: groupedOwnerOptions,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
)}
|
|
495
|
+
</>
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
bulkActions: [addOwnerAction, removeOwnerAction],
|
|
500
|
+
modals: modals,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export default useBulkOwnerActions;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GetReactElementFunction } from "../../Types/FunctionTypes";
|
|
2
|
-
import
|
|
3
|
-
import Icon
|
|
2
|
+
import { ButtonStyleType } from "../Button/Button";
|
|
3
|
+
import Icon from "../Icon/Icon";
|
|
4
4
|
import ConfirmModal, {
|
|
5
5
|
ComponentProps as ConfirmModalProps,
|
|
6
6
|
} from "../Modal/ConfirmModal";
|
|
@@ -317,76 +317,86 @@ const BulkUpdateForm: <T extends GenericObject>(
|
|
|
317
317
|
menuChildren.push(renderMenuItem(button, safeButtons.length + index));
|
|
318
318
|
});
|
|
319
319
|
|
|
320
|
+
const showLimitWarning: boolean =
|
|
321
|
+
props.isAllItemsSelected &&
|
|
322
|
+
props.selectedItems.length === LIMIT_PER_PROJECT;
|
|
323
|
+
|
|
320
324
|
return (
|
|
321
325
|
<div>
|
|
322
326
|
<div>
|
|
323
|
-
<div className="
|
|
324
|
-
<div className="-
|
|
325
|
-
<div className="flex
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
{props.pluralLabel} at a time. This is for performance
|
|
336
|
-
reasons.)
|
|
337
|
-
</span>
|
|
338
|
-
)}
|
|
327
|
+
<div className="mt-5 mb-5 bg-gray-50 rounded-xl p-4 border-2 border-gray-100">
|
|
328
|
+
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
329
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
330
|
+
{/** Selected Count Badge */}
|
|
331
|
+
<div className="inline-flex items-center gap-1.5 rounded-lg bg-indigo-50 px-3 py-1.5 text-sm font-semibold text-indigo-700 border border-indigo-100">
|
|
332
|
+
<Icon
|
|
333
|
+
icon={IconProp.CheckCircle}
|
|
334
|
+
className="h-4 w-4 text-indigo-600"
|
|
335
|
+
/>
|
|
336
|
+
<span>
|
|
337
|
+
{props.selectedItems.length} {props.pluralLabel} Selected
|
|
338
|
+
</span>
|
|
339
339
|
</div>
|
|
340
|
-
|
|
341
|
-
|
|
340
|
+
|
|
341
|
+
{/** Divider */}
|
|
342
|
+
<div className="h-6 w-px bg-gray-300 mx-1" />
|
|
343
|
+
|
|
342
344
|
{/** Select All Button */}
|
|
343
345
|
{!props.isAllItemsSelected && (
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
icon={IconProp.CheckCircle}
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
347
348
|
onClick={() => {
|
|
348
349
|
props.onSelectAllClick();
|
|
349
350
|
}}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-indigo-50 hover:border-indigo-300 hover:text-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-all duration-150 cursor-pointer select-none"
|
|
352
|
+
>
|
|
353
|
+
<Icon icon={IconProp.CheckCircle} className="h-3.5 w-3.5" />
|
|
354
|
+
<span>Select All {props.pluralLabel}</span>
|
|
355
|
+
</button>
|
|
354
356
|
)}
|
|
355
357
|
|
|
356
358
|
{/** Clear Selection Button */}
|
|
357
|
-
<
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
358
361
|
onClick={() => {
|
|
359
362
|
props.onClearSelectionClick();
|
|
360
363
|
}}
|
|
361
|
-
className="font-medium text-gray-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-red-50 hover:border-red-300 hover:text-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-all duration-150 cursor-pointer select-none"
|
|
365
|
+
>
|
|
366
|
+
<Icon icon={IconProp.Close} className="h-3.5 w-3.5" />
|
|
367
|
+
<span>Clear Selection</span>
|
|
368
|
+
</button>
|
|
366
369
|
</div>
|
|
367
|
-
</div>
|
|
368
370
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
371
|
+
<div className="flex items-center">
|
|
372
|
+
{menuChildren.length > 0 && (
|
|
373
|
+
<MoreMenu
|
|
374
|
+
elementToBeShownInsteadOfButton={
|
|
375
|
+
<div className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-3.5 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-all duration-150 cursor-pointer select-none">
|
|
376
|
+
<Icon
|
|
377
|
+
icon={IconProp.Bolt}
|
|
378
|
+
className="h-4 w-4 text-gray-500"
|
|
379
|
+
/>
|
|
380
|
+
<span>Bulk Actions</span>
|
|
381
|
+
<Icon
|
|
382
|
+
icon={IconProp.ChevronDown}
|
|
383
|
+
className="h-3.5 w-3.5 text-gray-400 ml-0.5"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
}
|
|
387
|
+
>
|
|
388
|
+
{menuChildren}
|
|
389
|
+
</MoreMenu>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
389
392
|
</div>
|
|
393
|
+
|
|
394
|
+
{showLimitWarning && (
|
|
395
|
+
<div className="mt-2 text-xs text-gray-500">
|
|
396
|
+
You can only select {LIMIT_PER_PROJECT} {props.pluralLabel} at a
|
|
397
|
+
time. This is for performance reasons.
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
390
400
|
</div>
|
|
391
401
|
</div>
|
|
392
402
|
|