@rh-support/manage 2.5.272 → 2.5.276

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.
Files changed (56) hide show
  1. package/lib/esm/ManageTabs.d.ts.map +1 -1
  2. package/lib/esm/ManageTabs.js +46 -18
  3. package/lib/esm/Routes.d.ts +1 -0
  4. package/lib/esm/Routes.d.ts.map +1 -1
  5. package/lib/esm/Routes.js +6 -0
  6. package/lib/esm/components/Groups/AddMembersModal/AddMembersModal.d.ts +12 -0
  7. package/lib/esm/components/Groups/AddMembersModal/AddMembersModal.d.ts.map +1 -0
  8. package/lib/esm/components/Groups/AddMembersModal/AddMembersModal.js +80 -0
  9. package/lib/esm/components/Groups/AddMembersModal/index.d.ts +2 -0
  10. package/lib/esm/components/Groups/AddMembersModal/index.d.ts.map +1 -0
  11. package/lib/esm/components/Groups/AddMembersModal/index.js +1 -0
  12. package/lib/esm/components/Groups/DefualtGroupSwitch/DefaultGroupSwitch.d.ts +5 -3
  13. package/lib/esm/components/Groups/DefualtGroupSwitch/DefaultGroupSwitch.d.ts.map +1 -1
  14. package/lib/esm/components/Groups/DefualtGroupSwitch/DefaultGroupSwitch.js +21 -14
  15. package/lib/esm/components/Groups/GroupSelect/GroupSelect.d.ts +1 -0
  16. package/lib/esm/components/Groups/GroupSelect/GroupSelect.d.ts.map +1 -1
  17. package/lib/esm/components/Groups/GroupSelect/GroupSelect.js +160 -85
  18. package/lib/esm/components/Groups/ManageGroupUsers/ManageGroupUsers.d.ts.map +1 -1
  19. package/lib/esm/components/Groups/ManageGroupUsers/ManageGroupUsers.js +163 -42
  20. package/lib/esm/components/ManageCaseLabels/AddEditCaseLabelModal.d.ts +19 -0
  21. package/lib/esm/components/ManageCaseLabels/AddEditCaseLabelModal.d.ts.map +1 -0
  22. package/lib/esm/components/ManageCaseLabels/AddEditCaseLabelModal.js +97 -0
  23. package/lib/esm/components/ManageCaseLabels/ManageCaseLabelsTab.d.ts +3 -0
  24. package/lib/esm/components/ManageCaseLabels/ManageCaseLabelsTab.d.ts.map +1 -0
  25. package/lib/esm/components/ManageCaseLabels/ManageCaseLabelsTab.js +220 -0
  26. package/lib/esm/components/ManageCaseLabels/index.d.ts +2 -0
  27. package/lib/esm/components/ManageCaseLabels/index.d.ts.map +1 -0
  28. package/lib/esm/components/ManageCaseLabels/index.js +1 -0
  29. package/lib/esm/components/ManageCaseLabels/types.d.ts +6 -0
  30. package/lib/esm/components/ManageCaseLabels/types.d.ts.map +1 -0
  31. package/lib/esm/components/ManageCaseLabels/types.js +1 -0
  32. package/lib/esm/components/ManagePreferences/sections/PreferencesACL.d.ts.map +1 -1
  33. package/lib/esm/components/ManagePreferences/sections/PreferencesACL.js +3 -4
  34. package/lib/esm/components/ManagePreferences/sections/PreferencesCaseCreate.d.ts.map +1 -1
  35. package/lib/esm/components/ManagePreferences/sections/PreferencesCaseCreate.js +73 -58
  36. package/lib/esm/components/ManagePreferences/sections/PreferencesCaseView.d.ts.map +1 -1
  37. package/lib/esm/components/ManagePreferences/sections/PreferencesCaseView.js +43 -98
  38. package/lib/esm/constants/GroupSelectActions.d.ts +2 -1
  39. package/lib/esm/constants/GroupSelectActions.d.ts.map +1 -1
  40. package/lib/esm/constants/GroupSelectActions.js +1 -0
  41. package/lib/esm/hooks/useCaseGroupGraphQL.d.ts +42 -0
  42. package/lib/esm/hooks/useCaseGroupGraphQL.d.ts.map +1 -0
  43. package/lib/esm/hooks/useCaseGroupGraphQL.js +109 -0
  44. package/lib/esm/hooks/useCaseLabelGraphQL.d.ts +26 -0
  45. package/lib/esm/hooks/useCaseLabelGraphQL.d.ts.map +1 -0
  46. package/lib/esm/hooks/useCaseLabelGraphQL.js +104 -0
  47. package/lib/esm/hooks/useCurrentUserContact.d.ts +8 -0
  48. package/lib/esm/hooks/useCurrentUserContact.d.ts.map +1 -0
  49. package/lib/esm/hooks/useCurrentUserContact.js +74 -0
  50. package/lib/esm/hooks/useVersionsDiffer.d.ts.map +1 -1
  51. package/lib/esm/index.js +1 -0
  52. package/lib/esm/scss/_main.scss +77 -0
  53. package/lib/esm/utils/caseGroupMapper.d.ts +18 -0
  54. package/lib/esm/utils/caseGroupMapper.d.ts.map +1 -0
  55. package/lib/esm/utils/caseGroupMapper.js +50 -0
  56. package/package.json +11 -8
@@ -7,39 +7,87 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { publicApi } from '@cee-eng/hydrajs';
11
- import { SearchInput } from '@patternfly/react-core';
12
- import { ErrorBoundary, ToastNotification, useFetch, usePrevious } from '@rh-support/components';
13
- import { useGlobalStateContext } from '@rh-support/react-context';
10
+ import { Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant, SearchInput, Tooltip, } from '@patternfly/react-core';
11
+ import { ErrorBoundary, ToastNotification, usePrevious } from '@rh-support/components';
12
+ import { GlobalMetadataReducerConstants, useGlobalDispatchContext, useGlobalStateContext, } from '@rh-support/react-context';
13
+ import { ability, resourceActions, resources } from '@rh-support/user-permissions';
14
14
  import isEmpty from 'lodash/isEmpty';
15
15
  import React, { useEffect, useMemo, useState } from 'react';
16
16
  import { Trans, useTranslation } from 'react-i18next';
17
17
  import { useLocation } from 'react-router-dom';
18
+ import { useCaseGroupMembers, useDeleteCaseGroupMember } from '../../../hooks/useCaseGroupGraphQL';
18
19
  import { ManageTable } from '../../ManageTable/ManageTable';
20
+ import { AddMembersModal } from '../AddMembersModal';
19
21
  import { DefaultGroupSwitch } from '../DefualtGroupSwitch';
20
22
  import { GroupSelect } from '../GroupSelect';
21
- import { GroupUserPermissionsSelect } from '../GroupUserPermissionsSelect';
23
+ // TODO: Uncomment when updateUsers is migrated to GraphQL
24
+ // import { GroupUserPermissionsSelect } from '../GroupUserPermissionsSelect';
25
+ const RemoveMemberButton = ({ user, onRemove, isDisabled }) => {
26
+ const { t } = useTranslation();
27
+ const button = (React.createElement(Button, { variant: "link", isDanger: true, onClick: () => onRemove(user), isAriaDisabled: user.isOrgAdmin || isDisabled }, t('Remove')));
28
+ if (user.isOrgAdmin) {
29
+ return (React.createElement(Tooltip, { content: t('Organization Administrators cannot be removed because they have access to all groups') }, button));
30
+ }
31
+ return button;
32
+ };
22
33
  export const ManageGroupUsers = () => {
34
+ var _a, _b;
23
35
  const { t } = useTranslation();
24
36
  const location = useLocation();
25
- const { globalMetadataState: { loggedInUsersAccount }, } = useGlobalStateContext();
37
+ const { globalMetadataState: { caseGroups, loggedInUsersAccount, loggedInUser }, } = useGlobalStateContext();
38
+ const dispatchToGlobalMetadataReducer = useGlobalDispatchContext();
26
39
  const disableGroupSelect = !loggedInUsersAccount || !loggedInUsersAccount.data || !loggedInUsersAccount.data.accountNumber;
27
40
  const [selectedGroup, setSelectedGroup] = useState();
28
- const { clearResponse, isFetching, request, data: response, } = useFetch(publicApi.caseGroups.getCaseGroupUsers);
29
- const groupUsers = useMemo(() => response || [], [response]);
30
- // errors
31
- function onPermissionError(error, user) {
32
- setData([...data]);
33
- const message = t(`Could not change permission for {{user}}: {{error}}`, {
34
- user: `${user.firstName} ${user.lastName}`,
35
- error: error.message,
36
- });
37
- ToastNotification.addDangerMessage(message);
38
- }
41
+ const { data: groupUsers, loading: isFetching, fetchMembers, refetch } = useCaseGroupMembers();
42
+ const defaultCaseGroupId = (_b = (_a = loggedInUser === null || loggedInUser === void 0 ? void 0 : loggedInUser.data) === null || _a === void 0 ? void 0 : _a.defaultCaseGroup) !== null && _b !== void 0 ? _b : null;
43
+ const membersWithOrgAdmin = useMemo(() => {
44
+ var _a, _b, _c, _d;
45
+ const loggedInContact = loggedInUser === null || loggedInUser === void 0 ? void 0 : loggedInUser.data;
46
+ const selectedGroupId = selectedGroup === null || selectedGroup === void 0 ? void 0 : selectedGroup.groupNum;
47
+ const syncLoggedInUserDefault = (members) => {
48
+ if (!(loggedInContact === null || loggedInContact === void 0 ? void 0 : loggedInContact.ssoUsername) || !selectedGroupId)
49
+ return members;
50
+ return members.map((member) => member.ssoUsername === loggedInContact.ssoUsername
51
+ ? Object.assign(Object.assign({}, member), { isDefault: defaultCaseGroupId === selectedGroupId }) : member);
52
+ };
53
+ const members = syncLoggedInUserDefault(groupUsers || []);
54
+ const user = loggedInContact;
55
+ if (!(user === null || user === void 0 ? void 0 : user.isOrgAdmin) || !user.ssoUsername || !selectedGroup)
56
+ return members;
57
+ const alreadyPresent = members.some((m) => m.ssoUsername === user.ssoUsername);
58
+ if (alreadyPresent)
59
+ return members;
60
+ const syntheticEntry = {
61
+ firstName: (_a = user.firstName) !== null && _a !== void 0 ? _a : '',
62
+ lastName: (_b = user.lastName) !== null && _b !== void 0 ? _b : '',
63
+ isOrgAdmin: true,
64
+ access: true,
65
+ write: false,
66
+ isDefault: defaultCaseGroupId === selectedGroup.groupNum,
67
+ ssoUsername: user.ssoUsername,
68
+ email: (_c = user.email) !== null && _c !== void 0 ? _c : '',
69
+ contactSfId: (_d = user.id) !== null && _d !== void 0 ? _d : '',
70
+ memberRecordId: '',
71
+ };
72
+ return [...members, syntheticEntry];
73
+ }, [groupUsers, loggedInUser === null || loggedInUser === void 0 ? void 0 : loggedInUser.data, selectedGroup, defaultCaseGroupId]);
74
+ const canManageCaseGroups = ability.can(resourceActions.CREATE, resources.CASE_GROUPS);
75
+ const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false);
76
+ const [memberToRemove, setMemberToRemove] = useState(null);
77
+ const { deleteMember, loading: isDeleting } = useDeleteCaseGroupMember();
78
+ // TODO: Uncomment when updateUsers is migrated to GraphQL
79
+ // function onPermissionError(error: Error, user: IGroupUser) {
80
+ // setData([...data]);
81
+ // const message = t(`Could not change permission for {{user}}: {{error}}`, {
82
+ // user: `${user.firstName} ${user.lastName}`,
83
+ // error: error.message,
84
+ // });
85
+ // ToastNotification.addDangerMessage(message);
86
+ // }
39
87
  // set and sort fetched data.
40
- const [initialData, setInitialData] = useState(groupUsers);
41
- const [data, setData] = useState(groupUsers);
42
- useEffect(() => setAllData(response || []), [response]);
88
+ const [initialData, setInitialData] = useState(membersWithOrgAdmin);
89
+ const [data, setData] = useState(membersWithOrgAdmin);
90
+ useEffect(() => setAllData(membersWithOrgAdmin || []), [membersWithOrgAdmin]);
43
91
  function setAllData(users) {
44
92
  setInitialData(users);
45
93
  setData(users);
@@ -58,21 +106,63 @@ export const ManageGroupUsers = () => {
58
106
  return tempUser;
59
107
  }));
60
108
  };
109
+ const onDefaultToggleSuccess = (user, isChecked) => {
110
+ var _a;
111
+ if (user.ssoUsername !== ((_a = loggedInUser === null || loggedInUser === void 0 ? void 0 : loggedInUser.data) === null || _a === void 0 ? void 0 : _a.ssoUsername) || !selectedGroup)
112
+ return;
113
+ const groupId = isChecked ? selectedGroup.groupNum : null;
114
+ const updatedLoggedInUserData = Object.assign(Object.assign({}, loggedInUser.data), { defaultCaseGroup: groupId });
115
+ dispatchToGlobalMetadataReducer({
116
+ type: GlobalMetadataReducerConstants.setLoggedInUser,
117
+ payload: {
118
+ loggedInUser: Object.assign(Object.assign({}, loggedInUser), { data: updatedLoggedInUserData }),
119
+ },
120
+ });
121
+ if (caseGroups === null || caseGroups === void 0 ? void 0 : caseGroups.data) {
122
+ const updatedGroups = caseGroups.data.map((group) => (Object.assign(Object.assign({}, group), { isDefault: groupId !== null && group.groupNum === groupId })));
123
+ dispatchToGlobalMetadataReducer({
124
+ type: GlobalMetadataReducerConstants.setCaseGroups,
125
+ payload: {
126
+ caseGroups: Object.assign(Object.assign({}, caseGroups), { data: updatedGroups }),
127
+ },
128
+ });
129
+ }
130
+ };
61
131
  const onGroupSelect = (group) => __awaiter(void 0, void 0, void 0, function* () {
62
132
  setSelectedGroup(group);
63
133
  if (group) {
64
- request(loggedInUsersAccount.data.accountNumber, group.groupNum);
134
+ fetchMembers(group.groupNum);
65
135
  }
66
136
  else {
67
- clearResponse();
137
+ setAllData([]);
68
138
  }
69
139
  });
70
- function onPermissionsChange(user) {
71
- const initialUsers = initialData.map((value) => (value.ssoUsername === user.ssoUsername ? user : value));
72
- const users = data.map((value) => (value.ssoUsername === user.ssoUsername ? user : value));
73
- setInitialData(initialUsers);
74
- setData(users);
75
- }
140
+ const handleRemoveMember = () => __awaiter(void 0, void 0, void 0, function* () {
141
+ if (!(memberToRemove === null || memberToRemove === void 0 ? void 0 : memberToRemove.memberRecordId))
142
+ return;
143
+ try {
144
+ yield deleteMember(memberToRemove.memberRecordId);
145
+ ToastNotification.addSuccessMessage(t('{{name}} has been removed from the group', {
146
+ name: `${memberToRemove.firstName} ${memberToRemove.lastName}`,
147
+ }));
148
+ refetch();
149
+ }
150
+ catch (error) {
151
+ ToastNotification.addDangerMessage(t('Could not remove member from group: {{message}}', {
152
+ message: error instanceof Error ? error.message : String(error),
153
+ }));
154
+ }
155
+ finally {
156
+ setMemberToRemove(null);
157
+ }
158
+ });
159
+ // TODO: Uncomment when updateUsers is migrated to GraphQL
160
+ // function onPermissionsChange(user: IGroupUser) {
161
+ // const initialUsers = initialData.map((value) => (value.ssoUsername === user.ssoUsername ? user : value));
162
+ // const users = data.map((value) => (value.ssoUsername === user.ssoUsername ? user : value));
163
+ // setInitialData(initialUsers);
164
+ // setData(users);
165
+ // }
76
166
  const columns = [
77
167
  {
78
168
  accessor: (user) => user.firstName,
@@ -92,16 +182,38 @@ export const ManageGroupUsers = () => {
92
182
  id: 'sso-username',
93
183
  title: t('Login'),
94
184
  },
95
- {
96
- id: 'permissions',
97
- title: t('Permissions'),
98
- cell: (user) => user.isOrgAdmin ? (t('Admin/Full access')) : (React.createElement(GroupUserPermissionsSelect, { onChange: onPermissionsChange, account: loggedInUsersAccount.data, group: selectedGroup, users: data, user: user, onError: onPermissionError })),
99
- },
100
- {
101
- id: 'default',
102
- title: t('Default group'),
103
- cell: (user, index) => (React.createElement(DefaultGroupSwitch, { id: `${index}`, onToggle: onDefaultToggle, group: selectedGroup, user: user })),
104
- },
185
+ // TODO: Uncomment when updateUsers is migrated to GraphQL
186
+ // {
187
+ // id: 'permissions',
188
+ // title: t('Permissions'),
189
+ // cell: (user) =>
190
+ // user.isOrgAdmin ? (
191
+ // t('Admin/Full access')
192
+ // ) : (
193
+ // <GroupUserPermissionsSelect
194
+ // onChange={onPermissionsChange}
195
+ // account={loggedInUsersAccount.data}
196
+ // group={selectedGroup}
197
+ // users={data}
198
+ // user={user}
199
+ // onError={onPermissionError}
200
+ // />
201
+ // ),
202
+ // },
203
+ ...(canManageCaseGroups
204
+ ? [
205
+ {
206
+ id: 'default',
207
+ title: t('Default group'),
208
+ cell: (user, index) => (React.createElement(DefaultGroupSwitch, { id: `${index}`, onToggle: onDefaultToggle, onToggleSuccess: onDefaultToggleSuccess, group: selectedGroup, user: user })),
209
+ },
210
+ {
211
+ id: 'remove',
212
+ title: '',
213
+ cell: (user) => (React.createElement(RemoveMemberButton, { user: user, onRemove: setMemberToRemove, isDisabled: isDeleting })),
214
+ },
215
+ ]
216
+ : []),
105
217
  ];
106
218
  // filter out search query.
107
219
  const [userSearch, setUserSearch] = useState('');
@@ -121,12 +233,12 @@ export const ManageGroupUsers = () => {
121
233
  else {
122
234
  setData(initialData);
123
235
  }
124
- }, [initialData, userSearch, setData, previousUserSearch, groupUsers]);
236
+ }, [initialData, userSearch, setData, previousUserSearch, membersWithOrgAdmin]);
125
237
  useEffect(() => {
126
238
  if (location.pathname !== '/manage/groups') {
127
239
  return;
128
240
  }
129
- setAllData(response || []);
241
+ setAllData(membersWithOrgAdmin || []);
130
242
  // eslint-disable-next-line react-hooks/exhaustive-deps
131
243
  }, [location.pathname, loggedInUsersAccount.data]);
132
244
  return (React.createElement(React.Fragment, null,
@@ -134,11 +246,20 @@ export const ManageGroupUsers = () => {
134
246
  React.createElement("h2", { className: "pf-v6-u-pb-sm" }, t('Groups')),
135
247
  React.createElement("p", null, t('To filter cases while searching, add or edit groups.'))),
136
248
  React.createElement("div", { className: "toolbar pf-v6-u-justify-content-space-between" },
137
- React.createElement(GroupSelect, { isDisabled: isFetching || disableGroupSelect, onSelect: onGroupSelect }),
249
+ React.createElement(GroupSelect, { isDisabled: isFetching || disableGroupSelect, onSelect: onGroupSelect, onAddMembers: () => setIsAddMembersModalOpen(true) }),
138
250
  React.createElement("span", { className: "toolbar-right" },
139
251
  React.createElement("label", { htmlFor: "account-group-filter" },
140
252
  React.createElement(Trans, null, "Filter by")),
141
253
  React.createElement(SearchInput, { className: "pf-v6-u-flex-grow-1 pf-v6-c-search-input pf-v6-u-background-color-100", id: "account-group-filter", value: userSearch, isDisabled: isFetching || !selectedGroup, onChange: (_, value) => setUserSearch(value), placeholder: t('Search for people in this group'), "aria-label": "Group search input", onClear: () => setUserSearch('') }))),
142
254
  React.createElement(ErrorBoundary, { errorMsgInfo: { message: t('There was an error loading group users list') } },
143
- React.createElement(ManageTable, { ariaLabel: t('Table to manage groups'), columns: columns, data: data, sortInfo: { column: 'first-name', direction: 'asc' }, isFetching: isFetching, isError: false }))));
255
+ React.createElement(ManageTable, { ariaLabel: t('Table to manage groups'), columns: columns, data: data, sortInfo: { column: 'first-name', direction: 'asc' }, isFetching: isFetching, isError: false, pagination: false })),
256
+ memberToRemove && (React.createElement(Modal, { variant: ModalVariant.small, isOpen: true, onClose: () => setMemberToRemove(null), "aria-label": t('Confirm removal') },
257
+ React.createElement(ModalHeader, { title: t('Remove member') }),
258
+ React.createElement(ModalBody, null, t('Are you sure you want to remove {{name}} from this group?', {
259
+ name: `${memberToRemove.firstName} ${memberToRemove.lastName}`,
260
+ })),
261
+ React.createElement(ModalFooter, null,
262
+ React.createElement(Button, { variant: "danger", onClick: handleRemoveMember, isLoading: isDeleting, isDisabled: isDeleting }, t('Remove')),
263
+ React.createElement(Button, { variant: "link", onClick: () => setMemberToRemove(null), isDisabled: isDeleting }, t('Cancel'))))),
264
+ canManageCaseGroups && isAddMembersModalOpen && selectedGroup && (React.createElement(AddMembersModal, { group: selectedGroup, existingMembers: initialData, onSuccess: refetch, onClose: () => setIsAddMembersModalOpen(false) }))));
144
265
  };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { ICaseLabel } from './types';
3
+ export type SaveCaseLabelResult = {
4
+ success: boolean;
5
+ fieldError?: string;
6
+ };
7
+ interface IProps {
8
+ isOpen: boolean;
9
+ addMode: boolean;
10
+ label?: ICaseLabel | null;
11
+ isSaving?: boolean;
12
+ onClose: () => void;
13
+ onSave: (name: string, options?: {
14
+ keepOpen?: boolean;
15
+ }) => Promise<SaveCaseLabelResult> | SaveCaseLabelResult;
16
+ }
17
+ export declare function AddEditCaseLabelModal({ isOpen, addMode, label, isSaving, onClose, onSave }: IProps): React.JSX.Element;
18
+ export {};
19
+ //# sourceMappingURL=AddEditCaseLabelModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AddEditCaseLabelModal.d.ts","sourceRoot":"","sources":["../../../../src/components/ManageCaseLabels/AddEditCaseLabelModal.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,MAAM,MAAM,mBAAmB,GAAG;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,UAAU,MAAM;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,GAAG,mBAAmB,CAAC;CAClH;AAED,wBAAgB,qBAAqB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,qBAkJ1G"}
@@ -0,0 +1,97 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { Button, ButtonVariant, Flex, FlexItem, FormGroup, FormHelperText, HelperText, HelperTextItem, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant, TextInput, } from '@patternfly/react-core';
11
+ import { CASE_LABEL_NAME_MAX_LENGTH } from '@rh-support/utils';
12
+ import isEmpty from 'lodash/isEmpty';
13
+ import React, { useEffect, useState } from 'react';
14
+ import { Trans, useTranslation } from 'react-i18next';
15
+ export function AddEditCaseLabelModal({ isOpen, addMode, label, isSaving = false, onClose, onSave }) {
16
+ const { t } = useTranslation();
17
+ const [name, setName] = useState('');
18
+ const [error, setError] = useState('');
19
+ useEffect(() => {
20
+ if (isOpen) {
21
+ setName(addMode ? '' : (label === null || label === void 0 ? void 0 : label.name) || '');
22
+ setError('');
23
+ }
24
+ }, [isOpen, addMode, label]);
25
+ const isOverMaxLength = name.length > CASE_LABEL_NAME_MAX_LENGTH;
26
+ const isNameEmpty = isEmpty(name.trim());
27
+ const canSave = !isNameEmpty && !isOverMaxLength && !isSaving;
28
+ const resetForm = () => {
29
+ setName('');
30
+ setError('');
31
+ };
32
+ const handleSave = () => __awaiter(this, void 0, void 0, function* () {
33
+ if (isNameEmpty) {
34
+ setError(t('Case label name cannot be empty.'));
35
+ return;
36
+ }
37
+ if (isOverMaxLength) {
38
+ return;
39
+ }
40
+ const result = yield onSave(name.trim(), { keepOpen: false });
41
+ if (!result.success && result.fieldError) {
42
+ setError(result.fieldError);
43
+ }
44
+ });
45
+ const handleSaveAndNew = () => __awaiter(this, void 0, void 0, function* () {
46
+ if (isNameEmpty) {
47
+ setError(t('Case label name cannot be empty.'));
48
+ return;
49
+ }
50
+ if (isOverMaxLength) {
51
+ return;
52
+ }
53
+ const result = yield onSave(name.trim(), { keepOpen: true });
54
+ if (result.success) {
55
+ resetForm();
56
+ return;
57
+ }
58
+ if (result.fieldError) {
59
+ setError(result.fieldError);
60
+ }
61
+ });
62
+ const handleClose = () => {
63
+ resetForm();
64
+ onClose();
65
+ };
66
+ return (React.createElement(Modal, { id: "add-edit-case-label-modal", "aria-describedby": "add-edit-case-label-modal", isOpen: isOpen, onClose: handleClose, variant: ModalVariant.small },
67
+ React.createElement(ModalHeader, null,
68
+ React.createElement("h2", null, addMode ? t('Create new case label') : t('Rename case label'))),
69
+ React.createElement(ModalBody, null,
70
+ React.createElement(FormGroup, { label: t('Case label name'), isRequired: true },
71
+ React.createElement(TextInput, { isRequired: true, type: "text", id: "case-label-name-input", value: name, onChange: (_, value) => {
72
+ setName(value);
73
+ if (!isEmpty(error)) {
74
+ setError('');
75
+ }
76
+ }, placeholder: t('Enter a unique case label name'), validated: isOverMaxLength || !isEmpty(error) ? 'error' : 'default', isDisabled: isSaving }),
77
+ React.createElement(FormHelperText, null,
78
+ React.createElement(Flex, { justifyContent: { default: 'justifyContentFlexEnd' } },
79
+ React.createElement(FlexItem, null,
80
+ React.createElement(HelperText, null,
81
+ React.createElement(HelperTextItem, { variant: "default", className: `pf-v6-u-font-size-sm ${isOverMaxLength
82
+ ? 'pf-v6-u-text-color-status-danger'
83
+ : 'pf-v6-u-text-color-subtle'}` }, t('{{count}}/{{max}} characters', {
84
+ count: name.length,
85
+ max: CASE_LABEL_NAME_MAX_LENGTH,
86
+ })))))),
87
+ !isEmpty(error) && (React.createElement(FormHelperText, null,
88
+ React.createElement(HelperText, null,
89
+ React.createElement(HelperTextItem, { variant: "error" }, error)))))),
90
+ React.createElement(ModalFooter, null,
91
+ React.createElement(Button, { key: "save", variant: ButtonVariant.primary, onClick: handleSave, isDisabled: !canSave, isLoading: isSaving },
92
+ React.createElement(Trans, null, "Save")),
93
+ addMode && (React.createElement(Button, { key: "save-and-new", variant: ButtonVariant.secondary, onClick: handleSaveAndNew, isDisabled: !canSave, isLoading: isSaving },
94
+ React.createElement(Trans, null, "Save & New"))),
95
+ React.createElement(Button, { key: "cancel", variant: ButtonVariant.link, onClick: handleClose, isDisabled: isSaving },
96
+ React.createElement(Trans, null, "Cancel")))));
97
+ }
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare function ManageCaseLabelsTab(): React.JSX.Element;
3
+ //# sourceMappingURL=ManageCaseLabelsTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ManageCaseLabelsTab.d.ts","sourceRoot":"","sources":["../../../../src/components/ManageCaseLabels/ManageCaseLabelsTab.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAoC5D,wBAAgB,mBAAmB,sBAwUlC"}
@@ -0,0 +1,220 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { ActionList, ActionListItem, AlertVariant, Button, ButtonVariant, EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateVariant, ModalVariant, SearchInput, } from '@patternfly/react-core';
11
+ import CubesIcon from '@patternfly/react-icons/dist/js/icons/cubes-icon';
12
+ import PencilAltIcon from '@patternfly/react-icons/dist/js/icons/pencil-alt-icon';
13
+ import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon';
14
+ import { ErrorBoundary, ToastNotification, useConfirmation } from '@rh-support/components';
15
+ import { useGlobalStateContext } from '@rh-support/react-context';
16
+ import { getStringifiedParams } from '@rh-support/utils';
17
+ import isEmpty from 'lodash/isEmpty';
18
+ import React, { useEffect, useMemo, useState } from 'react';
19
+ import { Trans, useTranslation } from 'react-i18next';
20
+ import { useHistory } from 'react-router-dom';
21
+ import { useCaseLabels, useCreateCaseLabel, useDeleteCaseLabel, useUpdateCaseLabel, } from '../../hooks/useCaseLabelGraphQL';
22
+ import { ManageTable } from '../ManageTable';
23
+ import { AddEditCaseLabelModal } from './AddEditCaseLabelModal';
24
+ const isDuplicateCaseLabelGraphQLError = (error) => {
25
+ var _a, _b, _c, _d;
26
+ if (!error || typeof error !== 'object') {
27
+ return false;
28
+ }
29
+ const apolloError = error;
30
+ const messages = [
31
+ ...((_b = (_a = apolloError.graphQLErrors) === null || _a === void 0 ? void 0 : _a.map((graphQLError) => graphQLError.message || '')) !== null && _b !== void 0 ? _b : []),
32
+ ...((_d = (_c = apolloError.errors) === null || _c === void 0 ? void 0 : _c.map((graphQLError) => graphQLError.message || '')) !== null && _d !== void 0 ? _d : []),
33
+ apolloError.message || '',
34
+ ]
35
+ .join(' ')
36
+ .toLowerCase();
37
+ return messages.includes('duplicate value found');
38
+ };
39
+ export function ManageCaseLabelsTab() {
40
+ var _a;
41
+ const { t } = useTranslation();
42
+ const history = useHistory();
43
+ const confirmation = useConfirmation();
44
+ const { globalMetadataState: { loggedInUsersAccount, loggedInUserRights }, } = useGlobalStateContext();
45
+ const accountId = (_a = loggedInUsersAccount === null || loggedInUsersAccount === void 0 ? void 0 : loggedInUsersAccount.data) === null || _a === void 0 ? void 0 : _a.salesforceAccountId;
46
+ const canManageCaseLabels = loggedInUserRights.data.isOrgAdmin();
47
+ const { labels: fetchedLabels, loading: isFetchingLabels, error: labelsError, refetch } = useCaseLabels(accountId);
48
+ const { createCaseLabel, loading: isCreatingLabel } = useCreateCaseLabel();
49
+ const { updateCaseLabel, loading: isUpdatingLabel } = useUpdateCaseLabel();
50
+ const { deleteCaseLabel } = useDeleteCaseLabel();
51
+ const [labels, setLabels] = useState([]);
52
+ const [searchString, setSearchString] = useState('');
53
+ const [isModalOpen, setIsModalOpen] = useState(false);
54
+ const [isInAddMode, setIsInAddMode] = useState(true);
55
+ const [selectedLabel, setSelectedLabel] = useState(null);
56
+ useEffect(() => {
57
+ setLabels(fetchedLabels);
58
+ }, [fetchedLabels]);
59
+ const filteredLabels = useMemo(() => {
60
+ if (isEmpty(searchString)) {
61
+ return labels;
62
+ }
63
+ return labels.filter((label) => label.name.toLowerCase().includes(searchString.toLowerCase()));
64
+ }, [labels, searchString]);
65
+ const hasNoLabels = !isFetchingLabels && !labelsError && labels.length === 0;
66
+ const onOpenAddModal = () => {
67
+ setIsInAddMode(true);
68
+ setSelectedLabel(null);
69
+ setIsModalOpen(true);
70
+ };
71
+ const onOpenEditModal = (label) => {
72
+ setIsInAddMode(false);
73
+ setSelectedLabel(label);
74
+ setIsModalOpen(true);
75
+ };
76
+ const closeModal = () => {
77
+ setIsModalOpen(false);
78
+ setSelectedLabel(null);
79
+ setIsInAddMode(true);
80
+ };
81
+ const onSaveLabel = (name, options) => __awaiter(this, void 0, void 0, function* () {
82
+ if (isInAddMode) {
83
+ if (!accountId) {
84
+ ToastNotification.addDangerMessage(t('Unable to create case label. Account information is missing.'));
85
+ return { success: false };
86
+ }
87
+ try {
88
+ yield createCaseLabel(name, accountId);
89
+ yield refetch();
90
+ ToastNotification.addSuccessMessage(t('Case label has been successfully created.'));
91
+ if (!(options === null || options === void 0 ? void 0 : options.keepOpen)) {
92
+ closeModal();
93
+ }
94
+ return { success: true };
95
+ }
96
+ catch (error) {
97
+ if (isDuplicateCaseLabelGraphQLError(error)) {
98
+ return {
99
+ success: false,
100
+ fieldError: t('Label name already in use. Try a different name.'),
101
+ };
102
+ }
103
+ ToastNotification.addDangerMessage(t('Case label failed to create.'));
104
+ return { success: false };
105
+ }
106
+ }
107
+ if (selectedLabel) {
108
+ try {
109
+ yield updateCaseLabel(selectedLabel.id, name);
110
+ yield refetch();
111
+ ToastNotification.addSuccessMessage(t('Case label has been successfully updated.'));
112
+ closeModal();
113
+ return { success: true };
114
+ }
115
+ catch (error) {
116
+ if (isDuplicateCaseLabelGraphQLError(error)) {
117
+ return {
118
+ success: false,
119
+ fieldError: t('Label name already in use. Try a different name.'),
120
+ };
121
+ }
122
+ ToastNotification.addDangerMessage(t('Case label failed to update.'));
123
+ return { success: false };
124
+ }
125
+ }
126
+ return { success: false };
127
+ });
128
+ const onDeleteLabel = (label) => __awaiter(this, void 0, void 0, function* () {
129
+ try {
130
+ yield confirmation({
131
+ catchOnCancel: true,
132
+ confirmText: t('Delete label'),
133
+ confirmButtonVariant: ButtonVariant.danger,
134
+ cancelText: t('Cancel'),
135
+ modalVariant: ModalVariant.large,
136
+ title: t('Delete case label'),
137
+ description: (React.createElement("div", { className: "case-label-delete-description", style: { display: 'flex', flexDirection: 'column', gap: '1.5rem' } },
138
+ React.createElement("div", null, t('Are you sure you want to delete "{{name}}" case label?', { name: label.name })),
139
+ React.createElement("div", { style: { whiteSpace: 'nowrap' } },
140
+ t('This label is currently assigned to'),
141
+ ' ',
142
+ React.createElement("strong", null, t('{{count}} cases', { count: label.assignedCaseCount })),
143
+ '. ',
144
+ t('Deleting it will remove the label from all associated cases.')))),
145
+ });
146
+ }
147
+ catch (_a) {
148
+ // user cancelled
149
+ return;
150
+ }
151
+ try {
152
+ yield deleteCaseLabel(label.id);
153
+ yield refetch();
154
+ ToastNotification.addSuccessMessage(t('Case label has been successfully removed.'));
155
+ }
156
+ catch (_b) {
157
+ ToastNotification.addDangerMessage(t('Case label failed to remove.'));
158
+ }
159
+ });
160
+ const onViewAssignedCases = (label) => {
161
+ // Match Case List URL format (SolrQueryBuilder): caseLabel: ("<id>") orderBy lastModifiedDate desc
162
+ const query = `caseLabel: ("${label.id}") orderBy lastModifiedDate desc`;
163
+ history.push({
164
+ pathname: '/case/list',
165
+ search: getStringifiedParams({
166
+ query: encodeURIComponent(query),
167
+ searchType: 'basic',
168
+ }),
169
+ });
170
+ };
171
+ const columns = [
172
+ {
173
+ accessor: (label) => label.name,
174
+ sortable: false,
175
+ id: 'case-label-row',
176
+ title: '',
177
+ cellWidth: 100,
178
+ cell: (label) => (React.createElement("div", { className: "case-label-row" },
179
+ React.createElement("div", { className: "case-label-row__info" },
180
+ React.createElement("div", { className: "pf-v6-u-font-weight-bold" }, label.name),
181
+ React.createElement("div", { className: "pf-v6-u-color-200 pf-v6-u-font-size-sm" }, t('{{count}} cases assigned', { count: label.assignedCaseCount }))),
182
+ React.createElement("div", { className: "case-label-row__actions" },
183
+ label.assignedCaseCount > 0 && (React.createElement(Button, { variant: ButtonVariant.link, isInline: true, onClick: () => onViewAssignedCases(label), "data-tracking-id": "case-label-view-assigned-cases" },
184
+ React.createElement(Trans, null, "View assigned cases"))),
185
+ canManageCaseLabels && (React.createElement(ActionList, { isIconList: true },
186
+ React.createElement(ActionListItem, null,
187
+ React.createElement(Button, { icon: React.createElement(PencilAltIcon, null), "aria-label": t('Edit case label {{name}}', { name: label.name }), variant: "plain", onClick: () => onOpenEditModal(label), "data-tracking-id": "case-label-edit" })),
188
+ React.createElement(ActionListItem, null,
189
+ React.createElement(Button, { icon: React.createElement(TrashIcon, null), "aria-label": t('Delete case label {{name}}', { name: label.name }), variant: "plain", onClick: () => onDeleteLabel(label), "data-tracking-id": "case-label-delete" }))))))),
190
+ },
191
+ ];
192
+ return (React.createElement(React.Fragment, null,
193
+ React.createElement("header", null,
194
+ React.createElement("h2", null, t('Case labels')),
195
+ React.createElement("p", null,
196
+ React.createElement(Trans, null, "Case labels help you organize and categorize cases with custom tags, making them easier to find, filter, and manage."))),
197
+ React.createElement("div", { className: "toolbar case-labels-toolbar" },
198
+ React.createElement("div", { className: "toolbar-left" },
199
+ hasNoLabels ? (React.createElement("span", null,
200
+ React.createElement(Trans, null, "Case labels"))) : (React.createElement("label", { htmlFor: "search-case-label-input" },
201
+ React.createElement(Trans, null, "Case labels"))),
202
+ !hasNoLabels && (React.createElement(React.Fragment, null,
203
+ React.createElement(SearchInput, { className: "pf-v6-u-flex-grow-1 pf-v6-c-search-input pf-v6-u-background-color-100", value: searchString, id: "search-case-label-input", placeholder: t('Search case label by name.'), "aria-label": t('Search case label by name'), onChange: (_, val) => setSearchString(val), onClear: () => setSearchString('') }),
204
+ canManageCaseLabels && (React.createElement("span", { className: "pf-v6-u-ml-sm" },
205
+ React.createElement(Button, { variant: ButtonVariant.primary, isInline: true, onClick: onOpenAddModal, "data-tracking-id": "case-label-add-button" },
206
+ React.createElement(Trans, null, "Add"))))))),
207
+ !hasNoLabels && (React.createElement("div", { className: "toolbar-right case-labels-toolbar__loaded pf-v6-u-color-200" }, t('{{loaded}} of {{total}} loaded.', {
208
+ loaded: filteredLabels.length,
209
+ total: filteredLabels.length,
210
+ })))),
211
+ React.createElement(AddEditCaseLabelModal, { isOpen: isModalOpen, addMode: isInAddMode, label: selectedLabel, isSaving: isCreatingLabel || isUpdatingLabel, onClose: closeModal, onSave: onSaveLabel }),
212
+ React.createElement(ErrorBoundary, { errorMsgInfo: { message: t('There was an error loading case labels list') } }, hasNoLabels ? (React.createElement("div", { className: "table-wrapper case-labels-empty-state" },
213
+ React.createElement(EmptyState, { headingLevel: "h2", icon: CubesIcon, titleText: canManageCaseLabels ? t('Create your first case label') : t('No case labels available'), variant: EmptyStateVariant.lg },
214
+ React.createElement(EmptyStateBody, null, canManageCaseLabels ? (React.createElement(Trans, null, "Case labels help you organize and categorize cases with custom tags, making them easier to find, filter, and manage.")) : (React.createElement(Trans, null, "Your organization admin hasn't created any labels yet. Contact them to add labels for organizing and categorizing cases."))),
215
+ canManageCaseLabels && (React.createElement(EmptyStateFooter, null,
216
+ React.createElement(Button, { variant: ButtonVariant.link, onClick: onOpenAddModal, "data-tracking-id": "case-label-empty-create-button" },
217
+ React.createElement(Trans, null, "Create label"))))))) : (React.createElement(ManageTable, { ariaLabel: t('Table to manage case labels'), columns: columns, data: filteredLabels, errorVariant: AlertVariant.warning, isFetching: isFetchingLabels, isError: !!labelsError, errorTitle: t('Case labels error'), errorComponent: React.createElement(Trans, null, "Could not get case labels"), noResultText: canManageCaseLabels
218
+ ? t('There are no results to display. Try adding a case label or refining your search')
219
+ : t('There are no results to display. Try refining your search') })))));
220
+ }
@@ -0,0 +1,2 @@
1
+ export * from './ManageCaseLabelsTab';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ManageCaseLabels/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './ManageCaseLabelsTab';
@@ -0,0 +1,6 @@
1
+ export interface ICaseLabel {
2
+ id: string;
3
+ name: string;
4
+ assignedCaseCount: number;
5
+ }
6
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/components/ManageCaseLabels/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,MAAM,CAAC;CAC7B"}