@oneuptime/common 7.0.4066 → 7.0.4080

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 (33) hide show
  1. package/Server/API/StatusPageAPI.ts +426 -0
  2. package/Server/EnvironmentConfig.ts +7 -0
  3. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1 -1
  4. package/Server/Utils/StartServer.ts +34 -5
  5. package/Types/Email/EmailTemplateType.ts +1 -0
  6. package/UI/Components/Button/DropdownButton.tsx +93 -0
  7. package/UI/Components/Forms/BasicForm.tsx +14 -2
  8. package/UI/Components/Forms/BasicModelForm.tsx +8 -1
  9. package/UI/Components/Forms/Fields/FormField.tsx +32 -18
  10. package/UI/Components/Forms/ModelForm.tsx +13 -3
  11. package/UI/Components/Forms/Types/Field.ts +7 -1
  12. package/UI/Images/favicon/status-green.png +0 -0
  13. package/build/dist/Server/API/StatusPageAPI.js +320 -36
  14. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  15. package/build/dist/Server/EnvironmentConfig.js +2 -1
  16. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  17. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +1 -1
  18. package/build/dist/Server/Utils/StartServer.js +16 -4
  19. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  20. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  21. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  22. package/build/dist/UI/Components/Button/DropdownButton.js +20 -0
  23. package/build/dist/UI/Components/Button/DropdownButton.js.map +1 -0
  24. package/build/dist/UI/Components/Forms/BasicForm.js +8 -2
  25. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  26. package/build/dist/UI/Components/Forms/BasicModelForm.js +1 -1
  27. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  28. package/build/dist/UI/Components/Forms/Fields/FormField.js +23 -18
  29. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  30. package/build/dist/UI/Components/Forms/ModelForm.js +2 -2
  31. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  32. package/package.json +2 -2
  33. package/UI/webpack-middleware.js +0 -65
@@ -79,6 +79,17 @@ import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
79
79
  import { Green } from "../../Types/BrandColors";
80
80
  import UptimeUtil from "../../Utils/Uptime/UptimeUtil";
81
81
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
82
+ import URL from "Common/Types/API/URL";
83
+ import SMS from "../../Types/SMS/SMS";
84
+ import SmsService from "../Services/SmsService";
85
+ import ProjectCallSMSConfigService from "../Services/ProjectCallSMSConfigService";
86
+ import MailService from "../Services/MailService";
87
+ import EmailTemplateType from "../../Types/Email/EmailTemplateType";
88
+ import Hostname from "../../Types/API/Hostname";
89
+ import Protocol from "../../Types/API/Protocol";
90
+ import DatabaseConfig from "../DatabaseConfig";
91
+ import { FileRoute } from "../../ServiceRoute";
92
+ import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
82
93
 
83
94
  export default class StatusPageAPI extends BaseAPI<
84
95
  StatusPage,
@@ -87,6 +98,178 @@ export default class StatusPageAPI extends BaseAPI<
87
98
  public constructor() {
88
99
  super(StatusPage, StatusPageService);
89
100
 
101
+ // get title, description of the page. This is used for SEO.
102
+ this.router.get(
103
+ `${new this.entityType().getCrudApiPath()?.toString()}/seo/:statusPageIdOrDomain`,
104
+ UserMiddleware.getUserMiddleware,
105
+ async (req: ExpressRequest, res: ExpressResponse) => {
106
+ const statusPageIdOrDomain: string = req.params[
107
+ "statusPageIdOrDomain"
108
+ ] as string;
109
+
110
+ let statusPageId: ObjectID | null = null;
111
+
112
+ if (statusPageIdOrDomain && statusPageIdOrDomain.includes(".")) {
113
+ // then this is a domain and not the status page id. We need to get the status page id from the domain.
114
+
115
+ const statusPageDomain: StatusPageDomain | null =
116
+ await StatusPageDomainService.findOneBy({
117
+ query: {
118
+ fullDomain: statusPageIdOrDomain,
119
+ domain: {
120
+ isVerified: true,
121
+ } as any,
122
+ },
123
+ select: {
124
+ statusPageId: true,
125
+ },
126
+ props: {
127
+ isRoot: true,
128
+ },
129
+ });
130
+
131
+ if (!statusPageDomain || !statusPageDomain.statusPageId) {
132
+ return Response.sendErrorResponse(
133
+ req,
134
+ res,
135
+ new NotFoundException("Status Page not found"),
136
+ );
137
+ }
138
+
139
+ statusPageId = statusPageDomain.statusPageId;
140
+ } else {
141
+ // then this is a status page id. We need to get the status page id from the id.
142
+ try {
143
+ statusPageId = new ObjectID(statusPageIdOrDomain);
144
+ } catch (err) {
145
+ return Response.sendErrorResponse(
146
+ req,
147
+ res,
148
+ new NotFoundException("Status Page not found"),
149
+ );
150
+ }
151
+ }
152
+
153
+ const statusPage: StatusPage | null = await StatusPageService.findOneBy(
154
+ {
155
+ query: {
156
+ _id: statusPageId,
157
+ },
158
+ select: {
159
+ pageTitle: true,
160
+ pageDescription: true,
161
+ name: true,
162
+ },
163
+ props: {
164
+ isRoot: true,
165
+ },
166
+ },
167
+ );
168
+
169
+ if (!statusPage) {
170
+ return Response.sendErrorResponse(
171
+ req,
172
+ res,
173
+ new NotFoundException("Status Page not found"),
174
+ );
175
+ }
176
+
177
+ return Response.sendJsonObjectResponse(req, res, {
178
+ title: statusPage.pageTitle || statusPage.name,
179
+ description: statusPage.pageDescription,
180
+ });
181
+ },
182
+ );
183
+
184
+ // favicon api.
185
+ this.router.get(
186
+ `${new this.entityType().getCrudApiPath()?.toString()}/favicon/:statusPageIdOrDomain`,
187
+ async (req: ExpressRequest, res: ExpressResponse) => {
188
+ const statusPageIdOrDomain: string = req.params[
189
+ "statusPageIdOrDomain"
190
+ ] as string;
191
+
192
+ let statusPageId: ObjectID | null = null;
193
+
194
+ if (statusPageIdOrDomain && statusPageIdOrDomain.includes(".")) {
195
+ // then this is a domain and not the status page id. We need to get the status page id from the domain.
196
+
197
+ const statusPageDomain: StatusPageDomain | null =
198
+ await StatusPageDomainService.findOneBy({
199
+ query: {
200
+ fullDomain: statusPageIdOrDomain,
201
+ domain: {
202
+ isVerified: true,
203
+ } as any,
204
+ },
205
+ select: {
206
+ statusPageId: true,
207
+ },
208
+ props: {
209
+ isRoot: true,
210
+ },
211
+ });
212
+
213
+ if (!statusPageDomain || !statusPageDomain.statusPageId) {
214
+ return Response.sendErrorResponse(
215
+ req,
216
+ res,
217
+ new NotFoundException("Status Page not found"),
218
+ );
219
+ }
220
+
221
+ statusPageId = statusPageDomain.statusPageId;
222
+ } else {
223
+ // then this is a status page id. We need to get the status page id from the id.
224
+ try {
225
+ statusPageId = new ObjectID(statusPageIdOrDomain);
226
+ } catch (err) {
227
+ return Response.sendErrorResponse(
228
+ req,
229
+ res,
230
+ new NotFoundException("Status Page not found"),
231
+ );
232
+ }
233
+ }
234
+
235
+ const statusPage: StatusPage | null = await StatusPageService.findOneBy(
236
+ {
237
+ query: {
238
+ _id: statusPageId,
239
+ },
240
+ select: {
241
+ faviconFile: {
242
+ file: true,
243
+ _id: true,
244
+ type: true,
245
+ name: true,
246
+ },
247
+ },
248
+ props: {
249
+ isRoot: true,
250
+ },
251
+ },
252
+ );
253
+
254
+ if (!statusPage || !statusPage.faviconFile) {
255
+ logger.debug("Favicon file not found. Returning default favicon.");
256
+
257
+ // return default favicon.
258
+ return Response.sendFileByPath(
259
+ req,
260
+ res,
261
+ `/usr/src/Common/UI/Images/favicon/status-green.png`,
262
+ );
263
+ }
264
+
265
+ logger.debug(
266
+ `Favicon file found. Sending file: ${statusPage.faviconFile.name}`,
267
+ );
268
+
269
+ return Response.sendFileResponse(req, res, statusPage.faviconFile!);
270
+ },
271
+ );
272
+
90
273
  // confirm subscription api
91
274
  this.router.get(
92
275
  `${new this.entityType()
@@ -1344,6 +1527,22 @@ export default class StatusPageAPI extends BaseAPI<
1344
1527
  },
1345
1528
  );
1346
1529
 
1530
+ this.router.post(
1531
+ `${new this.entityType()
1532
+ .getCrudApiPath()
1533
+ ?.toString()}/manage-subscription/:statusPageId`,
1534
+ UserMiddleware.getUserMiddleware,
1535
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
1536
+ try {
1537
+ await this.manageExistingSubscription(req);
1538
+
1539
+ return Response.sendEmptySuccessResponse(req, res);
1540
+ } catch (err) {
1541
+ next(err);
1542
+ }
1543
+ },
1544
+ );
1545
+
1347
1546
  this.router.post(
1348
1547
  `${new this.entityType()
1349
1548
  .getCrudApiPath()
@@ -1941,6 +2140,233 @@ export default class StatusPageAPI extends BaseAPI<
1941
2140
  return response;
1942
2141
  }
1943
2142
 
2143
+ @CaptureSpan()
2144
+ public async manageExistingSubscription(req: ExpressRequest): Promise<void> {
2145
+ const objectId: ObjectID = new ObjectID(
2146
+ req.params["statusPageId"] as string,
2147
+ );
2148
+
2149
+ logger.debug(`Managing Existing Subscription for Status Page: ${objectId}`);
2150
+
2151
+ if (
2152
+ !(await this.service.hasReadAccess(
2153
+ objectId,
2154
+ await CommonAPI.getDatabaseCommonInteractionProps(req),
2155
+ req,
2156
+ ))
2157
+ ) {
2158
+ logger.debug(`No read access to status page with ID: ${objectId}`);
2159
+ throw new NotAuthenticatedException(
2160
+ "You are not authenticated to access this status page",
2161
+ );
2162
+ }
2163
+
2164
+ const statusPage: StatusPage | null = await StatusPageService.findOneBy({
2165
+ query: {
2166
+ _id: objectId.toString(),
2167
+ },
2168
+ select: {
2169
+ _id: true,
2170
+ projectId: true,
2171
+ enableEmailSubscribers: true,
2172
+ enableSmsSubscribers: true,
2173
+ allowSubscribersToChooseResources: true,
2174
+ allowSubscribersToChooseEventTypes: true,
2175
+ showSubscriberPageOnStatusPage: true,
2176
+ },
2177
+ props: {
2178
+ isRoot: true,
2179
+ },
2180
+ });
2181
+
2182
+ if (!statusPage) {
2183
+ logger.debug(`Status page not found with ID: ${objectId}`);
2184
+ throw new BadDataException("Status Page not found");
2185
+ }
2186
+
2187
+ if (!statusPage.showSubscriberPageOnStatusPage) {
2188
+ logger.debug(
2189
+ `Subscriber page not enabled for status page with ID: ${objectId}`,
2190
+ );
2191
+ throw new BadDataException(
2192
+ "Subscribes not enabled for this status page.",
2193
+ );
2194
+ }
2195
+
2196
+ logger.debug(`Status page found: ${JSON.stringify(statusPage)}`);
2197
+
2198
+ if (
2199
+ req.body.data["subscriberEmail"] &&
2200
+ !statusPage.enableEmailSubscribers
2201
+ ) {
2202
+ logger.debug(
2203
+ `Email subscribers not enabled for status page with ID: ${objectId}`,
2204
+ );
2205
+ throw new BadDataException(
2206
+ "Email subscribers not enabled for this status page.",
2207
+ );
2208
+ }
2209
+
2210
+ if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
2211
+ logger.debug(
2212
+ `SMS subscribers not enabled for status page with ID: ${objectId}`,
2213
+ );
2214
+ throw new BadDataException(
2215
+ "SMS subscribers not enabled for this status page.",
2216
+ );
2217
+ }
2218
+
2219
+ // if no email or phone, throw error.
2220
+
2221
+ if (
2222
+ !req.body.data["subscriberEmail"] &&
2223
+ !req.body.data["subscriberPhone"]
2224
+ ) {
2225
+ logger.debug(
2226
+ `No email or phone provided for subscription to status page with ID: ${objectId}`,
2227
+ );
2228
+ throw new BadDataException(
2229
+ "Email or phone is required to subscribe to this status page.",
2230
+ );
2231
+ }
2232
+
2233
+ const email: Email | undefined = req.body.data["subscriberEmail"]
2234
+ ? new Email(req.body.data["subscriberEmail"] as string)
2235
+ : undefined;
2236
+
2237
+ const phone: Phone | undefined = req.body.data["subscriberPhone"]
2238
+ ? new Phone(req.body.data["subscriberPhone"] as string)
2239
+ : undefined;
2240
+
2241
+ let statusPageSubscriber: StatusPageSubscriber | null = null;
2242
+
2243
+ if (email) {
2244
+ logger.debug(`Setting subscriber email: ${email}`);
2245
+ statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
2246
+ query: {
2247
+ subscriberEmail: email,
2248
+ statusPageId: objectId,
2249
+ },
2250
+ select: {
2251
+ _id: true,
2252
+ subscriberEmail: true,
2253
+ },
2254
+ props: {
2255
+ isRoot: true,
2256
+ },
2257
+ });
2258
+ }
2259
+
2260
+ if (phone) {
2261
+ logger.debug(`Setting subscriber phone: ${phone}`);
2262
+ statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
2263
+ query: {
2264
+ subscriberPhone: phone,
2265
+ statusPageId: objectId,
2266
+ },
2267
+ select: {
2268
+ _id: true,
2269
+ subscriberPhone: true,
2270
+ },
2271
+ props: {
2272
+ isRoot: true,
2273
+ },
2274
+ });
2275
+ }
2276
+
2277
+ if (!statusPageSubscriber) {
2278
+ // not found, return bad data
2279
+ logger.debug(
2280
+ `Subscriber not found for email: ${email} or phone: ${phone}`,
2281
+ );
2282
+
2283
+ let emailOrPhone: string = "email";
2284
+ if (phone) {
2285
+ emailOrPhone = "phone";
2286
+ }
2287
+
2288
+ throw new BadDataException(
2289
+ `Subscription not found for this status page. Please make sure your ${emailOrPhone} is correct.`,
2290
+ );
2291
+ }
2292
+
2293
+ const statusPageURL: string =
2294
+ await StatusPageService.getStatusPageURL(objectId);
2295
+
2296
+ const manageUrlink: string = StatusPageSubscriberService.getUnsubscribeLink(
2297
+ URL.fromString(statusPageURL),
2298
+ statusPageSubscriber.id!,
2299
+ ).toString();
2300
+
2301
+ const statusPages: Array<StatusPage> =
2302
+ await StatusPageSubscriberService.getStatusPagesToSendNotification([
2303
+ objectId,
2304
+ ]);
2305
+
2306
+ for (const statusPage of statusPages) {
2307
+ // send email to subscriber or sms if phone is provided.
2308
+
2309
+ if (email) {
2310
+ const host: Hostname = await DatabaseConfig.getHost();
2311
+ const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
2312
+
2313
+ MailService.sendMail(
2314
+ {
2315
+ toEmail: email,
2316
+ templateType:
2317
+ EmailTemplateType.ManageExistingStatusPageSubscriberSubscription,
2318
+ vars: {
2319
+ statusPageName: statusPage.name || "Status Page",
2320
+ statusPageUrl: statusPageURL,
2321
+ logoUrl: statusPage.logoFileId
2322
+ ? new URL(httpProtocol, host)
2323
+ .addRoute(FileRoute)
2324
+ .addRoute("/image/" + statusPage.logoFileId)
2325
+ .toString()
2326
+ : "",
2327
+ isPublicStatusPage: statusPage.isPublicStatusPage
2328
+ ? "true"
2329
+ : "false",
2330
+ subscriberEmailNotificationFooterText:
2331
+ statusPage.subscriberEmailNotificationFooterText || "",
2332
+
2333
+ manageSubscriptionUrl: manageUrlink,
2334
+ },
2335
+ subject:
2336
+ "Manage your Subscription for" +
2337
+ (statusPage.name || "Status Page"),
2338
+ },
2339
+ {
2340
+ mailServer: ProjectSmtpConfigService.toEmailServer(
2341
+ statusPage.smtpConfig,
2342
+ ),
2343
+ projectId: statusPage.projectId!,
2344
+ },
2345
+ );
2346
+ }
2347
+
2348
+ if (phone) {
2349
+ const sms: SMS = {
2350
+ message: `You have selected to manage your subscription for the status page: ${statusPage.name}. You can manage your subscription here: ${manageUrlink}`,
2351
+ to: phone,
2352
+ };
2353
+ // send sms here.
2354
+ SmsService.sendSms(sms, {
2355
+ projectId: statusPage.projectId,
2356
+ customTwilioConfig: ProjectCallSMSConfigService.toTwilioConfig(
2357
+ statusPage.callSmsConfig,
2358
+ ),
2359
+ }).catch((err: Error) => {
2360
+ logger.error(err);
2361
+ });
2362
+ }
2363
+
2364
+ logger.debug(
2365
+ `Subscription management link sent to subscriber with ID: ${statusPageSubscriber.id}`,
2366
+ );
2367
+ }
2368
+ }
2369
+
1944
2370
  @CaptureSpan()
1945
2371
  public async subscribeToStatusPage(req: ExpressRequest): Promise<void> {
1946
2372
  const objectId: ObjectID = new ObjectID(
@@ -3,6 +3,7 @@ import {
3
3
  AdminDashboardRoute,
4
4
  DashboardRoute,
5
5
  AppApiRoute,
6
+ StatusPageApiRoute,
6
7
  } from "Common/ServiceRoute";
7
8
  import BillingConfig from "./BillingConfig";
8
9
  import Hostname from "Common/Types/API/Hostname";
@@ -306,6 +307,12 @@ export const AdminDashboardClientURL: URL = new URL(
306
307
 
307
308
  export const AppApiClientUrl: URL = new URL(HttpProtocol, Host, AppApiRoute);
308
309
 
310
+ export const StatusPageApiClientUrl: URL = new URL(
311
+ HttpProtocol,
312
+ Host,
313
+ StatusPageApiRoute,
314
+ );
315
+
309
316
  export const DashboardClientUrl: URL = new URL(
310
317
  HttpProtocol,
311
318
  Host,
@@ -238,7 +238,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
238
238
  onCallDutyPolicyFeedEventType:
239
239
  OnCallDutyPolicyFeedEventType.RosterHandoff,
240
240
  displayColor: Green500,
241
- feedInfoInMarkdown: `📵 **${await UserService.getUserMarkdownString(
241
+ feedInfoInMarkdown: `🚫 **${await UserService.getUserMarkdownString(
242
242
  {
243
243
  userId: sendEmailToUserId,
244
244
  projectId: projectId!,
@@ -142,6 +142,10 @@ export interface InitFuctionOptions {
142
142
  port?: Port | undefined;
143
143
  isFrontendApp?: boolean;
144
144
  statusOptions: StatusAPIOptions;
145
+ getVariablesToRenderIndexPage?: (
146
+ req: ExpressRequest,
147
+ res: ExpressResponse,
148
+ ) => Promise<JSONObject>;
145
149
  }
146
150
 
147
151
  type InitFunction = (
@@ -200,11 +204,36 @@ const init: InitFunction = async (
200
204
  },
201
205
  );
202
206
 
203
- app.get("/*", (_req: ExpressRequest, res: ExpressResponse) => {
204
- return res.render("/usr/src/app/views/index.ejs", {
205
- enableGoogleTagManager: IsBillingEnabled || false,
206
- });
207
- });
207
+ app.get(
208
+ ["/*", `/${appName}/*`],
209
+ async (_req: ExpressRequest, res: ExpressResponse) => {
210
+ logger.debug("Rendering index page");
211
+
212
+ let variables: JSONObject = {};
213
+
214
+ if (data.getVariablesToRenderIndexPage) {
215
+ logger.debug("Getting variables to render index page");
216
+ try {
217
+ const variablesToRenderIndexPage: JSONObject =
218
+ await data.getVariablesToRenderIndexPage(_req, res);
219
+ variables = {
220
+ ...variables,
221
+ ...variablesToRenderIndexPage,
222
+ };
223
+ } catch (error) {
224
+ logger.error(error);
225
+ }
226
+ }
227
+
228
+ logger.debug("Rendering index page with variables: ");
229
+ logger.debug(variables);
230
+
231
+ return res.render("/usr/src/app/views/index.ejs", {
232
+ enableGoogleTagManager: IsBillingEnabled || false,
233
+ ...variables,
234
+ });
235
+ },
236
+ );
208
237
  }
209
238
 
210
239
  return app;
@@ -16,6 +16,7 @@ enum EmailTemplateType {
16
16
  SubscriberIncidentNoteCreated = "SubscriberIncidentNoteCreated.hbs",
17
17
  SubscriberIncidentStateChanged = "SubscriberIncidentStateChanged.hbs",
18
18
  SubscriberScheduledMaintenanceEventCreated = "SubscriberScheduledMaintenanceEventCreated.hbs",
19
+ ManageExistingStatusPageSubscriberSubscription = "ManageExistingStatusPageSubscriberSubscription.hbs",
19
20
  SubscriberScheduledMaintenanceEventStateChanged = "SubscriberScheduledMaintenanceEventStateChanged.hbs",
20
21
  StatusPageForgotPassword = "StatusPageForgotPassword.hbs",
21
22
  StatusPagePasswordChanged = "StatusPagePasswordChanged.hbs",
@@ -0,0 +1,93 @@
1
+ import React, { ReactElement, useState } from "react";
2
+
3
+ interface DropdownOption {
4
+ label: string;
5
+ onClick: () => void;
6
+ }
7
+
8
+ interface DropdownButtonProps {
9
+ title: string;
10
+ dropdownOptions: DropdownOption[];
11
+ onButtonClick: () => void;
12
+ }
13
+
14
+ export interface ComponentProps {
15
+ title: string;
16
+ dropdownOptions: DropdownOption[];
17
+ onButtonClick: () => void;
18
+ }
19
+
20
+ const DropdownButton: React.FC<DropdownButtonProps> = ({
21
+ title,
22
+ dropdownOptions,
23
+ onButtonClick,
24
+ }: ComponentProps): ReactElement => {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+
27
+ const toggleDropdown: VoidFunction = () => {
28
+ setIsOpen(!isOpen);
29
+ };
30
+
31
+ return (
32
+ <div className="inline-flex rounded-md shadow-sm">
33
+ <button
34
+ type="button"
35
+ className="relative inline-flex items-center rounded-l-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
36
+ onClick={onButtonClick}
37
+ >
38
+ {title}
39
+ </button>
40
+ <div className="relative -ml-px block">
41
+ <button
42
+ type="button"
43
+ className="relative inline-flex items-center rounded-r-md bg-white px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
44
+ id="option-menu-button"
45
+ aria-expanded={isOpen}
46
+ aria-haspopup="true"
47
+ onClick={toggleDropdown}
48
+ >
49
+ <span className="sr-only">Open options</span>
50
+ <svg
51
+ className="size-5"
52
+ viewBox="0 0 20 20"
53
+ fill="currentColor"
54
+ aria-hidden="true"
55
+ >
56
+ <path
57
+ fillRule="evenodd"
58
+ d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
59
+ clipRule="evenodd"
60
+ />
61
+ </svg>
62
+ </button>
63
+
64
+ {isOpen && (
65
+ <div
66
+ className="absolute right-0 z-10 -mr-1 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"
67
+ role="menu"
68
+ aria-orientation="vertical"
69
+ aria-labelledby="option-menu-button"
70
+ >
71
+ <div className="py-1" role="none">
72
+ {dropdownOptions.map((option: DropdownOption, index: number) => {
73
+ return (
74
+ <a
75
+ key={index}
76
+ href="#"
77
+ className="block px-4 py-2 text-sm text-gray-700"
78
+ role="menuitem"
79
+ onClick={option.onClick}
80
+ >
81
+ {option.label}
82
+ </a>
83
+ );
84
+ })}
85
+ </div>
86
+ </div>
87
+ )}
88
+ </div>
89
+ </div>
90
+ );
91
+ };
92
+
93
+ export default DropdownButton;
@@ -60,7 +60,12 @@ export interface BaseComponentProps<T> {
60
60
  initialValues?: FormValues<T> | undefined;
61
61
  values?: FormValues<T> | undefined;
62
62
  onValidate?: undefined | ((values: FormValues<T>) => JSONObject);
63
- onChange?: undefined | ((values: FormValues<T>) => void);
63
+ onChange?:
64
+ | undefined
65
+ | ((
66
+ values: FormValues<T>,
67
+ setNewFormValues: (newValues: FormValues<T>) => void,
68
+ ) => void);
64
69
  fields: Fields<T>;
65
70
  steps?: undefined | Array<FormStep<T>>;
66
71
  submitButtonText?: undefined | string;
@@ -352,7 +357,10 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
352
357
  setCurrentValue(refCurrentValue.current);
353
358
 
354
359
  if (props.onChange && isInitialValuesSet.current) {
355
- props.onChange(refCurrentValue.current);
360
+ props.onChange(refCurrentValue.current, (values: FormValues<T>) => {
361
+ refCurrentValue.current = values;
362
+ setCurrentValue(refCurrentValue.current);
363
+ });
356
364
  }
357
365
  };
358
366
 
@@ -657,6 +665,10 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
657
665
  disableAutofocus={
658
666
  props.disableAutofocus || false
659
667
  }
668
+ setFormValues={(values: FormValues<T>) => {
669
+ refCurrentValue.current = values;
670
+ setCurrentValue(refCurrentValue.current);
671
+ }}
660
672
  />
661
673
  }
662
674
  {field.footerElement}