@oneuptime/common 9.5.7 → 9.5.9

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 (178) hide show
  1. package/Models/DatabaseModels/Alert.ts +8 -9
  2. package/Models/DatabaseModels/Incident.ts +5 -5
  3. package/Models/DatabaseModels/IncidentTemplate.ts +4 -3
  4. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -1
  5. package/Models/DatabaseModels/UserOnCallLog.ts +1 -1
  6. package/Models/DatabaseModels/UserPush.ts +2 -1
  7. package/Server/API/UserPushAPI.ts +51 -4
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1770833704656-MigrationName.ts +156 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1770834237090-MigrationName.ts +119 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/UserAuthorization.ts +14 -9
  12. package/Server/Services/AlertEpisodeFeedService.ts +50 -0
  13. package/Server/Services/AlertEpisodeInternalNoteService.ts +162 -0
  14. package/Server/Services/AlertEpisodeMemberService.ts +7 -0
  15. package/Server/Services/AlertEpisodeOwnerTeamService.ts +186 -0
  16. package/Server/Services/AlertEpisodeOwnerUserService.ts +180 -0
  17. package/Server/Services/AlertEpisodeService.ts +68 -0
  18. package/Server/Services/AlertEpisodeStateTimelineService.ts +5 -0
  19. package/Server/Services/AlertService.ts +3 -0
  20. package/Server/Services/IncidentEpisodeFeedService.ts +50 -0
  21. package/Server/Services/IncidentEpisodeInternalNoteService.ts +163 -0
  22. package/Server/Services/IncidentEpisodeMemberService.ts +7 -0
  23. package/Server/Services/IncidentEpisodeOwnerTeamService.ts +189 -0
  24. package/Server/Services/IncidentEpisodeOwnerUserService.ts +183 -0
  25. package/Server/Services/IncidentEpisodePublicNoteService.ts +8 -0
  26. package/Server/Services/IncidentEpisodeService.ts +91 -12
  27. package/Server/Services/IncidentEpisodeStateTimelineService.ts +5 -0
  28. package/Server/Services/IncidentService.ts +5 -0
  29. package/Server/Services/PushNotificationService.ts +129 -27
  30. package/Server/Services/UserNotificationRuleService.ts +13 -3
  31. package/Server/Services/UserPushService.ts +2 -1
  32. package/Server/Services/WorkspaceNotificationRuleService.ts +20 -0
  33. package/Server/Utils/PushNotificationUtil.ts +56 -0
  34. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +1 -1
  35. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +7 -6
  36. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +1 -1
  37. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +7 -6
  38. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +17 -0
  39. package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +27 -12
  40. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +17 -0
  41. package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +86 -28
  42. package/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.ts +6 -6
  43. package/Server/Utils/Workspace/Slack/Slack.ts +49 -0
  44. package/Server/Utils/Workspace/WorkspaceMessages/Alert.ts +2 -1
  45. package/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.ts +3 -1
  46. package/Server/Utils/Workspace/WorkspaceMessages/Incident.ts +2 -1
  47. package/Server/Utils/Workspace/WorkspaceMessages/IncidentEpisode.ts +3 -1
  48. package/Types/Permission.ts +641 -0
  49. package/Types/PushNotification/PushDeviceType.ts +7 -0
  50. package/Types/PushNotification/PushNotificationRequest.ts +3 -1
  51. package/UI/Components/Detail/Detail.tsx +13 -4
  52. package/UI/Components/Detail/Field.ts +2 -2
  53. package/UI/Components/Dropdown/Dropdown.tsx +38 -7
  54. package/UI/Components/Forms/BasicForm.tsx +35 -5
  55. package/UI/Components/Forms/Fields/PermissionPicker.tsx +261 -0
  56. package/UI/Components/Forms/Types/Field.ts +5 -3
  57. package/UI/Components/ModelDelete/ModelDelete.tsx +4 -1
  58. package/UI/Components/ModelDetail/CardModelDetail.tsx +4 -0
  59. package/UI/Components/ModelDetail/ModelDetail.tsx +4 -1
  60. package/UI/Components/Page/ModelPage.tsx +4 -1
  61. package/UI/Utils/Permission.ts +29 -6
  62. package/build/dist/Models/DatabaseModels/Alert.js +8 -8
  63. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/Incident.js +5 -5
  65. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +3 -3
  67. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -1
  69. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/UserOnCallLog.js +1 -1
  71. package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/UserPush.js +2 -1
  73. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  74. package/build/dist/Server/API/UserPushAPI.js +34 -3
  75. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770833704656-MigrationName.js +63 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770833704656-MigrationName.js.map +1 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770834237090-MigrationName.js +46 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770834237090-MigrationName.js.map +1 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  82. package/build/dist/Server/Middleware/UserAuthorization.js +10 -4
  83. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  84. package/build/dist/Server/Services/AlertEpisodeFeedService.js +33 -0
  85. package/build/dist/Server/Services/AlertEpisodeFeedService.js.map +1 -1
  86. package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js +132 -0
  87. package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js.map +1 -1
  88. package/build/dist/Server/Services/AlertEpisodeMemberService.js +7 -0
  89. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  90. package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js +163 -0
  91. package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js.map +1 -1
  92. package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js +156 -0
  93. package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js.map +1 -1
  94. package/build/dist/Server/Services/AlertEpisodeService.js +53 -0
  95. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  96. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +4 -0
  97. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
  98. package/build/dist/Server/Services/AlertService.js +3 -5
  99. package/build/dist/Server/Services/AlertService.js.map +1 -1
  100. package/build/dist/Server/Services/IncidentEpisodeFeedService.js +33 -0
  101. package/build/dist/Server/Services/IncidentEpisodeFeedService.js.map +1 -1
  102. package/build/dist/Server/Services/IncidentEpisodeInternalNoteService.js +132 -0
  103. package/build/dist/Server/Services/IncidentEpisodeInternalNoteService.js.map +1 -1
  104. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +7 -0
  105. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  106. package/build/dist/Server/Services/IncidentEpisodeOwnerTeamService.js +163 -0
  107. package/build/dist/Server/Services/IncidentEpisodeOwnerTeamService.js.map +1 -1
  108. package/build/dist/Server/Services/IncidentEpisodeOwnerUserService.js +156 -0
  109. package/build/dist/Server/Services/IncidentEpisodeOwnerUserService.js.map +1 -1
  110. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +8 -0
  111. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  112. package/build/dist/Server/Services/IncidentEpisodeService.js +72 -10
  113. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  114. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +4 -0
  115. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  116. package/build/dist/Server/Services/IncidentService.js +5 -5
  117. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  118. package/build/dist/Server/Services/PushNotificationService.js +77 -21
  119. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  120. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -9
  121. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  122. package/build/dist/Server/Services/UserPushService.js +2 -1
  123. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  124. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +16 -0
  125. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  126. package/build/dist/Server/Utils/PushNotificationUtil.js +32 -8
  127. package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
  128. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +1 -1
  129. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  130. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +7 -6
  131. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  132. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +1 -1
  133. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  134. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +7 -6
  135. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  136. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +16 -0
  137. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  138. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +25 -9
  139. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
  140. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +16 -0
  141. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  142. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +71 -25
  143. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
  144. package/build/dist/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.js +6 -6
  145. package/build/dist/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.js.map +1 -1
  146. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +40 -0
  147. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  148. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Alert.js +1 -1
  149. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Alert.js.map +1 -1
  150. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js +1 -1
  151. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js.map +1 -1
  152. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Incident.js +1 -1
  153. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Incident.js.map +1 -1
  154. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/IncidentEpisode.js +1 -1
  155. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/IncidentEpisode.js.map +1 -1
  156. package/build/dist/Types/Permission.js +637 -0
  157. package/build/dist/Types/Permission.js.map +1 -1
  158. package/build/dist/Types/PushNotification/PushDeviceType.js +8 -0
  159. package/build/dist/Types/PushNotification/PushDeviceType.js.map +1 -0
  160. package/build/dist/UI/Components/Detail/Detail.js +7 -1
  161. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  162. package/build/dist/UI/Components/Dropdown/Dropdown.js +17 -2
  163. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  164. package/build/dist/UI/Components/Forms/BasicForm.js +17 -3
  165. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  166. package/build/dist/UI/Components/Forms/Fields/PermissionPicker.js +129 -0
  167. package/build/dist/UI/Components/Forms/Fields/PermissionPicker.js.map +1 -0
  168. package/build/dist/UI/Components/ModelDelete/ModelDelete.js +2 -1
  169. package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
  170. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  171. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  172. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +2 -1
  173. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  174. package/build/dist/UI/Components/Page/ModelPage.js +2 -1
  175. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  176. package/build/dist/UI/Utils/Permission.js +17 -4
  177. package/build/dist/UI/Utils/Permission.js.map +1 -1
  178. package/package.json +2 -1
@@ -0,0 +1,7 @@
1
+ enum PushDeviceType {
2
+ Web = "web",
3
+ iOS = "ios",
4
+ Android = "android",
5
+ }
6
+
7
+ export default PushDeviceType;
@@ -1,3 +1,5 @@
1
+ import PushDeviceType from "./PushDeviceType";
2
+
1
3
  interface PushNotificationRequest {
2
4
  devices: Array<{
3
5
  token: string;
@@ -19,7 +21,7 @@ interface PushNotificationRequest {
19
21
  clickAction?: string;
20
22
  url?: string;
21
23
  };
22
- deviceType: "web";
24
+ deviceType: PushDeviceType;
23
25
  }
24
26
 
25
27
  export default PushNotificationRequest;
@@ -4,7 +4,7 @@ import CodeBlock from "../CodeBlock/CodeBlock";
4
4
  import ColorViewer from "../ColorViewer/ColorViewer";
5
5
  import CopyableButton from "../CopyableButton/CopyableButton";
6
6
  import DictionaryOfStringsViewer from "../Dictionary/DictionaryOfStingsViewer";
7
- import { DropdownOption } from "../Dropdown/Dropdown";
7
+ import { DropdownOption, DropdownOptionGroup } from "../Dropdown/Dropdown";
8
8
  import HiddenText from "../HiddenText/HiddenText";
9
9
  import MarkdownViewer from "../Markdown.tsx/LazyMarkdownViewer";
10
10
  import ObjectIDView from "../ObjectID/ObjectIDView";
@@ -72,13 +72,13 @@ const Detail: DetailFunction = <T extends GenericObject>(
72
72
 
73
73
  type GetDropdownViewerFunction = (
74
74
  data: string,
75
- options: Array<DropdownOption>,
75
+ options: Array<DropdownOption | DropdownOptionGroup>,
76
76
  placeholder: string,
77
77
  ) => ReactElement;
78
78
 
79
79
  const getDropdownViewer: GetDropdownViewerFunction = (
80
80
  data: string,
81
- options: Array<DropdownOption>,
81
+ options: Array<DropdownOption | DropdownOptionGroup>,
82
82
  placeholder: string,
83
83
  ): ReactElement => {
84
84
  if (!options) {
@@ -87,7 +87,16 @@ const Detail: DetailFunction = <T extends GenericObject>(
87
87
  );
88
88
  }
89
89
 
90
- const selectedOption: DropdownOption | undefined = options.find(
90
+ const flatOptions: Array<DropdownOption> = options.flatMap(
91
+ (item: DropdownOption | DropdownOptionGroup) => {
92
+ if ("options" in item && Array.isArray(item.options)) {
93
+ return item.options;
94
+ }
95
+ return [item as DropdownOption];
96
+ },
97
+ );
98
+
99
+ const selectedOption: DropdownOption | undefined = flatOptions.find(
91
100
  (i: DropdownOption) => {
92
101
  return i.value === data;
93
102
  },
@@ -1,5 +1,5 @@
1
1
  import AlignItem from "../../Types/AlignItem";
2
- import { DropdownOption } from "../Dropdown/Dropdown";
2
+ import { DropdownOption, DropdownOptionGroup } from "../Dropdown/Dropdown";
3
3
  import FieldType from "../Types/FieldType";
4
4
  import { Size } from "./FieldLabel";
5
5
  import Route from "../../../Types/API/Route";
@@ -18,7 +18,7 @@ export interface FieldBase<T> {
18
18
  description?: string | ReactElement;
19
19
  fieldTitleSize?: Size | undefined;
20
20
  fieldType?: FieldType;
21
- dropdownOptions?: Array<DropdownOption> | undefined;
21
+ dropdownOptions?: Array<DropdownOption | DropdownOptionGroup> | undefined;
22
22
  colSpan?: number | undefined;
23
23
  alignItem?: AlignItem | undefined;
24
24
  contentClassName?: string | undefined;
@@ -35,8 +35,13 @@ export interface DropdownOption {
35
35
  color?: Color;
36
36
  }
37
37
 
38
- export interface ComponentProps {
38
+ export interface DropdownOptionGroup {
39
+ label: string;
39
40
  options: Array<DropdownOption>;
41
+ }
42
+
43
+ export interface ComponentProps {
44
+ options: Array<DropdownOption | DropdownOptionGroup>;
40
45
  initialValue?: undefined | DropdownOption | Array<DropdownOption>;
41
46
  onClick?: undefined | (() => void);
42
47
  placeholder?: undefined | string;
@@ -61,6 +66,25 @@ const Dropdown: FunctionComponent<ComponentProps> = (
61
66
  const uniqueId: string = useId();
62
67
  const errorId: string = `dropdown-error-${uniqueId}`;
63
68
 
69
+ const isDropdownOptionGroup: (
70
+ item: DropdownOption | DropdownOptionGroup,
71
+ ) => item is DropdownOptionGroup = (
72
+ item: DropdownOption | DropdownOptionGroup,
73
+ ): item is DropdownOptionGroup => {
74
+ return (
75
+ "options" in item && Array.isArray((item as DropdownOptionGroup).options)
76
+ );
77
+ };
78
+
79
+ const flatOptions: Array<DropdownOption> = props.options.flatMap(
80
+ (item: DropdownOption | DropdownOptionGroup) => {
81
+ if (isDropdownOptionGroup(item)) {
82
+ return item.options;
83
+ }
84
+ return [item];
85
+ },
86
+ );
87
+
64
88
  type GetDropdownOptionFromValueFunctionProps =
65
89
  | undefined
66
90
  | DropdownValue
@@ -103,13 +127,14 @@ const Dropdown: FunctionComponent<ComponentProps> = (
103
127
  !Array.isArray(item) &&
104
128
  (typeof item === "string" || typeof item === "number")
105
129
  ) {
106
- const option: DropdownOption | undefined | Array<DropdownOption> =
107
- props.options.find((option: DropdownOption) => {
130
+ const option: DropdownOption | undefined = flatOptions.find(
131
+ (option: DropdownOption) => {
108
132
  return option.value === item;
109
- }) as DropdownOption | Array<DropdownOption>;
133
+ },
134
+ );
110
135
 
111
136
  if (option) {
112
- options.push(option as DropdownOption);
137
+ options.push(option);
113
138
  }
114
139
  }
115
140
  }
@@ -121,9 +146,9 @@ const Dropdown: FunctionComponent<ComponentProps> = (
121
146
  !Array.isArray(value) &&
122
147
  (typeof value === "string" || typeof value === "number")
123
148
  ) {
124
- return props.options.find((option: DropdownOption) => {
149
+ return flatOptions.find((option: DropdownOption) => {
125
150
  return option.value === value;
126
- }) as DropdownOption | Array<DropdownOption>;
151
+ });
127
152
  }
128
153
 
129
154
  return value as DropdownOption | Array<DropdownOption>;
@@ -569,6 +594,12 @@ const Dropdown: FunctionComponent<ComponentProps> = (
569
594
 
570
595
  return "px-3 py-2 text-sm text-gray-700";
571
596
  },
597
+ group: () => {
598
+ return "py-1";
599
+ },
600
+ groupHeading: () => {
601
+ return "px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500";
602
+ },
572
603
  noOptionsMessage: () => {
573
604
  return "px-3 py-2 text-sm text-gray-500";
574
605
  },
@@ -4,7 +4,11 @@ import Alert, { AlertType } from "../Alerts/Alert";
4
4
  import Button, { ButtonStyleType } from "../Button/Button";
5
5
  import ButtonTypes from "../Button/ButtonTypes";
6
6
 
7
- import { DropdownOption, DropdownValue } from "../Dropdown/Dropdown";
7
+ import {
8
+ DropdownOption,
9
+ DropdownOptionGroup,
10
+ DropdownValue,
11
+ } from "../Dropdown/Dropdown";
8
12
  import ErrorMessage from "../ErrorMessage/ErrorMessage";
9
13
  import FormField from "./Fields/FormField";
10
14
  import FormSummary from "./FormSummary";
@@ -301,7 +305,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
301
305
  setIsDropdownOptionsLoading(true);
302
306
  // If a dropdown has fetch optiosn then we need to fetch them
303
307
  try {
304
- const options: Array<DropdownOption> =
308
+ const options: Array<DropdownOption | DropdownOptionGroup> =
305
309
  await item.fetchDropdownOptions(refCurrentValue.current);
306
310
  item.dropdownOptions = options;
307
311
  } catch (err) {
@@ -501,8 +505,21 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
501
505
  field.fieldType === FormFieldSchemaType.Dropdown &&
502
506
  (values as any)[fieldName]
503
507
  ) {
508
+ const flatDropdownOptions: Array<DropdownOption> =
509
+ field.dropdownOptions?.flatMap(
510
+ (item: DropdownOption | DropdownOptionGroup) => {
511
+ if (
512
+ "options" in item &&
513
+ Array.isArray((item as DropdownOptionGroup).options)
514
+ ) {
515
+ return (item as DropdownOptionGroup).options;
516
+ }
517
+ return [item as DropdownOption];
518
+ },
519
+ ) || [];
520
+
504
521
  const dropdownOption: DropdownOption | undefined =
505
- field.dropdownOptions?.find((option: DropdownOption) => {
522
+ flatDropdownOptions.find((option: DropdownOption) => {
506
523
  let valueToCompare: DropdownValue = (values as any)[fieldName];
507
524
 
508
525
  if ((valueToCompare as any) instanceof ObjectID) {
@@ -519,8 +536,21 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
519
536
  field.fieldType === FormFieldSchemaType.MultiSelectDropdown &&
520
537
  (values as any)[fieldName]
521
538
  ) {
539
+ const flatDropdownOptions: Array<DropdownOption> =
540
+ field.dropdownOptions?.flatMap(
541
+ (item: DropdownOption | DropdownOptionGroup) => {
542
+ if (
543
+ "options" in item &&
544
+ Array.isArray((item as DropdownOptionGroup).options)
545
+ ) {
546
+ return (item as DropdownOptionGroup).options;
547
+ }
548
+ return [item as DropdownOption];
549
+ },
550
+ ) || [];
551
+
522
552
  const dropdownOptions: Array<DropdownOption> =
523
- field.dropdownOptions?.filter((option: DropdownOption) => {
553
+ flatDropdownOptions.filter((option: DropdownOption) => {
524
554
  let valueToCompare: Array<DropdownValue> = [
525
555
  ...(values as any)[fieldName],
526
556
  ];
@@ -534,7 +564,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
534
564
  });
535
565
 
536
566
  return valueToCompare.includes(option.value);
537
- }) || [];
567
+ });
538
568
 
539
569
  (values as any)[fieldName] = dropdownOptions.map(
540
570
  (option: DropdownOption) => {
@@ -0,0 +1,261 @@
1
+ import Permission, {
2
+ PermissionGroup,
3
+ PermissionHelper,
4
+ PermissionProps,
5
+ } from "../../../../Types/Permission";
6
+ import React, {
7
+ FunctionComponent,
8
+ ReactElement,
9
+ useEffect,
10
+ useMemo,
11
+ useState,
12
+ } from "react";
13
+
14
+ export interface ComponentProps {
15
+ onChange: (value: Permission | null) => void;
16
+ initialValue?: Permission | undefined;
17
+ placeholder?: string | undefined;
18
+ onFocus?: (() => void) | undefined;
19
+ tabIndex?: number | undefined;
20
+ onBlur?: (() => void) | undefined;
21
+ error?: string | undefined;
22
+ }
23
+
24
+ const PermissionPicker: FunctionComponent<ComponentProps> = (
25
+ props: ComponentProps,
26
+ ): ReactElement => {
27
+ const [selectedPermission, setSelectedPermission] =
28
+ useState<Permission | null>(props.initialValue || null);
29
+ const [activeGroup, setActiveGroup] = useState<PermissionGroup | null>(null);
30
+ const [searchQuery, setSearchQuery] = useState<string>("");
31
+
32
+ const allPermissions: Array<PermissionProps> = useMemo(() => {
33
+ return PermissionHelper.getTenantPermissionProps();
34
+ }, []);
35
+
36
+ const groupedPermissions: Map<
37
+ PermissionGroup,
38
+ Array<PermissionProps>
39
+ > = useMemo(() => {
40
+ const map: Map<PermissionGroup, Array<PermissionProps>> = new Map();
41
+ for (const perm of allPermissions) {
42
+ if (!map.has(perm.group)) {
43
+ map.set(perm.group, []);
44
+ }
45
+ map.get(perm.group)!.push(perm);
46
+ }
47
+ return map;
48
+ }, [allPermissions]);
49
+
50
+ const groups: Array<PermissionGroup> = useMemo(() => {
51
+ return Array.from(groupedPermissions.keys());
52
+ }, [groupedPermissions]);
53
+
54
+ // Auto-select the group for the initial value
55
+ useEffect(() => {
56
+ if (props.initialValue && !activeGroup) {
57
+ const match: PermissionProps | undefined = allPermissions.find(
58
+ (p: PermissionProps) => {
59
+ return p.permission === props.initialValue;
60
+ },
61
+ );
62
+ if (match) {
63
+ setActiveGroup(match.group);
64
+ }
65
+ }
66
+ }, [props.initialValue, allPermissions, activeGroup]);
67
+
68
+ // Default to first group if none selected
69
+ useEffect(() => {
70
+ if (!activeGroup && groups.length > 0 && !props.initialValue) {
71
+ setActiveGroup(groups[0]!);
72
+ }
73
+ }, [activeGroup, groups, props.initialValue]);
74
+
75
+ const isSearching: boolean = searchQuery.trim().length > 0;
76
+ const lowerQuery: string = searchQuery.toLowerCase();
77
+
78
+ const filteredPermissions: Array<PermissionProps> = useMemo(() => {
79
+ if (!isSearching) {
80
+ return [];
81
+ }
82
+ return allPermissions.filter((p: PermissionProps) => {
83
+ return (
84
+ p.title.toLowerCase().includes(lowerQuery) ||
85
+ p.description.toLowerCase().includes(lowerQuery)
86
+ );
87
+ });
88
+ }, [allPermissions, isSearching, lowerQuery]);
89
+
90
+ const searchMatchCountByGroup: Map<PermissionGroup, number> = useMemo(() => {
91
+ const counts: Map<PermissionGroup, number> = new Map();
92
+ if (!isSearching) {
93
+ return counts;
94
+ }
95
+ for (const perm of filteredPermissions) {
96
+ counts.set(perm.group, (counts.get(perm.group) || 0) + 1);
97
+ }
98
+ return counts;
99
+ }, [filteredPermissions, isSearching]);
100
+
101
+ const visiblePermissions: Array<PermissionProps> = useMemo(() => {
102
+ if (isSearching) {
103
+ if (activeGroup) {
104
+ return filteredPermissions.filter((p: PermissionProps) => {
105
+ return p.group === activeGroup;
106
+ });
107
+ }
108
+ return filteredPermissions;
109
+ }
110
+ if (!activeGroup) {
111
+ return [];
112
+ }
113
+ return groupedPermissions.get(activeGroup) || [];
114
+ }, [isSearching, activeGroup, filteredPermissions, groupedPermissions]);
115
+
116
+ type HandlePermissionClickFunction = (perm: PermissionProps) => void;
117
+
118
+ const handlePermissionClick: HandlePermissionClickFunction = (
119
+ perm: PermissionProps,
120
+ ): void => {
121
+ setSelectedPermission(perm.permission);
122
+ props.onChange(perm.permission);
123
+ };
124
+
125
+ type GetGroupCountFunction = (group: PermissionGroup) => number;
126
+
127
+ const getGroupCount: GetGroupCountFunction = (
128
+ group: PermissionGroup,
129
+ ): number => {
130
+ if (isSearching) {
131
+ return searchMatchCountByGroup.get(group) || 0;
132
+ }
133
+ return groupedPermissions.get(group)?.length || 0;
134
+ };
135
+
136
+ return (
137
+ <div
138
+ tabIndex={props.tabIndex}
139
+ onFocus={props.onFocus}
140
+ onBlur={props.onBlur}
141
+ >
142
+ <div
143
+ className={`border rounded-md overflow-hidden ${
144
+ props.error ? "border-red-400" : "border-gray-300"
145
+ }`}
146
+ style={{ height: "400px" }}
147
+ >
148
+ {/* Search bar */}
149
+ <div className="border-b border-gray-200 p-2">
150
+ <input
151
+ type="text"
152
+ className="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
153
+ placeholder={props.placeholder || "Search permissions..."}
154
+ value={searchQuery}
155
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
156
+ setSearchQuery(e.target.value);
157
+ }}
158
+ />
159
+ </div>
160
+
161
+ <div className="flex" style={{ height: "calc(100% - 45px)" }}>
162
+ {/* Left sidebar - groups */}
163
+ <div
164
+ className="border-r border-gray-200 overflow-y-auto bg-gray-50 flex-shrink-0"
165
+ style={{ width: "200px" }}
166
+ >
167
+ {groups.map((group: PermissionGroup) => {
168
+ const count: number = getGroupCount(group);
169
+ const isActive: boolean = activeGroup === group;
170
+ const isDimmed: boolean = isSearching && count === 0;
171
+
172
+ return (
173
+ <button
174
+ key={group}
175
+ type="button"
176
+ className={`w-full text-left px-3 py-2 text-sm flex items-center justify-between border-b border-gray-100 transition-colors ${
177
+ isActive
178
+ ? "bg-indigo-50 text-indigo-700 font-medium"
179
+ : isDimmed
180
+ ? "text-gray-300 cursor-default"
181
+ : "text-gray-700 hover:bg-gray-100"
182
+ }`}
183
+ onClick={() => {
184
+ if (!isDimmed) {
185
+ setActiveGroup(group);
186
+ }
187
+ }}
188
+ >
189
+ <span className="truncate">{group}</span>
190
+ <span
191
+ className={`ml-1 text-xs flex-shrink-0 ${
192
+ isActive ? "text-indigo-500" : "text-gray-400"
193
+ }`}
194
+ >
195
+ {count}
196
+ </span>
197
+ </button>
198
+ );
199
+ })}
200
+ </div>
201
+
202
+ {/* Right panel - permissions */}
203
+ <div className="flex-1 overflow-y-auto">
204
+ {visiblePermissions.length === 0 && (
205
+ <div className="flex items-center justify-center h-full text-gray-400 text-sm">
206
+ {isSearching
207
+ ? "No permissions match your search."
208
+ : "Select a group to view permissions."}
209
+ </div>
210
+ )}
211
+
212
+ {visiblePermissions.map((perm: PermissionProps) => {
213
+ const isSelected: boolean =
214
+ selectedPermission === perm.permission;
215
+ return (
216
+ <button
217
+ key={perm.permission}
218
+ type="button"
219
+ className={`w-full text-left px-4 py-2.5 border-b border-gray-100 transition-colors ${
220
+ isSelected
221
+ ? "bg-indigo-50 border-l-2 border-l-indigo-500"
222
+ : "hover:bg-gray-50"
223
+ }`}
224
+ onClick={() => {
225
+ handlePermissionClick(perm);
226
+ }}
227
+ >
228
+ <div className="flex items-start gap-2">
229
+ <div className="flex-1 min-w-0">
230
+ <div
231
+ className={`text-sm font-medium ${
232
+ isSelected ? "text-indigo-700" : "text-gray-900"
233
+ }`}
234
+ >
235
+ {perm.title}
236
+ </div>
237
+ <div className="text-xs text-gray-500 mt-0.5">
238
+ {perm.description}
239
+ </div>
240
+ </div>
241
+ {isSearching && (
242
+ <span className="inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-600 flex-shrink-0">
243
+ {perm.group}
244
+ </span>
245
+ )}
246
+ </div>
247
+ </button>
248
+ );
249
+ })}
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ {props.error && (
255
+ <p className="mt-1 text-sm text-red-400">{props.error}</p>
256
+ )}
257
+ </div>
258
+ );
259
+ };
260
+
261
+ export default PermissionPicker;
@@ -4,7 +4,7 @@ import {
4
4
  CheckboxCategory,
5
5
  } from "../../CategoryCheckbox/CategoryCheckboxTypes";
6
6
  import { CardSelectOption } from "../../CardSelect/CardSelect";
7
- import { DropdownOption } from "../../Dropdown/Dropdown";
7
+ import { DropdownOption, DropdownOptionGroup } from "../../Dropdown/Dropdown";
8
8
  import { RadioButton } from "../../RadioButtons/GroupRadioButtons";
9
9
  import FormFieldSchemaType from "./FormFieldSchemaType";
10
10
  import FormValues from "./FormValues";
@@ -50,10 +50,12 @@ export default interface Field<TEntity> {
50
50
  disabled?: boolean;
51
51
  stepId?: string | undefined;
52
52
  required?: boolean | ((item: FormValues<TEntity>) => boolean) | undefined;
53
- dropdownOptions?: Array<DropdownOption> | undefined;
53
+ dropdownOptions?: Array<DropdownOption | DropdownOptionGroup> | undefined;
54
54
  cardSelectOptions?: Array<CardSelectOption> | undefined;
55
55
  fetchDropdownOptions?:
56
- | ((item: FormValues<TEntity>) => Promise<Array<DropdownOption>>)
56
+ | ((
57
+ item: FormValues<TEntity>,
58
+ ) => Promise<Array<DropdownOption | DropdownOptionGroup>>)
57
59
  | undefined;
58
60
  showHorizontalRuleBelow?: boolean | undefined;
59
61
  showHorizontalRuleAbove?: boolean | undefined;
@@ -12,6 +12,7 @@ import React, { ReactElement, useState } from "react";
12
12
  export interface ComponentProps<TBaseModel extends BaseModel> {
13
13
  modelType: { new (): TBaseModel };
14
14
  modelId: ObjectID;
15
+ modelAPI?: typeof ModelAPI | undefined;
15
16
  onDeleteSuccess: () => void;
16
17
  }
17
18
 
@@ -29,7 +30,9 @@ const ModelDelete: <TBaseModel extends BaseModel>(
29
30
  const deleteItem: PromiseVoidFunction = async (): Promise<void> => {
30
31
  setIsLoading(true);
31
32
  try {
32
- await ModelAPI.deleteItem<TBaseModel>({
33
+ const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI;
34
+
35
+ await modelAPI.deleteItem<TBaseModel>({
33
36
  modelType: props.modelType,
34
37
  id: props.modelId,
35
38
  });
@@ -1,3 +1,4 @@
1
+ import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
1
2
  import PermissionUtil from "../../Utils/Permission";
2
3
  import User from "../../Utils/User";
3
4
  import Navigation from "../../Utils/Navigation";
@@ -33,6 +34,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
33
34
  formFields?: undefined | Fields<TBaseModel>;
34
35
  className?: string | undefined;
35
36
  name: string;
37
+ modelAPI?: typeof ModelAPI | undefined;
36
38
  createEditModalWidth?: ModalWidth | undefined;
37
39
  refresher?: boolean;
38
40
  createOrUpdateApiUrl?: URL | undefined;
@@ -130,6 +132,7 @@ const CardModelDetail: <TBaseModel extends BaseModel>(
130
132
  <ModelDetail
131
133
  refresher={refresher}
132
134
  {...props.modelDetailProps}
135
+ modelAPI={props.modelAPI}
133
136
  onItemLoaded={(item: TBaseModel) => {
134
137
  setItem(item);
135
138
  if (props.modelDetailProps.onItemLoaded) {
@@ -144,6 +147,7 @@ const CardModelDetail: <TBaseModel extends BaseModel>(
144
147
  <ModelFormModal<TBaseModel>
145
148
  title={`Edit ${model.singularName}`}
146
149
  modalWidth={props.createEditModalWidth}
150
+ modelAPI={props.modelAPI}
147
151
  onClose={() => {
148
152
  setShowModal(false);
149
153
  }}
@@ -28,6 +28,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
28
28
  fields: Array<Field<TBaseModel>>;
29
29
  onLoadingChange?: undefined | ((isLoading: boolean) => void);
30
30
  modelId: ObjectID;
31
+ modelAPI?: typeof ModelAPI | undefined;
31
32
  onError?: ((error: string) => void) | undefined;
32
33
  onItemLoaded?: (item: TBaseModel) => void | undefined;
33
34
  refresher?: undefined | boolean;
@@ -179,7 +180,9 @@ const ModelDetail: <TBaseModel extends BaseModel>(
179
180
  setOnBeforeFetchData(model);
180
181
  }
181
182
 
182
- const item: TBaseModel | null = await ModelAPI.getItem({
183
+ const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI;
184
+
185
+ const item: TBaseModel | null = await modelAPI.getItem({
183
186
  modelType: props.modelType,
184
187
  id: props.modelId,
185
188
  select: {
@@ -20,6 +20,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
20
20
  modelType: { new (): TBaseModel };
21
21
  modelId: ObjectID;
22
22
  modelNameField: string;
23
+ modelAPI?: typeof ModelAPI | undefined;
23
24
  }
24
25
 
25
26
  const ModelPage: <TBaseModel extends BaseModel>(
@@ -54,7 +55,9 @@ const ModelPage: <TBaseModel extends BaseModel>(
54
55
  } as Select<TBaseModel>;
55
56
  }
56
57
 
57
- const item: TBaseModel | null = await ModelAPI.getItem({
58
+ const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI;
59
+
60
+ const item: TBaseModel | null = await modelAPI.getItem({
58
61
  modelType: props.modelType,
59
62
  id: props.modelId,
60
63
  select: select as Select<TBaseModel>,
@@ -1,7 +1,11 @@
1
- import { DropdownOption } from "../Components/Dropdown/Dropdown";
1
+ import {
2
+ DropdownOption,
3
+ DropdownOptionGroup,
4
+ } from "../Components/Dropdown/Dropdown";
2
5
  import LocalStorage from "./LocalStorage";
3
6
  import { JSONObject } from "../../Types/JSON";
4
7
  import Permission, {
8
+ PermissionGroup,
5
9
  PermissionHelper,
6
10
  PermissionProps,
7
11
  UserGlobalAccessPermission,
@@ -60,16 +64,35 @@ export default class PermissionUtil {
60
64
  return userTenantAccessPermission;
61
65
  }
62
66
 
63
- public static projectPermissionsAsDropdownOptions(): Array<DropdownOption> {
67
+ public static projectPermissionsAsDropdownOptions(): Array<DropdownOptionGroup> {
64
68
  const permissions: Array<PermissionProps> =
65
69
  PermissionHelper.getTenantPermissionProps();
66
70
 
67
- return permissions.map((permissionProp: PermissionProps) => {
68
- return {
71
+ const groupMap: Map<PermissionGroup, Array<DropdownOption>> = new Map();
72
+
73
+ for (const permissionProp of permissions) {
74
+ const group: PermissionGroup = permissionProp.group;
75
+
76
+ if (!groupMap.has(group)) {
77
+ groupMap.set(group, []);
78
+ }
79
+
80
+ groupMap.get(group)!.push({
69
81
  value: permissionProp.permission,
70
82
  label: permissionProp.title,
71
- };
72
- });
83
+ });
84
+ }
85
+
86
+ const groups: Array<DropdownOptionGroup> = [];
87
+
88
+ for (const [group, options] of groupMap) {
89
+ groups.push({
90
+ label: group,
91
+ options,
92
+ });
93
+ }
94
+
95
+ return groups;
73
96
  }
74
97
 
75
98
  public static setGlobalPermissions(