From 267af348bce5847490f04cbd870dbbe83fa07c2f Mon Sep 17 00:00:00 2001 From: aschmidt34 <124093649+aschmidt34@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:08:56 -0500 Subject: [PATCH 01/13] HR On Call Calendar Fix (#793) * Updated on-call phone numbers with a warning about contacting HR per ticket #20205. * Fixed issue with food deprive query in colony alerts revamp (requested via ticket #20180). --- .../wnprc_ehr/notification/ColonyAlertsNotificationRevamp.java | 2 +- .../src/org/labkey/wnprc_ehr/pages/calendars/OnCallCalendar.jsp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ColonyAlertsNotificationRevamp.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ColonyAlertsNotificationRevamp.java index 6a0a36b53..03be5ae58 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ColonyAlertsNotificationRevamp.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ColonyAlertsNotificationRevamp.java @@ -1470,7 +1470,7 @@ private void getFoodDeprivesTodayAnimalUnassigned() { // Filters data. for (HashMap result : unfilteredReturnArray) { - if (result.get("Id/activeAssignments/projects").contains(result.get("project"))) { + if (!result.get("Id/activeAssignments/projects").contains(result.get("project"))) { filteredReturnArray.add(result); } } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/calendars/OnCallCalendar.jsp b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/calendars/OnCallCalendar.jsp index 2b3d79f93..5eddd1b26 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/calendars/OnCallCalendar.jsp +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/calendars/OnCallCalendar.jsp @@ -119,6 +119,8 @@ In the event of an injury or potential exposure occurring on the weekend that requires a trip the UW Hospital Emergency Department, contact
either the Colony Management Supervisor or the Veterinarian on-call, and they will contact the appropriate HR representative. +
+ NOTE: HR contacts should be used only for emergencies during non-business hours.

From 7f943838321482e40a1610ef4edb9c402d25a87b Mon Sep 17 00:00:00 2001 From: Chad Sebranek Date: Thu, 17 Apr 2025 16:08:58 -0500 Subject: [PATCH 02/13] Necropsy alerts and misc (#800) * make location required, make performed by allowed to be blank * Add alert for tissues going to WIMR * Do not auto fill the stain column * Add 3 more courier locations * make java constants queryable --- .../queries/study/Necropsy Schedule.sql | 49 ++++++++++++++++++- .../web/wnprc_ehr/model/sources/Necropsy.js | 12 ++++- .../pages/dataentry/NecropsySchedule.jsp | 28 +++++++++++ .../schemas/SqlQueryReferencePoints.java | 8 +++ .../NecropsySampleDeliveryDestination.java | 3 ++ 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql b/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql index 70408bb5c..972540635 100644 --- a/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql +++ b/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql @@ -22,10 +22,30 @@ SELECT lsid ,shipping_comment AS delivery_comment ,animalid.Demographics.necropsyAbstractNotes.remark AS remark ,CASE - WHEN hasTissuesForAvrl IS NULL + WHEN hasTissuesForAvrl IS NULL THEN FALSE ELSE TRUE END AS has_tissues_for_avrl + ,CASE + WHEN hasTissuesForWimr IS NULL + THEN FALSE + ELSE TRUE + END AS has_tissues_for_wimr + ,CASE + WHEN hasTissuesForCcourt IS NULL + THEN FALSE + ELSE TRUE + END AS has_tissues_for_ccourt + ,CASE + WHEN hasTissuesForBmq IS NULL + THEN FALSE + ELSE TRUE + END AS has_tissues_for_bmq + ,CASE + WHEN hasTissuesForElements IS NULL + THEN FALSE + ELSE TRUE + END AS has_tissues_for_elements ,state FROM (SELECT taskid AS lsid ,taskid.rowid AS taskid @@ -53,4 +73,29 @@ SELECT lsid FROM tissue_samples WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_AVRL') -- 'COURIER_AVRL' GROUP BY taskid) avrl_tissues - ON necropsy.lsid = avrl_tissues.taskid \ No newline at end of file + ON necropsy.lsid = avrl_tissues.taskid +/* Flag necropsies that have tissues that need to be couriered to WIMR. */ + LEFT JOIN (SELECT taskid + ,TRUE AS hasTissuesForWimr + FROM tissue_samples + WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_WIMR') -- 'COURIER_WIMR' + GROUP BY taskid) wimr_tissues + ON necropsy.lsid = wimr_tissues.taskid + LEFT JOIN (SELECT taskid + ,TRUE AS hasTissuesForCcourt + FROM tissue_samples + WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_CCOURT') -- 'COURIER_WIMR' + GROUP BY taskid) ccourt_tissues + ON necropsy.lsid = ccourt_tissues.taskid + LEFT JOIN (SELECT taskid + ,TRUE AS hasTissuesForBmq + FROM tissue_samples + WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_BMQ') -- 'COURIER_WIMR' + GROUP BY taskid) bmq_tissues + ON necropsy.lsid = bmq_tissues.taskid + LEFT JOIN (SELECT taskid + ,TRUE AS hasTissuesForElements + FROM tissue_samples + WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_ELEMENTS') -- 'COURIER_WIMR' + GROUP BY taskid) elements_tissues + ON necropsy.lsid = elements_tissues.taskid diff --git a/WNPRC_EHR/resources/web/wnprc_ehr/model/sources/Necropsy.js b/WNPRC_EHR/resources/web/wnprc_ehr/model/sources/Necropsy.js index fcabf13f7..1742b507e 100644 --- a/WNPRC_EHR/resources/web/wnprc_ehr/model/sources/Necropsy.js +++ b/WNPRC_EHR/resources/web/wnprc_ehr/model/sources/Necropsy.js @@ -96,6 +96,7 @@ EHR.model.DataModelManager.registerMetadata('Necropsy', { }, location: { xtype: 'combo', + allowBlank: false, lookup: { schemaName: 'wnprc', queryName: 'necropsy_suite', @@ -118,7 +119,7 @@ EHR.model.DataModelManager.registerMetadata('Necropsy', { }, performedby: { xtype: 'combo', - allowBlank: false, + allowBlank: true, hasOwnTpl: true, hidden: false, lookup: { @@ -365,7 +366,14 @@ EHR.model.DataModelManager.registerMetadata('Necropsy', { header: "#", width: 45 } - } + }, + stain: { + defaultValue: null, + editorConfig: { + plugins: ['ldk-usereditablecombo'] + }, + shownInGrid: false + }, }, 'study.histology': { Id: { diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp index 2acad5be1..20a33cb0c 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp @@ -90,6 +90,30 @@ This animal has tissue samples that need to be couried to AVRL. + + + + + + + + + + + + @@ -431,6 +455,10 @@ who_delivers: ko.observable(), delivery_comment: ko.observable(), has_tissues_for_avrl: ko.observable(), + has_tissues_for_wimr: ko.observable(), + has_tissues_for_ccourt: ko.observable(), + has_tissues_for_bmq: ko.observable(), + has_tissues_for_elements: ko.observable(), project: ko.observable(), protocol: ko.observable(), sex: ko.observable(), diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/SqlQueryReferencePoints.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/SqlQueryReferencePoints.java index fc9266a2e..76a2cefeb 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/SqlQueryReferencePoints.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/SqlQueryReferencePoints.java @@ -25,4 +25,12 @@ public class SqlQueryReferencePoints { */ @Queryable public static final String COURIER_TO_AVRL = NecropsySampleDeliveryDestination.SampleDeliveryDestination.COURIER_AVRL.name(); + @Queryable + public static final String COURIER_TO_WIMR = NecropsySampleDeliveryDestination.SampleDeliveryDestination.COURIER_WIMR.name(); + @Queryable + public static final String COURIER_TO_CCOURT = NecropsySampleDeliveryDestination.SampleDeliveryDestination.COURIER_CCOURT.name(); + @Queryable + public static final String COURIER_TO_BMQ = NecropsySampleDeliveryDestination.SampleDeliveryDestination.COURIER_BMQ.name(); + @Queryable + public static final String COURIER_TO_ELEMENTS = NecropsySampleDeliveryDestination.SampleDeliveryDestination.COURIER_ELEMENTS.name(); } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/enum_lookups/NecropsySampleDeliveryDestination.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/enum_lookups/NecropsySampleDeliveryDestination.java index c6f6057bd..46dfd2628 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/enum_lookups/NecropsySampleDeliveryDestination.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/schemas/enum_lookups/NecropsySampleDeliveryDestination.java @@ -54,6 +54,9 @@ public enum SampleDeliveryDestination { PICK_UP ("Pick Up", "", null), COURIER_AVRL ("Courier to AVRL", "", null), COURIER_WIMR ("Courier to WIMR", "", null), + COURIER_CCOURT ("Courier to CCourt", "", null), + COURIER_BMQ ("Courier to BMQ", "", null), + COURIER_ELEMENTS ("Courier to Elements", "", null), FEDEX ("FedEx", "", null), UPS ("UPS", "", null), OTHER ("Other", "", null); From 0dc2f8db58912be7e5f3fcbf11f27dee7709a6a3 Mon Sep 17 00:00:00 2001 From: Chad Sebranek Date: Tue, 22 Apr 2025 14:11:00 -0500 Subject: [PATCH 03/13] Update Necropsy Schedule fullcalendar library (#799) * update necropsy schedule to full calendar 6 * fix typo * set a background color * fix location bug * refetch calendar events correctly * remove old fullcalendar --- .../pages/dataentry/NecropsySchedule.jsp | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp index 20a33cb0c..576b1f264 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp @@ -12,11 +12,17 @@ <%@ page import="org.labkey.webutils.api.json.JsonUtils" %> <%@ page import="org.labkey.wnprc_ehr.WNPRC_EHRController" %> <%@ page import="java.util.List" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("fullcalendar"); + } +%> - - <%----%> <%----%> @@ -327,18 +333,25 @@ var necropsySuiteLookup = <%=necropsySuiteLookup%>; WebUtils.VM.necropsySuiteLookup = necropsySuiteLookup; - var $calendar = $('#calendar'); $(document).ready(function() { - $calendar.fullCalendar({ - header: { - left: 'prev,next today', + let calendarEl = document.getElementById('calendar'); + calendar = new FullCalendar.Calendar(calendarEl, { + themeSystem: 'bootstrap', + height: 800, + initialView: 'dayGridMonth', + headerToolbar: { + left: 'prev,next,today', center: 'title', - right: 'month,agendaWeek' + right: 'dayGridMonth,timeGridWeek,timeGridDay' }, - events: function(startMoment, endMoment, timezone, callback) { + eventSources: [{ + events: function (fetchInfo, callback) { + console.log(" startStr " + fetchInfo.startStr); + console.log(" endtStr " + moment(fetchInfo.startStr).format( "YYYY-MM-DD")); + WebUtils.API.selectRows("study", "Necropsy Schedule", { - "date~gte": startMoment.format('Y-MM-DD'), - "date~lte": endMoment.format('Y-MM-DD') + "date~gte": moment(fetchInfo.startStr).format( "YYYY-MM-DD"), + "date~lte": moment(fetchInfo.endStr).format( "YYYY-MM-DD") }).then(function(data) { var events = data.rows; @@ -346,7 +359,8 @@ var eventObj = { title: row.animalid, start: row.date, - rawRowData: row + rawRowData: row, + display: 'block' }; if (row.location in necropsySuiteLookup) { @@ -356,9 +370,9 @@ return eventObj; })) }) - }, + }}], eventClick: function(calEvent, jsEvent, view) { - jQuery.each(calEvent.rawRowData, function(key, value) { + jQuery.each(calEvent.event.extendedProps.rawRowData, function(key, value) { if (key in WebUtils.VM.taskDetails) { if (key == "date") { value = displayDate(value); @@ -367,7 +381,8 @@ } }); } - }) + },); + calendar.render() }); @@ -670,6 +685,7 @@ rowsToUpdate[0] = rowsToUpdate[0].map(function(row) { row.performedby = pathologistLookup[form.pathologist]; row.assistant = pathologistLookup[form.assistant]; + row.location = form.location; return row; }); @@ -682,7 +698,7 @@ ]); }).then(function() { // Refresh the calendar view. - $calendar.fullCalendar('refetchEvents'); + calendar.refetchEvents(); WebUtils.VM.pendingRequestTable.rows.remove(WebUtils.VM.requestRowInForm); From 54263184ff897ab22d91734c2e60afcf4e24fc2d Mon Sep 17 00:00:00 2001 From: aschmidt34 <124093649+aschmidt34@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:33:18 -0500 Subject: [PATCH 04/13] Removed automatic alerts to send perl notifications. (#808) --- .../scripts/automaticAlerts/automaticAlerts.cron | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docker/ehrcron/scripts/automaticAlerts/automaticAlerts.cron b/docker/ehrcron/scripts/automaticAlerts/automaticAlerts.cron index 141d6be49..222f5b8c7 100644 --- a/docker/ehrcron/scripts/automaticAlerts/automaticAlerts.cron +++ b/docker/ehrcron/scripts/automaticAlerts/automaticAlerts.cron @@ -19,14 +19,3 @@ MAIL_SERVER=mailserver #--------------------------------------------------------------------------------------- #Min Hour Day Month Weekday User Command #--------------------------------------------------------------------------------------- -0 10 * * * root $ALERT_DIR/adminAlerts.pl > /dev/null -5 7-17 * * * root $ALERT_DIR/clinpathAbnormalResultAlerts.pl > /dev/null -0 11,13,16 * * * root $ALERT_DIR/clinpathAlerts.pl > /dev/null -00 10 * * * root $ALERT_DIR/clinpathResultAlerts.pl > /dev/null -12 6 * * * root $ALERT_DIR/largeInfantAlerts.pl > /dev/null -15 9 * * * root $ALERT_DIR/overdueWeightAlerts.pl > /dev/null -25 7-17 * * * root $ALERT_DIR/siteErrorAlerts.pl > /dev/null -0 6,10,13 * * * root $ALERT_DIR/treatmentAlerts.pl > /dev/null -0 15,17,19 * * * root $ALERT_DIR/treatmentAlerts.pl > /dev/null -30 13,20 * * * root $ALERT_DIR/treatmentAlerts.pl > /dev/null -10 15 * * * root $ALERT_DIR/weightAlerts.pl > /dev/null \ No newline at end of file From 0a61373324485b54e2f315be05ff833f3c87fc40 Mon Sep 17 00:00:00 2001 From: aschmidt34 <124093649+aschmidt34@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:08:05 -0500 Subject: [PATCH 05/13] Removing Unused Notifications (#812) * Removed all the unused perl notifications. * Removed the old unused java notification. --- .../labkey/wnprc_ehr/TriggerScriptHelper.java | 38 -- .../org/labkey/wnprc_ehr/WNPRC_EHRModule.java | 8 - .../AnimalRequestNotification.java | 147 ----- .../AnimalRequestNotificationUpdate.java | 204 ------ .../notification/DeathNotification.java | 84 --- .../TreatmentAlertsNotification.java | 581 ------------------ .../scripts/automaticAlerts/adminAlerts.pl | 206 ------- .../clinpathAbnormalResultAlerts.pl | 203 ------ .../scripts/automaticAlerts/clinpathAlerts.pl | 180 ------ .../automaticAlerts/clinpathResultAlerts.pl | 175 ------ .../automaticAlerts/largeInfantAlerts.pl | 97 --- .../automaticAlerts/overdueWeightAlerts.pl | 186 ------ .../automaticAlerts/siteErrorAlerts.pl | 136 ---- .../automaticAlerts/treatmentAlerts.pl | 409 ------------ .../scripts/automaticAlerts/weightAlerts.pl | 209 ------- 15 files changed, 2863 deletions(-) delete mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotification.java delete mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotificationUpdate.java delete mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/DeathNotification.java delete mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotification.java delete mode 100644 docker/ehrcron/scripts/automaticAlerts/adminAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/clinpathAbnormalResultAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/clinpathAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/clinpathResultAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/largeInfantAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/overdueWeightAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/siteErrorAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/treatmentAlerts.pl delete mode 100644 docker/ehrcron/scripts/automaticAlerts/weightAlerts.pl diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java index a9ece0664..a4fdea6c3 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java @@ -53,13 +53,10 @@ import org.labkey.dbutils.api.SimpleQueryUpdater; import org.labkey.dbutils.api.SimplerFilter; import org.labkey.webutils.api.json.JsonUtils; -import org.labkey.wnprc_ehr.notification.AnimalRequestNotification; import org.labkey.wnprc_ehr.notification.AnimalRequestNotificationRevamp; -import org.labkey.wnprc_ehr.notification.AnimalRequestNotificationUpdate; import org.labkey.wnprc_ehr.notification.AnimalRequestUpdateNotificationRevamp; import org.labkey.wnprc_ehr.notification.BloodDrawReviewTriggerNotification; import org.labkey.wnprc_ehr.notification.BloodOverdrawTriggerNotification; -import org.labkey.wnprc_ehr.notification.DeathNotification; import org.labkey.wnprc_ehr.notification.DeathNotificationRevamp; import org.labkey.wnprc_ehr.notification.ProjectRequestNotification; import org.labkey.wnprc_ehr.notification.VvcNotification; @@ -198,19 +195,6 @@ public void sendDeathNotification(final List ids, String hostName) { Module ehr = ModuleLoader.getInstance().getModule("EHR"); //Verifies 'Notification Service' is enabled before sending notification. if (NotificationService.get().isServiceEnabled()){ - //Sends original Death Notification. - //Remove this if-else when new notification is approved. - if (NotificationService.get().isActive(new DeathNotification(), container)) { - for (String id : ids) { - DeathNotification idNotification = new DeathNotification(); - idNotification.setParam(DeathNotification.idParamName, id); - idNotification.sendManually(container, user); - } - } - else { - _log.info("Death Notification is not enabled, will not send death notification"); - } - //Sends revamped Death Notification. if (NotificationService.get().isActive(new DeathNotificationRevamp(ehr), container)) { for (String id : ids) { @@ -872,17 +856,6 @@ public void sendAnimalRequestNotification(Integer rowid, String hostName){ //Verifies 'Notification Service' is enabled before sending notification. if (NotificationService.get().isServiceEnabled()){ - //Sends original Animal Request Notification. - //TODO: Remove this if-else when new notification is approved. - if (NotificationService.get().isActive(new AnimalRequestNotification(ehr), container)) { - _log.info("Using java helper to send email for animal request record: "+rowid); - AnimalRequestNotification notification = new AnimalRequestNotification(ehr, rowid, user, hostName); - notification.sendManually(container, user); - } - else { - _log.info("Animal Request Notification is not enabled, will not send animal request notification"); - } - //Sends revamped Animal Request Notification. if (NotificationService.get().isActive(new AnimalRequestNotificationRevamp(ehr), container)) { _log.info("Using java helper to send animal request notification revamp for animal request record: "+rowid); @@ -902,17 +875,6 @@ public void sendAnimalRequestNotificationUpdate(Integer rowid, Map> getClientDependencies(Container c) { public void registerNotifications() { List notifications = Arrays.asList( new BehaviorNotification(this), - new DeathNotification(), new ColonyAlertsNotification(this), - new TreatmentAlertsNotification(this), new VvcNotification(this), new FoodNotStartedNotification(this), new FoodNotStartedNoonNotification(this), new FoodNotCompletedNotification(this), new FoodCompletedProblemsNotification(this), - new AnimalRequestNotification(this), - new AnimalRequestNotificationUpdate(this), new ProjectRequestNotification(this), new IrregularObsBehaviorNotification(this), new WaterOrdersAlertNotification(this), diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotification.java deleted file mode 100644 index 754d5f042..000000000 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotification.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.labkey.wnprc_ehr.notification; - -import jakarta.mail.Address; -import jakarta.mail.Message; -import org.apache.commons.lang3.StringUtils; -import org.labkey.api.data.Container; -import org.labkey.api.ldk.notification.NotificationService; -import org.labkey.api.module.Module; -import org.labkey.api.module.ModuleLoader; -import org.labkey.api.security.User; -import org.labkey.api.security.UserPrincipal; -import org.labkey.api.util.MailHelper; -import org.labkey.wnprc_ehr.WNPRC_EHRModule; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; - -import static org.labkey.api.search.SearchService._log; - -public class AnimalRequestNotification extends AbstractEHRNotification -{ - public Integer rowId; - public User currentUser; - public String hostName; - - public AnimalRequestNotification(Module owner) - { - super(owner); - } - - public AnimalRequestNotification(Module owner, Integer rowid, User currentuser, String hostname) - { - super(owner); - rowId = rowid; - currentUser = currentuser; - hostName = hostname; - - } - - @Override - public String getCategory() - { - return ModuleLoader.getInstance().getModule(WNPRC_EHRModule.class).getName(); - } - - public String getName() - { - return "Animal Request Notification"; - } - - public String getScheduleDescription() - { - return "As soon as Animal Request is submitted"; - } - - public String getDescription() - { - return "This notification gets sent every time there is a new Animal Request form submitted"; - } - - @Override - public String getEmailSubject(Container c) - { - return "[EHR Services] New Animal Request Submitted on " + _dateTimeFormat.format(new Date()); - } - - @Override - public String getMessageBodyHTML(Container c, User u) - { - final StringBuilder msg = new StringBuilder(); - Date now = new Date(); - msg.append("

There was a new animal request submitted on: " + - AbstractEHRNotification._dateFormat.format(now) + - " at " + - AbstractEHRNotification._timeFormat.format(now) + - ".

"); - - msg.append("

Click here to review the request.

"); - - msg.append("

View all of the animal requests " + - "here.

"); - - return msg.toString(); - } - - public void sendManually (Container container, User user) - { - Collection recipients = getRecipients(container); - sendMessage(getEmailSubject(container),getMessageBodyHTML(container,user),recipients,user,container); - - } - - public Collection getRecipients(Container container) - { - return NotificationService.get().getRecipients(this, container); - } - - public void sendMessage(String subject, String bodyHtml, Collection recipients, User currentUser,Container container) - { - _log.info("AnimalRequestNotification.java: sending animal request email..."); - try - { - MailHelper.MultipartMessage msg = MailHelper.createMultipartMessage(); - msg.setFrom(NotificationService.get().getReturnEmail(container)); - msg.setSubject(subject); - - List emails = new ArrayList<>(); - for (UserPrincipal u : recipients) - { - List
addresses = NotificationService.get().getEmailsForPrincipal(u); - if (addresses != null) - { - for (Address a : addresses) - { - if (a.toString() != null) - emails.add(a.toString()); - } - } - } - - if (emails.size() == 0) - { - _log.warn("AnimalRequestNotification.java: no emails, unable to send EHR trigger script email"); - return; - } - - msg.setRecipients(Message.RecipientType.TO, StringUtils.join(emails, ",")); - msg.setEncodedHtmlContent(bodyHtml); - - MailHelper.send(msg, currentUser, container); - } - catch (Exception e) - { - _log.error("AnimalRequestNotification.java: unable to send email from EHR trigger script", e); - } - } - -} diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotificationUpdate.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotificationUpdate.java deleted file mode 100644 index 14ca55d8a..000000000 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnimalRequestNotificationUpdate.java +++ /dev/null @@ -1,204 +0,0 @@ -package org.labkey.wnprc_ehr.notification; - -import jakarta.mail.Address; -import jakarta.mail.Message; -import org.apache.commons.lang3.StringUtils; -import org.labkey.api.data.Container; -import org.labkey.api.data.TableInfo; -import org.labkey.api.ldk.notification.NotificationService; -import org.labkey.api.module.Module; -import org.labkey.api.module.ModuleLoader; -import org.labkey.api.query.QueryService; -import org.labkey.api.security.User; -import org.labkey.api.security.UserPrincipal; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.util.MailHelper; -import org.labkey.wnprc_ehr.TriggerScriptHelper; -import org.labkey.wnprc_ehr.WNPRC_EHRModule; -import org.labkey.wnprc_ehr.security.permissions.WNPRCAnimalRequestsEditPermission; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import static org.labkey.api.search.SearchService._log; - -public class AnimalRequestNotificationUpdate extends AbstractEHRNotification -{ - public Integer _rowId; - public User _currentUser; - public String _hostName; - public Map _row; - public Map _oldrow; - public Map> _theDifferences; - - - public AnimalRequestNotificationUpdate(Module owner) - { - super(owner); - } - - public AnimalRequestNotificationUpdate(Module owner, Integer rowid, Map row, Map oldRow, User currentuser, String hostname) - { - super(owner); - _rowId = rowid; - _currentUser = currentuser; - _hostName = hostname; - _row = row; - _oldrow = oldRow; - } - - @Override - public String getCategory() - { - return ModuleLoader.getInstance().getModule(WNPRC_EHRModule.class).getName(); - } - - public String getName() - { - return "Animal Request Notification Updates"; - } - - public String getScheduleDescription() - { - return "As soon as Animal Request is updated"; - } - - public String getDescription() - { - return "This notification gets sent every time an Animal Request is updated"; - } - - public String getUserName(User user) - { - if (!"".equals(user.getFullName())) - { - return user.getFullName(); - } - if (!"".equals(user.getDisplayName(user))) - { - return user.getDisplayName(user); - } - return user.getEmail(); - } - - @Override - public String getEmailSubject(Container c) - { - return "[EHR Services] Animal Request Updated on " + _dateTimeFormat.format(new Date()); - } - - - @Override - public String getMessageBodyHTML(Container c, User u) - { - final StringBuilder msg = new StringBuilder(); - Date now = new Date(); - msg.append("

"); - msg.append(getUserName(_currentUser)); - msg.append(" updated an animal request entry on: " + - AbstractEHRNotification._dateFormat.format(now) + - " at " + - AbstractEHRNotification._timeFormat.format(now) + - ".

"); - msg.append("

The following changes were made:

"); - msg.append(""); - - for (Map.Entry> change : _theDifferences.entrySet()) - { - msg.append(""); - msg.append(""); - msg.append(""); - msg.append(""); - msg.append(""); - - } - - msg.append("
Field changedOld valueNew value
"); - msg.append(change.getKey()); - msg.append(""); - msg.append(change.getValue().get(0)); - msg.append(""); - msg.append(change.getValue().get(1)); - msg.append("
"); - msg.append("

"); - - msg.append("

Click here to review the request.

"); - - msg.append("

View all of the animal requests " + - "here.

"); - - return msg.toString(); - } - - public void sendManually (Container container, User user) - { - Collection recipients = getRecipients(container); - if (!container.hasPermission(_currentUser, WNPRCAnimalRequestsEditPermission.class) && !container.hasPermission(_currentUser, AdminPermission.class)) - { - TableInfo ti = QueryService.get().getUserSchema(user, container, "wnprc").getTable("animal_requests"); - _theDifferences = TriggerScriptHelper.buildDifferencesMap(ti, _oldrow, _row); - if (_theDifferences.size() > 0) - { - sendMessage(getEmailSubject(container),getMessageBodyHTML(container,user),recipients,user, container); - - } - } - - } - - public Collection getRecipients(Container container) - { - return NotificationService.get().getRecipients(this, container); - } - - public void sendMessage(String subject, String bodyHtml, Collection recipients, User currentUser, Container container) - { - _log.info("AnimalRequestNotificationUpdate.java: sending animal request email..."); - try - { - MailHelper.MultipartMessage msg = MailHelper.createMultipartMessage(); - msg.setFrom(NotificationService.get().getReturnEmail(container)); - msg.setSubject(subject); - - List emails = new ArrayList<>(); - for (UserPrincipal u : recipients) - { - List
addresses = NotificationService.get().getEmailsForPrincipal(u); - if (addresses != null) - { - for (Address a : addresses) - { - if (a.toString() != null) - emails.add(a.toString()); - } - } - } - - if (emails.size() == 0) - { - _log.warn("AnimalRequestNotification.java: no emails, unable to send EHR trigger script email"); - return; - } - - msg.setRecipients(Message.RecipientType.TO, StringUtils.join(emails, ",")); - msg.setEncodedHtmlContent(bodyHtml); - - MailHelper.send(msg, currentUser, container); - } - catch (Exception e) - { - _log.error("AnimalRequestNotification.java: unable to send email from EHR trigger script", e); - } - } - -} diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/DeathNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/DeathNotification.java deleted file mode 100644 index d7f4ee383..000000000 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/DeathNotification.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.labkey.wnprc_ehr.notification; - -import jakarta.mail.Address; -import org.labkey.api.data.Container; -import org.labkey.api.ldk.notification.NotificationService; -import org.labkey.api.security.User; -import org.labkey.api.security.UserPrincipal; -import org.labkey.api.security.ValidEmail; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** - * Created by jon on 7/13/16. - */ -public class DeathNotification extends AbstractJspEmailNotification { - public static String idParamName = "Id"; - - @Override - public String getName() { - return "WNPRC Death Notification"; - } - - @Override - public String getEmailSubject(Container c) { - String subject = "Death Notification"; - - if (params.has(idParamName)) { - subject += ": " + getParam(idParamName); - } - return subject; - } - - @Override - public String getCronString() { - return null; - } - - @Override - public String getScheduleDescription() { - return "Sent immediately when an animal is marked as dead"; - } - - @Override - public String getDescription() { - return "The report sends an alert whenever an animal is marked as dead."; - } - - @Override - String getPathToJsp() { - return "/org/labkey/wnprc_ehr/email_templates/notifications/DeathNotification.jsp"; - } - - - public void sendManually (Container container, User user){ - List emails = getRecipientEmailAddresses(container); - sendManually(container, user, emails); - - } - - public List getRecipientEmailAddresses(Container container) { - Set recipients = NotificationService.get().getRecipients(this, container); - - List emails = new ArrayList<>(); - for (UserPrincipal u : recipients) { - try { - List
addresses = NotificationService.get().getEmailsForPrincipal(u); - if (addresses != null) { - for (Address a : addresses) { - if (a.toString() != null) { - emails.add(a.toString()); - } - } - } - } - catch (ValidEmail.InvalidEmailException e) { - log.error("Could not get emails for UserPrincipal " + u.getUserId() + " of type " + u.getType()); - } - } - - return emails; - } -} diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotification.java deleted file mode 100644 index 23e180414..000000000 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotification.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright (c) 2012-2014 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.labkey.wnprc_ehr.notification; - -import org.apache.commons.lang3.time.DateUtils; -import org.labkey.api.data.ColumnInfo; -import org.labkey.api.data.CompareType; -import org.labkey.api.data.Container; -import org.labkey.api.data.ConvertHelper; -import org.labkey.api.data.Results; -import org.labkey.api.data.ResultsImpl; -import org.labkey.api.data.RuntimeSQLException; -import org.labkey.api.data.Selector; -import org.labkey.api.data.SimpleFilter; -import org.labkey.api.data.Sort; -import org.labkey.api.data.TableInfo; -import org.labkey.api.data.TableSelector; -import org.labkey.api.module.Module; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.QueryService; -import org.labkey.api.security.User; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.wnprc_ehr.WNPRC_EHREmail; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Map; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.TreeMap; - -/** - * User: dnicolalde - * Date: 11/11/15 - * Time: 8:29 PM - */ -public class TreatmentAlertsNotification extends AbstractEHRNotification -{ - public TreatmentAlertsNotification(Module owner) - { - super(owner); - } - - @Override - public String getName() - { - return "WNPRC Treatment Alerts"; - } - - @Override - public String getDescription() - { - return "This runs every day at 10AM, 1PM, 3PM, and 5PM if there are treatments scheduled that have not yet been marked complete"; - } - - @Override - public String getEmailSubject(Container c) - { - return "Daily Treatment Alerts: " + _dateTimeFormat.format(new Date()); - } - - @Override - public String getCronString() - { - return "0 0 10,13,15,17 * * ?"; - } - - @Override - public String getScheduleDescription() - { - return "daily at 10AM, 1PM, 3PM, 5PM"; - } - - @Override - public String getMessageBodyHTML(Container c, User u) - { - StringBuilder msg = new StringBuilder(); - - //Find today's date - final Date now = new Date(); - msg.append("This email contains any treatments not marked as completed. It was run on: " + _dateFormat.format(now) + " at " + _timeFormat.format(now) + ".

"); - - findTreatmentsWithoutProject(c, u, msg); - findRoomswithObs(c,u,msg,new Date()); - processTreatments(c, u, msg, new Date()); - noProjectAssignment(c, u, msg, new Date()); - processTreatment(c, u, msg, new Date(), "AM"); - processTreatment(c,u,msg,new Date(),"Noon"); - processTreatment(c,u,msg,new Date(),"PM"); - processTreatment(c,u,msg,new Date(),"Any Time"); - processTreatment(c, u, msg, new Date(), "Night"); - treatmentDiffer(c, u, msg); - treatmentNotLiveAnimals(c, u, msg); - problemListNotLive(c,u,msg); - - - - - return msg.toString(); - } - - private void findTreatmentsWithoutProject(Container c, User u, StringBuilder msg) - { - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive"); - filter.addCondition(FieldKey.fromString("projectStatus"), null, CompareType.NONBLANK); - filter.addCondition(FieldKey.fromString("date"), new Date(), CompareType.DATE_EQUAL); - - TableInfo ti = getStudySchema(c, u).getTable("treatmentSchedule"); - TableSelector ts = new TableSelector(ti, filter, new Sort("room")); - long total = ts.getRowCount(); - if (total > 0) - { - msg.append("WARNING: There are " + total + " scheduled treatments where the animal is not assigned to the project.
"); - msg.append("

Click here to view them
\n"); - msg.append("


\n"); - } - } - - private void processTreatments(Container c, User u, final StringBuilder msg, final Date maxDate) - { - Date curDate = new Date(); - Date roundedMax = new Date(); - roundedMax.setTime(maxDate.getTime()); - //roundedMax.setTime(maxDate.getTime() - 1* 24 * 60 * 60 * 1000); - roundedMax = DateUtils.truncate(roundedMax, Calendar.DATE); - - TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("treatmentSchedule"); - - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("date"), roundedMax, CompareType.DATE_EQUAL); - filter.addCondition(FieldKey.fromString("date"), maxDate, CompareType.LTE); - filter.addCondition(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive"); - - Set columns = new HashSet<>(); - columns.add(FieldKey.fromString("Id")); - columns.add(FieldKey.fromString("CurrentArea")); - columns.add(FieldKey.fromString("CurrentRoom")); - columns.add(FieldKey.fromString("CurrentCage")); - columns.add(FieldKey.fromString("TimeOfDay")); - - //columns.add(FieldKey.fromString("Id/curLocation/area")); - //columns.add(FieldKey.fromString("Id/curLocation/room")); - //columns.add(FieldKey.fromString("Id/curLoation/cage")); - columns.add(FieldKey.fromString("projectStatus")); - columns.add(FieldKey.fromString("treatmentStatus")); - columns.add(FieldKey.fromString("treatmentStatus/Label")); - columns.add(FieldKey.fromString("code")); - columns.add(FieldKey.fromString("code/meaning")); - - Map params = new HashMap<>(); - params.put("StartDate", roundedMax); - params.put("NumDays", 1); - - final Map colMap = QueryService.get().getColumns(ti, columns); - //TableSelector ts = new TableSelector(ti, colMap.values(), filter, new Sort("Id/curLocation/area,Id/curLocation/room")); - TableSelector ts = new TableSelector(ti, colMap.values(), filter, new Sort("CurrentArea,CurrentRoom")); - ts.setNamedParameters(params); - - String url = getExecuteQueryUrl(c, "study", "treatmentSchedule", null) + "&" + filter.toQueryString("query") + getParameterUrlString(params); - long total = ts.getRowCount(); - if (total == 0) - { - msg.append("There are no treatments scheduled on " + _dateFormat.format(maxDate) + " on or before " + _timeFormat.format(maxDate) + ". Treatments could be added after this email was sent, so please click here to check online closer to the time.
\n"); - } - else - { - final String completed = "completed"; - final String incomplete = "incomplete"; - final Map totals = new HashMap<>(); - totals.put(completed, 0); - totals.put(incomplete, 0); - - final Map totalByArea = new TreeMap<>(); - final Map totalByTime = new TreeMap<>(); - - ts.forEach(new Selector.ForEachBlock() - { - @Override - public void exec(ResultSet object) throws SQLException - { - Results rs = new ResultsImpl(object, colMap); - if ("Completed".equals(rs.getString(FieldKey.fromString("treatmentStatus/Label")))) - { - totals.put(completed, totals.get(completed) + 1); - } - else - { - totals.put(incomplete, totals.get(incomplete) + 1); - - String area = rs.getString(FieldKey.fromString("CurrentArea")); - Integer areaVal = totalByArea.containsKey(area) ? totalByArea.get(area) : 0; - areaVal++; - - totalByArea.put(area, areaVal); - - String timeofday = rs.getString(FieldKey.fromString("TimeOfDay")); - Integer timeVal = totalByTime.containsKey(timeofday) ? totalByTime.get(timeofday) : 0; - timeVal++; - - totalByTime.put(timeofday, timeVal); - } - } - }); - - msg.append("All Treatments Summary:

"); - msg.append("There are " + (totals.get(completed) + totals.get(incomplete)) + " scheduled treatments on or before " + _timeFormat.format(maxDate) + ". Click here to view them. Of these, " + totals.get(completed) + " have been marked completed.

\n"); - - if (totals.get(incomplete) == 0) - { - msg.append("All treatments scheduled prior to " + _timeFormat.format(maxDate) + " have been marked complete as of " + _dateTimeFormat.format(curDate) + ".

\n"); - } - else - { - msg.append("There are " + totals.get(incomplete) + " treatments that have not been marked complete:

\n"); - msg.append(""); - msg.append("\n"); - - for (String area : totalByArea.keySet()) - { - msg.append("\n"); - } - - msg.append("
AREANumber
" + area + ":" + totalByArea.get(area) + "

\n"); - - msg.append("Incomplete Treatments by time of day:

\n"); - msg.append(""); - msg.append("\n"); - - for (String timeofday : totalByTime.keySet()) - { - msg.append("\n"); - } - - msg.append("
Time of DayNumber
" + timeofday + ":" + totalByTime.get(timeofday) + "

\n"); - } - msg.append("


\n"); - } - } - private void findRoomswithObs(Container c, User u, final StringBuilder msg, final Date maxDate){ - - Date roundedMax = new Date(); - roundedMax.setTime(maxDate.getTime()); - roundedMax = DateUtils.truncate(roundedMax, Calendar.DATE); - - TableInfo ti = QueryService.get().getUserSchema(u, c, "ehr").getTable("RoomsWithoutObsToday"); - - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("hasObs"), 'N', CompareType.EQUAL); - //filter.addCondition(FieldKey.fromString("date"), maxDate, CompareType.LTE); - //filter.addCondition(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive"); - - TableSelector ts = new TableSelector(ti, PageFlowUtil.set("area", "room", "hasObs"), filter, null); - - String url = getExecuteQueryUrl(c, "ehr", "RoomsWithoutObsToday", null); - long total = ts.getRowCount(); - if (total == 0){ - - msg.append("There are no treatments scheduled on " + _dateFormat.format(maxDate) + " on or before " + _timeFormat.format(maxDate) + ". Treatments could be added after this email was sent, so please click here to check online closer to the time.
\n"); - - } - else - { - msg.append("The following rooms do not have any obs for today as of "+ _timeFormat.format(maxDate)+"
\n"); - ts.forEach(new Selector.ForEachBlock() - { - @Override - public void exec(ResultSet rs) throws SQLException - { - - msg.append(rs.getString("room") + "
"); - - } - - }); - msg.append("
\n"); - } - - - - } - private void noProjectAssignment (Container c, User u, final StringBuilder msg, final Date maxDate){ - - Date curDate = new Date(); - Date roundedMax = new Date(); - roundedMax.setTime(maxDate.getTime()); - roundedMax = DateUtils.truncate(roundedMax, Calendar.DATE); - - TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("treatmentSchedule"); - - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("date"), roundedMax, CompareType.DATE_EQUAL); - filter.addCondition(FieldKey.fromString("date"), maxDate, CompareType.LTE); - filter.addCondition(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive"); - filter.addCondition(FieldKey.fromString("projectStatus"),null,CompareType.NONBLANK); - - TableSelector ts = new TableSelector(ti,PageFlowUtil.set("id","frequency","meaning"),filter, null); - long total = ts.getRowCount(); - - if (total==0){ - msg.append("All scheduled treatments have projects associated with them

\n"); - } - else { - msg.append("There are "+total+" schedule treatments without a project associated with them.

"); - msg.append(""); - msg.append("\n"); - - ts.forEach(new Selector.ForEachBlock() - { - @Override - public void exec(ResultSet rs) throws SQLException - { - msg.append(""); - } - }); - msg.append("
IdShort NameFrequency
" + rs.getString("id") + " " + rs.getString("meaning") + "" + rs.getString("frequency") + "

\n"); - } - - - } - private void processTreatment(Container c, User u, final StringBuilder msg, final Date maxDate, String timeofDay ){ - - Date curDate = new Date(); - Date roundedMax = new Date(); - int hourofDay= Integer.parseInt(_hourFormat.format(curDate).toString()); - int hourofReport; - switch(timeofDay){ - case "AM": hourofReport = 10; - break; - case "Noon": hourofReport = 12; - break; - case "PM": hourofReport = 14; - break; - case "Any Time": hourofReport = 14; - break; - case "Night": hourofReport = 17; - break; - default : hourofReport = 14; - break; - } - - if (hourofDayTreatments " +timeofDay+":

"); - msg.append("It is too early in the day to send warnings about incomplete treatments for "+ timeofDay+ " treatments.\n"); - msg.append("


\n"); - return; - } - - //roundedMax.setTime(maxDate.getTime() - 1 * 24 * 60 * 60 * 1000); - roundedMax.setTime(maxDate.getTime()); - roundedMax = DateUtils.truncate(roundedMax, Calendar.DATE); - Set fieldKeys = PageFlowUtil.set( - FieldKey.fromParts("Id"), - FieldKey.fromParts("CurrentArea"), - FieldKey.fromParts("CurrentRoom"), - FieldKey.fromParts("CurrentCage"), - FieldKey.fromParts("projectStatus"), - FieldKey.fromParts("treatmentStatus"), - FieldKey.fromParts("treatmentStatus", "Label"), - FieldKey.fromParts("meaning"), - FieldKey.fromParts("code"), - FieldKey.fromParts("route"), - FieldKey.fromParts("remark")); - - - - - - TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("treatmentSchedule"); - - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("date"), roundedMax, CompareType.DATE_EQUAL); - filter.addCondition(FieldKey.fromString("timeOfDay"), timeofDay, CompareType.EQUAL); - filter.addCondition(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive"); - Collection cols = QueryService.get().getColumns(ti, fieldKeys).values(); - - TableSelector ts = new TableSelector(ti, cols, filter, null); - - Map params = new HashMap<>(); - params.put("StartDate", roundedMax); - params.put("NumDays", 1); - - String url = getExecuteQueryUrl(c, "study", "treatmentSchedule", null) + "&" + filter.toQueryString("query") + getParameterUrlString(params); - long total = ts.getRowCount(); - - if(total ==0){ - msg.append("There are no scheduled "+ timeofDay +" treatments as of "+ _timeFormat.format(curDate)+". Treatments could be added after this email was sent, so please check online closer to the time.
"); - } - else{ - String completed = "completed"; - final String incomplete = "incomplete"; - - final Map totals = new HashMap<>(); - totals.put(completed, 0); - totals.put(incomplete, 0); - - final Map totalByArea = new TreeMap<>(); - - Results r = ts.getResults(); - - try{ - - while (r.next()) - { - String status = r.getString(FieldKey.fromParts("treatmentStatus", "Label")); //getString returns null and Java cannot interpret that to a valid string - if ("Completed".equals(status)) - { - totals.put(completed, totals.get(completed) + 1); - } - else - { - totals.put(incomplete, totals.get(incomplete) + 1); - String area = r.getString(FieldKey.fromParts("CurrentArea")); - Integer areaVal = totalByArea.containsKey(area) ? totalByArea.get(area) : 0; - areaVal++; - totalByArea.put(area, areaVal); - - } - } - }catch (SQLException e){ - - throw new RuntimeSQLException(e); - } - finally{ - try{ - r.close(); - - }catch (SQLException e){ - - throw new RuntimeSQLException(e); - } - } - - msg.append("Treatments " +timeofDay+":

"); - msg.append("There are " + (totals.get(completed) + totals.get(incomplete)) + " scheduled treatments on or before " + _timeFormat.format(maxDate) + ". Click here to view them. Of these, " + totals.get(completed) + " have been marked completed.

\n"); - - if (totals.get(incomplete) == 0) - { - msg.append("All treatments scheduled prior to " + _timeFormat.format(maxDate) + " have been marked complete as of " + _timeFormat.format(curDate) + ".

\n"); - } - else - { - msg.append("There are " + totals.get(incomplete) + " treatments that have not been marked complete:

\n"); - msg.append(""); - msg.append("\n"); - - for (String area : totalByArea.keySet()) - { - msg.append("\n"); - } - - msg.append("
AREANumber
" + area + ":" + totalByArea.get(area) + "

\n"); - } - msg.append("


\n"); - - } - } - public void treatmentDiffer(Container c, User u, final StringBuilder msg){ - - Date roundedMax = new Date(); - Calendar currentDay = Calendar.getInstance(); - roundedMax.setTime(currentDay.getTimeInMillis()); - - //TODO: Added another method that can accept different days - /*Calendar currentDay = Calendar.getInstance(); - currentDay.setTime(roundedMax); - currentDay.add(Calendar.DATE, -1); - roundedMax.setTime(currentDay.getTimeInMillis());*/ - - TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("TreatmentsThatDiffer"); - - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("date"), roundedMax, CompareType.DATE_EQUAL); - - TableSelector ts = new TableSelector(ti,filter, new Sort("+CurrentArea,+CurrentRoom")); - - long total = ts.getRowCount(); - - if (total ==0){ - msg.append("All entered treatments given match what was ordered.
"); - } - else { - msg.append("Treatments that differ from what was ordered by Area

"); - Map[] resultsDifferTreatment = ts.getMapArray(); - List Area = new ArrayList<>(); - - HashMap areaHash = new HashMap<>(); - - - if (resultsDifferTreatment.length>0) - { - for (Map treatmentMap : resultsDifferTreatment){ - - String loopArea = ConvertHelper.convert(treatmentMap.get("CurrentArea"),String.class); - String loopRoom = ConvertHelper.convert(treatmentMap.get("CurrentRoom"),String.class); - - HashMap roomHash ; - if (!areaHash.containsKey(loopArea)){ - roomHash = new HashMap(); - areaHash.put(loopArea,roomHash); - } - - roomHash = areaHash.get(loopArea); - LinkedList differTreatments; - - if (!roomHash.containsKey(loopRoom)){ - differTreatments = new LinkedList(); - roomHash.put(loopRoom, differTreatments); - } - - differTreatments=roomHash.get(loopRoom); - differTreatments.add(treatmentMap); - roomHash.put(loopRoom, differTreatments); - } - - WNPRC_EHREmail> email = new WNPRC_EHREmail<>("/org/labkey/wnprc_ehr/emailViews/email.jsp"); - String emailContents = new String (); - try{ - emailContents = email.renderEmail(areaHash); - - }catch (Exception e){ - System.err.print("invalid areaHash sent to renderEmail"); - } - - msg.append(emailContents+ "


"); - } - } - } - - - public void treatmentNotLiveAnimals(Container c, User u, final StringBuilder msg ){ - - TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("treatment_order"); - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive",CompareType.NEQ_OR_NULL); - filter.addCondition(FieldKey.fromString("enddate"),' ', CompareType.ISBLANK); - - TableSelector ts = new TableSelector(ti,filter, null); - - long total = ts.getRowCount(); - if (total > 0) - { - msg.append("WARNING: There are " + total + " active treatments for animals not currently at WNPRC.
"); - msg.append("

Click here to view them
\n"); - msg.append("


\n"); - } - - - } - - public void problemListNotLive (Container c, User u, final StringBuilder msg ){ - - TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("problem"); - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id/DataSet/Demographics/calculated_status"), "Alive",CompareType.NEQ_OR_NULL); - filter.addCondition(FieldKey.fromString("enddate"),' ', CompareType.ISBLANK); - - TableSelector ts = new TableSelector(ti,filter, null); - - long total = ts.getRowCount(); - if (total > 0) - { - msg.append("WARNING: There are " + total + " unresolved problems for animals not currently at WNPRC.
"); - msg.append("

Click here to view them
\n"); - msg.append("


\n"); - } - - } -} \ No newline at end of file diff --git a/docker/ehrcron/scripts/automaticAlerts/adminAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/adminAlerts.pl deleted file mode 100644 index 97aea0705..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/adminAlerts.pl +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables an email a report. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Admin Alerts'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-do-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::Touch; -use File::Spec; -use File::Basename; -use Cwd 'abs_path'; -use List::MoreUtils qw/ uniq /; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); - -my $yesterday = localtime( ( time() - ( 24 * 60 * 60 ) ) ); -$yesterday = sprintf("%04d-%02d-%02d", $yesterday->year+1900, ($yesterday->mon)+1, $yesterday->mday); - - -my $email_html = "This email contains a series of automatic alerts about the WNPRC colony. It was run on: $datetimestr.

"; -my $results; - - -#summarize site usage in the past 7 days -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'core', - -queryName => 'SiteUsageByDay', - -filterArray => [ - ['date', 'dategte', '-7d'], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "Site Logins In The Past 7 Days:
\n"; - $email_html .= ""; - - foreach my $row (@{$results->{rows}}){ - $email_html .= ""; - } - $email_html .= "
Day of WeekDateLogins
".$$row{dayOfWeek}."".$$row{date}."".$$row{Logins}."

\n"; -} - -#Client Errors: -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => 'shared', - -schemaName => 'auditlog', - -queryName => 'audit', - -viewName => 'EHR Client Errors', - -filterArray => [ - ['date', 'dategte', $yesterday], - ['key1', 'neq', 'LabKey Server Backup'], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - - -if(@{$results->{rows}}){ - $email_html .= "WARNING: There were ".(@{$results->{rows}})." client errors since $yesterday:"; - - $email_html .= "

Click here to them

\n"; - $email_html .= '
'; -} - - -#we print some stats on data entry: - -$email_html .= "Data Entry Stats:

"; - -#$results = LabKey::Query::executeSql( -# -baseUrl => $baseUrl, -# -containerPath => $studyContainer, -# -schemaName => 'ehr', -# -sql => "SELECT formtype, count(*) as total FROM ehr.tasks WHERE cast(created as date) = '$yesterday' GROUP BY formtype ORDER BY formtype", -# #-debug => 1, -#); - -#if(@{$results->{rows}}){ -# $email_html .= "Number of Forms Created Yesterday:
\n"; -# foreach my $row (@{$results->{rows}}){ -# $email_html .= $row->{'formtype'}.": ".$row->{'total'}."
\n"; -# }; - -# $email_html .= "

\n"; -#} - -#$results = LabKey::Query::executeSql( -# -baseUrl => $baseUrl, -# -containerPath => $studyContainer, -# -schemaName => 'ehr', -# -sql => "SELECT Dataset.Label as label, count(*) as total FROM study.studydata WHERE cast(created as date) = '$yesterday' and taskid is not null GROUP BY Dataset.Label ORDER BY Dataset.Label", - #-debug => 1, -#); - -#if(@{$results->{rows}}){ -# $email_html .= "Number of Records Created Yesterday Through LabKey:
\n"; -# foreach my $row (@{$results->{rows}}){ -# $email_html .= $row->{'label'}.": ".$row->{'total'}."
\n"; -# }; - -# $email_html .= "

\n"; -#} - -#$results = LabKey::Query::executeSql( -# -baseUrl => $baseUrl, -# -containerPath => $studyContainer, -# -schemaName => 'ehr', -# -sql => "SELECT DataSet.Label as label, count(*) as total FROM study.studydata WHERE cast(created as date) = '$yesterday' and taskid is null and requestid is null GROUP BY DataSet.Label ORDER BY DataSet.Label", -# #-debug => 1, -#); -# -#if(@{$results->{rows}}){ -# $email_html .= "Number of Records Created Yesterday Through MySQL:
\n"; -# foreach my $row (@{$results->{rows}}){ -# $email_html .= $row->{'label'}.": ".$row->{'total'}."
\n"; -# }; -# -# $email_html .= "

\n"; -#} - - - - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; -#die; - -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, -); -if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - @email_recipients = uniq @email_recipients; - - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: Daily Admin Alerts: $datestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } -} - - -touch(File::Spec->catfile(dirname(abs_path($0)), '.adminAlertsLastRun')); diff --git a/docker/ehrcron/scripts/automaticAlerts/clinpathAbnormalResultAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/clinpathAbnormalResultAlerts.pl deleted file mode 100644 index 49bb09c4e..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/clinpathAbnormalResultAlerts.pl +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables and email a report. -The report is designed to identify potential problems related to clinpath. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; - -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Clinpath Abnormal Results'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-no-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::stat; -use File::Touch; -use Cwd 'abs_path'; -use File::Basename; -use File::Spec; -use List::MoreUtils qw/ uniq /; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); - -my $email_html = ""; -my $results; -my $doSend = 0; - -#touch a file when complete for monit -my $file = File::Spec->catfile(dirname(abs_path($0)), '.clinpathAbnormalResultAlertsLastRun'); -if(!-e $file){ - touch($file); -} -my $lastRun = localtime(stat($file)->mtime); - -my $lastRunFormatted = sprintf("%04d-%02d-%02d %02d:%02d", $lastRun->year+1900, ($lastRun->mon)+1, $lastRun->mday, $lastRun->hour, $lastRun->min); -my $lastRunMinusAWeek = sprintf("%04d-%02d-%02d %02d:%02d", $lastRun->year+1900, ($lastRun->mon)+1, ($lastRun->mday)-7, $lastRun->hour, $lastRun->min); - -#we find any record requested since the last email -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'ClinpathRefRange', - #-viewName => 'Abnormal', - -columns => 'Id,date,Id/curLocation/area,Id/curLocation/room,Id/curLocation/cage,alertStatus,taskid/datecompleted,testid,result,units,status,ref_range_min,ref_range_max,ageAtTime', - -filterArray => [ - ['qcstate/PublicData', 'eq', 'true'], - ['taskid/datecompleted', 'gte', $lastRunFormatted], - ['taskid/datecompleted', 'nonblank', ''], - ['date', 'gte', $lastRunMinusAWeek], - #['alertStatus', 'eq', 'true'], - ], - #-debug => 1, -); - -my $summary = {}; - -if(@{$results->{rows}}){ - foreach my $row (@{$results->{rows}}){ - if($row->{alertStatus}){ - $doSend = 1; - - if(!$row->{'Id/curLocation/area'}){ - $row->{'Id/curLocation/area'} = 'No Active Housing'; - } - if(!$row->{'Id/curLocation/room'}){ - $row->{'Id/curLocation/room'} = 'No Room'; - } - - if(!$$summary{$row->{'Id/curLocation/area'}}){ - $$summary{$row->{'Id/curLocation/area'}} = {}; - } - if(!$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}){ - $$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}} = []; - } - - if($$row{'requestid/description'}){ - $$row{'requestid/description'} =~ s/\n/\/g; - } - push(@{$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}}, $row); - } - }; - -} - -$email_html .= "There have been ".@{$results->{rows}}." clinpath tasks completed since $lastRunFormatted.
"; -$email_html .= "

Click here to view them

\n"; - -$email_html .= "

Listed below are the abnormal records.

\n"; - -my $prevRoom = ''; -foreach my $area (sort(keys %$summary)){ - my $rooms = $$summary{$area}; - $email_html .= "$area:
\n"; - foreach my $room (sort(keys %$rooms)){ - $email_html .= "$room:
\n"; - $email_html .= ""; - - foreach my $rec (@{$$rooms{$room}}){ - - my $color; - if($$rec{'ref_range_min'} && $$rec{'result'} < $$rec{'ref_range_min'}){ - $color = '#FBEC5D'; - } - elsif ($$rec{'ref_range_max'} && $$rec{'result'} > $$rec{'ref_range_max'}) { - $color = '#E3170D'; - } - - $email_html .= "". - "". - "". - "". - "". - "". - "".($$rec{'status'} ? $$rec{'status'} : '')."". - "". - "". - "". - ""; - } - - $email_html .= "
IdCollect DateDate CompletedTest IdResultUnitsStatusRef Range MinRef Range MaxAge At Time
".$$rec{Id}."".$$rec{date}."".$$rec{'taskid/datecompleted'}."".$$rec{testId}."".($$rec{'result'} ? $$rec{'result'} : '')."".($$rec{'units'} ? $$rec{'units'} : '')."".($$rec{'ref_range_min'} ? $$rec{'ref_range_min'} : '')."".($$rec{'ref_range_max'} ? $$rec{'ref_range_max'} : '')."".($$rec{'AgeAtTime'} ? $$rec{'AgeAtTime'} : '')."

\n"; - } - $email_html .= "

"; -} - -$email_html .= "


\n"; - - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; -#die; - -if($doSend){ - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, - ); - if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - @email_recipients = uniq @email_recipients; - - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: Abnormal Clinpath Results: $datetimestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } - } -} - -touch($file); diff --git a/docker/ehrcron/scripts/automaticAlerts/clinpathAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/clinpathAlerts.pl deleted file mode 100644 index d93df8a5d..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/clinpathAlerts.pl +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables and email a report. -The report is designed to identify potential problems related to clinpath. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; - -my $studyContainer = 'WNPRC/EHR/'; -my $notificationtypes = 'Clinpath Admin Alerts'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-no-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::stat; -use File::Touch; -use Cwd 'abs_path'; -use File::Basename; -use File::Spec; -use List::MoreUtils qw/ uniq /; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); -my $timestr = sprintf("%02d:%02d", $tm->hour, $tm->min); - -my $email_html = "This email contains reports on Clinpath Requests. It was run on: $datetimestr.

"; -my $results; - - -#touch a file when complete for monit -my $file = File::Spec->catfile(dirname(abs_path($0)), '.clinpathAlertsLastRun'); -if(!-e $file){ - touch($file); -} -my $lastRun = localtime(stat($file)->mtime); -$lastRun = sprintf("%04d-%02d-%02d %02d:%02d", $lastRun->year+1900, ($lastRun->mon)+1, $lastRun->mday, $lastRun->hour, $lastRun->min); - - -#we find any record requested since the last email -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Clinpath Runs', - -filterArray => [ - ['qcstate/label', 'eq', 'Request: Pending'], - ['created', 'gte', $lastRun], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -$email_html .= "Clinpath requests created since the last time this email was sent ($lastRun):
\n"; -if(@{$results->{rows}}){ - $email_html .= "There are ".@{$results->{rows}}." requests.
"; - $email_html .= "

Click here to view them
\n"; -} -else { - $email_html .= "No requests have been entered.
"; -} -$email_html .= "


\n"; - - - - -#we find any requests not yet approved -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Clinpath Runs', - -filterArray => [ - ['qcstate/label', 'eq', 'Request: Pending'], - ['date', 'dategte', $datestr], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -$email_html .= "Clinpath requests that have not been approved or denied yet:
\n"; -if(@{$results->{rows}}){ - $email_html .= "WARNING: There are ".@{$results->{rows}}." requests that have not been approved or denied yet.
"; - $email_html .= "

Click here to view them
\n"; -} -else { - $email_html .= "There are no requests that have not been approved or denied yet.
"; -} -$email_html .= "


\n"; - - -#we find any record not completed where the date requested is today -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Clinpath Runs', - -filterArray => [ - ['qcstate/label', 'neq', 'Completed'], - ['date', 'datelte', $datestr], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "WARNING: There are ".@{$results->{rows}}." requests that were requested for today or earlier, but have not been marked complete.
"; - $email_html .= "

Click here to view them
\n"; - $email_html .= "


\n"; -} - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; -#die; - -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, -); -if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - #print (@email_recipients);die; - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: Daily Clinpath Alerts: $datestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } -} - -touch($file); diff --git a/docker/ehrcron/scripts/automaticAlerts/clinpathResultAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/clinpathResultAlerts.pl deleted file mode 100644 index fc84586c6..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/clinpathResultAlerts.pl +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables and email a report. -The report is designed to identify potential problems related to clinpath. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Clinpath Results'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-no-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::stat; -use File::Touch; -use Cwd 'abs_path'; -use File::Basename; -use File::Spec; -use List::MoreUtils qw/ uniq /; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); -my $timestr = sprintf("%02d:%02d", $tm->hour, $tm->min); - -my $yesterday = localtime( ( time() - ( 24 * 60 * 60 ) ) ); -$yesterday = sprintf("%04d-%02d-%02d", $yesterday->year+1900, ($yesterday->mon)+1, $yesterday->mday); - - -my $email_html = "This email contains clinpath results entered since: $yesterday.

"; -my $results; - - - -#we find any record requested since the last email -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Clinpath Runs', - -columns => 'Id,date,Id/curLocation/area,Id/curLocation/room,Id/curLocation/cage,serviceRequested,requestId,requestid/description,reviewedBy,dateReviewed', - -sort => 'Id,date', - -filterArray => [ - ['qcstate/PublicData', 'eq', 'true'], - ['taskid/datecompleted', 'dategte', $yesterday], - ['taskid/datecompleted', 'nonblank', ''], - #['dateReviewed', 'isblank', ''], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "There are ".@{$results->{rows}}." completed requests since $yesterday. Below is a summary. Click the animal ID for more detail.
"; - $email_html .= "

Click here to view them

\n"; - - my $summary = {}; - foreach my $row (@{$results->{rows}}){ - if(!$row->{'Id/curLocation/area'}){ - $row->{'Id/curLocation/area'} = 'No Active Housing'; - } - if(!$row->{'Id/curLocation/room'}){ - $row->{'Id/curLocation/room'} = 'No Room'; - } - - if(!$$summary{$row->{'Id/curLocation/area'}}){ - $$summary{$row->{'Id/curLocation/area'}} = {}; - } - if(!$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}){ - $$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}} = []; - } - - if($$row{'requestid/description'}){ - $$row{'requestid/description'} =~ s/\n/\/g; - } - push(@{$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}}, $row); - }; - - my $prevRoom = ''; - foreach my $area (sort(keys %$summary)){ - my $rooms = $$summary{$area}; - $email_html .= "$area:
\n"; - foreach my $room (sort(keys %$rooms)){ - $email_html .= "$room:
\n"; - $email_html .= ""; - foreach my $rec (@{$$rooms{$room}}){ - no warnings 'uninitialized'; # some of these values appear to be empty occasionally... - clay, 26 Mar 2018 - $email_html .= "".($$rec{'reviewedBy'} ? $$rec{'reviewedBy'} : '').""; - } - - $email_html .= "
IdCollect DateService RequestedRequestorDate ReviewedReviewed By
".$$rec{Id}."".$$rec{date}."".$$rec{'serviceRequested'}."".($$rec{'requestid/description'} ? $$rec{'requestid/description'} : '')."".($$rec{'dateReviewed'} ? $$rec{'dateReviewed'} : '')."

\n"; - } - $email_html .= "

"; - } - - $email_html .= "


\n"; -} -else { - $email_html .= "No requests have been completed.
"; - $email_html .= "
\n"; -} - - - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; -#die; - -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, -); -if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - #print (@email_recipients);die; - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: New Clinpath Results: $datetimestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } -} - - -touch(File::Spec->catfile(dirname(abs_path($0)), '.clinpathResultAlertsLastRun')); diff --git a/docker/ehrcron/scripts/automaticAlerts/largeInfantAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/largeInfantAlerts.pl deleted file mode 100644 index 1f7c796eb..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/largeInfantAlerts.pl +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env perl - -# Selection query - - -use strict; -use LabKey::Query; -use Time::localtime; -use Net::SMTP; -use MIME::Lite; -use List::MoreUtils qw(uniq); - -# Create string for currentDate -my $tm = localtime; -my $dateString = sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); - -# Opening line of alert's message -my $email_html = "This email lists orphans assigned to cages that do not meet minimum size requirements as of $dateString.

"; -my $results; -my $mail_server = $ENV{'MAIL_SERVER'}; -my $from = 'ehr-do-not-reply@primate.wisc.edu'; - - -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; -my $default_container='/WNPRC/EHR/'; -my $dataExists = 0; -getHeavyInfants(); -if ($dataExists) { - sendEmail(); -} -sub getHeavyInfants { - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $default_container, - -schemaName => 'study', - -queryName => 'InfantsWithExcessWeight', - -requiredVersion => 8.3, - ); - if (@{$results->{rows}}){ - $dataExists = 1; - if (@{$results->{rows}} > 1) { - $email_html .= "WARNING: There are " .@{$results->{rows}}. " orphans under the age of 6 months residing in cages that do not accommodate the animals's size.
"; - } else { - $email_html .= "WARNING: There is " .@{$results->{rows}}. " orphan under the age of 6 months residing in a cage that does not accommodate the animals's size.
"; - } - - $email_html .= "

"; - $email_html .= "Animal
"; - foreach my $row (@{$results->{rows}}) { - $email_html .= $$row{'id'}; - #$email_html .= "\t"; - #$email_html .= $$row{'Room'}; - #$email_html .= "-"; - #$email_html .= $$row{'cage'}; - $email_html .= "
"; - } - $email_html .= "

"; - - $email_html .= "

Click here to view them

"; - $email_html .= '
'; - } - -} -sub sendEmail { - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $default_container, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'eq', 'Orphan Alert'], - ], - ); - if (@{$results->{rows}}) { - my @email_recipients; - foreach my $row (@{$results->{rows}}) { - push(@email_recipients, $$row{email}) - } - if (@email_recipients) { - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To => join(", ", @email_recipients), - From => $from, - Subject => "Subject: Orphans Not in Compliant Cage Alert on $dateString", - Type => 'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } - } -} - diff --git a/docker/ehrcron/scripts/automaticAlerts/overdueWeightAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/overdueWeightAlerts.pl deleted file mode 100644 index 8b0b6ea3c..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/overdueWeightAlerts.pl +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables an email a report. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Overdue Weight Alerts'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-no-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::Touch; -use File::Spec; -use File::Basename; -use Cwd 'abs_path'; -use List::MoreUtils qw/ uniq /; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); - -my $yesterday = localtime( ( time() - ( 24 * 60 * 60 ) ) ); -$yesterday = sprintf("%04d-%02d-%02d", $yesterday->year+1900, ($yesterday->mon)+1, $yesterday->mday); - -my $threeDaysAgo = localtime( ( time() - ( 3 * 24 * 60 * 60 ) ) ); -$threeDaysAgo = sprintf("%04d-%02d-%02d", $threeDaysAgo->year+1900, ($threeDaysAgo->mon)+1, $threeDaysAgo->mday); - - -my $email_html = "This email contains alerts of animals not weighed in the past 60 days. It was run on: $datetimestr.

"; -my $results; - - -#first we find all living animals without a weight: -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Demographics', - -filterArray => [ - ['calculated_status', 'eq', 'Alive'], - ['Id/MostRecentWeight/MostRecentWeightDate', 'isblank', ''], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -$email_html .= "Living animals without a weight:
"; - -if(!@{$results->{rows}}){ - $email_html .= "There are no living animals without a weight.


"; -} -else { - foreach my $row (@{$results->{rows}}){ - $email_html .= $row->{'Id'}."
"; - }; - - $email_html .= "

Click here to view these animals

"; - $email_html .= '
'; -} - - -#find animals not weighed in the past 60 days -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Demographics', - -viewName => 'Weight Detail', - -filterArray => [ - ['calculated_status', 'eq', 'Alive'], - ['Id/MostRecentWeight/DaysSinceWeight', 'gt', 60], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - - -if(@{$results->{rows}}){ - $email_html .= "WARNING: The following animals have not been weighed in the past 60 days:
"; - - $email_html .= "

Click here to view them

\n"; - - my $summary = {}; - foreach my $row (@{$results->{rows}}){ - if(!$$summary{$row->{'Id/curLocation/area'}}){ - $$summary{$row->{'Id/curLocation/area'}} = {}; - } - if(!$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}){ - $$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}} = []; - } - - push(@{$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}}, $row); - }; - - my $prevRoom = ''; - foreach my $area (sort(keys %$summary)){ - my $rooms = $$summary{$area}; - $email_html .= "$area:
\n"; - foreach my $room (sort(keys %$rooms)){ - $email_html .= "$room:
\n"; - $email_html .= ""; - - foreach my $rec (@{$$rooms{$room}}){ - $email_html .= ""; - } - - $email_html .= "
CageIdDays Since Weight
".$$rec{'Id/curLocation/cage'}."".$$rec{Id}."".$$rec{'Id/MostRecentWeight/DaysSinceWeight'}."

\n"; - } - $email_html .= "

"; - } - - $email_html .= "


\n"; -} - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; -#die; - -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, -); -if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - #print (@email_recipients);die; - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: Overdue Weights: $datestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } -} - -touch(File::Spec->catfile(dirname(abs_path($0)), '.overdueWeightAlertsLastRun')); - diff --git a/docker/ehrcron/scripts/automaticAlerts/siteErrorAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/siteErrorAlerts.pl deleted file mode 100644 index 8c86026d3..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/siteErrorAlerts.pl +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables and email a report. -The report is designed to identify potential problems related to clinpath. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'} -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Site Error Alerts'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-no-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::stat; -use File::Touch; -use Cwd 'abs_path'; -use File::Basename; -use File::Spec; -use List::MoreUtils qw/ uniq /; - - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); -my $timestr = sprintf("%02d:%02d", $tm->hour, $tm->min); - -my $email_html = "This email contains site errors since: $datetimestr.

"; -my $results; -my $doSend = 0; - -#touch a file when complete for monit -my $file = File::Spec->catfile(dirname(abs_path($0)), '.siteErrorAlertsLastRun'); -if(!-e $file){ - touch($file); -} -my $lastRun = localtime(stat($file)->mtime); -$lastRun = sprintf("%04d-%02d-%02d %02d:%02d", $lastRun->year+1900, ($lastRun->mon)+1, $lastRun->mday, $lastRun->hour, $lastRun->min); - - -#we find any record requested since the last email -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => 'shared', - -schemaName => 'auditlog', - -queryName => 'audit', - -viewName => 'EHR Client Errors', - -filterArray => [ - ['date', 'gte', $lastRun], - ['key1', 'neq', 'LabKey Server Backup'], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $doSend = 1; - $email_html .= "WARNING: There were ".(@{$results->{rows}})." client errors since $lastRun:"; - - $email_html .= "

Click here to them

\n"; - $email_html .= '
'; -} - - - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; -#die; - -if($doSend){ - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, - ); - if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - #print (@email_recipients);die; - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: EHR Site Error: $datetimestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } - } -} - -touch($file); diff --git a/docker/ehrcron/scripts/automaticAlerts/treatmentAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/treatmentAlerts.pl deleted file mode 100644 index 109a6d485..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/treatmentAlerts.pl +++ /dev/null @@ -1,409 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables an email a report -summarizing daily treatments and related issues. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Incomplete Treatments'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-do-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -#use Time::Piece; -use File::Touch; -use File::Spec; -use File::Basename; -use Cwd 'abs_path'; -use List::MoreUtils qw/ uniq /; - -# this is mostly safe since a lot of times some strings are left blank (from data entry) -no warnings 'uninitialized'; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); -my $timestr = sprintf("%02d:%02d", $tm->hour, $tm->min); -my $timeOfDay = $tm->hour; - -my $email_html = "This email contains any scheduled treatments not marked as completed. It was run on: $datetimestr.

"; -my $results; -my $send_email = 0; - -#we find any rooms lacking obs for today -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'RoomsWithoutObsToday', - -filterArray => [ - ['hasObs', 'eq', 'N'], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "WARNING: The following rooms do not have any obs for today as of $timestr. "; - $email_html .= "Click here to view them

\n"; - - foreach my $row (@{$results->{rows}}){ - $email_html .= $row->{'room'}."
"; - } - - $email_html .= "


\n"; -} - - -#we find any treatments where the animal is not assigned to that project -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'treatmentSchedule', - -filterArray => [ - ['Id/DataSet/Demographics/calculated_status', 'eq', 'Alive'], - ['projectStatus', 'isnonblank', ''], - ['date', 'dateeq', $datestr], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "WARNING: There are ".@{$results->{rows}}." scheduled treatments where the animal is not assigned to the project.
"; - $email_html .= "

Click here to view them
\n"; - $email_html .= "


\n"; -} - - -#we find treatments for each time of day: -processTreatments('AM', 9); -processTreatments('Noon', 12); -processTreatments('PM', 14); -processTreatments('Any Time', 14); -processTreatments('Night', 17, 1); - -my $hasTreatments = 0; - -sub processTreatments { - my $timeofday = shift; - my $minTime = shift; - my $noSendUnlessTreatments = shift; - - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'treatmentSchedule', - -columns => 'Id,CurrentArea,CurrentRoom,CurrentCage,projectStatus,treatmentStatus,treatmentStatus/Label,meaning,code,,volume2,conc2,route,amount2,remark,performedby', - -sort => 'CurrentArea,CurrentRoom', - -filterArray => [ - ['date', 'dateeq', $datestr], - ['timeofday', 'eq', $timeofday], - #['CurrentArea', 'in', join(';', @$areas)], - ['Id/DataSet/Demographics/calculated_status', 'eq', 'Alive'], - ], - -requiredVersion => 8.3, - #-debug => 1, - ); - - $email_html .= "$timeofday Treatments:
"; - - #print "We have rows: ".scalar @{$results->{rows}}."\n"; - if(!@{$results->{rows}}){ - $email_html .= "There are no scheduled $timeofday treatments as of $timestr. Treatments could be added after this email was sent, so please check online closer to the time.
"; - if($timeOfDay >= $minTime && $noSendUnlessTreatments){ - $send_email = 0; - } - } - else { - my $complete = 0; - my $incomplete = 0; - my $summary = {}; - foreach my $row (@{$results->{rows}}){ - if($row->{'treatmentStatus/Label'} && $row->{'treatmentStatus/Label'} eq 'Completed'){ - $complete++; - } - else { - if(!$$summary{$row->{'CurrentArea'}}){ - $$summary{$row->{'CurrentArea'}} = {}; - } - if(!$$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}}){ - $$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}} = {complete=>0,incomplete=>0,incompleteRecords=>[]}; - } - - $$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}}{incomplete}++; - push(@{$$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}}{incompleteRecords}}, $row); - - $incomplete++; - } - }; - - my $url = "Click here to view them

\n"; - $email_html .= "There are ".@{$results->{rows}}." scheduled $timeofday treatments. $complete have been completed. $url

\n"; - - if($timeOfDay >= $minTime){ - if(!$incomplete){ - $email_html .= "All scheduled $timeofday treatments have been marked complete as of $datetimestr.

\n"; - - if($noSendUnlessTreatments){ - $send_email = 0; - } - } - else { - $email_html .= "The following $timeofday treatments have not been marked complete as of $datetimestr:

\n"; - $send_email = 1; - - my $prevRoom = ''; - foreach my $area (sort(keys %$summary)){ - my $rooms = $$summary{$area}; - $email_html .= "$area:
\n"; - foreach my $room (sort(keys %$rooms)){ - if($$rooms{$room}{incomplete}){ - $email_html .= "$room: ".$$rooms{$room}{incomplete}."
\n"; - $email_html .= ""; - - foreach my $rec (@{$$rooms{$room}{incompleteRecords}}){ - $email_html .= ""; - } - - $email_html .= "
IdTreatmentRouteConcentrationAmount To GiveVolumeInstructionsOrdered By
".$$rec{Id}."".($$rec{meaning} ? $$rec{meaning} : '')."".($$rec{route} ? $$rec{route} : '')."".($$rec{conc2} ? $$rec{conc2} : '')."".($$rec{amount2} ? $$rec{amount2} : '')."".($$rec{volume2} ? $$rec{volume2} : '')."".($$rec{remark} ? $$rec{remark} : '')."".($$rec{performedby} ? $$rec{performedby} : '')."

\n"; - } - - } - $email_html .= "

"; - } - } - } - else { - $email_html .= "It is too early in the day to send warnings about incomplete treatments\n"; - } - - $email_html .= "


"; - } -} - - -#then any treatments from today that different from the order: -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'TreatmentsThatDiffer', - -columns => '*', - -filterArray => [ - ['date', 'dateeq', $datestr], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -$email_html .= "Treatments that differ from what was ordered:

"; - -if(!@{$results->{rows}}){ - $email_html .= "All entered treatments given match what was ordered.


"; -} -else { - $email_html .= "Click here to view them

\n"; - - my $summary = {}; - foreach my $row (@{$results->{rows}}){ - if(!$$summary{$row->{'CurrentArea'}}){ - $$summary{$row->{'CurrentArea'}} = {}; - } - if(!$$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}}){ - $$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}} = []; - } - - push(@{$$summary{$row->{'CurrentArea'}}{$row->{'CurrentRoom'}}}, $row); - }; - - my $prevRoom = ''; - foreach my $area (sort(keys %$summary)){ - my $rooms = $$summary{$area}; - $email_html .= "$area:
\n"; - foreach my $room (sort(keys %$rooms)){ - $email_html .= "$room: ".@{$$rooms{$room}}."
\n"; - $email_html .= "\n"; - foreach my $rec (@{$$rooms{$room}}){ - $email_html .= "\n"; - } - $email_html .= "
"; - $email_html .= 'Id: '.$$rec{id}."
\n"; - $email_html .= 'Date: '.$$rec{date}."
\n"; - $email_html .= 'Treatment: '.$$rec{meaning}."
\n"; - $email_html .= 'Ordered By: '.$$rec{performedby}."
\n"; - $email_html .= 'Performed By: '.$$rec{drug_performedby}."
\n"; - - if(defined $$rec{route} && $$rec{route} ne $$rec{drug_route}){ - $email_html .= 'Route Ordered: '.$$rec{route}."
\n"; - $email_html .= 'Route Entered: '.$$rec{drug_route}."
\n"; - } - if(defined $$rec{concentration} && ($$rec{concentration} != $$rec{drug_concentration} || $$rec{conc_units} ne $$rec{drug_conc_units})){ - $email_html .= 'Concentration Ordered: '.$$rec{concentration}.' '.$$rec{conc_units}."
\n"; - $email_html .= 'Concentration Entered: '.$$rec{drug_concentration}.' '.$$rec{drug_conc_units}."
\n"; - } - if(defined $$rec{dosage} && ($$rec{dosage} != $$rec{drug_dosage} || $$rec{dosage_units} ne $$rec{drug_dosage_units})){ - $email_html .= 'Dosage Ordered: '.$$rec{dosage}.' '.$$rec{dosage_units}."
\n"; - $email_html .= 'Dosage Entered: '.$$rec{drug_dosage}.' '.$$rec{drug_dosage_units}."
\n"; - } - if(defined $$rec{amount} && ($$rec{amount} != $$rec{drug_amount} || $$rec{amount_units} ne $$rec{drug_amount_units})){ - $email_html .= 'Amount Ordered: '.$$rec{amount}.' '.$$rec{amount_units}."
\n"; - $email_html .= 'Amount Entered: '.$$rec{drug_amount}.' '.$$rec{drug_amount_units}."
\n"; - } - if(defined $$rec{volume} && ($$rec{volume} != $$rec{drug_volume} || $$rec{vol_units} ne $$rec{drug_vol_units})){ - $email_html .= 'Volume Ordered: '.$$rec{volume}.' '.$$rec{vol_units}."
\n"; - $email_html .= 'Volume Entered: '.$$rec{drug_volume}.' '.$$rec{drug_vol_units}."
\n"; - } - - $email_html .= "
\n"; - $email_html .= "

\n"; - } - - $email_html .= "

"; - } - - $email_html .= "


"; -} - - -#we find any treatments where the animal is not alive -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Treatment Orders', - -filterArray => [ - ['Id/DataSet/Demographics/calculated_status', 'neqornull', 'Alive'], - ['enddate', 'isblank', ''], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "WARNING: There are ".@{$results->{rows}}." active treatments for animals not currently at WNPRC."; - $email_html .= "

Click here to view and update them
\n"; - $email_html .= "


\n"; -} - -#we find any problems where the animal is not alive -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Problem List', - -filterArray => [ - ['Id/DataSet/Demographics/calculated_status', 'neqornull', 'Alive'], - ['enddate', 'isblank', ''], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -if(@{$results->{rows}}){ - $email_html .= "WARNING: There are ".@{$results->{rows}}." unresolved problems for animals not currently at WNPRC."; - $email_html .= "

Click here to view and update them
\n"; - $email_html .= "


\n"; -} - - -# Check for missing In Rooms after 2:30pm, as specified in the SOP -my $hour = $tm->hour; -my $minute = $tm->min; -if ($hour > 14 || (($hour == 14) && ($minute >= 30))) { - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'inRoomNotSubmitted', - -columns => '*', - -requiredVersion => 8.3 - ); - - my $in_rooms_href = $baseUrl . "query/" . $studyContainer . "executeQuery.view?schemaName=study&query.queryName=inRoomNotSubmitted"; - my $num_incomplete_in_rooms = scalar(@{$results->{rows}}); - - my $prefix = ($num_incomplete_in_rooms > 0) ? "WARNING: " : ""; - $email_html .= "${prefix}There are " . $num_incomplete_in_rooms . " animals without In Rooms"; - - $email_html .= "
\n"; - $email_html .= "
\n"; -} - - -#print "Send email: \"$send_email\""; -if($send_email){ -# open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -# print HTML $email_html; -# close HTML; -# die; - - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, - ); - if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - #print (@email_recipients);die; - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: Animal Care Alerts: $datestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - #print $smtp->as_string(); - $smtp->send() || die; - } - } -} - -touch(File::Spec->catfile(dirname(abs_path($0)), '.treatmentAlertsLastRun')); diff --git a/docker/ehrcron/scripts/automaticAlerts/weightAlerts.pl b/docker/ehrcron/scripts/automaticAlerts/weightAlerts.pl deleted file mode 100644 index fb05f66c1..000000000 --- a/docker/ehrcron/scripts/automaticAlerts/weightAlerts.pl +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env perl - -=head1 DESCRIPTION - -This script is designed to run as a cron job. It will query a number of tables an email a report. -The report is designed to identify potential problems with the colony, primarily related to weights, housing -and assignments. - - -=head1 LICENSE - -This package and its accompanying libraries are free software; you can -redistribute it and/or modify it under the terms of the GPL (either -version 1, or at your option, any later version) or the Artistic -License 2.0. - -=head1 AUTHOR - -Ben Bimber - -=cut - -#config options: -my $baseUrl = $ENV{'LK_BASE_URL'}; -my $printableUrl = $ENV{'PERL_LINK_URL'}; -my $studyContainer = 'WNPRC/EHR/'; - -my $notificationtypes = 'Weight Drops'; -my $mail_server = $ENV{'MAIL_SERVER'}; - -#emails will be sent from this address -my $from = 'ehr-no-not-reply@primate.wisc.edu'; - - -############Do not edit below this line -use strict; -use warnings; -use LabKey::Query; -use Net::SMTP; -use MIME::Lite; -use Data::Dumper; -use Time::localtime; -use File::Touch; -use File::Spec; -use File::Basename; -use Cwd 'abs_path'; -use List::MoreUtils qw/ uniq /; - -# Find today's date -my $tm = localtime; -my $datetimestr=sprintf("%04d-%02d-%02d at %02d:%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday, $tm->hour, $tm->min); -my $datestr=sprintf("%04d-%02d-%02d", $tm->year+1900, ($tm->mon)+1, $tm->mday); - -my $yesterday = localtime( ( time() - ( 24 * 60 * 60 ) ) ); -$yesterday = sprintf("%04d-%02d-%02d", $yesterday->year+1900, ($yesterday->mon)+1, $yesterday->mday); - -my $threeDaysAgo = localtime( ( time() - ( 3 * 24 * 60 * 60 ) ) ); -$threeDaysAgo = sprintf("%04d-%02d-%02d", $threeDaysAgo->year+1900, ($threeDaysAgo->mon)+1, $threeDaysAgo->mday); - - -my $email_html = "This email contains alerts of weight changes of +/- 10% or greater. It was run on: $datetimestr.

"; -my $results; - - -#first we find all living animals without a weight: -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'Demographics', - -filterArray => [ - ['calculated_status', 'eq', 'Alive'], - ['Id/MostRecentWeight/MostRecentWeightDate', 'isblank', ''], - ], - -requiredVersion => 8.3, - #-debug => 1, -); - -$email_html .= "Living animals without a weight:
"; - -if(!@{$results->{rows}}){ - $email_html .= "There are no living animals without a weight.


"; -} -else { - foreach my $row (@{$results->{rows}}){ - $email_html .= $row->{'Id'}."
"; - }; - - $email_html .= "

Click here to view these animals

"; - $email_html .= '
'; -} - - -#then we find all weight drops of >10% in the past 30 days -processWeights(0, 30, 'lte', -10); -processWeights(0, 30, 'gte', 10); - -#processWeights(7, 30, 'lte', -10); -#processWeights(7, 30, 'gte', 10); - -#processWeights(30, 90, 'lte', -10); -#processWeights(30, 90, 'gte', 10); - -#processWeights(90, 180, 'le', -10); -#processWeights(90, 180, 'gte', 10); - - -sub processWeights { - my $min = shift; - my $max = shift; - my $pctFilter = shift; - my $pct = shift; - - $results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -containerPath => $studyContainer, - -schemaName => 'study', - -queryName => 'weightRelChange', - -columns => 'Id,Id/curLocation/area,Id/curLocation/room,Id/curLocation/cage,LatestWeightDate,LatestWeight,date,weight,PctChange,IntervalInDays', - -sort => 'Id/curLocation/area,Id/curLocation/room,Id/curLocation/cage,Id', - -filterArray => [ - ['Id/DataSet/Demographics/calculated_status', 'eq', 'Alive'], - ['PctChange', $pctFilter, $pct], - ['LatestWeightDate', 'dategte', $threeDaysAgo], - ['IntervalInDays', 'gte', $min], - ['IntervalInDays', 'lte', $max], - ], - -requiredVersion => 8.3, - #-debug => 1, - ); - - $email_html .= "Weights since $threeDaysAgo representing changes of ".$pct."% in the past $max days:
"; - - if(!@{$results->{rows}}){ - $email_html .= "There are no changes during this period.
"; - } - else { - my $total = 0; - my $summary; - foreach my $row (@{$results->{rows}}){ - if(!$$summary{$row->{'Id/curLocation/area'}}){ - $$summary{$row->{'Id/curLocation/area'}} = {}; - } - if(!$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}){ - $$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}} = {records=>[]}; - } - - push(@{$$summary{$row->{'Id/curLocation/area'}}{$row->{'Id/curLocation/room'}}{records}}, $row); - $total++; - }; - - my $prevRoom = ''; - $email_html .= ""; - foreach my $area (sort(keys %$summary)){ - my $rooms = $$summary{$area}; - foreach my $room (sort(keys %$rooms)){ - foreach my $rec (@{$$rooms{$room}{records}}){ - $email_html .= ""; - } - } - } - - $email_html .= "
IdAreaRoomCageCurrent Weight (kg)Weight DatePrevious Weight (kg)DatePercent ChangeDays Between
".$$rec{Id}."$area$room".$$rec{'Id/curLocation/cage'}."".$$rec{LatestWeight}."".$$rec{LatestWeightDate}."".$$rec{weight}."".$$rec{date}."".$$rec{PctChange}."".$$rec{IntervalInDays}."

\n"; -# $email_html .= "

Click here to view these animals

"; - $email_html .= '
'; - } -} - - -#open(HTML, ">", "C:\\Users\\Admin\\Desktop\\test.html"); -#print HTML $email_html; -#close HTML; - -$results = LabKey::Query::selectRows( - -baseUrl => $baseUrl, - -requiredVersion => 8.3, - -containerPath => $studyContainer, - -schemaName => 'ehr', - -queryName => 'NotificationRecipientsExpanded', - -filterArray => [ - ['notificationtype', 'in', $notificationtypes], - ], - #-debug => 1, -); -if(@{$results->{rows}}){ - my @email_recipients; - foreach my $row (@{$results->{rows}}){ - push(@email_recipients, $$row{email}) - } - - if(@email_recipients){ - #print (@email_recipients);die; - @email_recipients = uniq @email_recipients; - my $smtp = MIME::Lite->new( - To =>join(", ", @email_recipients), - From =>$from, - Subject =>"Subject: Weight Alerts: $datestr", - Type =>'multipart/alternative' - ); - $smtp->attach(Type => 'text/html', - Encoding => 'quoted-printable', - Data => $email_html - ); - $smtp->send() || die; - } -} - -touch(File::Spec->catfile(dirname(abs_path($0)), '.weightAlertsLastRun')); - From eefd9f45105ec1f97aca5332b86be06854770c60 Mon Sep 17 00:00:00 2001 From: "F. Daniel Nicolalde" Date: Wed, 30 Apr 2025 10:46:26 -0500 Subject: [PATCH 06/13] Adding a script to have a up to date test server that restore nightly (#784) * Adding timeout to docker down * new script to load a uptodate test server * adding timeout to docker down * making new file executable * adding tmp directory * adding restore file location * adding random tmp directory * removing / from restorefile * Adding wait time for postgres * Adding wait time for postgres to start. Starting rest of containers. * adding PATH for cron job * fixing typo * adding some echos * Adding whole path for postgres file * Changing directory * Adding more echo outputs * Updating docker image to latest build from Docker Hub * removing \n and adding jobs as parameter. * correcting typo * Adding default jobs * Chaging theme of test server and name * Changing different properties from production db * Prune system * Moving prune after docker start up * getting labkey version from .env file * Adding URL for test server via module property in WNPRC Module * Adding target to open a new tab when accessing secondary test server * Adding rsync for files folder * Adding file location via .env file * correcting new env format * adding additinal platform for arm hardware * adding special driver to build arm64 * Adding builder to use arm64 * improving comment * Adding two step process to download packages first and than install * adding support for multiple platform installation for R using rig * usign bash script to download R * Changin buildard to targetarch * adding specific builder to create mutliple platform image * removing download r script and using parallel install * removing parallel building * separating remotes to be installed first * removing remotes and vizpedigree * simplifying dockerfile for cranutils * using build script to build locally and adding platform variable * fixing location of dockerfile * enabling multiple cpus * Adding documentation about docker services * adding repos * Adding documentation * fixing warnings for dockerfile * Removing wite spaces * removing dowload only scripts * simplifying insllation remotes * adding some log for docker hub --- WNPRC_EHR/resources/module.xml | 6 + WNPRC_EHR/resources/views/ehrBegin.html | 3 +- docker/README.md | 52 +++- docker/cranrnutils/Dockerfile | 38 +-- docker/cranrnutils/hooks/build | 34 +++ docker/cranrnutils/install.r | 23 +- docker/labkey/Dockerfile | 10 +- docker/labkey/hooks/build | 10 +- docker/load_database_backup.sh | 4 +- docker/load_database_update_testserver.sh | 321 ++++++++++++++++++++++ 10 files changed, 450 insertions(+), 51 deletions(-) create mode 100755 docker/cranrnutils/hooks/build create mode 100755 docker/load_database_update_testserver.sh diff --git a/WNPRC_EHR/resources/module.xml b/WNPRC_EHR/resources/module.xml index 3105c0979..d1df20205 100644 --- a/WNPRC_EHR/resources/module.xml +++ b/WNPRC_EHR/resources/module.xml @@ -38,5 +38,11 @@ ADMIN + + URL for Nightly test server that gets updated from daily backups + + ADMIN + + \ No newline at end of file diff --git a/WNPRC_EHR/resources/views/ehrBegin.html b/WNPRC_EHR/resources/views/ehrBegin.html index 568b455cf..acc0da4c8 100644 --- a/WNPRC_EHR/resources/views/ehrBegin.html +++ b/WNPRC_EHR/resources/views/ehrBegin.html @@ -123,7 +123,8 @@ {name: 'Run SQL Directly', url: '<%=contextPath%>' + ctx['EHRStudyContainer'] + '/ehr-executeSql.view'}, {name: 'Search WNPRC SNOMED Codes', url: '<%=contextPath%>' + ctx['EHRStudyContainer'] + '/query-executeQuery.view?schemaName=ehr_lookups&query.queryName=snomed'}, {name: 'Search ALL SNOMED Codes', url: '<%=contextPath%>' + ctx['EHRStudyContainer'] + '/query-executeQuery.view?schemaName=ehr_lookups&query.queryName=full_snomed'}, - {name: 'Gestational Day Calculator', url: '<%=contextPath%>' + ctx['EHRStudyContainer'] + '/wnprc_ehr-gestation_calculator.view'} + {name: 'Gestational Day Calculator', url: '<%=contextPath%>' + ctx['EHRStudyContainer'] + '/wnprc_ehr-gestation_calculator.view'}, + {name: 'Nightly-EHRTestServer', url:ctx['NightlyTestServer'], target: '_blank'} ]}, {header: 'Data Entry', items: [ diff --git a/docker/README.md b/docker/README.md index 673bd17ad..d71c10987 100644 --- a/docker/README.md +++ b/docker/README.md @@ -101,18 +101,33 @@ docker build \ The LabKey image depends on the Tomcat image, which can be dowload from Docker Hub or build locally. This image takes a long time to build from scratch, it is best to download it from Docker Hub. Here are the commands to download or build this image. ``` -./gradlew downloadTomcat -PdockerString= -./gradlew downloadTomcatPlug -PdockerString= +./gradlew downloadTomcat -PbranchName= +./gradlew downloadTomcatPlug -PbranchName= -./gradlew buildTomcat -PdockerString= -./gradlew buildTomcatPlug -PdockerString= +./gradlew buildTomcat -PbranchName= +./gradlew buildTomcatPlug -PbranchName= docker build --no-cache -t wnprcehr/tomcat:tomcat9_ tomcat ``` ## Deploying the Docker Compose Services -Docker images including LabKey version number and branch are control by variables defined in the .env file. The compose (i.e. `compose.yaml`) file has the following string for the LabKey service `wnprcehr/labkey${LK_PROD}:$LK_VERSION${LK_FB}`. LK_PROD has to be empty except for the production environment which gets replace with **SNAPSHOT**. LK_VERSION gets replace with the version of LabKey that is going to be used (i.e. 22.11) and LK_FB get the name of the branch to test, in production this variable is blank. In production this string gets converted to `wnprcehr/labkeysnapshot:22.11` which match the tag in Docker Hub for the [labkeysnapshot repository](https://hub.docker.com/repository/docker/wnprcehr/labkeysnapshot/tags?page=1&ordering=last_updated). For feature branches the string gets converted to `wnprcehr/labkey:22.11_` with the corresponding fb name coming from GitHub, these images are hosted in the [labkey repository](https://hub.docker.com/repository/docker/wnprcehr/labkey/tags?page=1&ordering=last_updated) with their corresponding tags. +There are several services controlled by the compose.yaml and production.yaml files. Spliting the Docker services in these two files allows to use the same GitHub repository in two different server without having to make changes locally except for changes in the `.env` file. + +The Docker services are production EHR, nightly-ehr and test servers ran are the follwoing: +||Service|Functionality|YAML File|Repository| +|---|---|---|---|---| +|1|postgres|database|compose.yaml|[postgres](https://hub.docker.com/_/postgres)| +|2|labkey|Application|compose.yaml|[labkeysnapshot](https://hub.docker.com/repository/docker/wnprcehr/labkeysnapshot/general)| +|3|cadvisor|Monitor server resources|compose.yaml|[Google cadvisor](https://github.com/google/cadvisor/releases)| +|4|mailcatcher|Applicaton to record emails sent by server|compose.yaml|| +|5|mailserver|Postfix mail erver|compose.yaml|| +|6| ngnix|Web server|compose.yaml|| +|7| perlscripts|Manage cron jobs for backups and delete records|production.yaml|[ehrcronprod](https://hub.docker.com/repository/docker/wnprcehr/ehrcronprod/general), [ehrcron](https://hub.docker.com/repository/docker/wnprcehr/ehrcron/general)| + + + +Docker images including LabKey version number and branch are control by variables defined in the `.env` file. The compose (i.e. `compose.yaml`) file has the following string for the LabKey service `wnprcehr/labkey${LK_PROD}:$LK_VERSION${LK_FB}`. LK_PROD has to be empty except for the production environment which gets replace with **SNAPSHOT**. LK_VERSION gets replace with the version of LabKey that is going to be used (i.e. 24.11) and LK_FB get the name of the feature branch to test, in production LK_FB is blank. In production this string gets converted to `wnprcehr/labkeysnapshot:22.11` which match the tag in Docker Hub for the [labkeysnapshot repository](https://hub.docker.com/repository/docker/wnprcehr/labkeysnapshot/tags?page=1&ordering=last_updated). For feature branches the string gets converted to `wnprcehr/labkey:22.11_` with the corresponding fb name coming from GitHub, these images are hosted in the [labkey repository](https://hub.docker.com/repository/docker/wnprcehr/labkey/tags?page=1&ordering=last_updated) with their corresponding tags. To deploy the services, you again either use Gradle or use Docker Compose directly. To use Gradle, execute the following build tasks: ``` @@ -122,23 +137,42 @@ To deploy the services, you again either use Gradle or use Docker Compose direct # for tearing down all the services ./gradlew :docker:down ``` -To use Docker Compose, you can execute commands like the following (*from this directory*, where your `.env` file is located): +To use Docker Compose, you can execute commands like the following (*from this directory*, where your `.env` file is located), this commands will work on production server as well as other servers: +``` +# for spinning up all the services in production server +docker compose -f compose.yaml -f production.yaml up -d + +# for tearing down all the services in production server* +docker compose -f compose.yaml -f production.yaml down --timeout 60 +``` + +Add `-f compose.yaml -f production.yaml` to make changes in the production server. If this is not added the system will provide a warning that there is an orphan services running. ``` # for spinning up all the services docker compose up -d # for tearing down all the services* -docker compose down +docker compose down --timeout 60 # for spinning up just one of the services (e.g., postgres) docker compose up -d postgres # for taking down just one of the services (e.g., postgres) -docker compose stop postgres +docker compose stop postgres --timeout 60 + +# for accesing running services to inspect changes use the following commands (e.g., labkey or postgres) +docker compose exec labkey /bin/bash ``` All other Docker Compose commands (`logs`, `ps`, etc.) work also. -*Note that sometimes the postgres container closes before the database itself is completely shut down. Be sure to disconnect your pgAdmin and IntelliJ database connections, if any, stop labkey, and then do a shutdown. Otherwise the next time postgres starts it will go into an automatic recovery mode and take a long time to start back up. +*Note that sometimes the postgres container closes before the database itself is completely shut down. Be sure to disconnect your pgAdmin and IntelliJ database connections, if any, stop labkey, and then do a shutdown. Otherwise the next time postgres starts it will go into an automatic recovery mode and take a long time to start back up. By adding a timeout of 60 seconds it allows the database to close gracefully and avoid the recovery process. + +## Docker setup in production EHR +Running EHR in the production mode, requires different images for ehrcron and the **SNAPSHOT** version of LabKey. The verison of LabKey is controlled by the following variables stored in `.env` file: `LK_PROD`, `LK_VERSION` and `LK_FB`. These varaibles get replace in the following string `wnprcehr/labkey${LK_PROD}:$LK_VERSION${LK_FB}` during runtime. The string becomes `wnprcehr/labkeysnapshot:24.11` which are the tags in this [repo](https://hub.docker.com/repository/docker/wnprcehr/labkeysnapshot/tags). The version of ehrcron image is also controlled by the `.env` file. The variables `PERL_PROD`, `LK_VERSION` and `LK_FB` are used to modify the name of the image defined in the `production.yaml` file from `wnprcehr/ehrcron$PERL_PROD:$LK_VERSION${LK_FB}` to `wnprcehr/ehrcronprod:24.11`. These are the tags defined in this [repo](https://hub.docker.com/repository/docker/wnprcehr/ehrcronprod/general). The ehrcronprod image has the following scheduled jobs: +1. Email notifications - deprecated April, 2025 +1. Delete records with QCStatus of ` Delete Requested` from all study datasets +1. Backup script which rans overnight + ## Running multiple instances of LabKey in same Server diff --git a/docker/cranrnutils/Dockerfile b/docker/cranrnutils/Dockerfile index db00c5f01..b1ab16e88 100644 --- a/docker/cranrnutils/Dockerfile +++ b/docker/cranrnutils/Dockerfile @@ -1,27 +1,16 @@ -ARG R_VERSION=4.4.1 FROM eclipse-temurin:17-jre-jammy # Set noninteractive mode for apt-get -ENV DEBIAN_FRONTEND noninteractive +ENV DEBIAN_FRONTEND=noninteractive -ENV LANG en_US.utf8 +ENV LANG=en_US.utf8 -# Adding various application to download packages to install Postgres 15 and CRAN-R RUN apt-get update \ - && apt-get -qq install -y lsb-release \ - texinfo \ - tzdata \ - gettext \ - make \ - openssl \ - #Linux packages needed for installing CRAN-R + && apt-get -qq install -y lsb-release \ software-properties-common \ - dirmngr \ - #Linux packages needed for RlabKey - libcurl4-openssl-dev \ - libssl-dev \ && apt-get clean + # Download and add siging key for Postgres 15 # https://www.linuxtechi.com/how-to-install-postgresql-on-ubuntu/ # https://askubuntu.com/a/1456015 @@ -34,10 +23,21 @@ RUN wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | tee /etc/apt/ RUN wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc > /dev/null 2>&1 RUN add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/" -RUN apt-get update \ - && apt-get -qq install -y postgresql-client-15 \ - r-base \ - r-base-dev\ +# Adding various application to download packages to install Postgres 15 and CRAN-R +RUN apt-get update \ + && apt-get -qq install -y texinfo \ + tzdata \ + gettext \ + make \ + openssl \ + #Linux packages needed for installing CRAN-R + dirmngr \ + #Linux packages needed for RlabKey + libcurl4-openssl-dev \ + libssl-dev \ + postgresql-client-15 \ + r-base \ + r-base-dev \ && apt-get clean # install the necessary R packages diff --git a/docker/cranrnutils/hooks/build b/docker/cranrnutils/hooks/build new file mode 100755 index 000000000..9e8423fce --- /dev/null +++ b/docker/cranrnutils/hooks/build @@ -0,0 +1,34 @@ +#!/bin/bash +args=() +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + -t|--tag) ## the path to the dump file on the local machine + LOCAL_TAG="$2" + shift + shift + ;; + -i|--image) ## the path to the dump file on the local machine + IMAGE_NAME="$2" + shift + shift + ;; + *) ## positional arguments + args+=("$1") + shift + ;; + esac +done +set -- "${args[@]}" + + +if [[ -z $IMAGE_NAME ]] +then + IMAGE_NAME=${LOCAL_TAG} + docker build --builder container --platform linux/arm64,linux/amd64 -t $IMAGE_NAME --load ../ +else + # Defining new docker builder for arm64 + #platform=linux/arm64 + docker buildx create --name=container + docker build --builder container --platform $platform -t $IMAGE_NAME --load . +fi \ No newline at end of file diff --git a/docker/cranrnutils/install.r b/docker/cranrnutils/install.r index 80798ba23..9adc79ac2 100644 --- a/docker/cranrnutils/install.r +++ b/docker/cranrnutils/install.r @@ -1,14 +1,13 @@ -install.packages('devtools', repos='http://cran.us.r-project.org') -install.packages('quadprog', repos='http://cran.us.r-project.org') -install.packages('pedigree', repos='http://cran.us.r-project.org') -install.packages('Matrix', repos='http://cran.us.r-project.org') -install.packages('RCurl', repos='http://cran.us.r-project.org') -install.packages('kinship2', repos='http://cran.us.r-project.org') -install.packages('getopt', repos='http://cran.us.r-project.org') -install.packages('rjson', repos='http://cran.us.r-project.org') -install.packages('dplyr', repos='http://cran.us.r-project.org') -install.packages('remotes', repos='http://cran.us.r-project.org') -install.packages('Rlabkey', repos='http://cran.us.r-project.org') - +n <- max(parallel::detectCores() - 2L, 1L) +print(n) +#options(Ncpus=n) +install.packages('remotes', + quiet = FALSE, + verbose = TRUE) +install.packages(c('devtools','quadprog','pedigree','Matrix', 'RCurl','kinship2','getopt', 'rjson','dplyr','Rlabkey'), + quiet = FALSE, + verbose = TRUE, + Ncpus=n, + repos='http://cran.us.r-project.org') require('remotes') install_github("luansheng/visPedigree") diff --git a/docker/labkey/Dockerfile b/docker/labkey/Dockerfile index afa5d0a3a..eea97b456 100644 --- a/docker/labkey/Dockerfile +++ b/docker/labkey/Dockerfile @@ -2,7 +2,7 @@ # HELPER IMAGE (to download LabKey) ARG LK_VERSION ARG FB_NAME -FROM alpine:latest as download +FROM alpine:latest AS download ARG LABKEY_TEAMCITY_PASSWORD ARG LABKEY_TEAMCITY_USERNAME @@ -22,7 +22,7 @@ RUN echo -e "Downloading LabKey build from \033[1;33m${Z}\033[0m" \ # ----------------------------------------------------------------------------- # MAIN IMAGE BUILD DEFINITION -FROM wnprcehr/cranrnutils:cranrnutils_${LK_VERSION}${FB_NAME} +FROM --platform=$BUILDPLATFORM wnprcehr/cranrnutils:cranrnutils_${LK_VERSION}${FB_NAME} # creating folders for LabKey installation RUN mkdir -p /labkey/labkey @@ -36,9 +36,9 @@ RUN mkdir -p /labkey/labkey/config #COPY LabKey24.3Beta-79-UWisc/labkeyServer.jar /labkey/src # copy in LabKey from the other stage of the build and copy in the jars to tomcat -ENV LABKEY_HOME /labkey/labkey -ENV LABKEY_SRC /labkey/src -ENV LABKEY_LOGS /labkey/logs +ENV LABKEY_HOME=/labkey/labkey +ENV LABKEY_SRC=/labkey/src +ENV LABKEY_LOGS=/labkey/logs COPY --from=download /tmp/labkey $LABKEY_SRC WORKDIR ${LABKEY_HOME} diff --git a/docker/labkey/hooks/build b/docker/labkey/hooks/build index 9269d94fd..43b8c4b8c 100755 --- a/docker/labkey/hooks/build +++ b/docker/labkey/hooks/build @@ -1,5 +1,5 @@ #!/bin/bash -echo "build ver 1.9" +echo "build ver 2.0" echo "docker_tag text " $DOCKER_TAG # FB_REGEX will match if the docker tag looks like a feature branch, while @@ -65,11 +65,15 @@ echo -e "TEAMCTY_URL "${Z} echo "LabKey version: $LK_VERSION" echo "Branch name: $SHORT_BRANCH_NAME" +# Defining new docker builder for arm64 +docker buildx create --name=container + if [[ -z $IMAGE_NAME ]] then IMAGE_NAME=${GRADLE_IMAGE} echo "value of IMAGE_NAME " ${IMAGE_NAME} - docker build --build-arg LABKEY_TEAMCITY_USERNAME=$teamcityUser --build-arg LABKEY_TEAMCITY_PASSWORD=$teamCityPWD --build-arg TEAMCITY_URL=$Z --build-arg FB_NAME=$FB_NAME --build-arg LK_VERSION=$LK_VERSION --no-cache --rm=true -t $IMAGE_NAME ../ + docker build --builder container --platform linux/arm64,linux/amd64 --build-arg LABKEY_TEAMCITY_USERNAME=$teamcityUser --build-arg LABKEY_TEAMCITY_PASSWORD=$teamCityPWD --build-arg TEAMCITY_URL=$Z --build-arg FB_NAME=$FB_NAME --build-arg LK_VERSION=$LK_VERSION --no-cache --rm=true --load -t $IMAGE_NAME ../ else - docker build --build-arg LABKEY_TEAMCITY_USERNAME=$teamcityUser --build-arg LABKEY_TEAMCITY_PASSWORD=$teamCityPWD --build-arg TEAMCITY_URL=$Z --build-arg FB_NAME=$FB_NAME --build-arg LK_VERSION=$LK_VERSION --no-cache --rm=true -t $IMAGE_NAME . + echo "running inside Docker Hub for $platform" + docker build --builder container --platform $platform --build-arg LABKEY_TEAMCITY_USERNAME=$teamcityUser --build-arg LABKEY_TEAMCITY_PASSWORD=$teamCityPWD --build-arg TEAMCITY_URL=$Z --build-arg FB_NAME=$FB_NAME --build-arg LK_VERSION=$LK_VERSION --no-cache --rm=true --load -t $IMAGE_NAME . fi \ No newline at end of file diff --git a/docker/load_database_backup.sh b/docker/load_database_backup.sh index 3ede782ed..f5b0159bd 100755 --- a/docker/load_database_backup.sh +++ b/docker/load_database_backup.sh @@ -103,7 +103,7 @@ fi #------------------------------------------------------------------------------- if [[ -z $dock ]]; then - docker compose -f production.yaml -f compose.yaml down -v + docker compose -f production.yaml -f compose.yaml down -v --timeout 60 if [[ ! -e .env ]]; then cp default.env .env fi @@ -259,7 +259,7 @@ fi # postgresql configuration rather than the 'restore' one #------------------------------------------------------------------------------- if [[ -z $dock ]]; then - docker compose down -v + docker compose down -v --timeout 60 unset PG_CONF_FILE docker compose up -d postgres fi diff --git a/docker/load_database_update_testserver.sh b/docker/load_database_update_testserver.sh new file mode 100755 index 000000000..a68aab0ee --- /dev/null +++ b/docker/load_database_update_testserver.sh @@ -0,0 +1,321 @@ +#!/bin/bash + +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin" +export $(grep -v '^#' .env | xargs) + +#------------------------------------------------------------------------------- +# Read the named arguments (e.g., -f, -p) from the command line and replace the +# standard positional arguments with the non-named ones +#------------------------------------------------------------------------------- +args=() +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + -p|--path) ## the path to the dump file on the test server /mnt/IT-Backups/backups/labkey_backup/database/daily + filepath="$2" + shift + shift + ;; + -o|--project) ## the name of the docker compose project + export COMPOSE_PROJECT_NAME="$2" + shift + shift + ;; + --production) ## flag indicating to run in "production" mode + prod="true" + shift + ;; + --nodocker) ## flag to run this outside docker environment + dock="false" + shift + ;; + --postgres) ## path to the postgres bin directory + pgpath="$2" + shift + shift + ;; + --dbname) ## name of the target database (note: do not use upper and lower case) + dbname="$2" + shift + shift + ;; + --dbtime) ## time that the database backup was created in format HHMM (ex: 1543) + dbtime="$2" + shift + shift + ;; + -j|--jobs) ## time that the database backup was created in format HHMM (ex: 1543) + jobs="$2" + shift + shift + ;; + --port) ## port that postgresql is running on + pgport="$2" + shift + shift + ;; + --tablespace) ## tablespace that the db should be set to (e.g., for an external drive) + tablespace="$2" + shift + shift + ;; + --tmppath) + tmppath="$2" + shift + shift + ;; + --debug) ## flag indicating we are debugging and shouldn't delete the tmpdir + debug="true" + shift + ;; + *) ## positional arguments + args+=("$1") + shift + ;; + esac +done +set -- "${args[@]}" + +#------------------------------------------------------------------------------- +# Determining location for temporary folder +#------------------------------------------------------------------------------- +if [[ -z $tmppath ]]; then + tmppath="/tmp/" +fi + +#------------------------------------------------------------------------------- +# Create a temporary folder just for this particular run (to clean up later) +#------------------------------------------------------------------------------- + +tmpdir="$(mktemp -d "$tmppath"pg_restore.XXXXXXXX)" +echo $tmpdir +if [[ -z $debug ]]; then + trap 'rm -rf $tmpdir' EXIT +fi + +#------------------------------------------------------------------------------- +# Default database name to labkey, is dbname is not passed it will used +# labkey as the target database to restore +#------------------------------------------------------------------------------- +if [[ -z $dbname ]]; then + dbname="labkey" +fi + +#------------------------------------------------------------------------------- +# Default number of jobs to store +#------------------------------------------------------------------------------- +if [[ -z $jobs ]]; then + jobs=4 +fi + +#------------------------------------------------------------------------------- +# Take down the entire docker compose project, including the network and volumes +# then build a new postgresql configuration using the specified one as a base. +#------------------------------------------------------------------------------- + +if [[ -z $dock ]]; then + + echo -n 'Taking down all containers ... ' + new_dir="/space/application/wnprc-modules/docker/" + cd "$new_dir" + + if [ $? -eq 0 ]; then + echo "Successfully changed directory to: $(pwd) " + /usr/bin/docker compose -f /space/application/wnprc-modules/docker/compose.yaml down -v --timeout 60 + else + echo "Failed to change directory to: $new_dir" + fi + + if [[ ! -e .env ]]; then + cp default.env .env + fi + if [[ .env =~ "PG_CONF_FILE=(.*)" ]]; then + conf="${BASH_REMATCH[1]}" + else + conf="/space/application/wnprc-modules/docker/postgres/postgresql.conf" + fi + sed -e "s/^.*fsync *=.*$/fsync = off/" \ + -e "s/^.*synchronous_commit *=.*$/synchronous_commit = off/" \ + -e "s/^.*wal_level *=.*$/wal_level = minimal/" \ + -e "s/^.*full_page_writes *=.*$/full_page_writes = off/" \ + -e "s/^.*max_wal_size *=.*$/max_wal_size = 2048/" \ + -e "s/^.*min_wal_size *=.*$/min_wal_size = 160/" \ + -e "s/^.*max_wal_senders *=.*$/max_wal_senders = 0/" \ + -e "s/^.*wal_keep_size *=.*$/wal_keep_size = 0/" \ + -e "s/^.*archive_mode *=.*$/archive_mode = off/" \ + -e "s/^.*autovacuum *=.*$/autovacuum = off/" \ + -e "s/^.*log_min_duration_statement *=.*$/log_min_duration_statement = -1/" \ + -e "s/^.*log_checkpoints *=.*$/log_checkpoints = off/" \ + -e "s/^.*log_connections *=.*$/log_connections = off/" \ + -e "s/^.*log_disconnections *=.*$/log_disconnections = off/" \ + -e "s/^.*log_duration *=.*$/log_duration = off/" \ + -e "s/^.*log_hostname *=.*$/log_hostname = off/" \ + -e "s/^.*log_lock_waits *=.*$/log_lock_waits = off/" \ + -e "s/^.*log_statement *=.*$/log_statement = 'none'/" \ + -e "s/^.*log_temp_files *=.*$/log_temp_files = -1/" \ + -e "s/^.*effective_cache_size *=.*$/effective_cache_size = 2GB/" \ + -e "s/^.*shared_buffers *=.*$/shared_buffers = 128MB/" \ + -e "s/^.*maintenance_work_mem *=.*$/maintenance_work_mem = 1GB/" \ + $conf > $tmpdir/pg_restore.conf + export PG_CONF_FILE=$tmpdir/pg_restore.conf + + echo 'Bringing postgres up with special configuration ... ' + docker compose up -d postgres + pgport=$(docker compose port postgres 5432) +fi + +#------------------------------------------------------------------------------- +# Wait for the postgres instance to start accepting connections +#------------------------------------------------------------------------------- +if [[ -z $dock ]]; then + echo -n 'Waiting for postgres to start ... ' + docker compose exec postgres /bin/bash -c 'count=0;while [ $count -lt 120 ]; do if psql -U postgres -c "\l" &>/dev/null; then sleep 3; break; fi; sleep 1; let count=count+1; done;' &>/dev/null + echo -e '\033[0;32mdone\033[0m' +fi + +#------------------------------------------------------------------------------- +# If the user did not provide a path to an existing dump file, secure copy the +# latest daily from the EHR production server's backup folder +#------------------------------------------------------------------------------- + +if [[ -z $dbtime ]] +then + filename="labkey_$(date +'%Y%m%d')_0100.pg" +else + filename="labkey_$(date +'%Y%m%d')_$dbtime.pg" +fi + +restorefile="$filepath$filename" + +echo -n " Restoring from $restorefile" + + +#------------------------------------------------------------------------------- +# Drop and recreate the labkey database and the various roles that we use +#------------------------------------------------------------------------------- +if [[ -z $dock ]]; then + echo -n 'Preparing database and roles ... ' + docker compose exec postgres psql -U postgres -c "drop database if exists ${dbname};" &>/dev/null + docker compose exec postgres psql -U postgres -c "create database ${dbname};" &>/dev/null + docker compose exec postgres psql -U postgres -c 'drop role if exists labkey; create role labkey superuser; drop role if exists doconnor; create role doconnor superuser; drop role if exists oconnor; create role oconnor superuser; drop role if exists oconnorlab; create role oconnorlab superuser; drop role if exists sconnor; create role sconnor superuser; drop role if exists soconnorlab; create role soconnorlab superuser; drop role if exists soconnor_lab; create role soconnor_lab superuser;' &>/dev/null + echo -e '\033[0;32mdone\033[0m' +else + echo -n 'Preparing database and roles ... ' + ${pgpath}psql -h localhost -U postgres -p "${pgport#*:}" -c "drop database if exists ${dbname};" &>/dev/null + ${pgpath}psql -h localhost -U postgres -p "${pgport#*:}" -c "create database ${dbname};" &>/dev/null + ${pgpath}psql -h localhost -U postgres -p "${pgport#*:}" -c 'drop role if exists labkey; create role labkey superuser; drop role if exists doconnor; create role doconnor superuser; drop role if exists oconnor; create role oconnor superuser; drop role if exists oconnorlab; create role oconnorlab superuser; drop role if exists sconnor; create role sconnor superuser; drop role if exists soconnorlab; create role soconnorlab superuser; drop role if exists soconnor_lab; create role soconnor_lab superuser;' &>/dev/null + echo -e '\033[0;32mdone\033[0m' +fi + +#------------------------------------------------------------------------------- +# Actually restore the database, using a background proc so we can track progress +#------------------------------------------------------------------------------- +echo -n "Restoring database from $filename ... 0%" + +${pgpath}pg_restore -p "${pgport#*:}" -U postgres -l $restorefile > $tmpdir/pg_restore.list + +total=$(egrep -c '^[0-9]+;.*' $tmpdir/pg_restore.list) +trap 'kill -TERM $pg_restore_pid' TERM INT +${pgpath}pg_restore -h localhost -p "${pgport#*:}" -U postgres -d $dbname -j $jobs -L $tmpdir/pg_restore.list --verbose $restorefile &>$tmpdir/pg_restore.log & +pg_restore_pid=$! +while kill -0 "$pg_restore_pid" &>/dev/null; do + if [[ $total -ne 0 ]]; then + count=$(egrep -c '(processing|finished) item' $tmpdir/pg_restore.log) + perct=$(printf "%2d" $(( 100 * count / total ))) + echo -e -n "\b\b\b\033[0;33m${perct}%\033[0m" + fi + sleep 0.5 +done +trap - TERM INT +echo -e -n "\b\b\b\b\033[0;32mdone\033[0m" +echo + +#------------------------------------------------------------------------------- +# Run the scripts to clean up the instance for development purposes +#------------------------------------------------------------------------------- +echo -n "Preparing database for deployment ... " +${pgpath}psql -h localhost -p "${pgport#*:}" -U postgres -d $dbname &>/dev/null <<- XXX + update prop.properties p set value = 'https://$(hostname -f)' where (select s.category from prop.propertysets s where s.set = p.set) = 'SiteConfig' and p.name = 'baseServerURL'; + update prop.properties p set value = FALSE where (select s.category from prop.propertysets s where s.set = p.set) = 'SiteConfig' and p.name = 'sslRequired'; + update prop.properties p set value = 'Nigthly-EHRServer' where (select s.category from prop.propertysets s where s.set = p.set) = 'LookAndFeel' and p.name = 'systemShortName'; + update prop.properties p set value = 'EHR Development Server' where (select s.category from prop.propertysets s where s.set = p.set) = 'LookAndFeel' and p.name = 'systemDescription'; + update prop.properties p set value = 'Harvest' where (select s.category from prop.propertysets s where s.set = p.set) = 'LookAndFeel' and p.name = 'themeName'; + update prop.properties p set value = 'UA-12818769-2' where (select s.category from prop.propertysets s where s.set = p.set) = 'analytics' and p.name = 'accountId'; + update prop.properties p set value = replace(Value, 'saimiri', 'colony-test') where (select s.category from prop.propertysets s where s.set = p.set) = 'wnprc.ehr.etl.config' and p.name = 'jdbcUrl'; + update prop.properties p set value = 0 where (select s.category from prop.propertysets s where s.set = p.set) = 'wnprc.ehr.etl.config' and p.name = 'runIntervalInMinutes'; + update prop.properties p set value = '/usr/bin/R' where (select s.category from prop.propertysets s where s.set = p.set) = 'UserPreferencesMap' and p.name = 'RReport.RExe'; + update prop.properties p set value = '/usr/bin/R' where (select s.category from prop.propertysets s where s.set = p.set) = 'ScriptEngineDefinition_R,r' and p.name = 'exePath'; + update prop.properties p set value = 'false' where (select s.category from prop.propertysets s where s.set = p.set) = 'org.labkey.ehr.geneticcalculations' and p.name = 'enabled'; + update prop.properties p set value = 'false' where (select s.category from prop.propertysets s where s.set = p.set) = 'ldk.ldapConfig' and p.name = 'enabled'; + update prop.properties p set value = 'false' where (select s.category from prop.propertysets s where s.set = p.set) = 'org.labkey.ldk.notifications.config' and p.name = 'serviceEnabled'; + delete from prop.properties p where (select s.category from prop.propertysets s where s.set = p.set) = 'org.labkey.ldk.notifications.status'; + update ehr.module_properties p set stringvalue = 'test-ehr-do-not-reply@primate.wisc.edu' where p.prop_name = 'site_email'; + update exp.propertydescriptor set scale = 64 where name in ('FirstName', 'LastName', 'Phone', 'Mobile', 'Pager', 'IM') and propertyuri like '%:ExtensibleTable-core-Users.Folder-%' and scale = 0; + update exp.propertydescriptor set scale = 255 where name in ('Description') and propertyuri like '%:ExtensibleTable-core-Users.Folder-%' and scale = 0; + delete from googledrive.service_accounts where id = '8c4a933c-2f8e-4094-9f43-46e80f14e163'; + delete from ehr.notificationrecipients; +XXX +echo -e -n "\b\b\b\b\033[0;32mdone\033[0m" +echo + +#------------------------------------------------------------------------------- +# Updating Docker image +# +#------------------------------------------------------------------------------- +if [[ -z $dock ]]; then + echo -n 'Updating Docker images from Docker Hub' + docker pull wnprcehr/labkeysnapshot:$LK_VERSION + echo -e '\033[0;32mdone\033[0m' + +fi + +#------------------------------------------------------------------------------- +# Tear down and re-start the docker compose environment using the 'regular' +# postgresql configuration rather than the 'restore' one +#------------------------------------------------------------------------------- +if [[ -z $dock ]]; then + docker compose down -v --timeout 60 + unset PG_CONF_FILE + docker compose up -d postgres +fi + +#------------------------------------------------------------------------------- +# rsync the files folder from PrimateFS +# +#------------------------------------------------------------------------------- + +echo "$(date) files folder rsync started" >> /space/backups/scripts/rsync_status.log +/usr/bin/rsync -avP --log-file=/space/backups/scripts/rsync_files.log --delete /mnt/IT-Backups/backups/ehr-prod/files/ $LK_FILES_DIR && files_job=$? + +if [ "$files_job" != "0" ] ; +then + echo "$(date) files folder rsync exit code "$files_job >> /space/backups/scripts/rsync_status.log +else + echo "$(date) files folder rsync exit code "$files_job >> /space/backups/scripts/rsync_status.log + touch /space/backups/scripts/.files_backup +fi + +#------------------------------------------------------------------------------- +# Wait for the postgres instance to start accepting connections +#------------------------------------------------------------------------------- +if [[ -z $dock ]]; then + echo -n 'Waiting for postgres to start ... ' + docker compose exec postgres /bin/bash -c 'count=0;while [ $count -lt 120 ]; do if psql -U postgres -c "\l" &>/dev/null; then sleep 3; break; fi; sleep 1; let count=count+1; done;' &>/dev/null + echo -e '\033[0;32mdone\033[0m' +fi + +#------------------------------------------------------------------------------- +# Starting all the container for the test instance +# After waiting for postgres to start +#------------------------------------------------------------------------------- +if [[ -z $dock ]]; then + echo -n 'Bring up all containers ... ' + docker compose up -d + echo -e '\033[0;32mdone\033[0m' +fi +#------------------------------------------------------------------------------- +# Removing unused images in the system +#------------------------------------------------------------------------------- +echo -n 'Removing unused images from the OS' +docker image prune -a -f +echo -e '\033[0;32mdone\033[0m' From 09d46225059665d6eab45d2ffe8ce32e67ec9d44 Mon Sep 17 00:00:00 2001 From: "F. Daniel Nicolalde" Date: Tue, 13 May 2025 19:11:16 -0500 Subject: [PATCH 07/13] 24.11 fb current blood updates (#817) * add background color to current vol, change future draws calc * take out past blood as well * Add mL to label * Rounding some results. * Adding conditional formatting * Expanding the interval to 31 days * Expanding the interval to 31 days via JS --------- Co-authored-by: Chad Sebranek --- .../queries/study/bloodDrawChanges.sql | 2 +- .../queries/study/currentBloodDraws.query.xml | 25 +++++++++++++++++-- .../queries/study/currentBloodDraws.sql | 11 +++++--- .../resources/web/wnprc_ehr/wnprcReports.js | 2 +- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/WNPRC_EHR/resources/queries/study/bloodDrawChanges.sql b/WNPRC_EHR/resources/queries/study/bloodDrawChanges.sql index f91415d21..0f6cac296 100644 --- a/WNPRC_EHR/resources/queries/study/bloodDrawChanges.sql +++ b/WNPRC_EHR/resources/queries/study/bloodDrawChanges.sql @@ -15,7 +15,7 @@ */ --this query is designed to return any dates when allowable blood draw volume changes --this includes dates of blood draws, plus the date those draws drop off -PARAMETERS(DATE_INTERVAL INTEGER DEFAULT 30) +PARAMETERS(DATE_INTERVAL INTEGER DEFAULT 31) SELECT b2.id, diff --git a/WNPRC_EHR/resources/queries/study/currentBloodDraws.query.xml b/WNPRC_EHR/resources/queries/study/currentBloodDraws.query.xml index 1542a7d93..6088e017e 100644 --- a/WNPRC_EHR/resources/queries/study/currentBloodDraws.query.xml +++ b/WNPRC_EHR/resources/queries/study/currentBloodDraws.query.xml @@ -16,6 +16,20 @@ Status + + + + + + FFA500 + + + + + + FBEC5D + + core qcstate @@ -69,6 +83,7 @@ Volume Available Today (mL) + /WNPRC/EHR/ehr-animalHistory.view#subjects:${Id}&inputType:singleSubject&showReport:1&activeReport:BloodSummary This shows the amount of blood remaining today. It includes blood draws that happened 30 days in the past up to today. @@ -77,12 +92,18 @@ FF0000 + + + + + f3ee99 + - Volume Remaining After All Draws (mL) + Volume Available Including Next 30 days (mL) /WNPRC/EHR/ehr-animalHistory.view#subjects:${Id}&inputType:singleSubject&showReport:1&activeReport:BloodSummary - This column shows the amount of blood remaining for the animal while considering future draws. It includes approved blood draws 30 days into the future and past blood draws 30 days in the past. + This column shows the amount of blood remaining for future blood draws and also 30 days into the past. diff --git a/WNPRC_EHR/resources/queries/study/currentBloodDraws.sql b/WNPRC_EHR/resources/queries/study/currentBloodDraws.sql index f90605794..8501f5955 100644 --- a/WNPRC_EHR/resources/queries/study/currentBloodDraws.sql +++ b/WNPRC_EHR/resources/queries/study/currentBloodDraws.sql @@ -27,14 +27,17 @@ SELECT t.mostRecentWeightDate, t.death, cast(round(t.allowableBlood,1) as numeric) as maxAllowableBlood, - cast(t.bloodPrevious as double) as bloodPrevious, - cast((t.allowableBlood - t.bloodPrevious) as double) as allowablePrevious, + round(cast(t.bloodPrevious as double),1) as bloodPrevious, + round(cast((t.allowableBlood - t.bloodPrevious) as double),1) as allowablePrevious, - cast(t.bloodFuture as double) as bloodFuture, + round(cast(t.bloodFuture as double),1) as bloodFuture, cast((t.allowableBlood - t.bloodFuture) as double) as allowableFuture, ROUND(CAST((t.allowableBlood - t.bloodPrevious) AS double),1) as allowableBlood, - ROUND(CAST((t.allowableBlood - t.bloodPrevious - t.bloodFuture) AS double),1) as allowableBloodIncludingFutureDraws, + CASE + WHEN t.date < CURDATE() THEN NULL + ELSE ROUND(CAST((t.allowableBlood - t.bloodFuture - t.bloodPrevious) AS double),1) + END AS allowableBloodIncludingFutureDraws, t.minDate, t.maxDate diff --git a/WNPRC_EHR/resources/web/wnprc_ehr/wnprcReports.js b/WNPRC_EHR/resources/web/wnprc_ehr/wnprcReports.js index 2bebd2370..cec198a13 100644 --- a/WNPRC_EHR/resources/web/wnprc_ehr/wnprcReports.js +++ b/WNPRC_EHR/resources/web/wnprc_ehr/wnprcReports.js @@ -669,7 +669,7 @@ EHR.reports.currentBloodDraws = function(panel, tab) { schemaName: 'study', queryName: 'currentBloodDraws', title: "Current Blood " + title, - parameters: {'interval': '30'}, + parameters: {'DATE_INTERVAL': '31'}, filters: filterArray.nonRemovable, removeableFilters: filterArray.removable }); From 6aa90fc6fec02655e1482143574294391e9e0f8b Mon Sep 17 00:00:00 2001 From: LeviCameron1 <86750204+LeviCameron1@users.noreply.github.com> Date: Wed, 14 May 2025 17:03:13 -0500 Subject: [PATCH 08/13] Add emergency contact urls to misc pages (#818) --- WNPRC_EHR/resources/views/wnprcUnits.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WNPRC_EHR/resources/views/wnprcUnits.html b/WNPRC_EHR/resources/views/wnprcUnits.html index fb6b487a1..d98974af5 100644 --- a/WNPRC_EHR/resources/views/wnprcUnits.html +++ b/WNPRC_EHR/resources/views/wnprcUnits.html @@ -158,13 +158,14 @@ sections: [ {header: 'Misc. Pages', items: [ - {name: 'Change WNPRC Password', url: 'https://www.mynetid.wisc.edu/recover/pwdreset'}, {name: 'Employee List', url: '<%=contextPath%>' + '/WNPRC/EHR/query-executeQuery.view?schemaName=study&query.queryName=EmployeeList'}, {name: 'View My Requirement Status', url: '<%=contextPath%>' + '/WNPRC/WNPRC_Units/Animal_Services/Compliance_Training/Public/EHR_ComplianceDB-My_Requirements.view'}, {name: 'View/Download PDFs of Signs, Posters and Guides', url: '<%=contextPath%>' + '/WNPRC/WNPRC_Units/Animal_Services/Compliance_Training/Public/query-executeQuery.view?schemaName=lists&query.queryName=Signs'}, {name: 'Download Paper Forms', url: '<%=contextPath%>' + '/WNPRC/WNPRC_Units/Animal_Services/Compliance_Training/Public/list-grid.view?listId=1399'}, {name: 'PowerDMS', url: 'https://go.wisc.edu/mkg850'}, - {name: 'Sample Manager', url: ' https://ehr.primate.wisc.edu/samplemanager/SampleManager/app.view#/home'} + {name: 'Sample Manager', url: ' https://ehr.primate.wisc.edu/samplemanager/SampleManager/app.view#/home'}, + {name: 'Emergency Contacts', url: 'https://go.wisc.edu/62ke03'}, + {name: 'Vet Emergency Contacts', url: 'https://go.wisc.edu/gdtw22'} ]} ] }); From 300dd90b7b64579e9a199e4fbbceb7cbcc81d7cf Mon Sep 17 00:00:00 2001 From: "F. Daniel Nicolalde" Date: Tue, 3 Jun 2025 08:34:36 -0500 Subject: [PATCH 09/13] 24.11 fb necropsy requester button (#829) * Initial button setup to send back to request qc state * set assignedTo field for necropsy, untested * add notification, support groups in email sending * add request on hold color to calendar * improve error handling * improve email wording * get original requester and set default value for assignedTo field * Add save button to necropsy request, change necropsy link to go to request if its a request --------- Co-authored-by: Chad Sebranek --- .../queries/study/Necropsy Schedule.sql | 2 + .../resources/web/wnprc_ehr/datasetButtons.js | 3 +- .../wnprc_ehr/ext4/utils/DataEntryButtons.js | 122 ++++++++++++++++ .../labkey/wnprc_ehr/WNPRC_EHRController.java | 137 ++++++++++++++++++ .../org/labkey/wnprc_ehr/WNPRC_EHRModule.java | 1 + .../forms/Necropsy/NecropsyForm.java | 1 + .../forms/Necropsy/NecropsyRequestForm.java | 1 + .../NecropsyEditRequestNotification.java | 110 ++++++++++++++ .../pages/dataentry/NecropsySchedule.jsp | 6 + .../wnprc_ehr/table/WNPRC_EHRCustomizer.java | 31 +++- 10 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NecropsyEditRequestNotification.java diff --git a/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql b/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql index 972540635..44871fc77 100644 --- a/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql +++ b/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql @@ -20,6 +20,7 @@ SELECT lsid ,location ,delivery_option.title AS who_delivers ,shipping_comment AS delivery_comment + ,qcstate ,animalid.Demographics.necropsyAbstractNotes.remark AS remark ,CASE WHEN hasTissuesForAvrl IS NULL @@ -61,6 +62,7 @@ SELECT lsid ,shipping_comment ,location ,performedby + ,qcstate.label as qcstate ,taskid.qcstate AS state FROM study.necropsy WHERE taskid IS NOT NULL) necropsy diff --git a/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js b/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js index a02a93d60..692d84df2 100644 --- a/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js +++ b/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js @@ -117,7 +117,8 @@ WNPRC_EHR.DatasetButtons = new function(){ width: 200, value: row.formtype, ref: 'titleField' - },{ + }, + { xtype: 'combo', fieldLabel: 'Assigned To', width: 200, diff --git a/WNPRC_EHR/resources/web/wnprc_ehr/ext4/utils/DataEntryButtons.js b/WNPRC_EHR/resources/web/wnprc_ehr/ext4/utils/DataEntryButtons.js index 2078018f6..a00791087 100644 --- a/WNPRC_EHR/resources/web/wnprc_ehr/ext4/utils/DataEntryButtons.js +++ b/WNPRC_EHR/resources/web/wnprc_ehr/ext4/utils/DataEntryButtons.js @@ -534,6 +534,128 @@ return true; } }); + registerBtn('SEND_BACK_TO_REQUESTOR', { + text: 'Request changes from requester', + requiredQC: 'In Progress', + targetQC: 'Request: On Hold', + errorThreshold: 'INFO', + successURL: false, + disabled: false, + //disabled: this.storeCollection.getServerStoreForQuery("study", "Necropsy").getAt(0).getData().requestid == '', + disableOn: 'ERROR', + tooltip: "This starts the task and inserts into or updates either the Prenatal Deaths or the regular Deaths table with information from the necropsy. The insertion will cause any assigned treatments, housing, etc. to be closed, and will send a Death Notification email out.", + handler: function(btn) { + const storeCollection = this.storeCollection; + const necropsyStore = storeCollection.getServerStoreForQuery("study", "Necropsy"); + const necropsyRecord = necropsyStore.getAt(0).getData(); + const requestId = necropsyRecord.requestid; + const animalId = necropsyRecord.Id; + const taskStore = storeCollection.getServerStoreForQuery("ehr", "tasks").getAt(0).getData(); + const taskId = taskStore.taskid; + + + this.saveRecords(btn).then(function() { + + LABKEY.Query.selectRows({ + schemaName: "ehr", + queryName: "requests", + filterArray: [LABKEY.Filter.create("requestid", requestId, LABKEY.Filter.Types.EQUAL)], + success: function (result) { + console.log(result); + new Ext.Window({ + title: 'Send email to requester', + closeAction: 'destroy', + width: 400, + autoHeight: true, + items: [{ + xtype: 'form', + ref: 'theForm', + bodyStyle: 'padding: 5px;', + items: [{ + xtype: 'textarea', + fieldLabel: 'Message', + id: 'messageField', + width: 350, + autoHeight: true, + ref: 'messageField' + }, { + xtype: 'combo', + fieldLabel: 'Assigned To', + width: 200, + value: result.rows[0].createdby, + triggerAction: 'all', + mode: 'local', + store: new LABKEY.ext.Store({ + xtype: 'labkey-store', + schemaName: 'core', + queryName: 'PrincipalsWithoutAdmin', + columns: 'UserId,DisplayName', + sort: 'Type,DisplayName', + autoLoad: true + }), + displayField: 'DisplayName', + valueField: 'UserId', + ref: 'assignedTo', + id: 'assignedTo', + } + ] + }], + buttons: [{ + text:'Submit', + disabled:false, + formBind: true, + ref: '../submit', + scope: this, + handler: function(o){ + var message = o.ownerCt.ownerCt.theForm.messageField.getValue(); + var assignedTo = o.ownerCt.ownerCt.theForm.assignedTo.getValue(); + + if(!message){ + alert('Must enter a message'); + return; + } else { + LABKEY.Ajax.request({ + url: LABKEY.ActionURL.buildURL('wnprc_ehr', 'SendNecropsyEditRequestNotification', null), + method: 'POST', + jsonData: { + message: message, + animalId: animalId, + requestId: requestId, + assignedTo: assignedTo, + taskId: taskId, + }, + success: function(data){ + let resp = JSON.parse(data.response); + if (resp.success) { + window.location = LABKEY.ActionURL.buildURL('wnprc_ehr', 'dataEntry.view'); + } else { + alert(resp.message); + } + }, + failure: LDK.Utils.getErrorCallback() + }); + } + + + } + },{ + text: 'Close', + handler: function(o){ + o.ownerCt.ownerCt.close(); + } + }] + }).show(); + }, + failure: function () { + + } + }) + + //window.location.reload(); + + })['catch'](cancelErrorHandler); + }, + }); registerBtn('UPDATE_DEATH', { text: 'Update Death', diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index 9a0636135..aa818eb9b 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -72,10 +72,12 @@ import org.labkey.api.security.CSRF; import org.labkey.api.security.Group; import org.labkey.api.security.GroupManager; +import org.labkey.api.security.MemberType; import org.labkey.api.security.RequiresLogin; import org.labkey.api.security.RequiresNoPermission; import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.RequiresSiteAdmin; +import org.labkey.api.security.SecurityManager; import org.labkey.api.security.User; import org.labkey.api.security.UserManager; import org.labkey.api.security.permissions.AdminPermission; @@ -114,6 +116,7 @@ import org.labkey.wnprc_ehr.dataentry.validators.ProjectVerifier; import org.labkey.wnprc_ehr.dataentry.validators.exception.InvalidAnimalIdException; import org.labkey.wnprc_ehr.dataentry.validators.exception.InvalidProjectException; +import org.labkey.wnprc_ehr.notification.NecropsyEditRequestNotification; import org.labkey.wnprc_ehr.schemas.WNPRC_Schema; import org.labkey.wnprc_ehr.service.dataentry.BehaviorDataEntryService; import org.springframework.validation.BindException; @@ -2316,4 +2319,138 @@ public ApiResponse execute(CompareBloodSchedulesForm form, BindException errors) } } + public static class NecropsyEditRequestNotificationForm + { + private String _message; + private String _animalId; + private String _requestId; + private Integer _assignedTo; + private String _taskId; + + public String getMessage() + { + return _message; + } + + public void setMessage(String message) + { + _message = message; + } + + public String getAnimalId() + { + return _animalId; + } + + public void setAnimalId(String animalId) + { + _animalId = animalId; + } + + public String getRequestId() + { + return _requestId; + } + + public void setRequestId(String requestId) + { + _requestId = requestId; + } + + public void setAssignedTo(Integer assignedTo) + { + _assignedTo = assignedTo; + } + + public Integer getAssignedTo() + { + return _assignedTo; + } + + public void setTaskId(String taskId) + { + _taskId = taskId; + } + public String getTaskId() + { + return _taskId; + } + } + + + @ActionNames("SendNecropsyEditRequestNotification") + @RequiresNoPermission() + @RequiresLogin + public static class SendNecropsyEditRequestNotificationAction extends MutatingApiAction + { + + @Override + public Object execute(NecropsyEditRequestNotificationForm form, BindException errors) throws Exception + { + + try + { + String message = form.getMessage(); + String animalId = form.getAnimalId(); + String requestId = form.getRequestId(); + Integer assignedTo = form.getAssignedTo(); + String taskId = form.getTaskId(); + User u = UserManager.getUser(assignedTo); + + ArrayList emails = new ArrayList<>(); + if (u == null) + { + Group g = SecurityManager.getGroup(assignedTo); + Set users = SecurityManager.getAllGroupMembers(g, MemberType.ACTIVE_USERS); + for (User user : users) + { + emails.add(user.getEmail()); + } + if (emails.isEmpty()) + { + JSONObject response = new JSONObject(); + response.put("success",false); + response.put("message", "There are no members in the group " + g.getName()); + return response; + } + } + else + { + emails.add(u.getEmail()); + } + + + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService service = ti.getUpdateService(); + List> keys = new ArrayList<>(); + Map row = new HashMap<>(); + row.put("taskid", taskId); + keys.add(row); + List> rows = service.getRows(getUser(), getContainer(), keys); + + rows.get(0).put("assignedTo", assignedTo); + rows.get(0).put("aspurasignedTo", assignedTo); + + List> updatedRows = service.updateRows(getUser(), getContainer(), rows, null, null, null); + + + Module WNPRC_EHRModule = ModuleLoader.getInstance().getModule("WNPRC_EHR"); + NecropsyEditRequestNotification editRequestNotification = new NecropsyEditRequestNotification(WNPRC_EHRModule, message, animalId, requestId, emails); + editRequestNotification.sendManually(getContainer(), getUser()); + JSONObject response = new JSONObject(); + response.put("success",true); + return response; + } + catch (Exception e) + { + JSONObject response = new JSONObject(); + response.put("success",false); + response.put("message",new Date().toInstant().toString() + e.getMessage()); + return response; + } + + + } + } + } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java index 0037e87b7..21f40c0d8 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java @@ -383,6 +383,7 @@ public void registerNotifications() { new ColonyManagementNotificationRevamp(this), new ColonyAlertsLiteNotificationRevamp(this), new BloodOverdrawTriggerNotification(this), + new NecropsyEditRequestNotification(this), new EmptyNotificationRevamp(this), new AnimalRequestUpdateNotificationRevamp(this), new TreatmentAlertsNotificationRevamp(this), diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyForm.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyForm.java index 613795f26..87f9dd7a2 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyForm.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyForm.java @@ -54,6 +54,7 @@ protected List getMoreActionButtonConfigs() { List buttons = super.getMoreActionButtonConfigs(); buttons.add("FINALIZE_DEATH"); + buttons.add("SEND_BACK_TO_REQUESTOR"); buttons.add("UPDATE_DEATH"); return buttons; diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyRequestForm.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyRequestForm.java index 470ca1138..4117869aa 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyRequestForm.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/dataentry/forms/Necropsy/NecropsyRequestForm.java @@ -53,6 +53,7 @@ protected List getButtonConfigs() { buttons.addAll(super.getButtonConfigs()); buttons.remove("REQUEST"); + buttons.add("WNPRC_SAVE"); buttons.add("WNPRC_REQUEST"); return buttons; diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NecropsyEditRequestNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NecropsyEditRequestNotification.java new file mode 100644 index 000000000..50c6dcd19 --- /dev/null +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NecropsyEditRequestNotification.java @@ -0,0 +1,110 @@ +package org.labkey.wnprc_ehr.notification; + +import org.labkey.api.data.Container; +import org.labkey.api.module.Module; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.security.User; +import org.labkey.api.util.Path; +import org.labkey.api.view.ActionURL; + +import java.util.ArrayList; + + +public class NecropsyEditRequestNotification extends AbstractEHRNotification { + //Class Variables + NotificationToolkit notificationToolkit = new NotificationToolkit(); + NotificationToolkit.StyleToolkit styleToolkit = new NotificationToolkit.StyleToolkit(); + NotificationToolkit.DateToolkit dateToolkit = new NotificationToolkit.DateToolkit(); + String message = null; + String animalId = null; + String requestId = null; + String baseUrl = null; + ArrayList emails = null; + + + //Constructors + /** + * This constructor is used to register the notification in WNPRC_EHRModule.java. + * @param owner + */ + public NecropsyEditRequestNotification(Module owner) {super(owner);} + + //This constructor is used to actually send the notification via the "TriggerScriptHelper.java" class. + public NecropsyEditRequestNotification(Module owner, String message, String animalId, String requestId, ArrayList emails) { + super(owner); + this.message = message; + this.animalId = animalId; + this.requestId = requestId; + this.emails = emails; + } + + + + //Notification Details + @Override + public String getName() { + return "Your Necropsy Request Requires Modification"; + } + @Override + public String getDescription() { + return "This email warns sends a notification to the user who requested the necropsy requires modification."; + } + @Override + public String getEmailSubject(Container c) { + String subject = "Your Necropsy Requires Modification"; + if (this.animalId != null) { + subject += ": " + animalId; + } + return subject; + } + @Override + public String getScheduleDescription() { + return "Triggered when an animal's necropsy needs modification."; + } + @Override + public String getCategory() { + return "Revamped Notifications - Necropsy Requirements`"; + } + + + + //Sending Options + public void sendManually (Container container, User user){ + notificationToolkit.sendNotification(this, user, container, emails); + } + + + + // Message Creation + @Override + public String getMessageBodyHTML(Container c, User u) { + // Set up. + StringBuilder messageBody = new StringBuilder(); + // Verifies blood is an overdraw. + if (this.message!= null) { + // message info. + messageBody.append("

The pathology group is requesting changes for " + this.animalId + "

"); + DetailsURL details = DetailsURL.fromString("ehr-dataEntryForm.view?formType=NecropsyRequest&requestid=" + this.requestId, c); + messageBody.append("

The following changes are requested:

"); + messageBody.append("

" + this.message+ "

"); + String editNecropsyRequestUrl = (new Path(new ActionURL().getBaseServerURI(), details.getActionURL().toString())).toString(); + messageBody.append("

You can edit the necropsy request " + notificationToolkit.createHyperlink("here", editNecropsyRequestUrl) + ".

"); + + // Returns message info. + this.resetClass(); + return messageBody.toString(); + } + // Sends no message if there is no overdraw. + else { + this.resetClass(); + return null; + } + + } + + public void resetClass() { + this.animalId = null; + this.message = null; + } + +} diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp index 576b1f264..aea88ea34 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp @@ -170,6 +170,7 @@ {{$root.necropsySuiteLookup[$data].displayName}} + Waiting for Requester @@ -366,6 +367,11 @@ if (row.location in necropsySuiteLookup) { eventObj.color = necropsySuiteLookup[row.location].color; } + debugger; + + if (row.qcstate == "Request: On Hold"){ + eventObj.color = "purple" + } return eventObj; })) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java index 606830674..a821eba94 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java @@ -192,7 +192,7 @@ public Object getDisplayValue(RenderContext ctx) } }); - BaseColumnInfo updateTitleCol = (BaseColumnInfo) ti.getColumn("updateTitle"); + BaseColumnInfo updateTitleCol = (BaseColumnInfo) ti.getColumn("title"); if (updateTitleCol != null && us != null) { updateTitleCol.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo) @@ -203,6 +203,7 @@ public void renderGridCellContents(RenderContext ctx, Writer out) throws IOExcep String updateTitle = (String) ctx.get(new FieldKey(getBoundColumn().getFieldKey().getParent(), "updateTitle")); String taskId = (String) ctx.get(new FieldKey(getBoundColumn().getFieldKey().getParent(), "taskid")); String formType = (String) ctx.get(new FieldKey(getBoundColumn().getFieldKey().getParent(), "formtype")); + String qcState = (String) ctx.get(new FieldKey(getBoundColumn().getFieldKey().getParent(), "QCState$Label")); if (isExt4Form("form", formType)) { @@ -222,7 +223,33 @@ public void renderGridCellContents(RenderContext ctx, Writer out) throws IOExcep } else { - super.renderGridCellContents(ctx, out); + if ("Necropsy".equalsIgnoreCase(formType) && qcState.contains("Request")) + { + ActionURL url = new ActionURL("ehr", "dataEntryForm.view", us.getContainer()); + formType = "NecropsyRequest"; + url.addParameter("formType", formType); + SimplerFilter filter = new SimplerFilter("taskid", CompareType.EQUAL, taskId); + TableInfo necropsyTable = getRealTableForDataset(ti, "necropsy"); + + TableSelector ts = new TableSelector(necropsyTable, filter, null); + String requestid; + if (ts.getMap() != null && ts.getMap().get("requestid") != null) + { + requestid = (String) ts.getMap().get("requestid"); + url.addParameter("requestid",requestid); + } + + StringBuilder urlString = new StringBuilder(); + urlString.append(""); + urlString.append(PageFlowUtil.filter(updateTitle)); + urlString.append(""); + out.write(urlString.toString()); + + } else + { + super.renderGridCellContents(ctx, out); + } + } } }); From 007e6f9068c2e879a66ba4cc5a0c9b549c26e282 Mon Sep 17 00:00:00 2001 From: LeviCameron1 <86750204+LeviCameron1@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:01:55 -0500 Subject: [PATCH 10/13] CageUI Layout Editor (#824) * Start Cage UI project * work on loading cage ui svg images * Testing ReactSVG Library * Cage Review Popup added * Added some styles for modifictiona * Work on modification table UI * Separated options for selecting divider/floors/extras * Converted state management to context manager * fixed a bug where the opposite cages wouldn't properly identify the change in state for dividers * Updated cage details to combine rooms together when they are without dividers/floors * Working cage modification deletion * Removed option for multiple extra mods * Added UI catch to make sure users are aware of unsaved data before exiting * Added ability to minimize the legend * Added some legend styling * Fixed bug with changing and saving modifications not working properly * Fixed bug with cage details not getting updated * Removed unnecessary clickedCagePartners array in favor of the clicked rack This also fixed the bug with some mods not getting updated across certain scenarios * Basic Styling Changes * Added scrolling for cage modifications pop up * Added ability to add or remove racks, within limits to the SVG image * Fixed bug where name wasn't getting updated when removing a rack * Added some changes to the way racks are added and maintained * Moved layout editor to separate page * Fixed the issue with combining racks not working as expected for vertical racks * Fixed a bug with dragging and merging racks causing random positioning to occur * fixed bug with not detecting possible merges for racks on some sides * Fixed bug causing merges after the first merge to be different than expected * Fixed bug with dragging room utility objects and dragging in the layout * Removed 1x2 racks, some auto numbering, and popup for cage numbering * Implemented basic version of manual id editing * fixed issue with text getting changed instead of tspan * Fixed zoom for the grid and objects inside the grid. * Adjusted grid ratio sizes with zoom applied * Fixed drag to layout not being correct x and y coords * Fixed bug with dragging in layout editor would randomly jump objects * Code cleanup * Fixed adjacency function to work with zoom * working merging for two groups of one element, no state updates. * Fixed bug with cage numbers throwing error when trying to set * fixed issue with current merge that didn't account for zoom * Introduced a state object to track cage num, x, y and scale coordinates * Fixed bug relaying incorrect cage locations to the state * Fixed bug with checking already merged cages again when the rack was moved * Fixed bug with naming new cages after merging * Fixed bugs with merging two groups and renaming cage ids * Moved cage num event to helper since it needed to be used in more places * Update the method in which cages are initially numbered * Updated cage numbering and rack numbering when cages are placed to use the latest number plus 1 * Fixed bug with cage locations not being correctly numbered * Added context menu on right click for editing cages in the layout editor * Introduced checks to prevent two different "random" crashes in case of incorrect bindings * General Cleanup and fixed moveRack function to work with both cage locs and local room * Fixed bug with cage location not being right when multiple racks were in a room * Fixed bug causing merging to not be detected * Fixed bug with cage state not getting correct local coords for the rack group * Switched state management to work with four types of racks, and given them each their own numbering system * Added test data for beginning work on data storage * Switch room type to be array of room items instead of racks Room item is either a rack or room object (anything other than caging units) * Additions to switching from room represented by racks to room items * added some EHR types and switched logic of svgs to follow unique rack ids * added room type and fixed bug with dragging shapes to the layout * Fixed bugs with merging connecting and renaming * State management and adjacency checker updated for dealing with connections * Fixed bug with connecting merged cages not being reset correctly * Fixed bug with crashing when closing context menu then dragging new cage * Fixed bug with renaming cages of different types * changed rack group design to fit better with data * Basic support for loading previous data for single grouped racks and room objects, fixed grid disable/enable function * Implemented basic saving for layout * remove unused types/functions * Fixed bug with racks and grouped racks not being hard locked to the grid when dragging and zooming the grid * Added the ability to delete cages from the layout editor * Start Cage UI project * work on loading cage ui svg images * Testing ReactSVG Library * Cage Review Popup added * Added some styles for modifictiona * Work on modification table UI * Separated options for selecting divider/floors/extras * Converted state management to context manager * fixed a bug where the opposite cages wouldn't properly identify the change in state for dividers * Updated cage details to combine rooms together when they are without dividers/floors * Working cage modification deletion * Removed option for multiple extra mods * Added UI catch to make sure users are aware of unsaved data before exiting * Added ability to minimize the legend * Added some legend styling * Fixed bug with changing and saving modifications not working properly * Fixed bug with cage details not getting updated * Removed unnecessary clickedCagePartners array in favor of the clicked rack This also fixed the bug with some mods not getting updated across certain scenarios * Basic Styling Changes * Added scrolling for cage modifications pop up * Added ability to add or remove racks, within limits to the SVG image * Fixed bug where name wasn't getting updated when removing a rack * Added some changes to the way racks are added and maintained * Moved layout editor to separate page * Fixed the issue with combining racks not working as expected for vertical racks * Fixed a bug with dragging and merging racks causing random positioning to occur * fixed bug with not detecting possible merges for racks on some sides * Fixed bug causing merges after the first merge to be different than expected * Fixed bug with dragging room utility objects and dragging in the layout * Removed 1x2 racks, some auto numbering, and popup for cage numbering * Implemented basic version of manual id editing * fixed issue with text getting changed instead of tspan * Fixed zoom for the grid and objects inside the grid. * Adjusted grid ratio sizes with zoom applied * Fixed drag to layout not being correct x and y coords * Fixed bug with dragging in layout editor would randomly jump objects * Code cleanup * Fixed adjacency function to work with zoom * working merging for two groups of one element, no state updates. * Fixed bug with cage numbers throwing error when trying to set * fixed issue with current merge that didn't account for zoom * Introduced a state object to track cage num, x, y and scale coordinates * Fixed bug relaying incorrect cage locations to the state * Fixed bug with checking already merged cages again when the rack was moved * Fixed bug with naming new cages after merging * Fixed bugs with merging two groups and renaming cage ids * Moved cage num event to helper since it needed to be used in more places * Update the method in which cages are initially numbered * Updated cage numbering and rack numbering when cages are placed to use the latest number plus 1 * Fixed bug with cage locations not being correctly numbered * Added context menu on right click for editing cages in the layout editor * Introduced checks to prevent two different "random" crashes in case of incorrect bindings * General Cleanup and fixed moveRack function to work with both cage locs and local room * Fixed bug with cage location not being right when multiple racks were in a room * Fixed bug causing merging to not be detected * Fixed bug with cage state not getting correct local coords for the rack group * Switched state management to work with four types of racks, and given them each their own numbering system * Added test data for beginning work on data storage * Switch room type to be array of room items instead of racks Room item is either a rack or room object (anything other than caging units) * Additions to switching from room represented by racks to room items * added some EHR types and switched logic of svgs to follow unique rack ids * added room type and fixed bug with dragging shapes to the layout * Fixed bugs with merging connecting and renaming * State management and adjacency checker updated for dealing with connections * Fixed bug with connecting merged cages not being reset correctly * Fixed bug with crashing when closing context menu then dragging new cage * Fixed bug with renaming cages of different types * changed rack group design to fit better with data * Basic support for loading previous data for single grouped racks and room objects, fixed grid disable/enable function * Implemented basic saving for layout * remove unused types/functions * Fixed bug with racks and grouped racks not being hard locked to the grid when dragging and zooming the grid * Added the ability to delete cages from the layout editor * Change permission classes to fit with labkey 24.11 * Fixed bug not letting merging happen after rename * Added resizeable room border and set up default room sizes * Added room selector for saving, clear messages for saving, error for incorrect room in url, and fixed loading prev room issues * Fixed issue with inconsistent grid generation * Updated layout history table columns, fixed loading in data to work with room border, and compatibility for default rack types * Fixed bugs with loading in room border and assigning new groups after load in * Added popup after submission, fixed some bugs with submission and room checks * Fixed bug causing confirmation to reset room name unexpectedly, fixed bug with dragging grouped racks * Allowed pens to be saved * Fixed issue with saving and moving pens and loading empty rooms that were cleared * Merge in docker build for 24.11 * update docker build url * Fix bug with incorrect scaling in connected rack groups * Changed redirect window from dev link to build link * undo docker build change * Fixed bug with renaming cage numbers * Fixed bug with context menu popups not closing * Switch context menu styles * Prep for moving to own module * Switched CageUI to java based module * Fixed cage ui schema not showing up properly * Allow users to change racks in the layout editor * Simplified cage object and fixed bug with connecting racks * added room templates and clear grid * Removed option to click past the popup for layout popups * Added overlay to other two layout pop ups * Fix bug with deleting multiple cages * Added option to delete entire racks * Fixed bug loading in previous rooms * Added room gate object, Room template fixes and drag off-center bug fix * Added Gate and created reusable context menu * Fixed bug causing the start drag in layout function to not be called correctly * Refactoring * More refactoring * Bug fix for redirect causing crash for template renaming * Test for checking existance of layout editor * Somehow context was renamed. This is a fix for that. * Update WNPRC_EHRTest.java Added test for cage UI layout editor * Added security and permissions * Room list in home page now fetches data * Update cageui.xml * Added extra context for tracking default racks * Added room search list to home page * Tests and some fixes for that * Added home page map for backtracking easily * Permissions and Roles added * Added ability to open and close room gates * Added sub views for room pages and loading in previous rooms * Fixed border and scaling issues with loading rooms in the view pages * Added permissions for cage ui tables and front end checks to enable/disable certain buttons/pages * Bug fix for crashing when the user drags to the layout by clicking on a certain part of the selected object * Fix deleteRows bug in table permissions * Bug fix for cage numbering on svg load for the home view * Bug fixes for loading in a room with connected and merged racks * Added modification table and cage positioning * Add group tracking and fixed bug for finding racks in the same group * Fixed layout editor bug not allowing small cages to be paired with larger cages in some locations * Added rack modification section in cage UI home * Schema and db script for cage modifications * Bug fix for fast drag end calls would produce incorrect ids for racks * Bug fix causing rename crashing * Bug fix for layout merging being detected on incorrect cage positions * Fixed adding room callbacks so errors don't crash * Bug fix for deleting racks causing incorrect state changes * Fixed bug with deleting cages not splitting groups correctly * Bug fix for loading prev room. Effect got deleted somehow, this is adding it back. * Rack modification storage * Fix bug with group ids and same rackId naming * Modification and Cage popup added, modification state, modification fetching * added dynamic cage mods for single cages * Added table customizer and modification tables * Switched modification id convention for constants * Ctunnel capabilities using new constant naming convention * Work to the modifications editing table for individual cages * Fix bugs with mod table causing left mod to be incorrect and duplicate cage svgs * bug fix for template layouts getting cleared when saving them as another room * Cleanup logs * Update package.lock * Remove later edits to branch keeping only layout files * Fixed bug with incorrectly ending template dates * remove unused svgs for layout editor * Changing copyright to UW * Add copywriting to files --------- Co-authored-by: F. Daniel Nicolalde --- CageUI/build.gradle | 27 + CageUI/module.properties | 25 + CageUI/package-lock.json | 11192 ++++++++++++++++ CageUI/package.json | 100 + .../domain-templates/ehr_lookups.template.xml | 44 + .../queries/cageui/layout_history.query.xml | 70 + .../queries/cageui/rack_types.query.xml | 66 + .../queries/cageui/rack_types/.qview.xml | 37 + .../resources/queries/cageui/racks.query.xml | 46 + .../queries/ehr_lookups/cage/.qview.xml | 34 + .../queries/ehr_lookups/rooms/.qview.xml | 32 + CageUI/resources/schemas/cageui.xml | 69 + .../postgresql/cageui-25.000-25.001.sql | 149 + .../web/CageUI/static/RoomBorder.svg | 25 + CageUI/resources/web/CageUI/static/cage.svg | 50 + CageUI/resources/web/CageUI/static/door.svg | 32 + CageUI/resources/web/CageUI/static/drain.svg | 23 + .../web/CageUI/static/gateClosed.svg | 27 + .../resources/web/CageUI/static/gateOpen.svg | 24 + CageUI/resources/web/CageUI/static/pen.svg | 48 + .../web/CageUI/static/roomDivider.svg | 35 + CageUI/src/client/api/labkeyActions.ts | 116 + CageUI/src/client/cageui.scss | 1381 ++ .../client/components/ConfirmationPopup.tsx | 58 + CageUI/src/client/components/TextInput.tsx | 60 + .../components/layoutEditor/ChangeRack.tsx | 85 + .../client/components/layoutEditor/Editor.tsx | 1022 ++ .../layoutEditor/EditorContextMenu.tsx | 158 + .../layoutEditor/GateChangeRoom.tsx | 100 + .../components/layoutEditor/GateSwitch.tsx | 73 + .../components/layoutEditor/LayoutTooltip.tsx | 44 + .../components/layoutEditor/RoomHeader.tsx | 33 + .../layoutEditor/RoomItemTemplate.tsx | 44 + .../layoutEditor/RoomSelectorPopup.tsx | 122 + .../layoutEditor/RoomSizeSelector.tsx | 77 + .../context/LayoutEditorContextManager.tsx | 1357 ++ CageUI/src/client/entryPoints.js | 30 + .../pages/layoutEditor/LayoutEditor.tsx | 221 + CageUI/src/client/pages/layoutEditor/app.tsx | 30 + CageUI/src/client/pages/layoutEditor/dev.tsx | 38 + .../client/types/layoutEditorContextTypes.ts | 74 + CageUI/src/client/types/layoutEditorTypes.ts | 91 + CageUI/src/client/types/typings.ts | 253 + .../src/client/utils/LayoutEditorHelpers.ts | 1185 ++ CageUI/src/client/utils/constants.ts | 210 + CageUI/src/client/utils/helpers.ts | 459 + .../cageui/CageUIContainerListener.java | 57 + .../org/labkey/cageui/CageUIController.java | 51 + .../src/org/labkey/cageui/CageUIManager.java | 34 + .../src/org/labkey/cageui/CageUIModule.java | 125 + .../src/org/labkey/cageui/CageUISchema.java | 69 + .../labkey/cageui/query/CageUIUserSchema.java | 103 + .../cageui/query/LayoutHistoryTable.java | 119 + .../labkey/cageui/query/RackTypesTable.java | 114 + .../org/labkey/cageui/query/RacksTable.java | 121 + .../CageUIAnimalEditorPermission.java | 31 + .../CageUIAnimalReviewerPermission.java | 31 + .../CageUILayoutEditorAccessPermission.java | 31 + .../CageUIModificationEditorPermission.java | 31 + .../CageUINotesEditorPermission.java | 31 + .../CageUIRoomCreatorPermission.java | 32 + .../CageUIRoomModifierPermission.java | 30 + .../CageUITemplateCreatorPermission.java | 30 + .../permissions/CageUIUserPermission.java | 31 + .../security/roles/CageUIAdminRole.java | 54 + .../roles/CageUIModificationEditorRole.java | 42 + .../security/roles/CageUIRoomCreatorRole.java | 52 + .../roles/CageUIRoomModifierRole.java | 47 + .../labkey/cageui/table/CageUICustomizer.java | 57 + CageUI/src/org/labkey/cageui/view/hello.jsp | 43 + CageUI/tsconfig.json | 14 + WNPRC_EHR/resources/data/lookup_sets.tsv | 4 +- .../queries/ehr_lookups/rooms/.qview.xml | 10 + WNPRC_EHR/resources/views/ehrAdmin.html | 3 +- .../resources/web/wnprc_ehr/wnprcOverRides.js | 1 + WNPRC_EHR/src/client/query/helpers.ts | 1 - .../sampledata/wnprc_ehr/cageui/rackTypes.tsv | 3 + .../sampledata/wnprc_ehr/cageui/racks.tsv | 2 + .../test/tests/wnprc_ehr/WNPRC_EHRTest.java | 88 +- 79 files changed, 20964 insertions(+), 4 deletions(-) create mode 100644 CageUI/build.gradle create mode 100644 CageUI/module.properties create mode 100644 CageUI/package-lock.json create mode 100644 CageUI/package.json create mode 100644 CageUI/resources/domain-templates/ehr_lookups.template.xml create mode 100644 CageUI/resources/queries/cageui/layout_history.query.xml create mode 100644 CageUI/resources/queries/cageui/rack_types.query.xml create mode 100644 CageUI/resources/queries/cageui/rack_types/.qview.xml create mode 100644 CageUI/resources/queries/cageui/racks.query.xml create mode 100644 CageUI/resources/queries/ehr_lookups/cage/.qview.xml create mode 100644 CageUI/resources/queries/ehr_lookups/rooms/.qview.xml create mode 100644 CageUI/resources/schemas/cageui.xml create mode 100644 CageUI/resources/schemas/dbscripts/postgresql/cageui-25.000-25.001.sql create mode 100644 CageUI/resources/web/CageUI/static/RoomBorder.svg create mode 100644 CageUI/resources/web/CageUI/static/cage.svg create mode 100644 CageUI/resources/web/CageUI/static/door.svg create mode 100644 CageUI/resources/web/CageUI/static/drain.svg create mode 100644 CageUI/resources/web/CageUI/static/gateClosed.svg create mode 100644 CageUI/resources/web/CageUI/static/gateOpen.svg create mode 100644 CageUI/resources/web/CageUI/static/pen.svg create mode 100644 CageUI/resources/web/CageUI/static/roomDivider.svg create mode 100644 CageUI/src/client/api/labkeyActions.ts create mode 100644 CageUI/src/client/cageui.scss create mode 100644 CageUI/src/client/components/ConfirmationPopup.tsx create mode 100644 CageUI/src/client/components/TextInput.tsx create mode 100644 CageUI/src/client/components/layoutEditor/ChangeRack.tsx create mode 100644 CageUI/src/client/components/layoutEditor/Editor.tsx create mode 100644 CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx create mode 100644 CageUI/src/client/components/layoutEditor/GateChangeRoom.tsx create mode 100644 CageUI/src/client/components/layoutEditor/GateSwitch.tsx create mode 100644 CageUI/src/client/components/layoutEditor/LayoutTooltip.tsx create mode 100644 CageUI/src/client/components/layoutEditor/RoomHeader.tsx create mode 100644 CageUI/src/client/components/layoutEditor/RoomItemTemplate.tsx create mode 100644 CageUI/src/client/components/layoutEditor/RoomSelectorPopup.tsx create mode 100644 CageUI/src/client/components/layoutEditor/RoomSizeSelector.tsx create mode 100644 CageUI/src/client/context/LayoutEditorContextManager.tsx create mode 100644 CageUI/src/client/entryPoints.js create mode 100644 CageUI/src/client/pages/layoutEditor/LayoutEditor.tsx create mode 100644 CageUI/src/client/pages/layoutEditor/app.tsx create mode 100644 CageUI/src/client/pages/layoutEditor/dev.tsx create mode 100644 CageUI/src/client/types/layoutEditorContextTypes.ts create mode 100644 CageUI/src/client/types/layoutEditorTypes.ts create mode 100644 CageUI/src/client/types/typings.ts create mode 100644 CageUI/src/client/utils/LayoutEditorHelpers.ts create mode 100644 CageUI/src/client/utils/constants.ts create mode 100644 CageUI/src/client/utils/helpers.ts create mode 100644 CageUI/src/org/labkey/cageui/CageUIContainerListener.java create mode 100644 CageUI/src/org/labkey/cageui/CageUIController.java create mode 100644 CageUI/src/org/labkey/cageui/CageUIManager.java create mode 100644 CageUI/src/org/labkey/cageui/CageUIModule.java create mode 100644 CageUI/src/org/labkey/cageui/CageUISchema.java create mode 100644 CageUI/src/org/labkey/cageui/query/CageUIUserSchema.java create mode 100644 CageUI/src/org/labkey/cageui/query/LayoutHistoryTable.java create mode 100644 CageUI/src/org/labkey/cageui/query/RackTypesTable.java create mode 100644 CageUI/src/org/labkey/cageui/query/RacksTable.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalEditorPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalReviewerPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUILayoutEditorAccessPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIModificationEditorPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUINotesEditorPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomCreatorPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomModifierPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUITemplateCreatorPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIUserPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/roles/CageUIAdminRole.java create mode 100644 CageUI/src/org/labkey/cageui/security/roles/CageUIModificationEditorRole.java create mode 100644 CageUI/src/org/labkey/cageui/security/roles/CageUIRoomCreatorRole.java create mode 100644 CageUI/src/org/labkey/cageui/security/roles/CageUIRoomModifierRole.java create mode 100644 CageUI/src/org/labkey/cageui/table/CageUICustomizer.java create mode 100644 CageUI/src/org/labkey/cageui/view/hello.jsp create mode 100644 CageUI/tsconfig.json create mode 100644 WNPRC_EHR/resources/queries/ehr_lookups/rooms/.qview.xml create mode 100644 WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/rackTypes.tsv create mode 100644 WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/racks.tsv diff --git a/CageUI/build.gradle b/CageUI/build.gradle new file mode 100644 index 000000000..8e4a647f5 --- /dev/null +++ b/CageUI/build.gradle @@ -0,0 +1,27 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import org.labkey.gradle.util.BuildUtils + +plugins { + id 'org.labkey.build.module' +} +dependencies { + BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") + BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: 'published', depExtension: 'module') +} \ No newline at end of file diff --git a/CageUI/module.properties b/CageUI/module.properties new file mode 100644 index 000000000..e11f4c105 --- /dev/null +++ b/CageUI/module.properties @@ -0,0 +1,25 @@ +# +# /* +# * Copyright (c) 2025 Board of Regents of the University of Wisconsin System +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# */ +# + +ModuleClass: org.labkey.cageui.CageUIModule +Label: CageUI +Description: Cage UI and layout module +URL: WNPRC/EHR/cageui-editLayout.view +Name: CageUI +License: Apache 2.0 +LicenseURL: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/CageUI/package-lock.json b/CageUI/package-lock.json new file mode 100644 index 000000000..812b381e9 --- /dev/null +++ b/CageUI/package-lock.json @@ -0,0 +1,11192 @@ +{ + "name": "cageui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cageui", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@labkey/api": "1.35.6", + "d3": "^7.9.0", + "dayjs": "^1.11.8", + "react": "~18.3.1", + "react-bootstrap": "~2.10.4", + "react-bootstrap-typeahead": "6.3.2", + "react-dom": "~18.3.1", + "react-hot-loader": "^4.13.1", + "react-select": "^5.10.0", + "react-svg": "^16.3.0", + "ts-loader": "^9.5.2", + "typescript": "^5.7.3", + "urijs": "^1.19.11" + }, + "devDependencies": { + "@babel/core": "7.26.7", + "@labkey/build": "8.2.0", + "@labkey/components": "5.20.4", + "@types/d3": "^7.4.3", + "@types/react": "~18.3.3", + "@types/react-bootstrap": "~0.32.32", + "@types/react-dom": "~18.3.0", + "@types/react-select": "^5.0.1", + "better-npm-run": "0.1.1", + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "webpack": "5.94.0", + "webpack-cli": "6.0.1", + "webpack-dev-server": "5.2.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", + "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", + "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", + "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "dependencies": { + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", + "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz", + "integrity": "sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", + "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", + "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.4.tgz", + "integrity": "sha512-Glp/0n8xuj+E1588otw5rjJkTXfzW7FjH3IIUrfqiZOPQCd2vbg8e+DQE8jK9g4V5/zrxFW+D9WM9gboRPELpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", + "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", + "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz", + "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.3.1.tgz", + "integrity": "sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@emotion/core/node_modules/@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "dev": true, + "dependencies": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "node_modules/@emotion/core/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "node_modules/@emotion/core/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true + }, + "node_modules/@emotion/core/node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/core/node_modules/@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", + "dev": true + }, + "node_modules/@emotion/core/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "dev": true + }, + "node_modules/@emotion/core/node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true + }, + "node_modules/@emotion/core/node_modules/@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", + "dev": true + }, + "node_modules/@emotion/core/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + }, + "node_modules/@emotion/css": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", + "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "dev": true, + "dependencies": { + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3", + "babel-plugin-emotion": "^10.0.27" + } + }, + "node_modules/@emotion/css/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "node_modules/@emotion/css/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true + }, + "node_modules/@emotion/css/node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/css/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "dev": true + }, + "node_modules/@emotion/css/node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true + }, + "node_modules/@emotion/css/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "dev": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/styled": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", + "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "dev": true, + "dependencies": { + "@emotion/styled-base": "^10.3.0", + "babel-plugin-emotion": "^10.0.27" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, + "node_modules/@emotion/styled-base": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.3.0.tgz", + "integrity": "sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/is-prop-valid": "0.8.8", + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3" + }, + "peerDependencies": { + "@emotion/core": "^10.0.28", + "react": ">=16.3.0" + } + }, + "node_modules/@emotion/styled-base/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "node_modules/@emotion/styled-base/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true + }, + "node_modules/@emotion/styled-base/node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/styled-base/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "dev": true + }, + "node_modules/@emotion/styled-base/node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true + }, + "node_modules/@emotion/styled-base/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "dev": true + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "node_modules/@floating-ui/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", + "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", + "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dev": true, + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", + "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "dev": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@hello-pangea/dnd": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.6.0.tgz", + "integrity": "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.24.1", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "dev": true, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", + "dev": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", + "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@labkey/api": { + "version": "1.35.6", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.35.6.tgz", + "integrity": "sha512-sRXZZU4FbhKHI4SDQ2wHSYt8uvQPwD/jvP5Yh68nm+6r9spsn6EbOhBuR5LAL4WMmMnsXAmmCdwN1JZ3IGg/Cw==" + }, + "node_modules/@labkey/build": { + "version": "8.2.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/build/-/@labkey/build-8.2.0.tgz", + "integrity": "sha512-FVVF5CHlGQ2Mqq8795mWrxaXxSt2hxei+gGRKkIFHgfhiJdKGv0uUrjRUCLU5iP7VJ+ORwkWlPhPp6aPqrWIrw==", + "dev": true, + "dependencies": { + "@babel/core": "~7.24.9", + "@babel/plugin-transform-class-properties": "~7.24.7", + "@babel/plugin-transform-object-rest-spread": "~7.24.7", + "@babel/preset-env": "~7.24.8", + "@babel/preset-react": "~7.24.7", + "@babel/preset-typescript": "~7.24.7", + "@pmmmwh/react-refresh-webpack-plugin": "~0.5.15", + "ajv": "~8.17.1", + "babel-loader": "~9.1.3", + "bootstrap-sass": "~3.4.3", + "circular-dependency-plugin": "~5.2.2", + "copy-webpack-plugin": "~12.0.2", + "cross-env": "~7.0.3", + "css-loader": "~7.1.2", + "fork-ts-checker-webpack-plugin": "~9.0.2", + "html-webpack-plugin": "~5.6.0", + "mini-css-extract-plugin": "~2.9.0", + "react-refresh": "~0.14.2", + "resolve-url-loader": "~5.0.0", + "rimraf": "~6.0.1", + "sass": "~1.77.8", + "sass-loader": "~14.2.1", + "style-loader": "~4.0.0", + "typescript": "~5.5.4", + "webpack": "~5.93.0", + "webpack-bundle-analyzer": "~4.10.2", + "webpack-cli": "~5.1.4", + "webpack-dev-server": "~5.0.4" + } + }, + "node_modules/@labkey/build/node_modules/@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@labkey/build/node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@labkey/build/node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@labkey/build/node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@labkey/build/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@labkey/build/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@labkey/build/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@labkey/build/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@labkey/build/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@labkey/build/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@labkey/build/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@labkey/build/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@labkey/build/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@labkey/build/node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@labkey/build/node_modules/webpack": { + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@labkey/build/node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@labkey/build/node_modules/webpack-dev-server": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@labkey/build/node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@labkey/build/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@labkey/build/node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@labkey/build/node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@labkey/build/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@labkey/components": { + "version": "5.20.4", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-5.20.4.tgz", + "integrity": "sha512-OpCA8QmiwxAgiyJnq+lcW9cYwsd2XfWgoN2wTRolqV1IJiCLxcirDb55AeMW8SrK/c+8QY1Ycu+/nDqxJ3B4kQ==", + "dev": true, + "dependencies": { + "@hello-pangea/dnd": "16.6.0", + "@labkey/api": "1.35.6", + "@testing-library/dom": "~10.4.0", + "@testing-library/jest-dom": "~6.5.0", + "@testing-library/react": "~16.0.1", + "@testing-library/user-event": "~14.5.2", + "bootstrap": "~3.4.1", + "classnames": "~2.5.1", + "date-fns": "~3.6.0", + "date-fns-tz": "~3.1.3", + "font-awesome": "~4.7.0", + "immer": "~10.1.1", + "immutable": "~3.8.2", + "normalizr": "~3.6.2", + "numeral": "~2.0.6", + "react": "~18.3.1", + "react-color": "~2.19.3", + "react-datepicker": "~7.3.0", + "react-dom": "~18.3.1", + "react-router-dom": "~6.24.1", + "react-select": "~5.8.0", + "react-treebeard": "~3.2.4", + "vis-data": "~7.1.9", + "vis-network": "~9.1.9" + } + }, + "node_modules/@labkey/components/node_modules/react-select": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz", + "integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz", + "integrity": "sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q==", + "dev": true, + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", + "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@remix-run/router": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz", + "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanem/svg-injector": { + "version": "10.1.68", + "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-10.1.68.tgz", + "integrity": "sha512-UkJajeR44u73ujtr5GVSbIlELDWD/mzjqWe54YMK61ljKxFcJoPd9RBSaO7xj02ISCWUqJW99GjrS+sVF0UnrA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "content-type": "^1.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", + "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "dev": true, + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-bootstrap": { + "version": "0.32.37", + "resolved": "https://registry.npmjs.org/@types/react-bootstrap/-/react-bootstrap-0.32.37.tgz", + "integrity": "sha512-CVHj++uxsj1pRnM3RQ/NAXcWj+JwJZ3MqQ28sS1OQUD1sI2gRlbeAjRT+ak2nuwL+CY+gtnIsMaIDq0RNfN0PA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-select": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-5.0.1.tgz", + "integrity": "sha512-h5Im0AP0dr4AVeHtrcvQrLV+gmPa7SA0AGdxl2jOhtwiE6KgXBFSogWw8az32/nusE6AQHlCOHQWjP1S/+oMWA==", + "deprecated": "This is a stub types definition. react-select provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "react-select": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-emotion": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "dev": true + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true + }, + "node_modules/babel-plugin-emotion/node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-emotion/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/babel-plugin-emotion/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-emotion/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + }, + "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-plugin-emotion/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.4", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.4" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/better-npm-run": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/better-npm-run/-/better-npm-run-0.1.1.tgz", + "integrity": "sha512-SBBYsUsb6bYcUMF9QUWy39GX5kzD4CoRBP11gx/k5jYkUr4Tr+irAokIeQX5FgfCRz0Q27rt8U0J4D2TlRgQFA==", + "dev": true, + "dependencies": { + "commander": "^2.9.0", + "dotenv": "^2.0.0", + "object-assign": "^4.0.1" + }, + "bin": { + "better-npm-run": "index.js", + "bnr": "index.js" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bootstrap": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/bootstrap-sass": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", + "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001720", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", + "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/circular-dependency-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", + "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.1" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.24.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dev": true, + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.1.3.tgz", + "integrity": "sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==", + "dev": true, + "peerDependencies": { + "date-fns": "^3.0.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dev": true, + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-2.0.0.tgz", + "integrity": "sha512-Y+zZAmv7p2zOdpyZcSIA+aIxohsyfTcNaMeh3YJn9exq85bQhso65Wz9IhjYYNB4zyvXnfi7Ae+FuygARljVJw==", + "dev": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.161", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz", + "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", + "dev": true, + "engines": { + "node": ">=0.10.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.3.tgz", + "integrity": "sha512-zUE6ABwfybBbSH5TIsdAFiR2kfy0nm2yT0DEtujNM4vHbKhbrIsfoC5cPfJa4vqlmcSBPxqIED+EWVPWEw1hTw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", + "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalizr": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/normalizr/-/normalizr-3.6.2.tgz", + "integrity": "sha512-30qCybsBaCBciotorvuOZTCGEg2AXrJfADMT2Kk/lvpIAcipHdK0zc33nNtwKzyfQAqIJXAcqET6YgflYUgsoQ==", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/prop-types-extra/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-bootstrap-typeahead": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.3.2.tgz", + "integrity": "sha512-N5Mb0WlSSMcD7Z0pcCypILgIuECybev0hl4lsnCa5lbXTnN4QdkuHLGuTLSlXBwm1ZMFpOc2SnsdSRgeFiF+Ow==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@popperjs/core": "^2.10.2", + "@restart/hooks": "^0.4.0", + "classnames": "^2.2.0", + "fast-deep-equal": "^3.1.1", + "invariant": "^2.2.1", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.8", + "react-overlays": "^5.2.0", + "react-popper": "^2.2.5", + "scroll-into-view-if-needed": "^3.1.0", + "warning": "^4.0.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "dev": true, + "dependencies": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-datepicker": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.3.0.tgz", + "integrity": "sha512-EqRKLAtLZUTztiq6a+tjSjQX9ES0Xd229JPckAtyZZ4GoY3rtvNWAzkYZnQUf6zTWT50Ki0+t+W9VRQIkSJLfg==", + "dev": true, + "dependencies": { + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-hot-loader": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.1.tgz", + "integrity": "sha512-ZlqCfVRqDJmMXTulUGic4lN7Ic1SXgHAFw7y/Jb7t25GBgTR0fYAJ8uY4mrpxjRyWGWmqw77qJQGnYbzCvBU7g==", + "dependencies": { + "fast-levenshtein": "^2.0.6", + "global": "^4.3.0", + "hoist-non-react-statics": "^3.3.0", + "loader-utils": "^2.0.3", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@types/react": "^15.0.0 || ^16.0.0 || ^17.0.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-onclickoutside": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", + "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", + "dev": true, + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/react-overlays": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", + "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "dependencies": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.11.6", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz", + "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==", + "dev": true, + "dependencies": { + "@remix-run/router": "1.17.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz", + "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==", + "dev": true, + "dependencies": { + "@remix-run/router": "1.17.1", + "react-router": "6.24.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-select": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.1.tgz", + "integrity": "sha512-roPEZUL4aRZDx6DcsD+ZNreVl+fM8VsKn0Wtex1v4IazH60ILp5xhdlp464IsEAlJdXeD+BhDAFsBVMfvLQueA==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-svg": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react-svg/-/react-svg-16.3.0.tgz", + "integrity": "sha512-MvoQbITgkmpPJYwDTNdiUyoncJFfoa0D86WzoZuMQ9c/ORJURPR6rPMnXDsLOWDCAyXuV9nKZhQhGyP0HZ0MVQ==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@tanem/svg-injector": "^10.1.68", + "@types/prop-types": "^15.7.14", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-treebeard": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/react-treebeard/-/react-treebeard-3.2.4.tgz", + "integrity": "sha512-TsvdUq2kbLavRXa8k4mmqfPse8HmSA9G9s1SZUtIpiYSccSwa0Tm6miMgx7DZ5gpKofQ+j/3Ua0rjsahM3/FQg==", + "dev": true, + "dependencies": { + "@emotion/core": "^10.0.10", + "@emotion/styled": "^10.0.10", + "deep-equal": "^1.0.1", + "shallowequal": "^1.1.0", + "velocity-react": "^1.4.1" + }, + "peerDependencies": { + "@babel/runtime": ">=7.0.0", + "@emotion/styled": "^10.0.10", + "prop-types": ">=15.7.2", + "react": ">=16.7.0", + "react-dom": ">=16.7.0" + } + }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dev": true, + "dependencies": { + "lodash": "^4.0.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.77.8", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", + "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz", + "integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz", + "integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", + "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==" + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "dev": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "dev": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/velocity-animate": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/velocity-animate/-/velocity-animate-1.5.2.tgz", + "integrity": "sha512-m6EXlCAMetKztO1ppBhGU1/1MR3IiEevO6ESq6rcrSQ3Q77xYSW13jkfXW88o4xMrkXJhy/U7j4wFR/twMB0Eg==", + "dev": true + }, + "node_modules/velocity-react": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/velocity-react/-/velocity-react-1.4.3.tgz", + "integrity": "sha512-zvefGm85A88S3KdF9/dz5vqyFLAiwKYlXGYkHH2EbXl+CZUD1OT0a0aS1tkX/WXWTa/FUYqjBaAzAEFYuSobBQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.5", + "prop-types": "^15.5.8", + "react-transition-group": "^2.0.0", + "velocity-animate": "^1.4.0" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0", + "react-dom": "^15.3.0 || ^16.0.0" + } + }, + "node_modules/velocity-react/node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/velocity-react/node_modules/react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dev": true, + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0" + } + }, + "node_modules/vis-data": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz", + "integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/vis-network": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.10.tgz", + "integrity": "sha512-7j71o99bZntr+sJGA58vWD6u9QL+qnt5oKj0Nn+F402L6o+FpO3P35zzPWPsCtqBwxPl0Cd+8xNuR3u7xleNPA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "vis-data": "^6.3.0 || ^7.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/memfs": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.7", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.1.tgz", + "integrity": "sha512-EWzBqw2ZH/hIXIWIdOTvFHij6MuYdDHZVL12bZb921CrmP9UqYhK9+a3OC/onMGeBYrt2aOivHCLy5E+x5wYOA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/CageUI/package.json b/CageUI/package.json new file mode 100644 index 000000000..8cb0967b8 --- /dev/null +++ b/CageUI/package.json @@ -0,0 +1,100 @@ +{ + "name": "cageui", + "version": "1.0.0", + "description": "", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook", + "build": "better-npm-run build", + "start": "cross-env NODE_ENV=development LK_MODULE=WNPRC_EHR webpack-dev-server --config node_modules/@labkey/build/webpack/watch.config.js", + "build-prod": "better-npm-run build-prod", + "clean": "rimraf resources/web/gen && rimraf resources/views/gen" + }, + "keywords": [], + "author": "Board of Regents of the University of Wisconsin System", + "license": "Apache-2.0", + "dependencies": { + "@labkey/api": "1.35.6", + "dayjs": "^1.11.8", + "react": "~18.3.1", + "react-bootstrap": "~2.10.4", + "react-bootstrap-typeahead": "6.3.2", + "react-dom": "~18.3.1", + "react-select": "^5.10.0", + "react-svg": "^16.3.0", + "react-hot-loader": "^4.13.1", + "ts-loader": "^9.5.2", + "typescript": "^5.7.3", + "urijs": "^1.19.11", + "d3": "^7.9.0" + }, + "devDependencies": { + "@babel/core": "7.26.7", + "@labkey/build": "8.2.0", + "@labkey/components": "5.20.4", + "@types/react": "~18.3.3", + "@types/react-bootstrap": "~0.32.32", + "@types/react-dom": "~18.3.0", + "@types/d3": "^7.4.3", + "@types/react-select": "^5.0.1", + "better-npm-run": "0.1.1", + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "webpack": "5.94.0", + "webpack-cli": "6.0.1", + "webpack-dev-server": "5.2.0" + }, + "betterScripts": { + "start": { + "command": "webpack serve --config node_modules/@labkey/build/webpack/watch.config.js", + "env": { + "NODE_ENV": "development" + } + }, + "build": { + "command": "webpack --config node_modules/@labkey/build/webpack/dev.config.js --color", + "env": { + "NODE_ENV": "development" + } + }, + "build-prod": { + "command": "webpack --config node_modules/@labkey/build/webpack/prod.config.js --color --progress --profile", + "env": { + "NODE_ENV": "production", + "PROD_SOURCE_MAP": "source-map" + } + } + }, + "jest": { + "globals": { + "LABKEY": {} + }, + "moduleFileExtensions": [ + "tsx", + "ts", + "js" + ], + "preset": "ts-jest", + "setupFilesAfterEnv": [ + "/test/jest.setup.ts" + ], + "testEnvironment": "jsdom", + "testMatch": null, + "testRegex": "(\\.(test|spec))\\.(ts|tsx)$", + "testResultsProcessor": "jest-teamcity-reporter", + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "isolatedModules": true, + "tsconfig": "node_modules/@labkey/build/webpack/tsconfig.json" + } + ] + } + } +} diff --git a/CageUI/resources/domain-templates/ehr_lookups.template.xml b/CageUI/resources/domain-templates/ehr_lookups.template.xml new file mode 100644 index 000000000..376ff62d9 --- /dev/null +++ b/CageUI/resources/domain-templates/ehr_lookups.template.xml @@ -0,0 +1,44 @@ + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/queries/cageui/layout_history.query.xml b/CageUI/resources/queries/cageui/layout_history.query.xml new file mode 100644 index 000000000..a9af39521 --- /dev/null +++ b/CageUI/resources/queries/cageui/layout_history.query.xml @@ -0,0 +1,70 @@ + + + + + + + Layout History + + + true + + + + ehr_lookups + rooms + room + + + + + integer + + ehr_lookups + cageui_item_types + value + title + + + + + + cageui + racks + rowid + rackid + + + + /EHR/cageDetails.view?room=${room}&cage=${cage}& + + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/CageUI/resources/queries/cageui/rack_types.query.xml b/CageUI/resources/queries/cageui/rack_types.query.xml new file mode 100644 index 000000000..76fffd06a --- /dev/null +++ b/CageUI/resources/queries/cageui/rack_types.query.xml @@ -0,0 +1,66 @@ + + + + + + + + Rack Types + + + true + + + + + ehr_lookups + cageui_item_types + value + title + + + + + + + + + + ehr_lookups + cageui_rack_manufacturers + value + title + + + + + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/CageUI/resources/queries/cageui/rack_types/.qview.xml b/CageUI/resources/queries/cageui/rack_types/.qview.xml new file mode 100644 index 000000000..1509bec5b --- /dev/null +++ b/CageUI/resources/queries/cageui/rack_types/.qview.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/queries/cageui/racks.query.xml b/CageUI/resources/queries/cageui/racks.query.xml new file mode 100644 index 000000000..13eddb2d3 --- /dev/null +++ b/CageUI/resources/queries/cageui/racks.query.xml @@ -0,0 +1,46 @@ + + + + + + + Racks + + + true + + + + + cageui + rack_types + rowid + name + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/CageUI/resources/queries/ehr_lookups/cage/.qview.xml b/CageUI/resources/queries/ehr_lookups/cage/.qview.xml new file mode 100644 index 000000000..e0e1dcc03 --- /dev/null +++ b/CageUI/resources/queries/ehr_lookups/cage/.qview.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/queries/ehr_lookups/rooms/.qview.xml b/CageUI/resources/queries/ehr_lookups/rooms/.qview.xml new file mode 100644 index 000000000..f68737aba --- /dev/null +++ b/CageUI/resources/queries/ehr_lookups/rooms/.qview.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/schemas/cageui.xml b/CageUI/resources/schemas/cageui.xml new file mode 100644 index 000000000..91ffeccf1 --- /dev/null +++ b/CageUI/resources/schemas/cageui.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-25.000-25.001.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-25.000-25.001.sql new file mode 100644 index 000000000..2b5c5bdf5 --- /dev/null +++ b/CageUI/resources/schemas/dbscripts/postgresql/cageui-25.000-25.001.sql @@ -0,0 +1,149 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +-- Create schema, tables, indexes, and constraints used for CageUI module here +-- All SQL VIEW definitions should be created in cageui-create.sql and dropped in cageui-drop.sql +DROP SCHEMA IF EXISTS cageui; + +CREATE SCHEMA cageui; + +-- Table for storing layout history data, either room object or (rack_group, rack, cage) must exist +-- If end_date is null, that is the current layout for the room +DROP TABLE IF EXISTS cageui.layout_history; +CREATE TABLE cageui.layout_history +( + rowid SERIAL NOT NULL, + room VARCHAR(50) NOT NULL, + object_type INTEGER, + extra_context VARCHAR, + rack_group INTEGER, + rack INTEGER, + cage VARCHAR(50), + start_date TIMESTAMP NOT NULL, + end_date TIMESTAMP, + x_coord INTEGER NOT NULL, + y_coord INTEGER NOT NULL, + container entityid NOT NULL, + createdby userid, + created TIMESTAMP, + modifiedby userid, + modified TIMESTAMP, + CONSTRAINT PK_layout_history PRIMARY KEY (rowid), + CONSTRAINT FK_layout_history_container FOREIGN KEY (container) REFERENCES core.Containers (EntityId) +); + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'cageui_item_types' as setname, + 'Room Item Type Field Values' as label, + 'List of items that can be placed into the cageUI layout editor' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 0 as value, 'Caging' as category, '4' as description, 'Default Cage' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 1 as value, 'Caging' as category, '8' as description, 'Default Pen' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 2 as value, 'Caging' as category, '4' as description, 'Default Temp Cage' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 3 as value, 'Caging' as category, '8' as description, 'Default Play Cage' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 4 as value, 'Caging' as category, '4' as description, 'Cage' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 5 as value, 'Caging' as category, '8' as description, 'Pen' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 6 as value, 'Caging' as category, '4' as description, 'Temp Cage' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, description, title) +select setname, container, 7 as value, 'Caging' as category, '8' as description, 'Play Cage' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, title) +select setname, container, 100 as value, 'Room Object' as category, 'Room Divider' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, title) +select setname, container, 101 as value, 'Room Object' as category, 'Drain' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, title) +select setname, container, 102 as value, 'Room Object' as category, 'Door' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, category, title) +select setname, container, 103 as value, 'Room Object' as category, 'Gate' as title from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'cageui_rack_manufacturers' as setname, + 'Rack Manufacturer Field Values' as label, + 'List of rack manufacturers' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'at' as value, 'Allentown' as title from ehr_lookups.lookup_sets where setname='cageui_rack_manufacturers'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'sb' as value, 'Suburban' as title from ehr_lookups.lookup_sets where setname='cageui_rack_manufacturers'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'lk' as value, 'Lenderking' as title from ehr_lookups.lookup_sets where setname='cageui_rack_manufacturers'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'wnprc' as value, 'WNPRC' as title from ehr_lookups.lookup_sets where setname='cageui_rack_manufacturers'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'uk' as value, 'Unknown' as title from ehr_lookups.lookup_sets where setname='cageui_rack_manufacturers'; + +DROP TABLE IF EXISTS cageui.racks; +CREATE TABLE cageui.racks +( + rowid SERIAL NOT NULL, + rackid INTEGER NOT NULL, + rack_type varchar(50) NOT NULL, + container entityid NOT NULL, + createdby userid, + created TIMESTAMP, + modifiedby userid, + modified TIMESTAMP, + CONSTRAINT PK_racks PRIMARY KEY (rowid), + CONSTRAINT FK_racks_container FOREIGN KEY (container) REFERENCES core.Containers (EntityId) +); + +DROP TABLE IF EXISTS cageui.rack_types; +CREATE TABLE cageui.rack_types +( + rowid SERIAL NOT NULL, + name VARCHAR(50) UNIQUE, + type INTEGER NOT NULL, + manufacturer VARCHAR(50) NOT NULL, + length INTEGER, + width INTEGER, + height INTEGER, + supportsTunnel BOOLEAN, + description VARCHAR(100), + container entityid NOT NULL, + createdby userid, + created TIMESTAMP, + modifiedby userid, + modified TIMESTAMP, + CONSTRAINT PK_rack_types PRIMARY KEY (rowid), + CONSTRAINT FK_rack_types_container FOREIGN KEY (container) REFERENCES core.Containers (EntityId) +); \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/RoomBorder.svg b/CageUI/resources/web/CageUI/static/RoomBorder.svg new file mode 100644 index 000000000..577ce4e2f --- /dev/null +++ b/CageUI/resources/web/CageUI/static/RoomBorder.svg @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/cage.svg b/CageUI/resources/web/CageUI/static/cage.svg new file mode 100644 index 000000000..bf84050b5 --- /dev/null +++ b/CageUI/resources/web/CageUI/static/cage.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + x + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/door.svg b/CageUI/resources/web/CageUI/static/door.svg new file mode 100644 index 000000000..b18896c88 --- /dev/null +++ b/CageUI/resources/web/CageUI/static/door.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/drain.svg b/CageUI/resources/web/CageUI/static/drain.svg new file mode 100644 index 000000000..4aa711426 --- /dev/null +++ b/CageUI/resources/web/CageUI/static/drain.svg @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/gateClosed.svg b/CageUI/resources/web/CageUI/static/gateClosed.svg new file mode 100644 index 000000000..e4d87e2c0 --- /dev/null +++ b/CageUI/resources/web/CageUI/static/gateClosed.svg @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/gateOpen.svg b/CageUI/resources/web/CageUI/static/gateOpen.svg new file mode 100644 index 000000000..87445105d --- /dev/null +++ b/CageUI/resources/web/CageUI/static/gateOpen.svg @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/pen.svg b/CageUI/resources/web/CageUI/static/pen.svg new file mode 100644 index 000000000..82e6316ff --- /dev/null +++ b/CageUI/resources/web/CageUI/static/pen.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + x + + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/resources/web/CageUI/static/roomDivider.svg b/CageUI/resources/web/CageUI/static/roomDivider.svg new file mode 100644 index 000000000..53397e027 --- /dev/null +++ b/CageUI/resources/web/CageUI/static/roomDivider.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/CageUI/src/client/api/labkeyActions.ts b/CageUI/src/client/api/labkeyActions.ts new file mode 100644 index 000000000..d9cfed1f8 --- /dev/null +++ b/CageUI/src/client/api/labkeyActions.ts @@ -0,0 +1,116 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; +import { ActionURL, Query, Security } from '@labkey/api'; +import { Command, QueryRequestOptions, SaveRowsOptions, SaveRowsResponse } from '@labkey/api/dist/labkey/query/Rows'; +import { GetUserPermissionsOptions } from '@labkey/api/dist/labkey/security/Permission'; +import { SelectDistinctOptions } from '@labkey/api/dist/labkey/query/SelectDistinctRows'; + +export function labkeyActionSelectWithPromise( + options: SelectRowsOptions, + signal?: any +): Promise { + return new Promise((resolve, reject) => { + options.success = (data) => {resolve(data)}; + options.failure = (data) => {reject(data)}; + Query.selectRows(options); + if(signal){ + if (signal.aborted) { + reject(new DOMException('Aborted', 'AbortError')); + } + + // Listen for the abort event + signal.addEventListener('abort', () => { + reject(new DOMException('Aborted', 'AbortError')); + }); + } + }); +} + +export function labkeyActionSelectDistinctWithPromise( + options: SelectDistinctOptions, + signal?: any +): Promise { + return new Promise((resolve, reject) => { + options.success = (data) => {resolve(data)}; + options.failure = (data) => {reject(data)}; + Query.selectDistinctRows(options); + if(signal){ + if (signal.aborted) { + reject(new DOMException('Aborted', 'AbortError')); + } + + // Listen for the abort event + signal.addEventListener('abort', () => { + reject(new DOMException('Aborted', 'AbortError')); + }); + } + }); +} + +export function labkeyActionInsertWithPromise( + options: QueryRequestOptions +): Promise { + return new Promise((resolve, reject) => { + options.success = (data) => {resolve(data)}; + options.failure = (data) => {reject(data)}; + Query.insertRows(options); + }); +} + +export function labkeyActionUpdateWithPromise( + options: QueryRequestOptions +): Promise { + return new Promise((resolve, reject) => { + options.success = (data) => {resolve(data)}; + options.failure = (data) => {reject(data)}; + Query.updateRows(options); + }); +} + +export const labkeySaveRows = (commands: Command[]):Promise => { + + return new Promise((resolve, reject) => { + let options: SaveRowsOptions = { + commands: commands, + containerPath: ActionURL.getContainer(), + success: (data) => {resolve(data)}, + failure: (data) => {reject(data)}, + }; + Query.saveRows(options); + }); +}; + +export const labkeyGetUserPermissions = (config?: GetUserPermissionsOptions) => { + return new Promise((resolve, reject) => { + const options: GetUserPermissionsOptions = { + ...config, + success: (data) => {resolve(data)}, + failure: (data) => {reject(data)}, + } + const req = Security.getUserPermissions(options); + req.onload = () => { + if (req.status >= 200 && req.status < 300) { + resolve(JSON.parse(req.responseText)); // Parse JSON response + }else{ + reject(new Error(`${req.status}`)); + } + }; + }) +} diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss new file mode 100644 index 000000000..4214c2bc5 --- /dev/null +++ b/CageUI/src/client/cageui.scss @@ -0,0 +1,1381 @@ +/*! + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + + +.room-header { + //background-color: lightgreen; +} + +.legend-header-close { + //background-color: lightcoral; + flex: 0 1 auto; /* Takes only the space it needs */ + padding: 20px; + box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2); + position: relative; + border-radius: 10px; /* Adjust the value as needed for the desired roundness */ + +} +.room-legend-open { + flex: 0 1 auto; /* Takes only the space it needs */ + padding: 20px; + box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2); + position: relative; + border-radius: 10px; /* Adjust the value as needed for the desired roundness */ + +} + +.room-toolbar { + background-color: lightpink; + margin-left: 10px; + margin-right: 10px; +} + +.room-toolbar-buttons { + display: flex; + flex-direction: row; + align-items: center; +} + +.room-display { + //background-color: lightblue; + //width: 100vw; + flex: 1; /* This will make the display take the remaining space */ + + //height: 100vh; +} + +.room-container { + display: flex; + flex-direction: column; /* Arrange children in a column */ + height: 100%; + width: 100%; + background-color: lightgrey; + border-radius: 10px; /* Adjust the value as needed for the desired roundness */ + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); /* Adds a shadow effect */ + padding: 20px; /* Adds some padding inside the card */ + box-sizing: border-box; +} +/* SVG Styling */ + +.room-svg { + width: 100%; + height: auto; + display: block; + justify-content: center; + align-items: center; +} + +.legend-svg { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + margin-left: 5px; +} + +.room-svg svg { + width: 100%; + height: 100%; + flex: 1; +} + +.room-sub-container { + display: flex; + flex: 1 1 auto; /* Take up remaining space */ +} + +.room-header, .room-display, .room-legend { + padding: 10px; +} + +/* Media query for landscape orientation */ +@media screen and (orientation: portrait) { + .room-container { + flex-direction: column; + } + + .room-sub-container { + flex-direction: column; + } + + .room-legend, .room-display { + margin-right: 0; + margin-bottom: 5px; + } +} + + +.room-header-name { + display: inline-block; + margin-left: 20px; +} +.room-header-text { + display: inline-block; +} + + + +.rack-container { + min-height: 100%; + max-width: 20%; + display: flex; + flex-wrap: wrap; + flex-direction: row; +} + +.cage-container { + display: flex; + flex: 1 0; + max-width: fit-content; + box-sizing: border-box; +} + + +.room-svg rect { + fill: none; + pointer-events: visibleFill; +} + +.room-svg tspan { + pointer-events: none; +} + +/* +.popup-content { + background-color: lightgrey; + margin: auto; + padding: 10px; + border: 1px solid #888; + width: 80%; + height: 90%; + overflow-y: auto; + + border-radius: 25px; +} + +.popup-close-button { + float: right; + font-size: 48px; + font-weight: bold; + padding: 5px; + border: none; + background: none; +} + +.popup-close-button:hover, +.popup-close-button:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.popup-header { + display: flex; + justify-content: space-between; + align-items: baseline; + + height: auto; + margin-inline: 10px +} + +.popup-header h1 { + margin: 0; +} +.popup-header button { + margin: 0; +} + + +.popup-subheader { + padding-left: 10px; +} +.popup-subheader h4{ + margin-top: 0; +} + +.popup-sub-content { + margin: 10px +} +*/ +.divider { + width: calc(100% - 20px); /* Adjust width as needed */ + height: 4px; /* Adjust height as needed */ + background-color: darkgrey; /* Adjust color as needed */ + border-radius: 5px; /* Adjust border radius for rounded corners */ +} + +.mini-divider { + width: calc(100% - 20px); /* Adjust width as needed */ + height: 4px; /* Adjust height as needed */ + background-color: darkgrey; /* Adjust color as needed */ + margin: 10px; /* Adjust margin as needed */ + border-radius: 5px; /* Adjust border radius for rounded corners */ +} + +.details-modifications { +} + +.details-mod-header { + display: flex; + align-content: center; + align-items: baseline; +} + + +.details-add-mod { + float: right; + font-size: x-large; + padding: 5px; + border: none; + background: none; + margin-left: 20px; +} + +.details-add-mod:hover, +.details-add-mod:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.details-modifications ul { + list-style-position: inside; + padding-left: 0; +} + +.details-modifications li { + font-size: large; +} + +.details-table { + font-family: arial, sans-serif; + border-collapse: collapse; + width: auto; +} + +.details-table thead{ + font-size: large; +} + +.details-table td, th { + text-align: left; + padding: 8px; +} + +.details-table tr:nth-child(even) { + background-color: #dddddd; +} + +// Confirmation pop up +.popup-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 2; + padding-top: 100px; + width: 100%; + height: 100%; + overflow: auto; +} + +.popup { + background: white; + padding: 20px; + border-radius: 8px; + text-align: center; + z-index: 10001; +} + +.popup-row { +margin-bottom: 10px; +} + +.popup-row input { + width: 100%; /* Make the input field take full width */ + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; /* Ensures padding is included in the width */ +} + +.popup-row label { + display: block; /* Make the label block-level so it appears above the input */ + margin-bottom: 5px; /* Space between label and input */ +} + +.popup-buttons { + display: flex; + justify-content: space-around; + margin-top: 20px; +} + +.popup-buttons button { + padding: 10px 20px; + margin-left: 10px; + margin-right: 10px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.popup-buttons button:hover { + background: #ddd; +} + +.legend-header-open { + margin-bottom: 20px; +} + +.legend-header-btn { + background-color: transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; +} + +/* CSS */ +.button-84 { + align-items: center; + background-color: initial; + background-image: linear-gradient(#464d55, #25292e); + border-radius: 8px; + border-width: 0; + box-shadow: 0 10px 20px rgba(0, 0, 0, .1),0 3px 6px rgba(0, 0, 0, .05); + box-sizing: border-box; + color: #fff; + cursor: pointer; + display: inline-flex; + flex-direction: column; + font-family: expo-brand-demi,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 18px; + height: 52px; + justify-content: center; + line-height: 1; + margin: 0; + outline: none; + overflow: hidden; + padding: 0 32px; + text-align: center; + text-decoration: none; + transform: translate3d(0, 0, 0); + transition: all 150ms; + vertical-align: baseline; + white-space: nowrap; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; +} + +.button-84:hover { + box-shadow: rgba(0, 1, 0, .2) 0 2px 8px; + opacity: .85; +} + +.button-84:active { + outline: 0; +} + +.button-84:focus { + box-shadow: rgba(0, 0, 0, .5) 0 0 0 3px; +} + +@media (max-width: 420px) { + .button-84 { + height: 48px; + } +} + +// Checkbox button css + .checkbox-wrapper-10 .tgl { + display: none; + } + +.checkbox-wrapper-10:not(:last-child) { + margin-right: 10px; /* Adjust the spacing as needed */ +} + +.checkbox-wrapper-10 .tgl, +.checkbox-wrapper-10 .tgl:after, +.checkbox-wrapper-10 .tgl:before, +.checkbox-wrapper-10 .tgl *, +.checkbox-wrapper-10 .tgl *:after, +.checkbox-wrapper-10 .tgl *:before, +.checkbox-wrapper-10 .tgl + .tgl-btn { + box-sizing: border-box; +} +.checkbox-wrapper-10 .tgl::-moz-selection, +.checkbox-wrapper-10 .tgl:after::-moz-selection, +.checkbox-wrapper-10 .tgl:before::-moz-selection, +.checkbox-wrapper-10 .tgl *::-moz-selection, +.checkbox-wrapper-10 .tgl *:after::-moz-selection, +.checkbox-wrapper-10 .tgl *:before::-moz-selection, +.checkbox-wrapper-10 .tgl + .tgl-btn::-moz-selection, +.checkbox-wrapper-10 .tgl::selection, +.checkbox-wrapper-10 .tgl:after::selection, +.checkbox-wrapper-10 .tgl:before::selection, +.checkbox-wrapper-10 .tgl *::selection, +.checkbox-wrapper-10 .tgl *:after::selection, +.checkbox-wrapper-10 .tgl *:before::selection, +.checkbox-wrapper-10 .tgl + .tgl-btn::selection { + background: none; +} +.checkbox-wrapper-10 .tgl + .tgl-btn { + outline: 0; + display: block; + width: 12em; + height: 2em; + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.checkbox-wrapper-10 .tgl + .tgl-btn:after, +.checkbox-wrapper-10 .tgl + .tgl-btn:before { + position: relative; + display: block; + content: ""; + width: 50%; + height: 100%; +} +.checkbox-wrapper-10 .tgl + .tgl-btn:after { + left: 0; +} +.checkbox-wrapper-10 .tgl + .tgl-btn:before { + display: none; +} +.checkbox-wrapper-10 .tgl:checked + .tgl-btn:after { + left: 50%; +} + +.checkbox-wrapper-10 .tgl-flip + .tgl-btn { + padding: 2px; + transition: all 0.2s ease; + font-family: sans-serif; + perspective: 100px; +} +.checkbox-wrapper-10 .tgl-flip + .tgl-btn:after, +.checkbox-wrapper-10 .tgl-flip + .tgl-btn:before { + display: inline-block; + transition: all 0.4s ease; + width: 100%; + text-align: center; + position: absolute; + line-height: 2em; + font-weight: bold; + color: #fff; + position: absolute; + top: 0; + left: 0; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + border-radius: 4px; +} +.checkbox-wrapper-10 .tgl-flip + .tgl-btn:after { + content: attr(data-tg-on); + background: #02C66F; + transform: rotateY(-180deg); +} +.checkbox-wrapper-10 .tgl-flip + .tgl-btn:before { + background: #FF3A19; + content: attr(data-tg-off); +} +.checkbox-wrapper-10 .tgl-flip + .tgl-btn:active:before { + transform: rotateY(-20deg); +} +.checkbox-wrapper-10 .tgl-flip:checked + .tgl-btn:before { + transform: rotateY(180deg); +} +.checkbox-wrapper-10 .tgl-flip:checked + .tgl-btn:after { + transform: rotateY(0); + left: 0; + background: #7FC6A6; +} +.checkbox-wrapper-10 .tgl-flip:checked + .tgl-btn:active:after { + transform: rotateY(20deg); +} + +// End Checkbox button css + +.cage-templates { + display: flex; + justify-content: space-around; /* Ensure equal spacing around the templates */ + gap: 10px; /* Add space between each template */ +} + +.cage-template { + display: flex; + flex-direction: column; /* Arrange items in a column */ + align-items: center; /* Center items horizontally */ + padding: 10px; /* Add padding around each template */ +} + +.cage-template h2 { + margin: 0 0 10px 0; /* Add space below the name */ + text-align: center; /* Center the name */ +} + +.layout-tooltip-container { + position: relative; + display: inline-block; +} + +.layout-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: black; + color: white; + padding: 5px 10px; + border-radius: 4px; + white-space: nowrap; + z-index: 1000; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + font-size: 14px; +} + +.layout-editor-toolbar { + flex: 1; +} + +.layout-editor { + display: grid; + grid-template-columns: 1fr 300px; + grid-template-rows: auto 1fr; + grid-template-areas: + "room-utils room-utils" + "layout-grid layout-toolbar"; + gap: 10px; /* Adjust the gap between elements as needed */ + width: 100%; + padding: 10px; + flex-grow: 1; +} + +#utils { + grid-area: room-utils; +} + +#layout-grid { + grid-area: layout-grid; + display: flex; + flex-direction: column; + height: auto; + width: auto; + background-color: white; +} + +#layout-toolbar { + grid-area: layout-toolbar; + display: flex; + flex-direction: column; + height: 100%; +} + +#layout-grid svg { + width: 100%; + height: 100%; + flex-grow: 1; + display: block; +} + + + /* CSS */ +.layout-toolbar-btn { + background-color: dimgray; + border-radius: 8px; + border-style: none; + box-sizing: border-box; + color: #FFFFFF; + cursor: pointer; + display: inline-block; + font-family: "Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + font-weight: 500; + height: 40px; + line-height: 20px; + list-style: none; + margin-top: 10px; + outline: none; + padding: 10px 16px; + position: relative; + text-align: center; + text-decoration: none; + transition: color 100ms; + vertical-align: baseline; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; +} + +.layout-toolbar-btn:hover, +.layout-toolbar-btn:focus { + background-color: gray; +} + +/* +
+ + +
*/ + + .checkbox-wrapper-8 .tgl { + display: none; + } +.checkbox-wrapper-8 .tgl, +.checkbox-wrapper-8 .tgl:after, +.checkbox-wrapper-8 .tgl:before, +.checkbox-wrapper-8 .tgl *, +.checkbox-wrapper-8 .tgl *:after, +.checkbox-wrapper-8 .tgl *:before, +.checkbox-wrapper-8 .tgl + .tgl-btn { + box-sizing: border-box; +} +.checkbox-wrapper-8 .tgl::-moz-selection, +.checkbox-wrapper-8 .tgl:after::-moz-selection, +.checkbox-wrapper-8 .tgl:before::-moz-selection, +.checkbox-wrapper-8 .tgl *::-moz-selection, +.checkbox-wrapper-8 .tgl *:after::-moz-selection, +.checkbox-wrapper-8 .tgl *:before::-moz-selection, +.checkbox-wrapper-8 .tgl + .tgl-btn::-moz-selection, +.checkbox-wrapper-8 .tgl::selection, +.checkbox-wrapper-8 .tgl:after::selection, +.checkbox-wrapper-8 .tgl:before::selection, +.checkbox-wrapper-8 .tgl *::selection, +.checkbox-wrapper-8 .tgl *:after::selection, +.checkbox-wrapper-8 .tgl *:before::selection, +.checkbox-wrapper-8 .tgl + .tgl-btn::selection { + background: none; +} +.checkbox-wrapper-8 .tgl + .tgl-btn { + outline: 0; + display: block; + height: 2em; + position: relative; + border-radius: 8px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.checkbox-wrapper-8 .tgl + .tgl-btn:after, +.checkbox-wrapper-8 .tgl + .tgl-btn:before { + position: relative; + display: block; + content: ""; + width: 50%; + height: 100%; +} +.checkbox-wrapper-8 .tgl + .tgl-btn:after { + left: 0; +} +.checkbox-wrapper-8 .tgl + .tgl-btn:before { + display: none; +} +.checkbox-wrapper-8 .tgl:checked + .tgl-btn:after { + left: 50%; +} + +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn { + overflow: hidden; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: all 0.2s ease; + font-family: sans-serif; + background: indianred; +} +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn:after, +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn:before { + display: inline-block; + transition: all 0.2s ease; + width: 100%; + text-align: center; + position: absolute; + line-height: 2em; + font-weight: bold; + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); +} +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn:after { + left: 100%; + content: attr(data-tg-on); +} +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn:before { + left: 0; + content: attr(data-tg-off); +} +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn:active { + background: #888; +} +.checkbox-wrapper-8 .tgl-skewed + .tgl-btn:active:before { + left: -10%; +} +.checkbox-wrapper-8 .tgl-skewed:checked + .tgl-btn { + background: #86d993; +} +.checkbox-wrapper-8 .tgl-skewed:checked + .tgl-btn:before { + left: -100%; +} +.checkbox-wrapper-8 .tgl-skewed:checked + .tgl-btn:after { + left: 0; +} +.checkbox-wrapper-8 .tgl-skewed:checked + .tgl-btn:active:after { + left: 10%; +} + + +.home-container { + display: flex; + height: 100vh; /* Full viewport height */ + width: 100%; /* Full viewport width */ +} +// Editor Context menu + + +/* Custom context menu styling */ +.context-menu { + position: absolute; + display: none; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + padding: 10px; + z-index: 500; + width: 200px; /* Set default width */ + height: 150px; /* Set default height */ + overflow: visible; /* Hide overflow during resizing */ + resize: none; /* Disable the native resize */ +} + +/* Menu item styling */ +.menu-item { + margin-bottom: 8px; +} + +.menu-item label { + display: block; + font-size: 14px; + margin-bottom: 4px; +} + +.menu-item-group { + display: flex; /* Aligns buttons in a row */ + justify-content: center; /* Centers the buttons horizontally */ + gap: 5px; /* Space between buttons */ + margin-top: 20px; + text-align: center; +} + +.menu-item input, +.menu-item select { + width: 100%; + padding: 5px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Default styles */ +.select-menu .select__option--is-selected { + background-color: white; + color: black; +} + +/* Highlight selected option */ +.select-menu .select__option:hover { + background-color: lightgray; + opacity: 0.8; +} + + +.context-menu-input { + overflow-y: visible; +} +// End Editor Context menu styles + +// Room Size Selector styles + +/* General popup styling */ +.room-size-selector-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.room-size-selector-content { + background: #fff; + padding: 20px; + border-radius: 8px; + width: 400px; + max-width: 90%; +} + +.room-size-selector-options-container { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} + +.room-size-selector-option-card { + padding: 15px; + border: 2px solid #ccc; + border-radius: 8px; + cursor: pointer; + transition: border-color 0.3s ease; +} + +.room-size-selector-option-card:hover { + border-color: #007bff; +} + +.room-size-selector-option-card.selected { + border-color: #007bff; + background-color: #e9f7fe; +} + +.room-size-selector-actions { + display: flex; + justify-content: center; +} + +.room-size-selector-close-btn, .room-size-selector-select-btn { + padding: 10px 20px; + border: none; + cursor: pointer; + border-radius: 5px; +} + +.room-size-selector-close-btn { + background-color: #ccc; +} + +.room-size-selector-select-btn { + background-color: #007bff; + color: white; +} + +.room-size-selector-select-btn:disabled { + background-color: #007bff80; + cursor: not-allowed; +} + +// Home Room Styles + +// START CSS FOR HOME + +.page-content-wrapper { + display: flex; + flex-direction: column; + border: 1px solid lightgray; + width: 90%; /* Adjust width as needed */ +} +.page-content { + flex-grow: 1; +} + +.page-map { + height: 3vh; /* Adjust height as needed */ + border-bottom: 1px solid lightgray; + border-top: 1px solid lightgray; + display: flex; + align-items: center; + justify-content: left; +} + +.page-map-url { + text-align: center; + padding: 2px; + font-size: large; + display: flex; + flex-direction: row; +} + +.page-map-link { + padding-right: 10px; + padding-left: 10px; + color: #007bff; + pointer-events: auto; + cursor: pointer; +} + +// END CSS FOR HOME + + +// START CSS FOR ROOM LIST +.room-list { + display: flex; + flex-direction: column; + // border-right: 1px solid black; +} + +.room-title, .room-list-title { + cursor: pointer; + padding: 5px; + border: 1px solid #ccc; + margin: 5px 0; +} + +.room-list-items { + padding: 5px; + overflow-y: auto; + height: 100%; +} + +.arrow { + display: inline-block; + width: 10px; + height: 10px; + border-top: 2px solid black; + border-right: 2px solid black; + transform: rotate(45deg); + transition: transform 0.3s ease; +} + +.room-dir { + width: 10%; /* Adjust width as needed */ + border: 1px solid black; +} + +.room-dir-header { + cursor: pointer; + font-weight: bold; + display: flex; + align-items: center; + justify-content: space-between; + font-size: large; +} + +.room-dir-room-obj { + margin: 10px 10px 10px 5px; +} + +.room-dir-rack-obj { + cursor: pointer; + font-weight: bold; + display: flex; + align-items: center; + justify-content: space-between; +} +.room-dir-cage-obj { + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; +} + +.room-dir-header.open .arrow { + transform: rotate(135deg); +} + + +.room-dir-rack-obj.open .arrow { + transform: rotate(135deg); +} + +.room-search { + width: 100%; + border: 3px solid #9DBFAF; + padding: 5px; + height: 3vh; + border-radius: 5px 0 0 5px; + outline: none; +} + +.room-search:focus{ + border-color: #00B4CC; +} +// END CSS FOR ROOM LIST + +.view-content { + display: block; + padding: 5px; + height: 100%; + width: 100%; +} + +.room-view-container { + position: relative; + margin: 10px; + width: 100%; + height: 100%; +} + +.room-view-title { + font-size: xx-large; + padding: 5px; +} + +.room-view-title label { + margin-left: 10px; +} + +.room-view-checkbox { + appearance: none; /* Hide default checkbox */ + -webkit-appearance: none; + -moz-appearance: none; + width: 20px; + height: 20px; + position: relative; + border: none; + outline: none; + pointer-events: none; +} + +/* Green checkmark when checked */ +.room-view-checkbox:checked::before { + content: "✔"; /* Checkmark symbol */ + color: green; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* Red X when unchecked */ +.room-view-checkbox:not(:checked)::before { + content: "✖"; + color: red; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + + + + +.page-tab-container { + font-family: Arial, sans-serif; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} +.tab-content { + flex: 1; + width: fit-content; + height: fit-content; +} +.tab-buttons { + display: flex; + gap: 20px; /* Space between buttons */ + margin-bottom: 20px; + border-bottom: 1px solid #ddd; /* Light gray line under all buttons */ + +} + +.tab-buttons button { + padding: 10px 20px; + border: none; + background-color: transparent; /* No background */ + cursor: pointer; + font-size: large; + color: #333; /* Default text color */ + position: relative; /* For the underline */ + transition: color 0.3s; +} + +/* Underline for each button */ +.tab-buttons button::after { + content: ""; + position: absolute; + left: 0; + bottom: -2px; /* Position the line below the button */ + width: 100%; + height: 2px; /* Thickness of the line */ + background-color: transparent; /* Default line color */ + transition: background-color 0.3s; +} + +/* Blue underline for the active button */ +.tab-buttons button.active::after { + background-color: #007bff; /* Blue color for active tab */ +} + +/* Hover effect for buttons */ +.tab-buttons button:hover { + color: #007bff; /* Blue text on hover */ +} + +/* Hover effect for the underline */ +.tab-buttons button:hover::after { + background-color: #007bff; /* Blue underline on hover */ +} + + +.mod-container { + width: 100%; + margin: 0 auto; +} + +.mod-container-columns { + display: flex; + gap: 20px; /* space between columns */ + margin-bottom: 20px; /* space between columns and full row */ +} + +.mod-container-column { + flex: 1; + padding: 20px; + background: #f0f0f0; + box-sizing: border-box; +} + +.mod-container-row { + width: 100%; + padding: 20px; + background: #e0e0e0; /* for visualization */ + box-sizing: border-box; +} + +// Mod table css +.mod-table-container { + max-width: 3000px; + margin-left: auto; + margin-right: auto; + padding-left: 10px; + padding-right: 10px; + overflow: visible; +} + +.mod-table { + list-style: none; + padding: 0; + margin: 0; + width: 100%; /* Full width */ + display: table; /* Use table layout */ + table-layout: auto; /* Allow columns to expand based on content */ +} + +/* Style for each row */ +.mod-table-row { + display: table-row; /* Use table-row layout */ + width: 100%; + border-bottom: 1px solid #ccc; /* Optional: Add a border between rows */ +} + +/* Style for header row */ +.mod-table-header { + font-weight: bold; + background-color: #f0f0f0; /* Optional: Add a background color for the header */ +} + +/* Style for each column */ +.mod-table-column { + display: table-cell; /* Use table-cell layout */ + padding: 10px; /* Adjust padding as needed */ + white-space: nowrap; /* Prevent text wrapping */ + min-width: 100px; /* Set a minimum width for columns */ + text-align: center; /* Center text horizontally */ +} + +.small-svg { + margin-inside: 10px; +} + +.small-svg svg { + overflow: scroll; +} +.small-svg g { + overflow: scroll; +} + +.cage-layout { + display: flex; + flex-direction: row; +} + +.cage-layout svg { + display: block; + margin: auto; +} + +.cage-mod-table { + margin-left: 25px; + display: table; + border-collapse: collapse; +} + +.cage-mod-table-column { + display: table-cell; + align-items: center; + gap: 8px; /* Adjust spacing between column header and cells */ + width: max-content; + min-width: 50px; + border: 2px solid lightgray; + //border-right: 1px solid black; + //border-left: 1px solid black; +} + +.cage-mod-table-cell { + padding: 8px; + border-radius: 4px; + width: 100%; + text-align: center; + background-color: white; +} + +.cage-mod-table-label { + font-size: large; + text-align: center; + padding: 10px; + width: 100%; + border-bottom: 2px solid lightgray; + //border: 1px solid black; +} + +.cage-mod-table-add-mod { + display: inline-block; + text-align: center; + width: 100%; + font-size: x-large; + cursor: pointer; +} + +.cage-mod-table-add-mod:hover { + background-color: lightblue; +} + +.modification-editor-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 20px; + width: auto; + min-width: 300px; + animation: fadeIn 0.2s ease-out; +} + +.modification-editor-popup-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; +} + +.modification-editor-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.modification-editor-popup-title { + font-size: 1.2rem; + font-weight: 600; + margin: 0; +} + +.modification-editor-popup-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #666; + padding: 0; + line-height: 1; +} + +.modification-editor-popup-close:hover { + color: #333; +} + +.modification-editor-popup-content { + margin-bottom: 20px; + display: flex; + flex-direction: row; + +} + +.modification-editor-popup-input { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.modification-editor-popup-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.modification-editor-popup-button { + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + +.modification-editor-popup-save { + background-color: #4CAF50; + color: white; + border: none; +} + +.modification-editor-popup-save:hover { + background-color: #45a049; +} + +.modification-editor-popup-cancel { + background-color: #f1f1f1; + border: 1px solid #ddd; + color: #333; +} + +.modification-editor-popup-cancel:hover { + background-color: #e7e7e7; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translate(-50%, -60%); + } + to { + opacity: 1; + transform: translate(-50%, -50%); + } +} \ No newline at end of file diff --git a/CageUI/src/client/components/ConfirmationPopup.tsx b/CageUI/src/client/components/ConfirmationPopup.tsx new file mode 100644 index 000000000..da6162805 --- /dev/null +++ b/CageUI/src/client/components/ConfirmationPopup.tsx @@ -0,0 +1,58 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC } from 'react'; +import '../cageui.scss'; + +interface ConfirmationPopupProps { + message: string; + onConfirm?: () => void; // if confirm exists, give popup confirmation with option to cancel + onCancel?: () => void; // if cancel exists, run function before closing popup Ex (resetting states) + onClose: () => void; // function to close popup, usually a boolean state going to false +} +export const ConfirmationPopup: FC = (props) => { + const { message, onConfirm, onCancel, onClose } = props + return ( +
+
+

+ {!onConfirm && +

+ +
+ } + {onConfirm && +
+ + +
+ } +
+
+ ); +} \ No newline at end of file diff --git a/CageUI/src/client/components/TextInput.tsx b/CageUI/src/client/components/TextInput.tsx new file mode 100644 index 000000000..e1730baf2 --- /dev/null +++ b/CageUI/src/client/components/TextInput.tsx @@ -0,0 +1,60 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, useState } from 'react'; + + +interface TextInputProps { + onSubmit: (value: any) => void; +} + +// input is a text but only submits if it is a number +export const TextInput: FC = (props) => { + const {onSubmit} = props; + const [inputValue, setInputValue] = useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && inputValue.trim() !== '') { + const numericValue = parseFloat(inputValue); + if (!isNaN(numericValue)) { + onSubmit(numericValue); + } + } + }; + + return ( +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/CageUI/src/client/components/layoutEditor/ChangeRack.tsx b/CageUI/src/client/components/layoutEditor/ChangeRack.tsx new file mode 100644 index 000000000..88aadd49b --- /dev/null +++ b/CageUI/src/client/components/layoutEditor/ChangeRack.tsx @@ -0,0 +1,85 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, useEffect, useState } from 'react'; +import Select from 'react-select'; +import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; +import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; +import { Filter } from '@labkey/api'; + + +interface ChangeRackProps { + onSubmit: (newType: {value: string, label: string}) => void; +} + +export const ChangeRack: FC = (props) => { + const {onSubmit} = props; + + const [options, setOptions] = useState<{value: string, label: string}[]>(null); + + const handleChange = (newVal) => { + onSubmit(newVal); + }; + + + useEffect(() => { + if(options){ + setOptions(options) + }else{ + const optConfig: SelectRowsOptions = { + schemaName: "cageui", + queryName: "racks", + columns: ['rackid', 'rack_type'] + } + const rackTypesConfig: SelectRowsOptions = { + schemaName: "cageui", + queryName: "rack_types", + columns: ['name', 'rowid'] + } + const rackPromise = labkeyActionSelectWithPromise(optConfig); + const rackTypesPromise = labkeyActionSelectWithPromise(rackTypesConfig); + + Promise.all([rackPromise, rackTypesPromise]).then(([rackResult, rackTypesResult]) => { + + if(rackResult.rows.length > 0){ + const tmp = []; + + for (const row of rackResult.rows) { + const rackTypeName = rackTypesResult.rows.find(r => r.rowid === parseInt(row.rack_type)).name; + tmp.push({label: `${row.rackid} - ${rackTypeName}`, value: `${row.rackid}`}); + } + setOptions(tmp); + } + }) + } + }, [options]); + + return ( +
+
+ setShowGrid(prevState => !prevState)} + /> + +
+ + { isTemplateCreator(user) && + + } + { (isRoomCreator(user) || isTemplateCreator(user)) && + + } + + +
+ {showSaveConfirm && + ${localRoom.name} ?`} + onConfirm={handleSave} + onCancel={handleCancelConfirm} + onClose={() => setShowSaveConfirm(false)} + /> + } + {showRoomSelector && + {setShowRoomSelector(false);setShowSaveConfirm(true);}} + onCancel={() => {setTemplateOptions(false);setShowRoomSelector(false);}} + /> + } + {showRoomSelectorTemplateLoad && + setLoadTemplate(true)} + onCancel={() => {setShowRoomSelectorTemplateLoad(false); setTemplateOptions(false);}} + /> + } + {showSaveResult && + handleSaveClose(showSaveResult.roomName)} + /> + } + {showCageContextMenu && + setShowCageContextMenu(false)} + menuItems={[ + { + element: + , + types: [], + title: "Change Rack" + }, + { + element: + { + changeCageNum(parseRoomItemNum((selectedObj as Cage).cageNum), num); + setShowCageContextMenu(false); + }} + />, + types: [], + title: "Change Cage Number" + } + ]} + /> + } + {showObjectContextMenu && + setShowObjectContextMenu(false)} + menuItems = {[{element: + , + types: [RoomObjectTypes.GateClosed, RoomObjectTypes.GateOpen], + title: "Change Connected Room" + }, + {element: + setShowObjectContextMenu(false)} + />, + types: [RoomObjectTypes.GateClosed, RoomObjectTypes.GateOpen], + title: "Switch Gate Status" + } + ]} + /> + } + + ); +}; + +export default Editor; \ No newline at end of file diff --git a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx new file mode 100644 index 000000000..0f5a5839e --- /dev/null +++ b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx @@ -0,0 +1,158 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, ReactElement, useEffect, useRef } from 'react'; +import '../../cageui.scss'; +import { Button } from 'react-bootstrap'; +import { parseRoomItemType, stringToRoomItem } from '../../utils/helpers'; +import { + Cage, + DefaultRackTypes, + RackStringType, + RackTypes, + RoomItemType, + RoomObject, + RoomObjectTypes +} from '../../types/typings'; +import { SelectedObj } from '../../types/layoutEditorTypes'; + +interface Option { + label: string; + value: number; +} + +interface EditorContextMenuProps { + ctxMenuStyle: { + display: string; + top: string; + left: string; + }; + type: "object" | 'caging'; // context menu for caging or room objects + selectedObj: SelectedObj; + closeMenu: () => void; + onClickDelete?: (type?: string) => void; + menuItems?: {element: ReactElement, types: RoomItemType[], title: string}[]; // for types, an array of types to render this element for. If empty it will render the component for all types. +} + +/* + Context menu for room item. Renders differently depending on assigned type and passed in components. + + */ +export const EditorContextMenu: FC = (props) => { + const { + ctxMenuStyle, + onClickDelete, + closeMenu, + menuItems, + selectedObj, + type + } = props; + + const menuRef = useRef(null); + + // Delete object for room objects + const handleDeleteObject = (e: React.MouseEvent) => { + e.stopPropagation(); + onClickDelete(); + }; + + // Delete cage and rack for caging units + const handleDeleteCage = (e: React.MouseEvent) => { + e.stopPropagation(); + onClickDelete("cage"); + }; + const handleDeleteRack = (e: React.MouseEvent) => { + e.stopPropagation(); + onClickDelete("rack"); + }; + + useEffect(() => { + const handleClickOutside = (event) => { + // Check if the click was outside the menu + if (menuRef.current && !menuRef.current.contains(event.target)){ + closeMenu(); + } + }; + + // Add event listener to detect clicks + document.addEventListener('mousedown', handleClickOutside); + + // Cleanup event listener on component unmount + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [menuRef]); + + return ( +
+ {menuItems && menuItems.map((item, index) => { + let selectedObjType = selectedObj.selectionType === 'obj' ? (selectedObj as RoomObject).type : stringToRoomItem(parseRoomItemType((selectedObj as Cage).cageNum) as RackStringType); + if(item.types.length === 0){// if no types were given render, otherwise only render elements for that type + return( +
+ + {item.element} +
+ ); + } + if(item.types.includes(selectedObjType as RackTypes | RoomObjectTypes | DefaultRackTypes)){ + return( +
+ + {item.element} +
+ ); + } + })} +
+ {type === 'object' ? + + : +
+ + + +
+ } +
+
+ ); +}; \ No newline at end of file diff --git a/CageUI/src/client/components/layoutEditor/GateChangeRoom.tsx b/CageUI/src/client/components/layoutEditor/GateChangeRoom.tsx new file mode 100644 index 000000000..3a4c90fff --- /dev/null +++ b/CageUI/src/client/components/layoutEditor/GateChangeRoom.tsx @@ -0,0 +1,100 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, useEffect, useState } from 'react'; +import Select from 'react-select'; +import { Room, RoomObject, RoomObjectTypes } from '../../types/typings'; +import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; +import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; +import { Option } from '@labkey/components'; +import { SelectedObj } from '../../types/layoutEditorTypes'; + +interface GateChangeRoomProps { + selectedObj: SelectedObj; + setLocalRoom: React.Dispatch>; +} + +/* + Changes the assigned room the gate goes to. + extraContext is {room: string, roomid: number} or GateContext + */ +export const GateChangeRoom: FC = (props) => { + const {setLocalRoom, selectedObj} = props; + const [selectedRoom, setSelectedRoom] = useState>(null); + const [options, setOptions] = useState[]>(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let initalRoom: Option; + if(selectedObj && (selectedObj as RoomObject).extraContext?.room){ + initalRoom = {label: (selectedObj as RoomObject).extraContext.room, value: (selectedObj as RoomObject).extraContext.roomId}; + setSelectedRoom(initalRoom); + setLoading(false); + }else{ + setLoading(false); + } + }, []); + useEffect(() => { + const roomsConfig: SelectRowsOptions = { + schemaName: 'ehr_lookups', + queryName: 'rooms', + columns: ['room', 'rowid'], + } + labkeyActionSelectWithPromise(roomsConfig).then(result => { + if (result.rows.length !== 0) { + const rowOptions: Option[] = []; + result.rows.forEach(row => { + rowOptions.push({label: row.room, value: row.rowid}); + }) + setOptions(rowOptions); + } + }).catch(err => { + console.log("Error fetching prev room", err); + }); + }, []); + + useEffect(() => { + if(!selectedRoom || loading) return; + + setLocalRoom(prevState => ({ + ...prevState, + objects: prevState.objects.map((obj, index) => { + if(obj.itemId === (selectedObj as RoomObject).itemId){ + return {...obj, extraContext: {...obj.extraContext, room: selectedRoom.label, roomId: selectedRoom.value}}; + } + return obj; + + }) + })) + }, [selectedRoom]); + + const handleChange = (option) => { + setSelectedRoom(option); + } + return ( +
+ setSelectedRoom(option.label)} + /> +
+ {(template && !templateLoad) && +
+ + setTemplateName(e.target.value)} + /> +
+ } +
+ + +
+ + + + ); +} \ No newline at end of file diff --git a/CageUI/src/client/components/layoutEditor/RoomSizeSelector.tsx b/CageUI/src/client/components/layoutEditor/RoomSizeSelector.tsx new file mode 100644 index 000000000..6ada3c7c9 --- /dev/null +++ b/CageUI/src/client/components/layoutEditor/RoomSizeSelector.tsx @@ -0,0 +1,77 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { useState } from 'react'; +import '../../cageui.scss'; + +// Define the structure of an option/card +export interface SelectorOptions { + id: number; + scale: number; + title: string; + description: string; +} + +interface PopupProps { + options: SelectorOptions[]; + onClose: () => void; + onSelect: (selectedOption: SelectorOptions | null) => void; +} + +// Popup window for selecting the side of the room in the layout editor +export const RoomSizeSelector: React.FC = ({ options, onClose, onSelect }) => { + const [selectedOption, setSelectedOption] = useState(null); + + // Handle the card click event + const handleOptionClick = (option: SelectorOptions) => { + setSelectedOption(option); + }; + + // Handle the select button click + const handleSelectClick = () => { + onSelect(selectedOption); + onClose(); // Close the popup after selection + }; + + return ( +
+
+

Select an Option

+ +
+ {options.map((option) => ( +
handleOptionClick(option)} + > +

{option.title}

+

{option.description}

+
+ ))} +
+
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/CageUI/src/client/context/LayoutEditorContextManager.tsx b/CageUI/src/client/context/LayoutEditorContextManager.tsx new file mode 100644 index 000000000..2fb14d77a --- /dev/null +++ b/CageUI/src/client/context/LayoutEditorContextManager.tsx @@ -0,0 +1,1357 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { createContext, FC, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import { + LayoutContextProps, + LayoutContextType +} from '../types/layoutEditorContextTypes'; +import { + Cage, + CageNumber, + DefaultRackId, + GroupId, + LayoutHistoryData, + LocationCoords, ModData, + Rack, + RackGroup, + RackStringType, + RackTypes, + RealRackId, + Room, + RoomItemClass, + RoomItemType, + RoomObject, + RoomObjectTypes, + UnitLocations, + UnitType +} from '../types/typings'; +import { + CellKey, + DeleteActions, + ExtraContext, + LayoutSaveResult, + RackActions, + SelectedObj +} from '../types/layoutEditorTypes'; +import { + convertCageNumToNum, + createEmptyUnitLoc, + findCageInGroup, + findRackInGroup, + findSelectObjRack, getNextGroupId, + getTranslation, + isRackDefault, + isRackEnum, + showLayoutEditorError, +} from '../utils/LayoutEditorHelpers'; +import * as d3 from 'd3'; +import { + defaultTypeToRackType, getNextDefaultRackId, + getSvgSize, + parseLongId, + parseRoomItemNum, + parseRoomItemType, + rackTypeToDefaultType, + roomItemToString, + zeroPadName +} from '../utils/helpers'; +import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; +import { ActionURL, Filter } from '@labkey/api'; +import { Command, CommandType } from '@labkey/api/dist/labkey/query/Rows'; +import { + labkeyActionSelectDistinctWithPromise, + labkeyActionSelectWithPromise, + labkeySaveRows, +} from '../api/labkeyActions'; +import { CELL_SIZE } from '../utils/constants'; + +const LayoutEditorContext = createContext(null); + +export const useLayoutEditorContext = () => { + const context = useContext(LayoutEditorContext); + + if (!context) { + throw new Error( + "useRoomContext has to be used within " + ); + } + + return context; +} + +export const LayoutEditorContextProvider: FC = ({children, prevRoom, user}) => { + // loaded in and unchanged since start of layout editing + const [room, setRoom] = useState({ + name: "new-layout", + rackGroups: [], + objects: [], + layoutData: null + }); + /* unit locations resembles each rack type and their respective locations in a room, since location is geospatial + it does not need to remember anything other than x and y coords for that group of racks. The reason for having + different objects for each rack type is to keep a separate numbering system for each type of rack. Additionally + this state is tracked for detecting merging. + */ + const [unitLocs, setUnitLocs] = useState(createEmptyUnitLoc()); + + // All changes made to room reflect here. Use room state to compare to the start of room editing vs the changes made here + const [localRoom, setLocalRoom] = useState({ + name:"new-layout", + rackGroups: [], + objects: [], + layoutData: null + }); + + const cageConnections = useRef<{ + parent: Record, + rank: Record, + root: Record, // Root with path compression + adjacency: Record>; // Track all adjacent cages + }>({ + parent: {}, + rank: {}, + adjacency: {}, + root: {} + }); + + const [reloadRoom, setReloadRoom] = useState(null); + + + const [layoutSvg, setLayoutSvg] = useState>(null); + + const [nextAvailGroup, setNextAvailGroup] = useState(`rack-group-1`); // Tracks currently active groups of racks + + const [cageNumChange, setCageNumChange] = useState<{before: number, after: number} | null>(null); + + const [isLoading, setIsLoading] = useState(prevRoom ? true : false); + + // the id of the clicked on svg group for either dragging or context menu opening. + const [selectedObj, setSelectedObj] = useState(null); + + // instead of tying scale to each location, manage one scale for the whole layout + const [scale, setScale] = useState(1); + + + const grid = useRef>(new Map()); + + const getCageLoc = (cageNum: CageNumber): LocationCoords => { + for (const cageLoc of unitLocs[parseRoomItemType(cageNum)]) { + if((cageLoc as LocationCoords).num === cageNum){ + return(cageLoc); + } + } + } + + const getCellKey = (x: number, y: number): CellKey => { + const gridX = Math.floor(x / CELL_SIZE); + const gridY = Math.floor(y / CELL_SIZE); + return `${gridX},${gridY}`; + } + + const getCageCells = (cage: Cage, cageLoc: LocationCoords): CellKey[] => { + const cells: CellKey[] = []; + const xEnd = cageLoc.cellX + (cage.size * CELL_SIZE); + const yEnd = cageLoc.cellY + (cage.size * CELL_SIZE); + + for (let x = cageLoc.cellX; x < xEnd; x += CELL_SIZE) { + for (let y = cageLoc.cellY; y < yEnd; y += CELL_SIZE) { + cells.push(getCellKey(x, y)); + } + } + return cells; + } + + const insertCageToMap = (cage: Cage, cageLoc: LocationCoords) => { + const cells = getCageCells(cage, cageLoc); + for (const cell of cells) { + if (!grid.current.has(cell)) { + grid.current.set(cell, []); + } + grid.current.get(cell)!.push(cageLoc); + } + } + + const getAdjCages = (cage: Cage, cageLoc: LocationCoords): LocationCoords[] => { + const adjacentCages = new Set(); + const { cellX: x, cellY: y } = cageLoc; + const width = cage.size * CELL_SIZE; + const height = cage.size * CELL_SIZE; + + // Check all four directions + const directions = [ + { dx: 0, dy: -1 }, // Top + { dx: 1, dy: 0 }, // Right + { dx: 0, dy: 1 }, // Bottom + { dx: -1, dy: 0 } // Left + ]; + + for (const dir of directions) { + const checkX = x + (dir.dx === 1 ? width : dir.dx === -1 ? -1 : 0); + const checkY = y + (dir.dy === 1 ? height : dir.dy === -1 ? -1 : 0); + + // Check along the entire edge + if (dir.dx !== 0) { // Left/Right check + for (let yCheck = y; yCheck < y + height; yCheck += CELL_SIZE) { + const cell = getCellKey(checkX, yCheck); + for (const otherBox of grid.current.get(cell) || []) { + if (otherBox.num !== cageLoc.num) adjacentCages.add(otherBox); + } + } + } else { // Top/Bottom check + for (let xCheck = x; xCheck < x + width; xCheck += CELL_SIZE) { + const cell = getCellKey(xCheck, checkY); + for (const otherBox of grid.current.get(cell) || []) { + if (otherBox.num !== cageLoc.num) adjacentCages.add(otherBox); + } + } + } + } + + return Array.from(adjacentCages); + } + + const removeCageFromMap = (cage: Cage, cageLoc: LocationCoords): void => { + const cells = getCageCells(cage, cageLoc); + for (const cell of cells) { + const cagesInCell = grid.current.get(cell); + if (cagesInCell) { + const updatedBoxes = cagesInCell.filter((b) => b.num !== cageLoc.num); + if (updatedBoxes.length === 0) { + grid.current.delete(cell); // Prune empty cells + } else { + grid.current.set(cell, updatedBoxes); + } + } + } + } + + const updateCageInMap = (cage: Cage, cageLoc: LocationCoords, oldCageLoc: LocationCoords): void => { + removeCageFromMap(cage, oldCageLoc); + insertCageToMap(cage, cageLoc); + } + + /* + Fixes the group ids of the rackGroups state in room and the svg ids for those state objects + */ + const fixGroupIds = () => { + + setLocalRoom((prevRoom) => { + + if(prevRoom.rackGroups.length === 0){ + return prevRoom; + } + const sortedGroups = prevRoom.rackGroups.sort((groupA, groupB) => { + const aId = parseLongId(groupA.groupId); + const bId = parseLongId(groupB.groupId); + return aId - bId; + }); + + const newGroups: RackGroup[] = sortedGroups.map((group, index) => { + const groupSvg = layoutSvg.select(`[id=${group.groupId}]`); + const newId = index + 1; + if(!groupSvg.empty()){ // if a group svg exists for the group, rename to new group + groupSvg.attr('id', `rack-group-${newId}`); + } + return { + ...group, + groupId: `rack-group-${newId}` as GroupId + }; + }); + const lastId: number = parseLongId(newGroups[newGroups.length - 1].groupId); + setNextAvailGroup(`rack-group-${lastId + 1}` as GroupId); + return { + ...prevRoom, + rackGroups: newGroups + }; + }) + } + + // This only adds default racks/cages to the layout, it is not used in loading in previous layouts + const addRack = async (id: string, x: number, y: number, newScale: number, rackType: RackTypes) => { + const newCageNum: CageNumber = `${roomItemToString(rackType) as RackStringType}-${getNextCageNum(roomItemToString(rackType) as RackStringType)}`; + + const svgSize = await getSvgSize(rackType); + if(!svgSize){ + await showLayoutEditorError("No size found for cage"); + return false; + } + + const newCage: Cage = { + selectionType: 'cage', + id: 1, + cageNum: newCageNum, + x: 0, + y: 0, + size: svgSize, + }; + + // First cage in rack is always at rack starting position as well + const newCageLoc: LocationCoords = { + num: newCageNum, + cellX: x, + cellY: y + }; + + let type: UnitType; + + const optConfig: SelectRowsOptions = { + schemaName: "cageui", + queryName: "rack_types", + filterArray: [ + Filter.create('type', rackTypeToDefaultType(rackType), Filter.Types.EQUAL) + ] + } + // grab and set first default of that type to same svg object + const rackTypeData = await labkeyActionSelectWithPromise(optConfig); + if(rackTypeData.rows.length === 0){ + await showLayoutEditorError("Unable to find rack type data"); + return false; + } + + // make first rack type + type = { + rowid: rackTypeData.rows[0].rowid, + name: rackTypeData.rows[0].name, + type: rackType, + isDefault: true, + }; + + const newRack: Rack = { + selectionType: 'rack', + cages: [newCage], + itemId: id as DefaultRackId, + isActive: false, // Default racks are not active by default (since they technically don't exist) + type: type, + x: 0, + y: 0 + }; + + const newRackGroup: RackGroup = { + selectionType: 'rackGroup', + groupId: nextAvailGroup, + racks: [newRack], + x: x, + y: y, + scale: newScale, + } + + setNextAvailGroup(prevState => { + const nextId = parseLongId(prevState) + 1; + return `rack-group-${nextId}` as GroupId + }); + setLocalRoom(prevRoom => ({ + ...prevRoom, + rackGroups: [...prevRoom.rackGroups, newRackGroup] + })); + + setUnitLocs(prevState => ({ + ...prevState, + [roomItemToString(rackType)]: [...prevState[roomItemToString(rackType)], newCageLoc] // Append the new location to the correct array + })); + setScale(newScale); + insertCageToMap(newCage, newCageLoc); + cageConnections.current = { + parent: { ...cageConnections.current.parent, [newCageNum]: newCageNum }, + rank: { ...cageConnections.current.rank, [newCageNum]: 0 }, + adjacency: {...cageConnections.current.adjacency}, + root: {...cageConnections.current.root, [newCageNum]: newCageNum}, + }; + return true; + }; + + const mergeLocalRacks = (newGroup: d3.Selection, targetCageNum, dragCageNum) => { + + setLocalRoom(prevRoom => { + + let {rack: targetRack, rackGroup: targetGroup, cage: targetCage} = findCageInGroup(targetCageNum, prevRoom.rackGroups); + let {rack: dragRack, rackGroup: dragGroup, cage: dragCage} = findCageInGroup(dragCageNum, prevRoom.rackGroups); + + if (!targetRack || !dragRack) { + console.log("One or both racks not found"); + return prevRoom; + } + // different rack types can be connected but not merged + if(targetRack.type.rowid !== dragRack.type.rowid){ + console.log("Impossible configuration detected, please only merge racks of the same type"); + return prevRoom; + } + + // Merge cages and reassign local IDs + const mergedCages = [...targetRack.cages, ...dragRack.cages].map((cage, index) => ({ + ...cage, + id: index + 1, // Reassign local IDs + })); + + + const updatedCages: Cage[] = mergedCages.map(cage => { + const newCage = newGroup.select(`#${cage.cageNum}`); + const cageCoords = getTranslation(newCage.attr('transform')); + + return { + ...cage, + x: cageCoords.x, + y: cageCoords.y, + } + }) + + // Create new merged rack + const mergedRack: Rack = { + itemId: targetRack.itemId, // Use the larger ID for the merged rack + selectionType: 'rack', + type: targetRack.type, + cages: updatedCages, + x: targetRack.x, + y: targetRack.y, + isActive: targetRack.isActive, + }; + + const mergedRackGroup: RackGroup = { + groupId: targetGroup.groupId, + selectionType: 'rackGroup', + x: targetGroup.x, + y: targetGroup.y, + scale: targetGroup.scale, + racks: targetGroup.racks.filter(r => { + return r.itemId !== targetRack.itemId; + }).concat(mergedRack) + } + + // Filter out the original racks and add the merged rack + return ({ + ...prevRoom, + rackGroups: prevRoom.rackGroups.filter(r => { + return r.groupId !== targetGroup.groupId && r.groupId !== dragGroup.groupId; + }).concat(mergedRackGroup) + }); + }); + } + + const connectLocalRacks = (targetId: string, dragId: string, newGroup: d3.Selection, targetCageNum, dragCageNum) => { + setLocalRoom(prevRoom => { + let {rack: targetRack, rackGroup: targetGroup} = findRackInGroup(targetId, prevRoom.rackGroups); + let {rack: dragRack, rackGroup: dragGroup} = findRackInGroup(dragId, prevRoom.rackGroups); + + if (!targetRack || !dragRack) { + console.error("One or both racks not found"); + return prevRoom; + } + + // Update the room to match the new group ids and update x and y to be local to the new rack group + const updatedRackGroups = prevRoom.rackGroups.map((group: RackGroup) => { + if (group.groupId === targetGroup.groupId) { + const updatedRacks = [ + ...group.racks, + ...dragGroup.racks.map((r: Rack) => { + const tempRack = newGroup.select(`#${r.itemId}`); + const newRackCoords = getTranslation(tempRack.attr('transform')); + return { + ...r, + x: newRackCoords.x, + y: newRackCoords.y + }; + }) + ]; + return { + ...group, + groupId: targetGroup.groupId, + racks: updatedRacks + }; + } + if (group.groupId === dragGroup.groupId) { + return null; + } + return group; + }).filter((group): group is RackGroup => group !== null); // filter out the drag group + + + // Return the updated room + return { + ...prevRoom, + rackGroups: updatedRackGroups + }; + }) + } + + const doRackAction = (action: RackActions, targetRackId: string, dragRackId: string, targetCageNum, dragCageNum, newGroup: d3.Selection, ) => { + const { rack: targetRack, rackGroup: targetGroup} = findCageInGroup(targetCageNum, localRoom.rackGroups); + const {rack: dragRack, rackGroup: dragGroup} = findCageInGroup(dragCageNum, localRoom.rackGroups); + if(action === 'merge'){ + mergeLocalRacks(newGroup, targetCageNum, dragCageNum); + + }else{ // action = connect + connectLocalRacks(targetRackId, dragRackId, newGroup, targetCageNum, dragCageNum); + + } + // Initialize if needed + if (!cageConnections.current.parent[targetCageNum]) { + cageConnections.current.parent[targetCageNum] = targetCageNum; + cageConnections.current.root[targetCageNum] = targetCageNum; + cageConnections.current.rank[targetCageNum] = 0; + } + if (!cageConnections.current.parent[dragCageNum]) { + cageConnections.current.parent[dragCageNum] = dragCageNum; + cageConnections.current.root[dragCageNum] = dragCageNum; + cageConnections.current.rank[dragCageNum] = 0; + } + + // Update adjacency + for (const dc of dragRack.cages) { + cageConnections.current.adjacency[dc.cageNum] = cageConnections.current.adjacency[dc.cageNum] || new Set(); + getAdjCages(dc, getCageLoc(dc.cageNum)).forEach((c) => { + // check if adj cage is in target group, if not ignore it + if(findCageInGroup(c.num, [targetGroup])?.cage){ + cageConnections.current.adjacency[dc.cageNum].add(c.num); + } + }); + } + + for (const tc of targetRack.cages){ + cageConnections.current.adjacency[tc.cageNum] = cageConnections.current.adjacency[tc.cageNum] || new Set(); + getAdjCages(tc, getCageLoc(tc.cageNum)).forEach((c) => { + if (findCageInGroup(c.num, [dragGroup])?.cage) { + cageConnections.current.adjacency[tc.cageNum].add(c.num) + } + }); + } + + // Perform union + union(targetCageNum, dragCageNum); + + // After merging / connecting fix the group ids so that they have no gaps + fixGroupIds(); + } + + // Adds item to the local room. return the new room for listeners. + const addRoomItem = async (itemType: RoomItemType, itemId: string, x: number, y: number, scale: number): Promise => { + if(isRackEnum(itemType)){ + return await addRack(itemId, x, y, scale, itemType as RackTypes); + }else{ + const newRoomObj: RoomObject = { + selectionType: 'obj', + itemId: itemId, + type: itemType as RoomObjectTypes, + x: x, + y: y, + scale: scale + } + setLocalRoom(prevRoom => ({ + ...prevRoom, + objects: [...prevRoom.objects, newRoomObj] + })); + return true; + } + } + + const moveObjLocation = (itemId: string, type: RoomItemClass, x: number, y: number, k: number) => { + // Update localRoom and then find the moved rack to update cageLocs + setLocalRoom(prevRoom => { + let updatedLocalRoom: Room; + + // Update cageLocs based on the new rack coordinates + if (type === 'caging') { + const idKey = itemId.includes('group') ? 'groupId' : 'itemId'; // determine if moving rack or group of racks + if(idKey === 'itemId'){ + updatedLocalRoom = { + ...prevRoom, + rackGroups: prevRoom.rackGroups.map(group => + group.racks.some(item => item.itemId === itemId) + ? { + ...group, + x: x, + y: y, + scale: k + } + : group + ) + }; + + const {rack: movedRack} = findRackInGroup(itemId, updatedLocalRoom.rackGroups); + + // Find the moved rack to access its cages + if(!movedRack){ + console.log("Failed to update cages location for rack"); + return prevRoom; // cannot find an available rack id to move + } + + setUnitLocs((prevUnitLocations) => + ({ + ...prevUnitLocations, + // Access the correct unit location array using rack type + [roomItemToString(movedRack.type.type)]: prevUnitLocations[roomItemToString(movedRack.type.type)].map(cage => { + // Check if the cage belongs to the moved rack using cageNum + const movedRackCage = movedRack.cages.find(rackCage => rackCage.cageNum === cage.num); + + if(movedRackCage) { + const oldLocCoords: LocationCoords = { + ...cage + } + const newLocCoords: LocationCoords = { + ...cage, + // Update the cage's coordinates by adding its own coordinates to the new rack's coordinates + cellX: x + movedRackCage.x, // Add new rack's x position to cage's local x + cellY: y + movedRackCage.y, // Add new rack's y position to cage's local y + } + updateCageInMap(movedRackCage,newLocCoords, oldLocCoords); + return newLocCoords; + }else{ + return cage; + } + }) + }) + ); + }else{ + updatedLocalRoom = { + ...prevRoom, + rackGroups: prevRoom.rackGroups.map(group => + group.groupId === itemId + ? { + ...group, + x: x, + y: y, + scale: k + } + : group + ) + }; + + const movedRacks = updatedLocalRoom.rackGroups.find(group => { + return group.groupId === itemId; + }).racks; + + // Find the moved rack to access its cages + if(movedRacks.length === 0){ + console.log("Failed to update cages location for rack"); + return prevRoom; // cannot find an available rack id to move + } + setUnitLocs((prevUnitLocations) => { + const updatedUnitLocations = { ...prevUnitLocations }; + movedRacks.forEach((movedRack) => { + updatedUnitLocations[roomItemToString(movedRack.type.type)] = updatedUnitLocations[roomItemToString(movedRack.type.type)].map((cage) => { + const movedRackCage = movedRack.cages.find((rackCage) => rackCage.cageNum === cage.num); + if (movedRackCage) { + const oldLocCoords: LocationCoords = { + ...cage + } + const newLocCoords: LocationCoords = { + ...cage, + // Update the cage's coordinates by adding its own coordinates to the new rack's coordinates + cellX: x + movedRackCage.x + movedRack.x, // Add new rack's x position to cage's local x + cellY: y + movedRackCage.y + movedRack.y, // Add new rack's y position to cage's local y + } + updateCageInMap(movedRackCage,newLocCoords, oldLocCoords); + return newLocCoords; + + } + return cage; + }); + }); + return updatedUnitLocations; + }); + } + }else{ + // Update non rack/caging object + updatedLocalRoom = { + ...prevRoom, + objects: prevRoom.objects.map(item => + item.itemId === itemId + ? { ...item, x, y, scale: k } + : item + ) + }; + } + return updatedLocalRoom; // Return the updated localRoom + }); + }; + + const delObject = (objectId: string) => { + setLocalRoom(prevRoom => ({ + ...prevRoom, + objects: prevRoom.objects.filter(obj => { + return obj.itemId !== objectId; + }) + })); + } + + // Modified find that only compresses the root structure + const find = useCallback((id: CageNumber, root: Record): CageNumber => { + if (root[id] !== id) { + root[id] = find(root[id], root); // Path compression + } + return root[id]; + }, []); + + // Modified union operation + const union = useCallback((a: CageNumber, b: CageNumber) => { + // Set direct parent relationship (maintains connection chain) + if (parseRoomItemNum(a) < parseRoomItemNum(b)) { + cageConnections.current.parent[b] = a; + } else { + cageConnections.current.parent[a] = b; + } + + // Standard union-by-rank on the root structure + const rootA = find(a, cageConnections.current.root); + const rootB = find(b, cageConnections.current.root); + + if (rootA !== rootB) { + if (cageConnections.current.rank[rootA] < cageConnections.current.rank[rootB]) { + cageConnections.current.root[rootA] = rootB; + } else if (cageConnections.current.rank[rootA] > cageConnections.current.rank[rootB]) { + cageConnections.current.root[rootB] = rootA; + } else { + cageConnections.current.root[rootB] = rootA; + cageConnections.current.rank[rootA]++; + } + } + }, [find]); + + const disconnectCage = (cageNum: CageNumber) => { + // Get current connections + const { parent, rank, adjacency } = cageConnections.current; + const location = findCageInGroup(cageNum, localRoom.rackGroups); + + if (!location) return; + + // 1. Remove from adjacency lists (direct connections) + const directlyConnected = adjacency[cageNum] ? Array.from(adjacency[cageNum]) : []; + for (const neighbor of directlyConnected) { + adjacency[neighbor]?.delete(cageNum); + } + delete adjacency[cageNum]; + + // 2. Handle DSU structure + // Find all cages that point to this cage and reset them + const children = Object.keys(parent).filter(id => parent[id] === cageNum); + for (const child of children) { + parent[child] = child; // Reset to self + rank[child] = 0; + } + + // Remove the cage from DSU + delete parent[cageNum]; + delete rank[cageNum]; + + // 3. Rebuild connections between remaining directly connected cages + for (let i = 0; i < directlyConnected.length; i++) { + for (let j = i + 1; j < directlyConnected.length; j++) { + const a = directlyConnected[i]; + const b = directlyConnected[j]; + if (adjacency[a]?.has(b)) { + union(a, b); + } + } + } + + // 4. Now use your existing group splitting logic with DSU-enhanced component detection + let updatedGroups = localRoom.rackGroups.map(group => { + if (group.groupId !== location.rackGroup.groupId) return group; + + // Cage deletion + return { + ...group, + racks: group.racks.map(r => { + if (r.itemId !== location.rack.itemId) return r; + return { + ...r, + cages: r.cages.filter(c => c.cageNum !== cageNum) + } + }).filter(r => r.cages.length > 0) // Remove empty racks + }; + }).filter(g => g.racks.length > 0); // Remove empty groups + + // 5. Enhanced component detection using DSU + const affectedGroup = updatedGroups.find(g => g.groupId === location.rackGroup.groupId); + if (!affectedGroup) return; + + // Build cage map for the affected group + const cageMap = Object.fromEntries( + affectedGroup.racks.flatMap(r => r.cages.map(c => [c.cageNum, c])) + ); + + // Find connected components using DSU roots + const components = new Map>(); + const processed = new Set(); + + for (const cageNumInGroup of Object.keys(cageMap)) { + if (processed.has(cageNumInGroup)) continue; + + const root = find(cageNumInGroup as CageNumber, parent); + if (!components.has(root)) { + components.set(root, new Set()); + } + + // BFS to find all cages in this component + const queue = [cageNumInGroup]; + processed.add(cageNumInGroup); + + while (queue.length > 0) { + const current = queue.shift()!; + components.get(root)!.add(current); + + for (const neighbor of adjacency[current] || []) { + if (cageMap[neighbor] && !processed.has(neighbor)) { + processed.add(neighbor); + queue.push(neighbor); + } + } + } + } + + // 6. Split groups based on components (your existing coordinate logic) + let finalGroups = updatedGroups.filter(g => g.groupId !== location.rackGroup.groupId); + let nextGroupId = nextAvailGroup; + + // In the group splitting logic: + if (components.size > 1) { + const componentList = Array.from(components.values()); + + // First component keeps original group ID + const firstComponent = componentList[0]; + const firstGroupRacks = affectedGroup.racks.map(r => { + const componentCages = r.cages.filter(c => firstComponent.has(c.cageNum)); + return componentCages.length > 0 ? { + ...r, + cages: componentCages + } : null; + }).filter(Boolean) as Rack[]; + + finalGroups.push({ + ...affectedGroup, + racks: firstGroupRacks + }); + + // Additional components become new groups + for (let i = 1; i < componentList.length; i++) { + const component = componentList[i]; + + // Separate racks with cages in this component + const componentRacks = affectedGroup.racks.map(r => { + const componentCages = r.cages.filter(c => component.has(c.cageNum)); + return componentCages.length > 0 ? { + ...r, + cages: componentCages + } : null; + }).filter(Boolean) as Rack[]; + + // Calculate absolute positions of all cages in this component + const absoluteCagePositions = componentRacks.flatMap(r => + r.cages.map(c => ({ + x: affectedGroup.x + r.x + c.x, + y: affectedGroup.y + r.y + c.y, + rack: r, + cage: c + })) + ); + + // Find minimum position for new group origin + const minX = Math.min(...absoluteCagePositions.map(p => p.x)); + const minY = Math.min(...absoluteCagePositions.map(p => p.y)); + + // Create new racks based on connection types + const newRacks: Rack[] = []; + + // Case 1: Cages from multiple racks → split racks + if (componentRacks.length > 1) { + newRacks.push(...componentRacks.map(r => ({ + ...r, + x: (affectedGroup.x + r.x) - minX, + y: (affectedGroup.y + r.y) - minY, + cages: r.cages.map(c => ({ + ...c, + // Cage coordinates remain relative to rack + x: c.x, + y: c.y + })) + }))); + } + // Case 2: Multiple cages in same rack → split into new racks + else if (componentRacks[0].cages.length > 1) { + const newCages: Cage[] = []; + for (let j = 0; j < absoluteCagePositions.length; j++) { + newCages.push({ + ...absoluteCagePositions[j].cage, + id: j + 1, + x: absoluteCagePositions[j].x - minX, + y: absoluteCagePositions[j].y - minY + }); + } + newRacks.push({ + ...absoluteCagePositions[0].rack, + itemId: getNextDefaultRackId([...finalGroups]) as DefaultRackId, + x: 0, + y: 0, + cages: newCages + }); + } + // Case 3: Single cage → keep as is + else { + newRacks.push({ + ...componentRacks[0], + x: (affectedGroup.x + componentRacks[0].x) - minX, + y: (affectedGroup.y + componentRacks[0].y) - minY, + cages: componentRacks[0].cages.map(c => ({ + ...c, + x: c.x, + y: c.y + })) + }); + } + + finalGroups.push({ + ...affectedGroup, + groupId: nextGroupId, + x: minX, + y: minY, + racks: newRacks + }); + + // Update next group ID + const nextIdNum = parseInt(nextGroupId.split('-')[2]) + 1; + nextGroupId = `rack-group-${nextIdNum}` as GroupId; + } + } else { + // No splitting needed, keep the modified group + finalGroups.push(affectedGroup); + } + + // 7. Update state + setNextAvailGroup(nextGroupId); + setLocalRoom(prev => ({ + ...prev, + rackGroups: finalGroups + })); + setReloadRoom({ + ...localRoom, + rackGroups: finalGroups + }); + }; + + const delCage = (cage: Cage, rack: Rack, rackGroup: RackGroup, action: DeleteActions) => { + if(action !== 'group'){ + disconnectCage(cage.cageNum); + setUnitLocs((prevLocs) => ({ + ...prevLocs, + [roomItemToString(rack.type.type)]: prevLocs[roomItemToString(rack.type.type)].filter((loc) => loc.num !== cage.cageNum) + })); + return; + }else{ + setLocalRoom((prevRoom) => { + let updatedRoom: Room; + // remove rack group + updatedRoom = { + ...prevRoom, + rackGroups: prevRoom.rackGroups.filter((group) => + group.groupId !== rackGroup.groupId + ) + } + // remove all cages in group + setUnitLocs((prevLocs) => { + const cageIds = new Set(); + rackGroup.racks.forEach((r) => { + r.cages.forEach((c) => { + cageIds.add(c.cageNum); + }) + }) + const filteredLocs: UnitLocations = {}; + Object.keys(prevLocs).forEach(key => { + filteredLocs[key] = prevLocs[key].filter((loc: LocationCoords) => !cageIds.has(loc.num)); + }) + return filteredLocs; + }); + setNextAvailGroup(rackGroup.groupId); + setReloadRoom(updatedRoom); // reload room with new groups + return updatedRoom; + }); + } + fixGroupIds(); + } + + const changeRack = async (newType: {value: string, label: string}): Promise => { + let {value: oldRackId, label: oldRackType} = newType; + const rackId = parseInt(oldRackId); + const rackType = oldRackType.split(' - ')[1]; + const optConfig: SelectRowsOptions = { + schemaName: "cageui", + queryName: "rack_types", + filterArray: [ + Filter.create('name', rackType, Filter.Types.EQUAL) + ] + } + const rackTypeData = await labkeyActionSelectWithPromise(optConfig); + + if(rackTypeData.rowCount === 1){ + const newRackType = rackTypeData.rows[0]; + const isDefault = isRackDefault(newRackType.type); + if(isDefault){ + newRackType.type = defaultTypeToRackType(newRackType.type); + } + setLocalRoom(prevRoom => { + const {rackGroup, rack, cage} = findCageInGroup((selectedObj as Cage).cageNum as CageNumber, prevRoom.rackGroups); + const roomToUpdate: Room = { + ...prevRoom, + rackGroups: prevRoom.rackGroups.map(group => + group.groupId === rackGroup.groupId + ? { + ...group, + racks: group.racks.map((r) => r.itemId === rack.itemId ? { + ...r, + itemId: `rack-${rackId.toString()}` as RealRackId, + type: { + ...r.type, + rowid: newRackType.rowid, + name: newRackType.name, + type: newRackType.type, + isDefault: isDefault // not stored in db + } + } : r) + } + : group + ) + } + return roomToUpdate; + }) + return `rack-${rackId}`; + }else{ + console.log("Error fetching rack type"); + return null; + } + } + + const changeCageNum = (numBefore: number, numAfter: number) => { + const selectedCage = (selectedObj as Cage).cageNum; + const objType = parseRoomItemType(selectedCage); + + if(unitLocs[objType].find(prevLoc => parseRoomItemNum(prevLoc.num) === numAfter)){ + console.log("Please add a different cage num that doesnt exist in the current room"); + return; + } + + setLocalRoom((prevRoom) => { + // Find the clicked rack + let currRack: Rack; + prevRoom.rackGroups.forEach(group => { + if(currRack) return; + currRack = findSelectObjRack(group.racks, selectedCage) + }); + + if (!currRack) return prevRoom; // If the clicked rack is not found, return the previous state + + // Update the local room by updating the cage numbers in the clicked rack + const updatedLocalRoom: Room = { + ...prevRoom, + rackGroups: prevRoom.rackGroups.map((group: RackGroup): RackGroup => ({ + ...group, + racks: group.racks.map((rack: Rack): Rack => + rack.cages.some((cage: Cage) => cage.cageNum === selectedCage) // Check if any cage matches selectedObj + ? { + ...rack, + cages: rack.cages.map((cage: Cage): Cage => + cage.cageNum === selectedCage // Only update the cage with matching cageNum + ? { ...cage, cageNum: `${roomItemToString(rack.type.type)}-${numAfter}` } as Cage + : cage + ) + } + : rack + ) + })) + }; + + + + // Now update the unit locations using the rackType from currRack + setUnitLocs(prevUnitLocations => ({ + ...prevUnitLocations, + // Access the correct unit location array based on clickedRack's rackType + [roomItemToString(currRack.type.type)]: prevUnitLocations[roomItemToString(currRack.type.type)].map(cage => + convertCageNumToNum(cage.num) === numBefore ? { ...cage, num: `${roomItemToString(currRack.type.type)}-${numAfter}` } : cage + ) + })); + + return updatedLocalRoom; // Return the updated local room state + }); + + setCageNumChange({before: numBefore, after: numAfter}); + } + + const getNextCageNum = (rackType: RackStringType) => { + const cages = unitLocs[rackType]; + + // If no cages exist for this rackType, return 1 as the first available cage number + if (!cages || cages.length === 0) { + return 1; + } + + // Get the maximum cageNum in the current array of cages + const maxCageNum = Math.max(...cages.map(cage => convertCageNumToNum(cage.num))); + + // Return the next available cageNum (max + 1) + return maxCageNum + 1; + }; + + const clearGrid = () => { + setLocalRoom(prevState => { + return { + ...prevState, + rackGroups: [], + objects: [] + } + }); + } + + const saveRoom = async (): Promise => { + const commands: Command[] = []; + const dataToSave: LayoutHistoryData[] = []; + // if template parse room name, 1 is the new name, 0 is the old name + const roomName = localRoom.name; + const oldRoomName: string = ActionURL.getParameter('room'); + const savingTemplate: boolean = roomName.toLowerCase().includes("template"); + const newEndDate = new Date(); + const newStartDate = new Date(); + let rowsToUpdate; + let templateHistory: LayoutHistoryData[]; + + //check if template already had layout in history and needs to be updated + if(prevRoom && prevRoom.room.name !== roomName){ + const prevRoomConfig: SelectRowsOptions = { + schemaName: 'cageui', + queryName: 'layout_history', + columns: ['object_type', 'rack_group', 'rack', 'cage', 'x_coord', 'y_coord', 'rowid'], + filterArray: [ + Filter.create('room', oldRoomName, Filter.Types.EQUALS), + Filter.create('end_date', null, Filter.Types.ISBLANK) + ] + } + const prevTemplate = await labkeyActionSelectWithPromise(prevRoomConfig) + + if(prevTemplate.rowCount > 0){ + templateHistory = prevTemplate.rows; + } + } + + await Promise.all(localRoom.rackGroups.map(async (group) => { + const groupId = parseLongId(group.groupId); + await Promise.all(group.racks.map(async (rack) => { + const newRackId = rack.type.isDefault ? parseLongId(rack.itemId) : parseRoomItemNum(rack.itemId); + let rackRowId; + if (!rack.type.isDefault) { + const rackConfig = { + schemaName: 'cageui', + queryName: 'racks', + column: 'rowid', + filterArray: [ + Filter.create('rackid', newRackId, Filter.Types.EQUALS), + Filter.create('rack_type', rack.type.rowid, Filter.Types.EQUALS), + ] + }; + const rackResult = await labkeyActionSelectDistinctWithPromise(rackConfig); + if (rackResult.values.length === 1) { + rackRowId = rackResult.values[0]; + } + } + rack.cages.forEach((cage) => { + const cageLocData = unitLocs[roomItemToString(rack.type.type)].find((loc) => loc.num === cage.cageNum); + let extraContext: ExtraContext = {}; + + // set up cage extra context + if (cage.extraContext) { + extraContext.cage = {}; + extraContext.cage.context = cage.extraContext; + } + // set up rack extra context + if (rack.type.isDefault) { + extraContext.rack = {}; + extraContext.rack.rackId = newRackId; // room id is for rebuilding a layout for default racks + } + + const newCageData: LayoutHistoryData = { + cage: zeroPadName(parseRoomItemNum(cage.cageNum), 4), // converts number into string with leading 0s + end_date: null, + extra_context: Object.keys(extraContext).length !== 0 ? JSON.stringify(extraContext) : null, + rack: rack.type.isDefault ? null : rackRowId, + object_type: rack.type.isDefault ? rackTypeToDefaultType(rack.type.type) : rack.type.type, + rack_group: groupId, + room: roomName, + start_date: newStartDate, + x_coord: cageLocData.cellX, + y_coord: cageLocData.cellY + }; + dataToSave.push(newCageData); + }); + })); + })); + + localRoom.objects.forEach((roomObj) => { + const newObjData: LayoutHistoryData = { + cage: null, + end_date: null, + rack: null, + object_type: roomObj.type, + rack_group: null, + extra_context: roomObj.extraContext ? JSON.stringify(roomObj.extraContext) : null, + room: roomName, + start_date: newStartDate, + x_coord: roomObj.x, + y_coord: roomObj.y + } + dataToSave.push(newObjData); + }); + + // get data for updating layout history end dates + if(prevRoom && prevRoom.data.length !== 0){ + // Dont update template room when saving as a new room + // if prev room is template and saving as same room, update + // if prev room is template and saving as different room, don't update + // if prev room is room and saving as room, update + if((prevRoom.room.name === oldRoomName) || savingTemplate){ + rowsToUpdate = prevRoom.data.reduce((acc, row) => { + return [ + ...acc, + { + ...row, + end_date: newEndDate + } + ]; + }, []); + } + }else if(templateHistory && templateHistory?.length !== 0) {// ensure template history exists and has data. + rowsToUpdate = templateHistory.reduce((acc, row) => { + return [ + ...acc, + { + ...row, + end_date: newEndDate + } + ]; + }, []); + } + + // update template name + if(savingTemplate && oldRoomName !== roomName){ + commands.push({ + command: "updateChangingKeys" as CommandType, + schemaName: "ehr_lookups", + queryName: "rooms", + extraContext: {keyField: 'room'}, + rows: [{ + oldKeys: {room: oldRoomName}, + values: {room: roomName} + }] + }); + } + + // update prevRoom rows to include end date marking end of layout for that time frame + if(rowsToUpdate){ + commands.push({ + command: "update", + schemaName: "cageui", + queryName: "layout_history", + rows: rowsToUpdate + }); + } + + // insert rows to layout history for cages and room objects, no end date + if(dataToSave.length !== 0){ + commands.push({ + command: "insert", + schemaName: "cageui", + queryName: "layout_history", + rows: dataToSave + }); + + } + + // update room border and scale + const layoutToSave = [{ + room: roomName, + layout_scale: localRoom.layoutData.scale, + border_width: localRoom.layoutData.borderWidth, + border_height: localRoom.layoutData.borderHeight + }]; + commands.push({ + command: "update", + schemaName: "ehr_lookups", + queryName: "rooms", + rows: layoutToSave + }); + + const result = await labkeySaveRows(commands); + // Determine success or failure + if(result.errorCount === 0){ + return { status: 'Success', roomName: roomName}; + }else{ + return { + status: 'Failure', + roomName: roomName, + reason: ["failures"] // Return an array of failure reasons + }; + } + } + + useEffect(() => { + if(!prevRoom) { + return; + } + + + if(prevRoom.room.rackGroups.length !== 0){ + setNextAvailGroup(getNextGroupId(prevRoom.room.rackGroups)); + } + + if(prevRoom.locs) { + setUnitLocs(prevRoom.locs); + } + setLocalRoom(prevRoom.room); + setRoom(prevRoom.room); + setIsLoading(false); + }, [prevRoom]); + + return ( + + {!isLoading ? children : null} + + ); +} \ No newline at end of file diff --git a/CageUI/src/client/entryPoints.js b/CageUI/src/client/entryPoints.js new file mode 100644 index 000000000..cedfdd6fb --- /dev/null +++ b/CageUI/src/client/entryPoints.js @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +module.exports = { + apps: [ + { + name: "editLayout", + title: "Room Layout Editor", + permissionClasses: [ + 'org.labkey.api.security.permissions.ReadPermission', + 'org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission', + ], + path: './src/client/pages/layoutEditor' + }] +}; diff --git a/CageUI/src/client/pages/layoutEditor/LayoutEditor.tsx b/CageUI/src/client/pages/layoutEditor/LayoutEditor.tsx new file mode 100644 index 000000000..b32d938ee --- /dev/null +++ b/CageUI/src/client/pages/layoutEditor/LayoutEditor.tsx @@ -0,0 +1,221 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, useEffect, useState } from 'react'; +import { RoomHeader } from '../../components/layoutEditor/RoomHeader'; +import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; +import '../../cageui.scss'; +import { ActionURL, Filter, UserWithPermissions } from '@labkey/api'; +import { LayoutData, LayoutHistoryData, PrevRoom, Room, UnitLocations } from '../../types/typings'; +import { LayoutEditorContextProvider } from '../../context/LayoutEditorContextManager'; +import Editor from '../../components/layoutEditor/Editor'; +import { labkeyActionSelectWithPromise, labkeyGetUserPermissions } from '../../api/labkeyActions'; +import { RoomSizeSelector, SelectorOptions } from '../../components/layoutEditor/RoomSizeSelector'; +import { ConfirmationPopup } from '../../components/ConfirmationPopup'; +import { buildNewLocalRoom, buildNewLocs, isTemplateCreator } from '../../utils/LayoutEditorHelpers'; +import {Security} from '@labkey/api'; +import { GetUserPermissionsResponse } from '@labkey/api/dist/labkey/security/Permission'; +import { SVG_HEIGHT, SVG_WIDTH } from '../../utils/constants'; + +export const LayoutEditor: FC = () => { + const roomName = ActionURL.getParameter("room"); + const [prevRoomData, setPrevRoomData] = useState({name: null, cagingData: [], layoutData: null}); + const [prevRoom, setPrevRoom] = useState<{room: Room, locs: UnitLocations, data: LayoutHistoryData[], isTemplate: boolean}>(null); + const [selectedSize, setSelectedSize] = useState(null); + const [showSelectionPopup, setShowSelectionPopup] = useState(true); + const [errorPopup, setErrorPopup] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [userProfile, setUserProfile] = useState(null); + const [access, setAccess] = useState(false); + + + // These are the options users can choose to select a room size. Scale adjusts the zoom level of the layout + const roomSizeOptions = [ + { + id: 1, + scale: 1.0, + title: "Small", + description: "Small room size fitting up to 10x5 cages" + }, + { + id: 2, + scale: 0.8, + title: "Medium", + description: "Medium room size fitting up to 12x6 cages" + }, + { + id: 3, + scale: 0.4, + title: "Large", + description: "Large room size fitting up to 17x8 cages" + } + ]; + + useEffect(() => { + const userProfile = labkeyGetUserPermissions(); + userProfile.then((profile: GetUserPermissionsResponse) => { + if(profile.user){ + setUserProfile(profile); + // if the user is a template creator grant access + if(!(!roomName && !isTemplateCreator(profile))){ + setAccess(true); + }else if(roomName) { + setAccess(true); + } + } + }).catch((e) => { + console.error(e); + }) + }, []); + + // Loads prev room into memory if it exists + useEffect(() => { + if(!roomName){ + setIsLoading(false); + return; + } + const prevRoomConfig: SelectRowsOptions = { + schemaName: 'cageui', + queryName: 'layout_history', + columns: ['object_type', 'rack_group', 'rack', 'cage', 'x_coord', 'y_coord', 'rowid', 'extra_context'], + filterArray: [ + Filter.create('room', roomName, Filter.Types.EQUALS), + Filter.create('end_date', null, Filter.Types.ISBLANK) + ], + sort: "-rack_group", + } + + const prevRoomBorderConfig: SelectRowsOptions = { + schemaName: 'ehr_lookups', + queryName: 'rooms', + columns: ['layout_scale', 'border_width','border_height'], + filterArray: [ + Filter.create('room', roomName, Filter.Types.EQUALS) + ] + } + const prevRoomPromise = labkeyActionSelectWithPromise(prevRoomConfig); + const prevRoomBorderPromise = labkeyActionSelectWithPromise(prevRoomBorderConfig); + + Promise.all([prevRoomPromise, prevRoomBorderPromise]).then(([prevRoomResult, borderResult]) => { + let borderObj: LayoutData; + if(borderResult.rowCount === 0){ + throw new Error(`No room found in EHR for ${roomName}`); + }else{ + + borderObj = { + scale: borderResult.rows[0].layout_scale || 1, + borderHeight: borderResult.rows[0].border_height || SVG_HEIGHT - 1, + borderWidth: borderResult.rows[0].border_width || SVG_WIDTH - 1, + }; + setSelectedSize(roomSizeOptions.find(opt => opt.scale === borderObj.scale)); + setShowSelectionPopup(false); + } + setPrevRoomData({name: roomName, cagingData: prevRoomResult.rows || [], layoutData: borderObj}); + }).catch(err => { + setErrorPopup(err.toString()); + }); + }, []); + + // Converts data from ehr into layout editor objects for use seen in typings + useEffect(() => { + if(prevRoomData.name !== null){ + let newLocalRoom: Room; + let isTemplate: boolean; + let newUnitLocs: UnitLocations; + + if(prevRoomData.cagingData.length !== 0){ + newUnitLocs = buildNewLocs(prevRoomData.cagingData); + buildNewLocalRoom(prevRoomData).then((d) => { + if(d){ + isTemplate = d.name.includes("template"); + newLocalRoom = d; + newLocalRoom = { + ...d, + name: isTemplate ? 'new-layout' : d.name + } + + newLocalRoom = { + ...newLocalRoom, + layoutData: { + scale: prevRoomData.layoutData.scale, + borderWidth: prevRoomData.layoutData.borderWidth, + borderHeight: prevRoomData.layoutData.borderHeight + } + } + setPrevRoom({room: newLocalRoom, locs: newUnitLocs, data: prevRoomData.cagingData, isTemplate: isTemplate}); + setIsLoading(false); + } + }); + }else{ + isTemplate = prevRoomData.name.includes("template"); + // Don't use template name instead treat the template as an empty room with objects already placed + newLocalRoom = {name: isTemplate ? 'new-layout' : prevRoomData.name, rackGroups: [], objects: [], layoutData: null} + //Always set layoutData if a prev room exists, its been set before and will go to the current border in rooms + newLocalRoom = { + ...newLocalRoom, + layoutData: { + scale: prevRoomData.layoutData.scale, + borderWidth: prevRoomData.layoutData.borderWidth, + borderHeight: prevRoomData.layoutData.borderHeight + } + } + setPrevRoom({room: newLocalRoom, locs: null, data: prevRoomData.cagingData, isTemplate: isTemplate}); + setIsLoading(false); + } + } + }, [prevRoomData]); + + return (!isLoading && userProfile && access) ? ( + + +
+ {selectedSize && + + } + {showSelectionPopup && + setShowSelectionPopup(false)} + onSelect={(selectedOption) => setSelectedSize(selectedOption)} + /> + } + {errorPopup && + setErrorPopup(null)} + /> + } +
+ } + /> + ) :
+

Error loading page. This could be due to a number of issues

+
    +
  • Insufficient permissions
  • +
  • Slow load times
  • +
  • New bugs on our end. If you believe this might be the issue please submit a ticket.
  • +
+
; +} \ No newline at end of file diff --git a/CageUI/src/client/pages/layoutEditor/app.tsx b/CageUI/src/client/pages/layoutEditor/app.tsx new file mode 100644 index 000000000..ecc9b9590 --- /dev/null +++ b/CageUI/src/client/pages/layoutEditor/app.tsx @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { LayoutEditor } from './LayoutEditor'; + + +// Need to wait for container element to be available in labkey wrapper before render +window.addEventListener('DOMContentLoaded', (event) => { + + createRoot(document.getElementById("app")).render( + + ); +}); \ No newline at end of file diff --git a/CageUI/src/client/pages/layoutEditor/dev.tsx b/CageUI/src/client/pages/layoutEditor/dev.tsx new file mode 100644 index 000000000..668b72e1e --- /dev/null +++ b/CageUI/src/client/pages/layoutEditor/dev.tsx @@ -0,0 +1,38 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { AppContainer } from 'react-hot-loader'; +import { createRoot } from 'react-dom/client'; +import { LayoutEditor } from './LayoutEditor'; + +const render = () => { + createRoot(document.getElementById("app")).render( + + + + ); +}; + +declare const module: any; + +if (module.hot) { + module.hot.accept(); +} + +render(); \ No newline at end of file diff --git a/CageUI/src/client/types/layoutEditorContextTypes.ts b/CageUI/src/client/types/layoutEditorContextTypes.ts new file mode 100644 index 000000000..525afb432 --- /dev/null +++ b/CageUI/src/client/types/layoutEditorContextTypes.ts @@ -0,0 +1,74 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { ReactNode } from 'react'; +import { + Cage, CageNumber, DefaultRackId, + LayoutHistoryData, LocationCoords, + Rack, + RackGroup, + RackStringType, RealRackId, + Room, + RoomItemClass, + RoomItemType, + UnitLocations +} from './typings'; +import { + DeleteActions, + LayoutSaveResult, + RackActions, + SelectedObj +} from './layoutEditorTypes'; +import * as d3 from 'd3'; +import { GetUserPermissionsResponse } from '@labkey/api/dist/labkey/security/Permission'; + +export interface LayoutContextProps { + children: ReactNode; + prevRoom: {room: Room, locs: UnitLocations, data: LayoutHistoryData[], isTemplate: boolean}; + user: GetUserPermissionsResponse; +} + +export interface LayoutContextType { + room: Room; + setRoom: React.Dispatch>; + saveRoom: () => Promise; + layoutSvg: d3.Selection; + setLayoutSvg: React.Dispatch>>; + unitLocs: UnitLocations; + localRoom: Room; + setLocalRoom: React.Dispatch>; + addRoomItem: (itemType: RoomItemType, itemId: string, x: number, y: number, scale: number) => Promise; + changeCageNum: (numBefore: number, numAfter: number) => void; + cageNumChange: {before: number, after: number}; + moveObjLocation: (itemId: string, type: RoomItemClass, x: number, y: number, k: number) => void; + doRackAction: (action: RackActions, targetId: string, dragId: string, targetCageNum: CageNumber, dragCageNum: CageNumber, newGroup: d3.Selection) => void; + getNextCageNum: (rackType: RackStringType) => number; + selectedObj: SelectedObj; + setSelectedObj: React.Dispatch>; + delCage: (cage: Cage, rack: Rack, rackGroup: RackGroup, action: DeleteActions) => void; + delObject: (objId: string) => void; + scale: number; + setScale: React.Dispatch>; + changeRack: (newType: {value: string, label: string}) => Promise; + clearGrid: () => void; + user: GetUserPermissionsResponse; + getAdjCages: (cage: Cage, cageLoc: LocationCoords) => LocationCoords[]; + reloadRoom: Room, + setReloadRoom: React.Dispatch>, +} \ No newline at end of file diff --git a/CageUI/src/client/types/layoutEditorTypes.ts b/CageUI/src/client/types/layoutEditorTypes.ts new file mode 100644 index 000000000..855475b0a --- /dev/null +++ b/CageUI/src/client/types/layoutEditorTypes.ts @@ -0,0 +1,91 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { Cage, CageNumber, Rack, RackGroup, Room, RoomItem, RoomItemType } from './typings'; +import * as d3 from 'd3'; +import * as React from 'react'; +import { MutableRefObject } from 'react'; +import {RoomItemClass} from './typings'; + +export type GateContext = {room: string, roomId: number}; // extra context for Gate Object, describes target room and status + +export type RackActions = 'merge' | 'connect' | 'cancel'; + +// deletion actions for state management, cage = delete cage from rack, rack = delete rack from rack group, group = delete entire rack group +export type DeleteActions = 'cage' | 'rack' | 'group'; + +// For mapping cells to room items +export type CellKey = `${number},${number}`; + +export type SelectedObj = RoomItem | RackGroup | Cage; + + +export interface ExtraContext { + cage?: {[key: string]: any}; + rack?: {[key: string]: any}; +} + +export interface LayoutSaveResult { + status: string; + roomName: string // redirect room + reason?: any; +} + +export interface OffsetProps { + clientX: number; + clientY: number; + layoutSvg: d3.Selection; +} + +export interface PendingRoomUpdate { + draggedShape: any; + updateItemType: RoomItemType; + cellX: number; + cellY: number; + itemId: string; +} + +export interface CageActionProps { + setSelectedObj: React.Dispatch>; + setCtxMenuStyle: React.Dispatch>; +} + +export interface LayoutDragProps { + gridSize: number; + moveItem: (itemId: string, type: RoomItemClass, x: number, y: number, k: number) => void; +} + +export interface MergeProps { + contextMenuRef: MutableRefObject; + targetRack: Rack; + targetCageNum: CageNumber; + draggedRack: Rack; + dragCageNum: CageNumber; + targetRackGroup: RackGroup; + dragRackGroup: RackGroup; + doRackAction: (action: RackActions, targetId: string, dragId: string, targetCageNum: CageNumber, dragCageNum: CageNumber, newGroup: d3.Selection) => void; + layoutDrag: d3.DragBehavior; + cageActionProps: CageActionProps; +} + +export interface StartDragProps { + setSelectedObj: React.Dispatch>; + localRoomRef: MutableRefObject; +} + + diff --git a/CageUI/src/client/types/typings.ts b/CageUI/src/client/types/typings.ts new file mode 100644 index 000000000..5f1ef0a79 --- /dev/null +++ b/CageUI/src/client/types/typings.ts @@ -0,0 +1,253 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { ExtraContext, GateContext } from './layoutEditorTypes'; + +/* + **IMPORTANT** + + DefaultRackTypes, RackTypes, and RoomObjectTypes must be defined in this file. + Type checking and enum looping is used throughout the cage UI project so it's important to have them written here. + + RackTypes, DefaultRackTypes and RoomObjectTypes enums equal the value in the ehr_lookups table cageui_item_types. + */ + + +// used in ehr to determine if the rack is default (doesn't have a rackid) +export enum DefaultRackTypes { + DefaultCage = 0, + DefaultPen = 1, + DefaultTempCage = 2, + DefaultPlayCage = 3 +} + + +// Enum of rack types, map to string first, used to store types as integers in the db +export enum RackTypes { + Cage = 4, + Pen = 5, + TempCage = 6, + PlayCage = 7 +} + +// Like rack types enum but for room objects, start at 100 to give buffer room for rack types +export enum RoomObjectTypes { + RoomDivider = 100, + Drain = 101, + Door = 102, + GateClosed = 103, + GateOpen = 104, +} + +// value in the cage modifications table in EHR +export enum ModTypes { + StandardFloor='sf', + MeshFloor='mf', + MeshFloorX2='dmf', + NoFloor='nf', + SolidDivider='sd', + PCDivider='pcd', // protected contact + VCDivider='vcd', // visual contact + PrivacyDivider='pd', + NoDivider='nd', + CTunnel='ct', + Extension='ex', + PlayCage='pc', +} + +export enum ModLocations { + Left, + Right, + Top, + Bottom, + Direct +} + +export enum CageDirection { + Left, + Right, + Top, + Bottom, +} + +export type DirectionCategory = "vertical" | "horizontal" | "direct"; + +export type RackStringType = string & { __brand: "RackStringType" }; +export type DefaultRackStringType = string & { __brand: "DefaultRackStringType" }; +export type RoomObjectStringType = string & { __brand: "RoomObjectStringType" }; +export type DefaultRackId = `default-rack-${number}`; +export type RealRackId = `rack-${number}`; + +export type GroupId = `rack-group-${number}` +export type CageNumber = `${RackStringType}-${number}` + +export type RoomItemStringType = RackStringType | RoomObjectStringType | DefaultRackStringType; + +export type RoomItemType = RackTypes | RoomObjectTypes | DefaultRackTypes; + +export type RoomItem = Rack | RoomObject; +//client side to determine which object type is currently selected +export type SelectionType = 'rack' | 'cage' | 'obj' | 'rackGroup'; + +// Classification of the objects, caging is for racks/cages/rack groups, roomObj is for things placed in the room not applied to caging +export type RoomItemClass = 'caging' | 'roomObj'; + +/* svgIds is an array of ids to apply the style to. + Each id is a string to the following these rules to match the id in the svg file. + 1. The first string before an optional '-' is always the first id or only id to find. The location id always follows this first string. + 2. ids following each "-" is the id of the next child to grab of the previous svg id. + Examples: + ["extension"]: This will find the id with the name "extension" in the svg file with the appropriate location id. (extension-locationId) + ["cTunnel-left", "cTunnel-circle"]: For each string, first find the id (cTunnel-locationId) then find the id "left" or "circle". + */ +export type Modification = { + name: string; + svgIds: { + [key in ModLocations]?: string[]; + }; + styles: { + property: string; + value: string; + }[] +} + +export type ModRecord = Record; + +export type CageModification = { + id: number; // id for duplicate mods in the same location, imagine 2 cages on one side of a pen + mod: CageModType; // Use mod in the Modifications constant to get styles for mod +} + +export type CageModType = ModTypes | 'newMod'; + +export interface Cage { + id: number; // Id local to rack + selectionType: SelectionType; + cageNum: CageNumber; // Id local to room + x: number; // x coordinate of cage in rack coordinate plane + y: number; // y coordinate of cage in rack coordinate plane + size: number; // length in cells of cage square of svg image + extraContext?: {[key: string]: any}; // extra context if needed for cage +} + +export type CageWithMods = Cage & Partial; + +export interface CageModifications { + mods: { + [ModLocations.Top]: CageModification[] + [ModLocations.Bottom]: CageModification[]; + [ModLocations.Left]: CageModification[]; + [ModLocations.Right]: CageModification[]; + [ModLocations.Direct]: CageModification[]; + }; + isDirty: boolean; // determines if modifications have been changed +} + +export interface Room { + name: string; + rackGroups: RackGroup[]; + objects: RoomObject[]; + layoutData: LayoutData; +} + +export interface LayoutData { + scale: number; + borderWidth: number; + borderHeight: number; + status?: boolean; +} + +export interface LayoutHistoryData { + object_type: RoomObjectTypes | RackTypes | DefaultRackTypes; + extra_context: string | null; + rack_group: number | null; + rack: number | null; + cage: string | null; + x_coord: number; + y_coord: number; + start_date?: Date; + end_date?: Date; + room?: string; + rowid?: number; +} + +export interface PrevRoom { + cagingData: LayoutHistoryData[]; + layoutData: LayoutData; + modData?: ModData[]; + name: string | null; +} + +export interface ModData { + rowid: number; + room: string; + rack: number; // rowid of rack + cage: number; + location: ModLocations; + locationId: number; + modification: ModTypes; +} + +export interface RackGroup { + racks: Rack[]; + selectionType: SelectionType; + groupId: GroupId; + x: number; // x coords relative to group of connected racks + y: number; // y coords relative to group of connected racks + scale: number // scale relative to group of connected racks +} + +export interface Rack { + itemId: DefaultRackId | RealRackId; // rack id + rowid?: number; // if real rack this will have a rowid + selectionType: SelectionType; + type: UnitType; + cages: CageWithMods[]; + x: number; // x coordinate of rack relative to the rack group + y: number; // y coordinate of rack relative to the rack group + isActive?: boolean; // Determines if rack is "in use or active" + extraContext?: {[key: string]: any}; +} + +export interface RoomObject { + itemId: string; // object id + selectionType: SelectionType; + type: RoomObjectTypes + x: number; + y: number; + scale: number; + extraContext?: GateContext; // add any additional context def here +} + +export interface UnitType { + rowid: number; + name: string; // naming convention is 'type-manufacturer-sqft' + type: RackTypes; // this cannot be a default, defaults are stored in layout history but not included in code. use isDefault to check if a rack is default outside of getting data + isDefault: boolean; +} + +export interface LocationCoords { + num: CageNumber; + cellX: number; + cellY: number; +} + +// keys here are the string for rack type, +export type UnitLocations = { + [key in RackStringType]: LocationCoords[]; +}; diff --git a/CageUI/src/client/utils/LayoutEditorHelpers.ts b/CageUI/src/client/utils/LayoutEditorHelpers.ts new file mode 100644 index 000000000..4ef2078ba --- /dev/null +++ b/CageUI/src/client/utils/LayoutEditorHelpers.ts @@ -0,0 +1,1185 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +// Layout Editor Helpers +import * as d3 from 'd3'; +import { zoomTransform } from 'd3'; +import { + defaultTypeToRackType, + getSvgSize, + getTypeClassFromElement, + parseLongId, + parseRoomItemNum, + parseRoomItemType, + roomItemToString +} from './helpers'; +import { + Cage, + CageDirection, + CageModification, + CageModifications, + CageNumber, + CageWithMods, + DefaultRackId, + DefaultRackTypes, + GroupId, + LayoutHistoryData, + LocationCoords, + ModLocations, + PrevRoom, + Rack, + RackGroup, + RackStringType, + RackTypes, + RealRackId, + Room, + RoomItemClass, + RoomItemStringType, + RoomItemType, + RoomObject, + RoomObjectTypes, + UnitLocations, + UnitType +} from '../types/typings'; +import { + ExtraContext, + LayoutDragProps, + MergeProps, + OffsetProps, + RackActions, + SelectedObj, + StartDragProps +} from '../types/layoutEditorTypes'; +import { labkeyActionSelectWithPromise } from '../api/labkeyActions'; +import * as React from 'react'; +import { MutableRefObject } from 'react'; +import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; +import { Filter, Security } from '@labkey/api'; +import { GetUserPermissionsResponse } from '@labkey/api/dist/labkey/security/Permission'; +import { CELL_SIZE } from './constants'; + + +export const isTemplateCreator = (user: GetUserPermissionsResponse) => { + return Security.hasEffectivePermission(user.container.effectivePermissions, 'org.labkey.cageui.security.permissions.CageUITemplateCreatorPermission'); +} + +export const isRoomCreator = (user: GetUserPermissionsResponse) => { + return Security.hasEffectivePermission(user.container.effectivePermissions, 'org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission'); +} + +export const isRoomModifier = (user: GetUserPermissionsResponse) => { + return Security.hasEffectivePermission(user.container.effectivePermissions, 'org.labkey.cageui.security.permissions.CageUIRoomModifierPermission'); +} + + +export const getTranslation = (transform) => { + // Regex to extract the translate(x, y) values + const translate = transform.match(/translate\(([^)]+)\)/); + if (translate) { + const [x, y] = translate[1].split(',').map(Number); + return { x, y }; + } + return { x: 0, y: 0 }; // Default to (0, 0) if no translation is found +} + +export const convertCageNumToNum = (num: CageNumber) => { + const parts = num.split('-'); + const cageNum = parts[1]; + return parseInt(cageNum); +} + +export const createEmptyUnitLoc = (): UnitLocations => { + return ( + Object.fromEntries( + Object.values(RackTypes) + .filter((value) => typeof value === "number") // Filter out the numeric values from enum + .map((rackType) => [ + roomItemToString(rackType as RackTypes), + [] as LocationCoords[], + ]) + ) as UnitLocations + ); +} + +export const parseWrapperId = (input: string): RoomItemStringType => { + const regex = /^[a-zA-Z]+/; // matches "x_template_wrapper" + + const match = input.match(regex); + if (match) { // if a match return whatever x is (any string of chars) + return match[0] as RoomItemStringType; + } + return; +} + + +export const drawGrid = (layoutSvg: d3.Selection, updateGridProps) => { + const transform = zoomTransform(layoutSvg.node()); + layoutSvg.select('.grid').remove(); + layoutSvg.append("g") + .attr("class", "grid") + .attr("id", "layout-grid") + .attr("width", updateGridProps.width) + .attr('height', updateGridProps.height) + .attr('transform', `translate(0,0) scale(${transform.k})`); + updateGrid(zoomTransform(layoutSvg.node()), updateGridProps.width, updateGridProps.height, updateGridProps.gridSize); // Draw grid with the initial view +} + +export const updateGrid = (transform, width, height, gridSize) => { + const g = d3.select("g.grid"); + g.selectAll(".cell").remove(); // Clear existing grid + + // Calculate grid bounds (starting and ending points) based on transform + const xMin = Math.floor(-transform.x / transform.k / gridSize) * gridSize; + const yMin = Math.floor(-transform.y / transform.k / gridSize) * gridSize; + const xMax = Math.ceil((width - transform.x) / transform.k / gridSize) * gridSize; + const yMax = Math.ceil((height - transform.y) / transform.k / gridSize) * gridSize; + + // Draw the grid within the current visible area + for (let x = xMin; x <= xMax; x += gridSize) { + for (let y = yMin; y <= yMax; y += gridSize) { + g.append("rect") + .attr("x", x) + .attr("y", y) + .attr("class", "cell") + .attr("width", gridSize) + .attr("height", gridSize) + .attr("fill", "none") + .attr("stroke", "lightgray"); + } + } +} + +// Confirmation popup for merging two racks, built using d3 svg manipulation. +function showConfirmationPopup(): Promise { + return new Promise((resolve) => { + + const overlay = d3.select('body').append('div') + .attr('class', 'overlay') + .style('position', 'fixed') + .style('top', '0') + .style('left', '0') + .style('width', '100vw') + .style('height', '100vh') + .style('background', 'rgba(0, 0, 0, 0.5)') + .style('z-index', '999') // Ensure it's above other content + .style('display', 'block'); // Initially hidden + + // Create a simple popup + const popup = overlay.append('div') + .attr('class', 'popup') + .style('position', 'absolute') + .style('top', '50%') + .style('left', '50%') + .style('transform', 'translate(-50%, -50%)') + .style('background', 'white') + .style('padding', '20px') + .style('border', '1px solid black'); + + popup.append('p') + .text('What action would you like to perform?'); + + // Merge button + popup.append('button') + .text('Merge Cages') + .on('click', () => { + overlay.remove(); + resolve('merge'); + }); + + // Connect button + popup.append('button') + .text('Connect Racks') + .on('click', () => { + overlay.remove(); + resolve('connect'); + }); + + // Cancel button + popup.append('button') + .text('Cancel') + .on('click', () => { + overlay.remove(); + resolve('cancel'); + }); + }); +} + +export function showLayoutEditorConfirmation(msg: string) { + return new Promise((resolve) => { + + const overlay = d3.select('body').append('div') + .attr('class', 'overlay') + .style('position', 'fixed') + .style('top', '0') + .style('left', '0') + .style('width', '100vw') + .style('height', '100vh') + .style('background', 'rgba(0, 0, 0, 0.5)') + .style('z-index', '999') // Ensure it's above other content + .style('display', 'block'); // Initially hidden + + // Create a simple popup + const popup = overlay.append('div') + .attr('class', 'popup') + .style('position', 'absolute') + .style('top', '50%') + .style('left', '50%') + .style('transform', 'translate(-50%, -50%)') + .style('background', 'white') + .style('padding', '20px') + .style('border', '1px solid black'); + + popup.append('p') + .text(msg); + + popup.append('button') + .text('Yes') + .on('click', () => { + overlay.remove(); + resolve(true); + }); + + popup.append('button') + .text('No') + .on('click', () => { + overlay.remove(); + resolve(false); + }); + }); +} + +// Confirmation popup for merging two racks +export function showLayoutEditorError(errorMsg: string) { + return new Promise((resolve) => { + // Create a simple popup + const overlay = d3.select('body').append('div') + .attr('class', 'overlay') + .style('position', 'fixed') + .style('top', '0') + .style('left', '0') + .style('width', '100vw') + .style('height', '100vh') + .style('background', 'rgba(0, 0, 0, 0.5)') + .style('z-index', '999') // Ensure it's above other content + .style('display', 'block'); // Initially hidden + + const popup = overlay.append('div') + .attr('class', 'popup') + .style('position', 'absolute') + .style('top', '50%') + .style('left', '50%') + .style('transform', 'translate(-50%, -50%)') + .style('background', 'white') + .style('padding', '20px') + .style('border', '1px solid black'); + + popup.append('p') + .text(errorMsg); + + // Cancel button + popup.append('button') + .text('Ok') + .on('click', () => { + overlay.remove(); + resolve(true); + }); + }); +} + +// Function to help merge/connect racks together by resetting groups to local coords +function resetNodeTranslationsWithZoom(targetNode, draggedNode, layoutSvg) { + + // Get the zoom transform of the layout SVG + const layoutTransform = d3.zoomTransform(layoutSvg.node()); + + // Get the translations of the two nodes (current positions) + const {x: translateX1, y: translateY1} = getTranslation(targetNode.getAttribute('transform')); + const {x: translateX2, y: translateY2} = getTranslation(draggedNode.getAttribute('transform')); + + // Calculate the dynamic distance between the two nodes before resetting + // Remove the zoom scale from the distance to keep it zoom-independent + const distanceX = (translateX2 - translateX1) / layoutTransform.k; // Correct the distance using zoom scale + const distanceY = (translateY2 - translateY1) / layoutTransform.k; // Correct Y in case there's any Y translation + + // Reset the first node to (0, 0) in the new group + targetNode.setAttribute("transform", `translate(0, 0)`); + + // Set the second node to be exactly at the dynamic distance relative to the first node + draggedNode.setAttribute("transform", `translate(${distanceX}, ${distanceY})`); +} + +export function setupEditCageEvent( + cageGroupElement: SVGGElement, + setSelectedObj: React.Dispatch>, + localRoomRef: MutableRefObject, + setCtxMenuStyle?: React.Dispatch>, + rackTypeString?: RackStringType +): () => void { + const handleContextMenu = (event: MouseEvent)=> { + event.preventDefault(); + const localRoom = localRoomRef.current; + let tempObj: SelectedObj; + const element = event.currentTarget as SVGGElement; + + //set selected object to either room object or cage + if(d3.select(element).classed('room-obj')){ + tempObj = localRoom.objects.find((obj) => obj.itemId === element.id); + }else{ + const cageGroupElement = element.closest(`[id^=${rackTypeString}-]`) as SVGGElement | null; + localRoom.rackGroups.forEach((g) => { + g.racks.forEach((r) => { + if(tempObj){ + return; + } + tempObj = r.cages.find(c => c.cageNum === cageGroupElement.id); + }) + }) + } + setSelectedObj(tempObj); + if(setCtxMenuStyle){ + setCtxMenuStyle((prevState) => ({ + ...prevState, + display: 'block', + left: `${event.pageX - 10}px`, + top: `${event.pageY - 10}px`, + })); + } + + }; + + // Attach context menu to the lowest level group for that cFage. + cageGroupElement.style.pointerEvents = 'bounding-box'; + cageGroupElement.addEventListener('contextmenu', handleContextMenu); + + return () => { + cageGroupElement.removeEventListener('contextmenu', handleContextMenu); + }; +} + +/* + Helper function to either connect racks or merge cages + + One can think of a merge as at the cage level and connections are at a rack level. + Even though cages can not be added/removed from racks in reality, for layout building purposes they can. + + */ +export async function mergeRacks(props: MergeProps) { + const { + contextMenuRef, + targetRack, + draggedRack, + targetRackGroup, + dragRackGroup, + doRackAction, + layoutDrag, + cageActionProps, + dragCageNum, + targetCageNum + } = props; + if(!d3.select('.popup').empty()) return false; + const action: RackActions = await showConfirmationPopup(); + const layoutSvg: d3.Selection = d3.select('[id=layout-svg]'); + + function isConnected(selectionNode){ + return !!selectionNode.closest(`[id*='group']`); + } + + // Make sure cages don't have the wrong styles, give merged cages a grouped class + function resetElementProperties(element: SVGGElement, shapeType, action) { + if(action === 'merge'){ + element.setAttribute('class',`grouped-${shapeType}`); + element.setAttribute('style', ""); + } + setupEditCageEvent(element, cageActionProps.setSelectedObj, contextMenuRef, cageActionProps.setCtxMenuStyle, shapeType); + } + + // add starting x and y for each group to then increment its local subgroup coords by. + // Example: 2 nodes, 0,0 and 120,0 start at 0,0 add 120,0 + // second 2 nodes, 0,0 and 120,0 start at 240,0 add 0,0 and 120,0. etc + function processChildNodes(element: SVGGElement, mergedGroup, action: RackActions) { + const {x: startX, y: startY} = getTranslation(element.getAttribute('transform')) + d3.select(element).selectAll(':scope > g').each(function () { + const targetShape = d3.select(this); + let shapeType: RackStringType; + if(action === 'merge'){ + shapeType = parseRoomItemType(targetShape.attr('id')) as RackStringType; + }else{ + shapeType = getTypeClassFromElement(targetShape.node()) as RackStringType; + } + const {x: localX, y: localY} = getTranslation(targetShape.attr('transform')); + const newX = startX + localX; + const newY = startY + localY; + targetShape.attr('transform', `translate(${newX},${newY})`); + + // When connecting merged groups that have been connected before make sure to reset each cage but + // add the rack shape instead of cage shape + const mergedChildren = d3.select(this).selectAll(':scope > g'); + if(!mergedChildren.empty()){ + mergedChildren.each(function () { + resetElementProperties(this as SVGGElement, shapeType, action); + }) + }else{ + resetElementProperties(this as SVGGElement, shapeType, action); + } + mergedGroup.node().appendChild(this); + }); + } + + function processShape(shape, action, mergedGroup) { + if(action === 'merge'){ + processChildNodes(shape, mergedGroup, action); + }else{ + if(shape.getAttribute('class').includes('rack-group')){ + processChildNodes(shape, mergedGroup, action); + }else{// When connecting racks for the first time + // this iteration is for connecting a merged rack, have to reset each cage in the rack but add the rack shape not the cage shape + d3.select(shape).selectAll(':scope > g').each(function () { + resetElementProperties(this as SVGGElement, getTypeClassFromElement(shape), action); + }); + + mergedGroup.node().appendChild(shape); + } + } + } + if (action !== 'cancel') { + let targetRackShape: d3.Selection + = layoutSvg.select(`[id^=${targetRack.itemId}]`); + + let draggedRackShape: d3.Selection + = layoutSvg.select(`[id^=${draggedRack.itemId}]`); + + let newGroup: d3.Selection; + + // Clone the target and dragged shapes before using + let clonedTargetShape = targetRackShape.node().cloneNode(true) as Element; + let clonedDraggedShape = draggedRackShape.node().cloneNode(true) as Element; + + let targetRackId = clonedTargetShape.id; + let draggedRackId = clonedDraggedShape.id; + + if(action === 'merge'){ + if(isConnected(draggedRackShape.node()) || isConnected(targetRackShape.node())){ + await showLayoutEditorError("Invalid Configuration: Please do not merge connected racks"); + return; + } + if(draggedRack.type.type !== targetRack.type.type){ + await showLayoutEditorError("Invalid Configuration: Please do not merge cages of different types, use connection instead"); + return; + } + newGroup = layoutSvg.append('g') + .attr('class', targetRackShape.attr('class')) + .attr('id', targetRackShape.attr('id')); + //Reset translates to new local group + resetNodeTranslationsWithZoom(clonedTargetShape, clonedDraggedShape, layoutSvg); + + processShape(clonedTargetShape, action, newGroup); + processShape(clonedDraggedShape, action, newGroup); + + // Copy any inline styles from the targetShape to the merged group + const styleAttr = targetRackShape.attr('style'); + if (styleAttr) { + newGroup.attr('style', styleAttr); + } + } + else{ // action = connect + + // If connecting already connected groups these will be populated + const connectedTargetGroupShape: d3.Selection + = layoutSvg.select(`#${targetRackGroup.groupId}`); + + const connectedDragGroupShape: d3.Selection + = layoutSvg.select(`#${dragRackGroup.groupId}`); + + if(!connectedTargetGroupShape.empty()){ + clonedTargetShape = connectedTargetGroupShape.node().cloneNode(true) as Element; + targetRackShape = connectedTargetGroupShape; + } + + if(!connectedDragGroupShape.empty()){ + clonedDraggedShape = connectedDragGroupShape.node().cloneNode(true) as Element; + draggedRackShape = connectedDragGroupShape; + } + + newGroup = layoutSvg.append('g') + .attr('class', 'draggable rack-group') + .attr('id', targetRackGroup.groupId); + + resetNodeTranslationsWithZoom(clonedTargetShape, clonedDraggedShape, layoutSvg); + + d3.select(clonedTargetShape).classed('draggable', false); + d3.select(clonedDraggedShape).classed('draggable', false); + + processShape(clonedTargetShape, action, newGroup); + processShape(clonedDraggedShape,action, newGroup); + } + + // Copy the transform attribute from the targetShape to the merged group + const transformAttr = targetRackShape.attr('transform'); + if (transformAttr) { + newGroup.attr('transform', transformAttr); + } + + //Attach data from target to new shape + const targetData = targetRackShape.datum() as { x: number; y: number }; + if(targetData) { + newGroup.data([{x: targetData.x, y: targetData.y}]) + } + + newGroup.call(layoutDrag); + + doRackAction(action,targetRackId, draggedRackId, targetCageNum, dragCageNum, newGroup); + + // Remove the original shapes from the DOM + targetRackShape.remove(); + draggedRackShape.remove(); + + return true; + }else{ + return false; + } +} + + +export const getAdjDirection = ( + draggedX, + draggedY, + targetX, + targetY, + draggedWidth, + draggedHeight, + targetWidth, + targetHeight): CageDirection => { + + // Check right side of A to left side of B + if (draggedX + draggedWidth === targetX) { + return CageDirection.Right; + } + + // Check left side of A to right side of B + if (draggedX === targetX + targetWidth) { + return CageDirection.Left; + } + + // Check bottom side of A to top side of B + if (draggedY + draggedHeight === targetY) { + return CageDirection.Bottom; + } + + // Check top side of A to bottom side of B + if (draggedY === targetY + targetHeight) { + return CageDirection.Top; + } +} + +// This checks the adjacency of two racks to determine if they can be merged +export function checkAdjacent(targetCage: LocationCoords, draggedCage: LocationCoords, draggedSize: number, targetSize: number) { + + const targetX = targetCage.cellX; + const targetY = targetCage.cellY; + const draggedX = draggedCage.cellX; + const draggedY = draggedCage.cellY; + + // Calculate widths and heights in pixels + const draggedWidth = draggedSize * CELL_SIZE; + const draggedHeight = draggedSize * CELL_SIZE; + const targetWidth = targetSize * CELL_SIZE; + const targetHeight = targetSize * CELL_SIZE; + + // Calculate corners of the dragged square + const draggedCorners = [ + { x: draggedX, y: draggedY }, // Top-left + { x: draggedX + draggedWidth, y: draggedY }, // Top-right + { x: draggedX, y: draggedY + draggedHeight }, // Bottom-left + { x: draggedX + draggedWidth, y: draggedY + draggedHeight }, // Bottom-right + ]; + + // Calculate corners of the target square + const targetCorners = [ + { x: targetX, y: targetY }, // Top-left + { x: targetX + targetWidth, y: targetY }, // Top-right + { x: targetX, y: targetY + targetHeight }, // Bottom-left + { x: targetX + targetWidth, y: targetY + targetHeight }, // Bottom-right + ]; + + /* True if valid bounds exist. In short this fixes the issue with the corner checking where corners + themselves count as adjacent with no sides touching. + + */ + const checkBounds = (corner) => { + let valid = false; + + if(corner === 0){ // top left corner match of drag cage + if(draggedCorners[corner].x === targetCorners[3].x && draggedCorners[corner].y === targetCorners[3].y){ + valid = true; + } + + }else if(corner === 1){ // top right corner match of drag cage + if(draggedCorners[corner].x === targetCorners[2].x && draggedCorners[corner].y === targetCorners[2].y){ + valid = true; + } + }else if(corner === 2){ // bottom left corner match of drag cage + if(draggedCorners[corner].x === targetCorners[1].x && draggedCorners[corner].y === targetCorners[1].y){ + valid = true; + } + }else if(corner === 3){ // bottom right corner match of drag cage + if(draggedCorners[corner].x === targetCorners[0].x && draggedCorners[corner].y === targetCorners[0].y){ + valid = true; + } + } + + return valid; + } + + // Check if any corner of the dragged square matches any corner of the target square with a matching side. + for (let i = 0; i < draggedCorners.length; i++) { + for (let j = 0; j < targetCorners.length; j++) { + if (draggedCorners[i].x === targetCorners[j].x && draggedCorners[i].y === targetCorners[j].y) { + if(checkBounds(i)){ + continue; + } + const direction = getAdjDirection(draggedX, draggedY, targetX, targetY, draggedWidth, draggedHeight, targetWidth, targetHeight); + + // Determine the direction of adjacency based on the matching corner + if (draggedCorners[i].x === draggedX && draggedCorners[i].y === draggedY) { + return {isAdjacent: true, direction: direction}; + } else if (draggedCorners[i].x === draggedX + draggedWidth && draggedCorners[i].y === draggedY) { + return {isAdjacent: true, direction: direction}; + } else if (draggedCorners[i].x === draggedX && draggedCorners[i].y === draggedY + draggedHeight) { + return {isAdjacent: true, direction: direction}; + } else if (draggedCorners[i].x === draggedX + draggedWidth && draggedCorners[i].y === draggedY + draggedHeight) { + return {isAdjacent: true, direction: direction}; + } + } + } + } + + return {isAdjacent: false, direction: "0"}; +} + +//Offset for the top left corner of the layout, without doing this objects will randomly jump when dragging and placing +export const getLayoutOffset = (props: OffsetProps) => { + const {layoutSvg, clientX, clientY} = props; + const svgRect = (layoutSvg.node() as SVGRectElement).getBoundingClientRect(); + const x = clientX - svgRect.left; + const y = clientY - svgRect.top; + return {x: x, y: y}; +} + +export const getTargetRect =(x, y, gridSize, transform) => { + + // Adjust the coordinates based on the current zoom and pan transform + const adjustedX = transform.invertX(x); + const adjustedY = transform.invertY(y); + + // Calculate the column and row index based on the adjusted grid size + const col = Math.floor(adjustedX / gridSize); + const row = Math.floor(adjustedY / gridSize); + // Return the top-left corner coordinates of the rectangle + return { + x: col * gridSize, + y: row * gridSize, + }; +} + +// Layout Drag Helpers +export function createStartDragInLayout(startDragProps: StartDragProps) { + return( + function startDragInLayout(event) { + const {setSelectedObj, localRoomRef} = startDragProps; + const localRoom = localRoomRef.current; + + const id = d3.select(this).attr('id'); + let foundObj: SelectedObj = localRoom.objects.find(obj => obj.itemId === id); + if(foundObj){ + setSelectedObj(foundObj); + }else{ + localRoom.rackGroups.forEach((group) => { + if(foundObj) return; + if(group.groupId === id){ + foundObj = group; + return; + } + foundObj = group.racks.find((rack) => rack.itemId === id) + }) + if(foundObj){ + setSelectedObj(foundObj); + } + } + + d3.select(this).raise().classed('active', true); + + } + ); +} + +export function createDragInLayout() { + return( + function dragInLayout(event) { + const layoutSvg: d3.Selection = d3.select('#layout-svg'); + const element = d3.select(this); + const transform = d3.zoomTransform(layoutSvg.node()); + const scale = transform.k; + + element.attr('transform', `translate(${event.x},${event.y}) scale(${scale})`); + } + ) +} + +export function createEndDragInLayout(props: LayoutDragProps) { + return ( + function endDragInLayout(event) { + const { + gridSize, + moveItem + } = props; + const shape = d3.select(this); + shape.classed('active', false); + const layoutSvg: d3.Selection = d3.select('[id=layout-svg]'); + + const transform = d3.zoomTransform(layoutSvg.node()); + const [pointerX,pointerY] = d3.pointer(event, layoutSvg.node()); // mouse position with respect to layout svg + const {x,y} = getLayoutOffset({ + clientX: pointerX, + clientY: pointerY, + layoutSvg: layoutSvg}); + + const targetCell = getTargetRect(pointerX, pointerY, gridSize, transform); + if (targetCell) { + const cellX = targetCell.x; + const cellY = targetCell.y; + const shapeType: RoomItemClass = shape.classed('room-obj') ? 'roomObj' : 'caging'; + placeAndScaleGroup(shape, cellX, cellY, transform); + // make sure border template is below all other shapes on the layout + if(shape.attr('id') === 'layout-border'){ + shape.lower(); + } + moveItem(shape.attr('id'),shapeType, cellX, cellY, transform.k); + } + } + ); +} + +export const placeAndScaleGroup = (group, x, y, transform) => { + // Scale the group to match the grid size relative to the current zoom level + const scale = transform.k; // Scale inversely to zoom + + // Adjust x and y for transform + const newX = transform.applyX(x); + const newY = transform.applyY(y); + // Apply the transform (translate to snap to the grid, and scale) + group.attr("transform", `translate(${newX}, ${newY}) scale(${scale})`) + .data([{x: x, y: y}]); // keep data x and y because these are pre transform coords +} + +export const areCagesInSameRack = (rack: Rack, cage1: LocationCoords, cage2: LocationCoords) => { + if (!rack.cages || !Array.isArray(rack.cages)) { + return false; + } + + const nums = rack.cages.map(item => item.cageNum); + return nums.includes(cage1.num) && nums.includes(cage2.num); +} + + +// input is the enum number for rack, default rack, or room obj type. Return true if it is in Rack or Default rack types +export const isRackEnum = (itemType: RoomItemType): itemType is RackTypes | DefaultRackTypes => { + return itemType in RackTypes || itemType in DefaultRackTypes; +}; + +export const isRackDefault = (itemType: RoomItemType): itemType is DefaultRackTypes => { + return itemType in DefaultRackTypes; +}; + +// finds a cage by cageNum in group of racks if it exists +export const findSelectObjRack = (racks: Rack[], obj: string): Rack => { + return racks.find(rack => { + return rack.cages.find((cage) => cage.cageNum === obj) + }); +} + +// finds a rack in room/groups of racks if it exists and return the rack and rack group it is apart of +export const findRackInGroup = (targetId: string, groups: RackGroup[]): {rack: Rack, rackGroup: RackGroup} | undefined => { + for (const group of groups) { + const targetRack = group.racks.find(rack => rack.itemId === targetId); + if (targetRack) { + return { rack: targetRack, rackGroup: group }; + } + } + return undefined; +}; + + +// finds a cage in room/groups of racks if it exists and return the rack, rack group and cage state +export const findCageInGroup = (targetId: CageNumber, groups: RackGroup[]): {cage: Cage, rack: Rack, rackGroup: RackGroup} | undefined => { + for (const group of groups) { + for (const rack of group.racks) { + const targetCage = rack.cages.find(cage => cage.cageNum === targetId); + if (targetCage) { + return { cage: targetCage, rack: rack, rackGroup: group }; + } + } + } + return undefined; +}; + +// FUNCTIONS FOR LOADING IN PREVIOUS DATA + +export const buildNewLocs = (prevRoomData: LayoutHistoryData[]): UnitLocations => { + // Empty Unit locations object + const newUnitLocs: UnitLocations = createEmptyUnitLoc(); + + prevRoomData.forEach(roomItem => { + if(!isRackEnum(roomItem.object_type)) return; // ignore room objects here + let rackType: RoomItemStringType; + if(isRackDefault(roomItem.object_type)){ + rackType = roomItemToString(defaultTypeToRackType(roomItem.object_type)); + }else{ + rackType = roomItemToString(roomItem.object_type); + } + newUnitLocs[rackType].push({ + num: `${rackType}-${parseInt(roomItem.cage)}` as CageNumber, + cellX: roomItem.x_coord, + cellY: roomItem.y_coord + }); + }) + return newUnitLocs; +} + +export const buildNewLocalRoom = async (prevRoom: PrevRoom): Promise => { + const newLocalRoom: Room = { + name: prevRoom.name, + rackGroups: [], + objects: [], + layoutData: null + }; + let roomObjNum = 1; + const loadMods: boolean = !!prevRoom.modData; + //check if a group exists for the groupId, if it does return, else create new group for the room + const findOrAddGroup = (rackItem: LayoutHistoryData): RackGroup => { + // groupId is a single number so check if the GroupId string contains it + let rackGroup: RackGroup = newLocalRoom.rackGroups.find(group => parseLongId(group.groupId) === rackItem.rack_group) + if (!rackGroup) { + //create new rack group if it doesn't exist + rackGroup = { + groupId: `rack-group-${rackItem.rack_group}` as GroupId, + selectionType: 'rackGroup', + scale: prevRoom.layoutData.scale, + x: rackItem.x_coord, + y: rackItem.y_coord, + racks: [] + }; + newLocalRoom.rackGroups.push(rackGroup); + } + return rackGroup; + } + + //check if a rack exists for the rackId, if it does return, else create new rack for the group + const findOrAddRack = async (rackGroup: RackGroup, rackItem: LayoutHistoryData): Promise => { + const isDefault = isRackDefault(rackItem.object_type); + let rackIdNum; + let rowId; + let extraContext: ExtraContext; + let rackData; + // if rack is default, use default rack id instead + if(rackItem.extra_context){ + extraContext = JSON.parse(rackItem.extra_context); + if(extraContext?.rack?.rackId){ + rackIdNum = extraContext.rack.rackId; + } + } + if(!isDefault){ + const optConfig: SelectRowsOptions = { + schemaName: "cageui", + queryName: "racks", + filterArray: [ + Filter.create('rowid', rackItem.rack, Filter.Types.EQUALS) + ] + } + rackData = await labkeyActionSelectWithPromise(optConfig); + if(rackData.rowCount > 0){ + rackIdNum = rackData.rows[0].rackid; + rowId = rackData.rows[0].rowid; + } + + } + let rack: Rack = rackGroup.racks.find(r => parseRoomItemNum(r.itemId) === rackIdNum); + if (!rack) { + //create new rack if it doesn't exist + let type: UnitType; + let rackId: DefaultRackId | RealRackId; + let typeRowId; + const rackPrefix = isDefault ? 'default-rack' : 'rack'; + + if(!isDefault){ + typeRowId = rackData.rows[0].rack_type; + rackId = `${rackPrefix}-${rackIdNum}` as RealRackId; + }else{ + rackId = `${rackPrefix}-${rackIdNum}` as DefaultRackId; + } + + + // if default get base type, else get rack type from rack id + const optConfig = { + schemaName: "cageui", + queryName: "rack_types", + filterArray: [ + Filter.create(isDefault ? 'type' : 'rowid', isDefault ? rackItem.object_type : typeRowId, Filter.Types.EQUALS) + ] + } + + const rackTypesData = await labkeyActionSelectWithPromise(optConfig); + + type = { + rowid: typeRowId, + name: rackTypesData.rows[0].name, + type: isDefault ? defaultTypeToRackType(rackTypesData.rows[0].type) : rackTypesData.rows[0].type, + isDefault: isDefault, + }; + + rack = { + rowid: rowId, + selectionType: 'rack', + cages: [], + isActive: !isDefault, + itemId: rackId, + type: type, + x: rackItem.x_coord - rackGroup.x, // subtract group coords from layout coords to get rack coords + y: rackItem.y_coord - rackGroup.y, + extraContext: extraContext?.rack + }; + rackGroup.racks.push(rack); + } + return rack; + } + + const addCageToRack = async (rack: Rack, rackItem: LayoutHistoryData, group: RackGroup) => { + // only string for RackTypes, not DefaultRackTypes, since cageNum is used for location tracking which uses RackTypes + let cageNumType: RoomItemStringType; + let extraContext: ExtraContext; + let cageNum = parseInt(rackItem.cage); + let cageMods: CageModifications = { + mods: { + [ModLocations.Top]: [], + [ModLocations.Bottom]: [], + [ModLocations.Left]: [], + [ModLocations.Right]: [], + [ModLocations.Direct]: [] + }, + isDirty: false, + } + if(rack.type.isDefault){ + cageNumType = roomItemToString(defaultTypeToRackType(rackItem.object_type as DefaultRackTypes)); + }else{ + cageNumType = roomItemToString(rackItem.object_type); + } + if(rackItem.extra_context){ + extraContext = JSON.parse(rackItem.extra_context); + } + const svgSize = await getSvgSize(rack.type.type); + //TODO Add mods if needed here + if(loadMods && !rack.type.isDefault){ + prevRoom.modData.forEach((mod) => { + if(rack.rowid === mod.rack && cageNum === mod.cage){ + (cageMods.mods[mod.location] as CageModification[]).push({ + id: mod.locationId, + mod: mod.modification + }) + } + }) + } + + const cage: CageWithMods = { + cageNum: `${cageNumType}-${cageNum}` as CageNumber, + extraContext: extraContext?.cage, + selectionType: 'cage', + id: rack.cages.length + 1, + x: rackItem.x_coord - rack.x - group.x, // get cage coords by subtracting from both rack and group + y: rackItem.y_coord - rack.y - group.y, + size: svgSize, + mods: cageMods.mods, + isDirty: cageMods.isDirty + } + rack.cages.push(cage); + } + + const handleRackItem = async (rackItem: LayoutHistoryData) => { + const rackGroup: RackGroup = findOrAddGroup(rackItem); + const rack: Rack = await findOrAddRack(rackGroup, rackItem); + await addCageToRack(rack, rackItem, rackGroup); + } + + // generates room object state for room objects from layout history data + const generateRoomObj = (roomObjItem: LayoutHistoryData): RoomObject => { + let context; + if(roomObjItem.extra_context){ + context = JSON.parse(roomObjItem.extra_context); + } + return({ + itemId: `${roomItemToString(roomObjItem.object_type)}-${roomObjNum++}`, // update room obj num after it is used to next num + type: roomObjItem.object_type as RoomObjectTypes, + selectionType: 'obj', + x: roomObjItem.x_coord, + y: roomObjItem.y_coord, + scale: prevRoom.layoutData.scale, + extraContext: context + }); + } + + for (const roomItem of prevRoom.cagingData) { + if (isRackEnum(roomItem.object_type)) { // Room item is an enclosure for animals + await handleRackItem(roomItem); + } else { // Room item is something else in the room, ex. Door + newLocalRoom.objects.push(generateRoomObj(roomItem)); + } + } + return(newLocalRoom); +} + +// END FUNCTIONS FOR LOADING IN PREVIOUS DATA +export function updateBorderSize(borderGroup: d3.Selection, newWidth: number, newHeight: number ){ + const currentRect = d3.select('#border-rect'); + const resizeHandler = borderGroup.selectAll('#resize-handle'); + + function updateSvgBounds(newSvgWidth: number, newSvgHeight: number, svgId: string) { + // Calculate new dimensions if necessary + const resizeSvg = borderGroup.select(`#${svgId}`); + + // Update the SVG's viewBox to accommodate the new size, + 1 to add a pixel of distance between the svg and everything inside + resizeSvg.attr("viewBox", `0 0 ${newSvgWidth + 1} ${newSvgHeight + 1}`); + resizeSvg.attr("width", newSvgWidth + 1); + resizeSvg.attr("height", newSvgHeight + 1); + } + // Update rect dimensions and position + currentRect + .attr('width', newWidth) + .attr('height', newHeight); + + //update resize rect handler + resizeHandler.attr("x", newWidth - 15) + .attr("y", newHeight - 15); + + updateSvgBounds(newWidth, newHeight, 'border_template'); + updateSvgBounds(newWidth, newHeight, 'border_template_wrapper'); +} + +const createStartResizeDrag = () => { + return( + function startResizeDrag(event) { + event.sourceEvent.stopPropagation(); + const borderRect = d3.select('#border-rect'); + const layoutSvg: d3.Selection = d3.select('#layout-svg'); + + + this.startWidth = parseFloat(borderRect.attr('width')); + this.startHeight = parseFloat(borderRect.attr('height')); + + // start x and y with respect to the layout svg + const [x, y] = d3.pointer(event.sourceEvent, layoutSvg.node()); + this.startX = x; + this.startY = y; + } + ); +} + + + +const createDragResizeDrag = (gridSize: number, borderGroup: d3.Selection) => { + return( + function dragResizeDrag(event) { + const layoutSvg: d3.Selection = d3.select('#layout-svg'); + + // get x and y in relation to the layout svg + const [x, y] = d3.pointer(event.sourceEvent, layoutSvg.node()); + + // calculate delta x and y (change) with respect to grid size for snapping + const dx = Math.round((x - this.startX) / gridSize); + const dy = Math.round((y - this.startY) / gridSize); + + // calculate new height and width using previous delta and grid size for snapping + const newLockedWidth: number = this.startWidth + (dx * gridSize); + const newLockedHeight: number = this.startHeight + (dy * gridSize); + + + updateBorderSize(borderGroup, newLockedWidth, newLockedHeight) + + } + ); +} +const createEndResizeDrag = (setLocalRoom) => { + return( + function startResizeDrag(event) { + const currentRect = d3.select('#border-rect'); + setLocalRoom(prevState => ({ + ...prevState, + layoutData: { + ...prevState.layoutData, + borderWidth: parseInt(currentRect.attr('width')), + borderHeight: parseInt(currentRect.attr('height')) + } + })) + } + ); +} +// functionality to drag/resize the border. closeMenu is a function that sets state of both context menus +export const dragBorder = (closeMenu, gridSize, borderGroup, setLocalRoom) => { + let targetId: string; + return d3.drag() + .on('start', function(event) { + // store target element to prevent switching + const targetElement = d3.select(event.sourceEvent.target); + // store target id, either resize handle id or room border group id + targetId = targetElement.attr('id'); + closeMenu(); + // Drag group if group is selected, otherwise resize using the rect handlers + if (targetElement.node().tagName === 'rect') { + createStartResizeDrag().call(this, event); + } + }) + .on('drag', function(event) { + // Retrieve the stored target element + const targetElement = d3.select(`#${targetId}`) as d3.Selection; + if (targetElement.node().tagName === 'rect') { + createDragResizeDrag(gridSize, borderGroup).call(this, event); + } + }) + .on('end', function(event) { + const targetElement = d3.select(`#${targetId}`) as d3.Selection; + if (targetElement.node().tagName === 'rect') { + createEndResizeDrag(setLocalRoom).call(this, event); + } + }) +} + +export const getNextGroupId = (groups: RackGroup[]): GroupId => { + // Extract all numbers from existing groupIds + const existingNumbers = groups + .map(obj => { + const match = obj.groupId.match(/^rack-group-(\d+)$/); + return match ? parseInt(match[1], 10) : 0; + }) + .filter(num => !isNaN(num) && num > 0); // Filter out invalid numbers + + // If no valid groupIds found, start with 1 + if (existingNumbers.length === 0) { + return 'rack-group-1'; + } + + // Find the highest number and add 1 + const maxNumber = Math.max(...existingNumbers); + return `rack-group-${maxNumber + 1}`; +} \ No newline at end of file diff --git a/CageUI/src/client/utils/constants.ts b/CageUI/src/client/utils/constants.ts new file mode 100644 index 000000000..a3625df1a --- /dev/null +++ b/CageUI/src/client/utils/constants.ts @@ -0,0 +1,210 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { Modification, ModLocations, ModRecord, ModTypes } from '../types/typings'; + +export const CELL_SIZE = 30; // number of pixels of a cell for length/width +export const SVG_WIDTH = 1290; // width of the layout svg +export const SVG_HEIGHT = 810; // height of the layout svg + +//TODO finish styles +export const Modifications: ModRecord = { + [ModTypes.StandardFloor]: { + name: "Standard Floor", + svgIds: { + [ModLocations.Bottom]: ['floor'], + [ModLocations.Top]: ['ceiling'] + }, + styles: [{ + property: "stroke", + value: "black" + }] + }, + [ModTypes.MeshFloor]: { + name: "Mesh Floor", + svgIds: { + [ModLocations.Bottom]: ['floor'], + [ModLocations.Top]: ['ceiling'] + }, + styles: [ + { + property: "stroke", + value: "black" + }, + { + property: "stroke-dasharray", + value: "10" + } + ] + }, + [ModTypes.MeshFloorX2]: { + name: "Mesh Floor x2", + svgIds: { + [ModLocations.Bottom]: ['floor'], + [ModLocations.Top]: ['ceiling'] + }, + styles: [ + { + property: "stroke", + value: "black" + }, + { + property: "stroke-dasharray", + value: "10 5 10" + },{ + property: "stroke-width", + value: "2" + } + ] + }, + [ModTypes.NoFloor]: { + name: "No Floor", + svgIds: { + [ModLocations.Bottom]: ['floor'], + [ModLocations.Top]: ['ceiling'] + }, + styles: [ + { + property: "stroke", + value: "none" + } + ] + }, + [ModTypes.SolidDivider]: { + name: "Solid Divider", + svgIds: { + [ModLocations.Left]: ['left'], + [ModLocations.Right]: ['right'] + }, + styles: [{ + property: "stroke", + value: "black" + }] + }, + [ModTypes.PCDivider]: { + name: "Protected Contact Divider", + svgIds: { + [ModLocations.Left]: ['left'], + [ModLocations.Right]: ['right'] + }, + styles: [{ + property: "stroke", + value: "black" + },{ + property: "stroke-dasharray", + value: "2 5 2" + },{ + property: "stroke-width", + value: "4" + }] + }, + [ModTypes.VCDivider]: { + name: "Visual Contact Divider", + svgIds: { + [ModLocations.Left]: ['left'], + [ModLocations.Right]: ['right'] + }, + styles: [{ + property: "stroke", + value: "black" + },{ + property: "stroke-dasharray", + value: "5 10 5 10 5 10" + },{ + property: "stroke-width", + value: "4" + }] + }, + [ModTypes.PrivacyDivider]: { + name: "Privacy Divider", + svgIds: { + [ModLocations.Left]: ['left'], + [ModLocations.Right]: ['right'] + }, + styles: [{ + property: "stroke", + value: "black" + },{ + property: "stroke-dasharray", + value: "1 1 1 1 1 1" + },{ + property: "stroke-width", + value: "4" + }] + }, + [ModTypes.NoDivider]: { + name: "No Divider", + svgIds: { + [ModLocations.Left]: ['left'], + [ModLocations.Right]: ['right'] + }, + styles: [{ + property: "stroke", + value: "none" + }] + }, + [ModTypes.CTunnel]: { + name: "C-Tunnel", + svgIds: { + [ModLocations.Top]: ['cTunnel-circle', 'cTunnel-top'], + [ModLocations.Bottom]: ['cTunnel-circle', 'cTunnel-bottom'], + [ModLocations.Left]: ['cTunnel-circle', 'cTunnel-left'], + [ModLocations.Right]: ['cTunnel-circle', 'cTunnel-right'], + }, + styles: [{ + property: "stroke", + value: "black", + },{ + property: "stroke-width", + value: "1px", + } + ] + }, + [ModTypes.Extension]: { + name: "Extension", + svgIds: { + [ModLocations.Direct]: ['extension'], + }, + styles: [{ + property: "stroke", + value: "black" + },{ + property: "stroke-width", + value: "1px" + },{ + property: "fill", + value: "#FCB017" + }] + }, + [ModTypes.PlayCage]: { + name: "Play Cage", + svgIds: { + [ModLocations.Direct]: ['playCage'], + }, + styles: [{ + property: "stroke", + value: "black" + },{ + property: "stroke-width", + value: "1px" + },{ + property: "fill", + value: "#6D88C4" + }] + }, +} \ No newline at end of file diff --git a/CageUI/src/client/utils/helpers.ts b/CageUI/src/client/utils/helpers.ts new file mode 100644 index 000000000..1e22ca19c --- /dev/null +++ b/CageUI/src/client/utils/helpers.ts @@ -0,0 +1,459 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { + Cage, CageWithMods, + DefaultRackStringType, + DefaultRackTypes, + ModLocations, + Rack, + RackGroup, + RackStringType, + RackTypes, + Room, + RoomItemStringType, + RoomItemType, + RoomObjectStringType, + RoomObjectTypes +} from '../types/typings'; +import * as d3 from 'd3'; +import { zoomTransform } from 'd3'; +import { MutableRefObject } from 'react'; +import { ActionURL, Filter } from '@labkey/api'; +import { placeAndScaleGroup, setupEditCageEvent } from './LayoutEditorHelpers'; +import { SelectDistinctOptions } from '@labkey/api/dist/labkey/query/SelectDistinctRows'; +import { selectDistinctRows } from '@labkey/components'; +import { Modifications } from './constants'; + +export const zeroPadName = (num, places) => {return(String(num).padStart(places, '0'))}; + +// Changes stroke color of svg element nodes keeping the other styles. +export const changeStyleProperty = (element: Element, property: string, newValue: string): void => { + const styleAttr = element.getAttribute('style'); + if (styleAttr) { + const styles = styleAttr.split(';').map(style => style.trim()).filter(style => style !== ""); + let updated = false; + const updatedStyles = styles.map(style => { + const [prop, value] = style.split(':').map(prop => prop.trim()).filter(prop => prop !== ""); + if (prop.toLowerCase() === property.toLowerCase()) { + updated = true; + return `${property}: ${newValue}`; + } else { + return `${prop}: ${value}`; + } + }); + if (!updated) { + updatedStyles.push(`${property}: ${newValue}`); + } + const updatedStyleAttr = updatedStyles.join(';'); + element.setAttribute('style', updatedStyleAttr); + } else { + element.setAttribute('style', `${property}: ${newValue}`); + } +} + +export const getSvgSize = async (type: RackTypes) => { + const config: SelectDistinctOptions = { + schemaName: "ehr_lookups", + queryName: "cageui_item_types", + column: 'description', + filterArray: [ Filter.create('value', type, Filter.Types.EQUAL)] + } + + const res = await selectDistinctRows(config); + + if(res.values.length === 1){ + return res.values[0]; + } + + return; +} + +// matches "string-number", if a match return the number +export const parseRoomItemNum = (input: string): number => { + const regex = /\w+-(\d+)/; + + const match = input.match(regex); + if (match) { + return parseInt(match[1]); + } + return; +} + +// matches "string-number", if a match return the type/string +export const parseRoomItemType = (input: string): string => { + const regex = /^(\w+)-\d+$/; + + const match = input.match(regex); + if (match) { + return match[1]; + } + return; +} +export const extractNumbers = (input: string): number => { + return parseInt(input.replace(/\D/g, '')); +} + +export const getTypeClassFromElement = (element) => { + const classes: string[] = Array.from(element.classList); + + // Define a regex to capture the part after "type-" + const regex = /^type-(\w+)/; + + // Find the class that matches the regex and capture the relevant part + const typeClass = classes.find(cls => regex.test(cls)); + + if (typeClass) { + const match = typeClass.match(regex); + return match[1]; // Return only the captured part (after "type-") + } + + return null; +} + + +export const parseLongId = (input: string) => { + const regex = /\w+-\w+-(\d+)/; // matches "string-string-number" + + const match = input.match(regex); + if (match) { // if a match return the number + return parseInt(match[1]); + } + return; +} + +export const parseLongDefaultId = (id: string): number => { + if (!id.startsWith("default-rack-")) return 0; // Skip non-default IDs + const numberPart = id.split('-')[2]; // Extract the number part + return parseInt(numberPart, 10) || 0; // Fallback to 0 if invalid +} + +export const formatRackId= (str: string) => { + // Split the string by hyphens + try {// if the rack is default split and correctly display it + const parts = str.split('-'); + // Process each part + const formattedParts = parts.map(part => { + // Capitalize first letter and lowercase the rest (if it's a word) + if (part.length > 0) { + return convertToTitleCase(part); + } + return part; + }); + // Join with spaces + return formattedParts.join(' '); + } + catch {// if the rack is real display it like so + return `Rack ${str}`; + } + + + +} + + +export const getNextDefaultRackId = (groups: RackGroup[]): string => { + // Extract & parse only "default-rack-*" IDs + const allRackNumbers = groups + .flatMap(group => + group.racks + .map(rack => parseLongDefaultId(rack.itemId)) + .filter(num => num > 0) // Only keep valid default-rack numbers + ) + .sort((a, b) => a - b); // Sort ascending + + // Find the first missing number (starting from 1) + let expectedNumber = 1; + for (const num of allRackNumbers) { + if (num > expectedNumber) { + // Gap found! Use the missing number + return `default-rack-${expectedNumber}`; + } + expectedNumber = num + 1; + } + + // No gaps? Use the next number after the max + return `default-rack-${expectedNumber}`; +} +export const convertToTitleCase = (str: string) => { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +} + + + +export const cleanString = (name: string) => { + return name.toLowerCase().replace(/[\s-]/g, ''); +} + +const generateTypeMaps = () => { + const rackTypeToDefaultTypeMap: { [key in RackTypes]?: DefaultRackTypes } = {}; + const defaultTypeToRackTypeMap: { [key in DefaultRackTypes]?: RackTypes } = {}; + + // Iterate through the enum keys and filter out numeric ones + Object.keys(RackTypes) + .filter((key) => isNaN(Number(key))) // Filters out numeric keys + .forEach((key) => { + const rackTypeKey = RackTypes[key as keyof typeof RackTypes]; + const defaultRackTypeKey = `Default${key}` as keyof typeof DefaultRackTypes; + + // Check if the corresponding DefaultRackType key exists + if (DefaultRackTypes[defaultRackTypeKey] !== undefined) { + const defaultRackType = DefaultRackTypes[defaultRackTypeKey]; + + // Assign mappings + rackTypeToDefaultTypeMap[rackTypeKey as RackTypes] = defaultRackType; + defaultTypeToRackTypeMap[defaultRackType] = rackTypeKey as RackTypes; + } + }); + + return { rackTypeToDefaultTypeMap, defaultTypeToRackTypeMap }; +} + +// These two maps can be imported and used to convert between the string and number of the rack type enum +const { rackTypeToDefaultTypeMap, defaultTypeToRackTypeMap } = generateTypeMaps(); + +export const rackTypeToDefaultType = (type: RackTypes) => { + return rackTypeToDefaultTypeMap[type]; +} + +export const defaultTypeToRackType = (type: DefaultRackTypes) => { + return defaultTypeToRackTypeMap[type]; +} + +/* + parse a room iteam to a string + */ +export const roomItemToString = (item: RoomItemType): RoomItemStringType => { + let itemString: RoomItemStringType; + // Uppercase the first letter of the string + const rackString = RackTypes[item]; + const roomObjString = RoomObjectTypes[item]; + const defaultRackString = DefaultRackTypes[item]; + + if(rackString){ + itemString = rackString.charAt(0).toLowerCase() + rackString.slice(1) as RackStringType; + }else if (defaultRackString){ + itemString = defaultRackString.charAt(0).toLowerCase() + defaultRackString.slice(1) as DefaultRackStringType; + + }else{ + itemString = roomObjString.charAt(0).toLowerCase() + roomObjString.slice(1) as RoomObjectStringType; + } + return itemString; +} + +/* + Extract the item type from a string + */ +export const stringToRoomItem = (formattedString: RoomItemStringType): RoomItemType => { + // Uppercase the first letter of the string + const itemKey = formattedString.charAt(0).toUpperCase() + formattedString.slice(1); + + const rackItem = RackTypes[itemKey as keyof typeof RackTypes]; + const objItem = RoomObjectTypes[itemKey as keyof typeof RoomObjectTypes]; + const defaultRackItem = DefaultRackTypes[itemKey as keyof typeof DefaultRackTypes]; + + // Use the EnumType object to look up the value + return rackItem || defaultRackItem || objItem; +} + +// Adds the svgs from the saved layouts to the DOM. Mode edit is version displayed in the layout editor and view is the one in the home views. +export const addPrevRoomSvgs = (mode: 'edit' | 'view', unitsToRender: Room | RackGroup | Rack | Cage, layoutSvg: d3.Selection, setSelectedObj?, contextMenuRef?: MutableRefObject, setCtxMenuStyle?, closeMenuThenDrag?) => { + let renderType: 'room' | 'group' | 'rack' | 'cage'; + let minX; + let minY; + if((unitsToRender as Room)?.rackGroups){ + renderType = 'room'; + } else if((unitsToRender as RackGroup)?.racks){ // we are rendering a single rack group + renderType = 'group'; + let tempX = (unitsToRender as RackGroup).x; + let tempY = (unitsToRender as RackGroup).y; + (unitsToRender as RackGroup).racks.forEach(r => { + if(tempX + r.x < tempX){ + tempX = tempX + r.x; + } + if(tempY + r.y < tempY){ + tempY = tempY + r.y; + } + r.cages.forEach(c => { + if(tempX + c.x < tempX){ + tempX = tempX + c.x + } + if(tempY + c.y < tempY){ + tempY = tempY + c.y; + } + }) + }) + minX = tempX; + minY = tempY; + }else if((unitsToRender as Rack)?.cages){ // we are rendering a single rack + renderType = 'rack'; + + }else{ // we are rendering a single cage + renderType = 'cage'; + } + + const loadCageMods = (cageToLoad: CageWithMods, shape: d3.Selection) => { + + Object.entries(cageToLoad.mods).forEach(([loc,modList]) => { + const modLoc = parseInt(loc) as ModLocations; + modList.forEach((mod) => { + if(mod.mod === "newMod") return; + const modObj = Modifications[mod.mod]; + modObj.svgIds[modLoc].forEach((svgId, idx) => { + const idParts = svgId.split('-'); + let modId = `${idParts[0]}-${mod.id}`; + let currentSelection: d3.Selection = shape.select(`#${modId}`); + for (let i = 1; i < idParts.length; i++) { + if (currentSelection.empty()) return null; + currentSelection = currentSelection.select(`#${idParts[i]}`); + } + modObj.styles.forEach((style) => { + changeStyleProperty(currentSelection.node() as SVGElement, style.property, style.value) + }) + }) + }) + }) + + } + + const createRackGroup = (parentGroup, rack: Rack, isSingleRack) => { + const rackTypeString: RackStringType = roomItemToString(rack.type.type) as RackStringType; + + const rackGroup = isSingleRack ? parentGroup : parentGroup.append('g') + .attr('id', rack.itemId) + .attr('class', `rack type-${rackTypeString}`) + .attr('transform', `translate(${rack.x},${rack.y})`) + .style('pointer-events', 'bounding-box'); + + rack.cages.forEach(async (cage) => { + const cageGroup = rackGroup.append('g') + .attr('id', cage.cageNum) + .attr('transform', `translate(${cage.x},${cage.y})`); + + let unitSvg: SVGElement; + if (mode === 'edit') { + unitSvg = (d3.select(`[id=${rackTypeString}_template_wrapper]`) as d3.Selection) + .node().cloneNode(true) as SVGElement; + } else if (mode === 'view') { + await d3.svg(`${ActionURL.getContextPath()}/cageui/static/${rackTypeString}.svg`).then((d) => { + unitSvg = d.querySelector(`svg[id*=template]`); + }); + } + + + // Only needed for layout editor to attach context menus + const shape = d3.select(unitSvg); + shape.classed('draggable', false); + shape.style('pointer-events', 'none'); + + const cageGroupContext = shape.select(`#${rackTypeString}`).node() as SVGGElement; + setupEditCageEvent( cageGroupContext, setSelectedObj, contextMenuRef,setCtxMenuStyle, rackTypeString); + (shape.select('tspan').node() as SVGTSpanElement).textContent = `${parseRoomItemNum(cage.cageNum)}`; + + if(mode ==='view'){ + loadCageMods(cage, shape); + } + + cageGroup.append(() => shape.node()); + + }); + + return rackGroup; + }; + + const createGroup = (group: RackGroup) => { + const isSingleRack = group.racks.length === 1; + const parentGroup = isSingleRack + ? layoutSvg.append('g') + .attr('id', group.racks[0].itemId) + .attr('class', `draggable rack type-${roomItemToString(group.racks[0].type.type)}`) + .style('pointer-events', 'bounding-box') + : layoutSvg.append('g') + .attr('id', group.groupId) + .attr('class', 'draggable rack-group'); + + group.racks.forEach(async rack => { + // Use parent group as rackGroup if only 1 rack, otherwise create a new rack group + await createRackGroup(parentGroup, rack, isSingleRack); + }); + let groupX = renderType === 'room' ? group.x : group.racks[0].x; + let groupY = renderType === 'room' ? group.y : group.racks[0].y; + placeAndScaleGroup(parentGroup, groupX, groupY, zoomTransform(layoutSvg.node())); + if (mode === 'edit') { + parentGroup.call(closeMenuThenDrag); + } + }; + + // We are loading an entire room into the svg + if(renderType === 'room'){ + (unitsToRender as Room).rackGroups.forEach((group) => { + createGroup(group); + }); + + (unitsToRender as Room).objects.forEach(async (roomObj) => { + const roomObjGroup = layoutSvg.append('g') + .data([{x: roomObj.x, y: roomObj.y}]) + .attr('id', roomObj.itemId) + .attr('class', 'draggable room-obj') + .attr('transform', `translate(${roomObj.x}, ${roomObj.y}) scale(${mode === "edit" ? roomObj.scale : 1})`) + .style('pointer-events', 'bounding-box'); + + let objSvg: SVGElement; + + if (mode === 'edit') { + objSvg = (d3.select(`[id=${roomItemToString(roomObj.type)}_template_wrapper]`) as d3.Selection).node().cloneNode(true) as SVGElement; + } else if (mode === 'view') { + await d3.svg(`${ActionURL.getContextPath()}/cageui/static/${roomItemToString(roomObj.type)}.svg`).then((d) => { + (roomObjGroup.node() as SVGElement).appendChild(d.documentElement); + }); + return; + } + + const shape = d3.select(objSvg) + .classed('draggable', false) + .attr('pointer-events', 'none'); + + + roomObjGroup.append(() => shape.node()); + placeAndScaleGroup(roomObjGroup, roomObj.x, roomObj.y, zoomTransform(layoutSvg.node())); + setupEditCageEvent(roomObjGroup.node() as SVGGElement, setSelectedObj, contextMenuRef, setCtxMenuStyle); + roomObjGroup.call(closeMenuThenDrag); + }); + } else if(renderType === 'group'){ // we are rendering a single rack group + createGroup(unitsToRender as RackGroup); + + }else if(renderType === 'rack'){ // we are rendering a single rack + }else{ // we are rendering a single cage + const cage: CageWithMods = unitsToRender as Cage; + const cageGroup = layoutSvg.append('g') + .attr('id', cage.cageNum) + .attr('transform', `translate(0,0)`); + let unitSvg: SVGElement; + + d3.svg(`${ActionURL.getContextPath()}/cageui/static/${parseRoomItemType((unitsToRender as Cage).cageNum)}.svg`).then((d) => { + unitSvg = d.querySelector(`svg[id*=template]`); + const shape = d3.select(unitSvg); + (shape.select('tspan').node() as SVGTSpanElement).textContent = `${parseRoomItemNum((unitsToRender as Cage).cageNum)}`; + + if(mode ==='view'){ + loadCageMods(cage, shape); + } + cageGroup.append(() => shape.node()); + }); + } +}; \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/CageUIContainerListener.java b/CageUI/src/org/labkey/cageui/CageUIContainerListener.java new file mode 100644 index 000000000..06d6fd434 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/CageUIContainerListener.java @@ -0,0 +1,57 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager.ContainerListener; +import org.labkey.api.security.User; +import java.util.Collections; +import java.util.Collection; + +import java.beans.PropertyChangeEvent; + +public class CageUIContainerListener implements ContainerListener +{ + @Override + public void containerCreated(Container c, User user) + { + } + + @Override + public void containerDeleted(Container c, User user) + { + } + + @Override + public void propertyChange(PropertyChangeEvent evt) + { + } + + @Override + public void containerMoved(Container c, Container oldParent, User user) + { + } + + @NotNull @Override + public Collection canMove(Container c, Container newParent, User user) + { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/CageUIController.java b/CageUI/src/org/labkey/cageui/CageUIController.java new file mode 100644 index 000000000..5dc5e353d --- /dev/null +++ b/CageUI/src/org/labkey/cageui/CageUIController.java @@ -0,0 +1,51 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui; + +import org.labkey.api.action.SimpleViewAction; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; + +public class CageUIController extends SpringActionController +{ + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(CageUIController.class); + public static final String NAME = "cageui"; + + public CageUIController() + { + setActionResolver(_actionResolver); + } + + @RequiresPermission(ReadPermission.class) + public class BeginAction extends SimpleViewAction + { + public ModelAndView getView(Object o, BindException errors) + { + return new JspView("/org/labkey/cageui/view/hello.jsp"); + } + + public void addNavTrail(NavTree root) { } + } + +} diff --git a/CageUI/src/org/labkey/cageui/CageUIManager.java b/CageUI/src/org/labkey/cageui/CageUIManager.java new file mode 100644 index 000000000..fb0e3148f --- /dev/null +++ b/CageUI/src/org/labkey/cageui/CageUIManager.java @@ -0,0 +1,34 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui; + +public class CageUIManager +{ + private static final CageUIManager _instance = new CageUIManager(); + + private CageUIManager() + { + // prevent external construction with a private default constructor + } + + public static CageUIManager get() + { + return _instance; + } +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/CageUIModule.java b/CageUI/src/org/labkey/cageui/CageUIModule.java new file mode 100644 index 000000000..c4fac97eb --- /dev/null +++ b/CageUI/src/org/labkey/cageui/CageUIModule.java @@ -0,0 +1,125 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.ldk.ExtendedSimpleModule; +import org.labkey.api.query.DefaultSchema; +import org.labkey.api.query.QuerySchema; +import org.labkey.api.security.roles.RoleManager; +import org.labkey.api.view.WebPartFactory; +import org.labkey.api.module.Module; +import org.labkey.cageui.query.CageUIUserSchema; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; +import org.labkey.cageui.security.permissions.CageUIAnimalReviewerPermission; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; +import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; +import org.labkey.cageui.security.permissions.CageUIRoomModifierPermission; +import org.labkey.cageui.security.permissions.CageUITemplateCreatorPermission; +import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; +import org.labkey.cageui.security.permissions.CageUINotesEditorPermission; +import org.labkey.cageui.security.permissions.CageUIUserPermission; +import org.labkey.cageui.security.roles.CageUIAdminRole; +import org.labkey.cageui.security.roles.CageUIModificationEditorRole; +import org.labkey.cageui.security.roles.CageUIRoomCreatorRole; +import org.labkey.cageui.security.roles.CageUIRoomModifierRole; + + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public class CageUIModule extends ExtendedSimpleModule +{ + public static final String NAME = "CageUI"; + + @Override + public String getName() + { + return NAME; + } + + @Override + public @Nullable Double getSchemaVersion() + { + return 25.001; + } + + @Override + @NotNull + protected Collection createWebPartFactories() + { + return Collections.emptyList(); + } + + @Override + protected void init() + { + addController(CageUIController.NAME, CageUIController.class); + registerRoles(); + registerPermissions(); + } + + private void registerPermissions() { + RoleManager.registerPermission(new CageUIRoomCreatorPermission()); + RoleManager.registerPermission(new CageUIRoomModifierPermission()); + RoleManager.registerPermission(new CageUILayoutEditorAccessPermission()); + RoleManager.registerPermission(new CageUITemplateCreatorPermission()); + RoleManager.registerPermission(new CageUIAnimalEditorPermission()); + RoleManager.registerPermission(new CageUIAnimalReviewerPermission()); + RoleManager.registerPermission(new CageUIModificationEditorPermission()); + RoleManager.registerPermission(new CageUINotesEditorPermission()); + RoleManager.registerPermission(new CageUIUserPermission()); + + } + + public void registerRoles() { + RoleManager.registerRole(new CageUIAdminRole()); + RoleManager.registerRole(new CageUIRoomCreatorRole()); + RoleManager.registerRole(new CageUIRoomModifierRole()); + RoleManager.registerRole(new CageUIModificationEditorRole()); + } + + @Override + @NotNull + public Collection getSummary(Container c) + { + return Collections.emptyList(); + } + + @Override + @NotNull + public Set getSchemaNames() + { + return Collections.singleton(CageUISchema.NAME); + } + + @Override + public void registerSchemas() { + DefaultSchema.registerProvider(CageUISchema.NAME, new DefaultSchema.SchemaProvider(this) { + @Override + public QuerySchema createSchema(final DefaultSchema schema, Module module) { + return new CageUIUserSchema(schema.getUser(), schema.getContainer(), CageUISchema.getInstance().getSchema()); + } + }); + } + +} diff --git a/CageUI/src/org/labkey/cageui/CageUISchema.java b/CageUI/src/org/labkey/cageui/CageUISchema.java new file mode 100644 index 000000000..a6ff15474 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/CageUISchema.java @@ -0,0 +1,69 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +import org.labkey.api.data.TableInfo; +import org.labkey.cageui.query.CageUIUserSchema; + + +public class CageUISchema { + private static final CageUISchema _instance = new CageUISchema(); + private static Logger _log = LogManager.getLogger(CageUISchema.class); + public static final String NAME = "cageui"; + public static String DESCRIPTION = "Schema for CageUI specific data."; + public static final String TABLE_LAYOUT_HISTORY = "layout_history"; + + public static CageUISchema getInstance() + { + return _instance; + } + + private CageUISchema() + { + // private constructor to prevent instantiation from + // outside this class: this singleton should only be + // accessed via org.labkey.ehr.EHRSchema.getInstance(). + } + + public DbSchema getSchema() + { + return DbSchema.get(NAME, DbSchemaType.Module); + } + + public TableInfo getLayoutHistoryTable() + { + return getSchema().getTable(CageUIUserSchema.LAYOUT_HISTORY_TABLE); + } + + public TableInfo getRackTypesTable() + { + return getSchema().getTable(CageUIUserSchema.RACK_TYPES_TABLE); + } + + public TableInfo getRacksTable() + { + return getSchema().getTable(CageUIUserSchema.RACKS_TABLE); + } + + +} diff --git a/CageUI/src/org/labkey/cageui/query/CageUIUserSchema.java b/CageUI/src/org/labkey/cageui/query/CageUIUserSchema.java new file mode 100644 index 000000000..391b49bd9 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/query/CageUIUserSchema.java @@ -0,0 +1,103 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.query; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.security.User; +import org.labkey.cageui.CageUISchema; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; + +public class CageUIUserSchema extends SimpleUserSchema +{ + public static final String NAME = "cageui"; + public static final String LAYOUT_HISTORY_TABLE = "layout_history"; + public static final String RACK_TYPES_TABLE = "rack_types"; + public static final String RACKS_TABLE = "racks"; + + public CageUIUserSchema(User user, Container container, DbSchema dbschema) + { + super(CageUISchema.NAME, "Cage UI Tables", user, container, dbschema); + } + + public enum TableType + { + layout_history + { + @Override + public TableInfo createTable(CageUIUserSchema schema, ContainerFilter cf) + { + if (schema.getContainer().hasPermission(schema.getUser(), CageUILayoutEditorAccessPermission.class)) + { + return new LayoutHistoryTable(schema, CageUISchema.getInstance().getLayoutHistoryTable(), cf).init(); + } + + return null; + } + }, + rack_types + { + @Override + public TableInfo createTable(CageUIUserSchema schema, ContainerFilter cf) + { + return new RackTypesTable(schema, CageUISchema.getInstance().getRackTypesTable(), cf).init(); + } + }, + racks + { + @Override + public TableInfo createTable(CageUIUserSchema schema, ContainerFilter cf) + { + return new RacksTable(schema, CageUISchema.getInstance().getRacksTable(), cf).init(); + } + }; + + public abstract TableInfo createTable(CageUIUserSchema schema, ContainerFilter cf); + } + + @Override + @Nullable + public TableInfo createTable(String name, ContainerFilter cf) + { + if (name != null) + { + TableType tableType = null; + for (TableType t : TableType.values()) + { + // Make the enum name lookup case insensitive + if (t.name().equalsIgnoreCase(name)) + { + tableType = t; + break; + } + } + if (tableType != null) + { + return tableType.createTable(this, cf); + } + } + return super.createTable(name, cf); + } + + +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/query/LayoutHistoryTable.java b/CageUI/src/org/labkey/cageui/query/LayoutHistoryTable.java new file mode 100644 index 000000000..0dc175656 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/query/LayoutHistoryTable.java @@ -0,0 +1,119 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.query; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DuplicateKeyException; +import org.labkey.api.query.InvalidKeyException; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.SimpleQueryUpdateService; +import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; +import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; +import org.labkey.cageui.security.permissions.CageUITemplateCreatorPermission; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.labkey.api.query.QueryUpdateService.ConfigParameters.PreferPKOverObjectUriAsKey; + +public class LayoutHistoryTable extends SimpleUserSchema.SimpleTable +{ + public LayoutHistoryTable(CageUIUserSchema schema, TableInfo table, ContainerFilter cf) + { + super(schema, table, cf); + } + + @Override + public QueryUpdateService getUpdateService() + { + return new LayoutHistoryTable.UpdateService(this); + } + + protected class UpdateService extends SimpleQueryUpdateService + { + public UpdateService(SimpleUserSchema.SimpleTable ti) + { + super(ti, ti.getRealTable()); + } + + // This checks permission before any data modification occurs + @Override + public boolean hasPermission(@NotNull UserPrincipal user, Class perm) + { + boolean hasPermission = super.hasPermission(user, perm); + boolean isEditPerm = perm == InsertPermission.class || perm == UpdatePermission.class || perm == DeletePermission.class; + if (isEditPerm){ + return super.hasPermission(user, CageUITemplateCreatorPermission.class) + || super.hasPermission(user, CageUIRoomCreatorPermission.class) + || super.hasPermission(user, CageUILayoutEditorAccessPermission.class); + } + + return hasPermission; + } + + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, @Nullable Map extraScriptContext) throws DuplicateKeyException, QueryUpdateServiceException, SQLException + { + List> result = null; + if(hasPermission(user, CageUITemplateCreatorPermission.class) || hasPermission(user, CageUIRoomCreatorPermission.class)){ + result = super._insertRowsUsingDIB(user, container, rows, getDataIteratorContext(errors, InsertOption.INSERT, configParameters), extraScriptContext); + } + afterInsertUpdate(result == null ? 0 : result.size(), errors); + return result; + } + + @Override + public List> updateRows(User user, Container container, List> rows, List> oldKeys, + BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + List> result = null; + if(hasPermission(user, CageUILayoutEditorAccessPermission.class)){ + result = super.updateRows(user, container, rows, oldKeys, errors, configParameters, extraScriptContext); + } + afterInsertUpdate(result == null ? 0 : result.size(), errors); + return result; + } + + @Override + public List> deleteRows(User user, Container container, List> keys, @Nullable Map configParameters, @Nullable Map extraScriptContext) + throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException + { + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + return super.deleteRows(user, container, keys, configParameters, extraScriptContext); + } + return null; + } + } +} diff --git a/CageUI/src/org/labkey/cageui/query/RackTypesTable.java b/CageUI/src/org/labkey/cageui/query/RackTypesTable.java new file mode 100644 index 000000000..fbd650254 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/query/RackTypesTable.java @@ -0,0 +1,114 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.query; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DuplicateKeyException; +import org.labkey.api.query.InvalidKeyException; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.SimpleQueryUpdateService; +import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.cageui.security.permissions.CageUITemplateCreatorPermission; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RackTypesTable extends SimpleUserSchema.SimpleTable +{ + public RackTypesTable(CageUIUserSchema schema, TableInfo table, ContainerFilter cf) + { + super(schema, table, cf); + } + + @Override + public QueryUpdateService getUpdateService() + { + return new RackTypesTable.UpdateService(this); + } + + protected class UpdateService extends SimpleQueryUpdateService + { + public UpdateService(SimpleUserSchema.SimpleTable ti) + { + super(ti, ti.getRealTable()); + } + + // This checks permission before any data modification occurs + @Override + public boolean hasPermission(@NotNull UserPrincipal user, Class perm) + { + boolean hasPermission = super.hasPermission(user, perm); + boolean isEditPerm = perm == InsertPermission.class || perm == UpdatePermission.class || perm == DeletePermission.class; + + if (isEditPerm){ + return super.hasPermission(user, CageUITemplateCreatorPermission.class); // Besides normal folder permissions check for CageUILayoutEditorPermission + } + + return hasPermission; + } + + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, @Nullable Map extraScriptContext) throws DuplicateKeyException, QueryUpdateServiceException, SQLException + { + List> result = null; + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + result = super._insertRowsUsingDIB(user, container, rows, getDataIteratorContext(errors, InsertOption.INSERT, configParameters), extraScriptContext); + } + afterInsertUpdate(result == null ? 0 : result.size(), errors); + return result; + } + + @Override + public List> updateRows(User user, Container container, List> rows, List> oldKeys, + BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + List> result = null; + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + result = super.updateRows(user, container, rows, oldKeys, errors, configParameters, extraScriptContext); + } + afterInsertUpdate(result == null ? 0 : result.size(), errors); + return result; + } + + @Override + public List> deleteRows(User user, Container container, List> keys, @Nullable Map configParameters, @Nullable Map extraScriptContext) + throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException + { + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + return super.deleteRows(user, container, keys, configParameters, extraScriptContext); + } + return null; + } + } +} diff --git a/CageUI/src/org/labkey/cageui/query/RacksTable.java b/CageUI/src/org/labkey/cageui/query/RacksTable.java new file mode 100644 index 000000000..eafb9f7fa --- /dev/null +++ b/CageUI/src/org/labkey/cageui/query/RacksTable.java @@ -0,0 +1,121 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.query; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DuplicateKeyException; +import org.labkey.api.query.InvalidKeyException; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.RuntimeValidationException; +import org.labkey.api.query.SimpleQueryUpdateService; +import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; +import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; +import org.labkey.cageui.security.permissions.CageUITemplateCreatorPermission; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RacksTable extends SimpleUserSchema.SimpleTable +{ + public RacksTable(CageUIUserSchema schema, TableInfo table, ContainerFilter cf) + { + super(schema, table, cf); + } + + @Override + public QueryUpdateService getUpdateService() + { + return new RacksTable.UpdateService(this); + } + + protected class UpdateService extends SimpleQueryUpdateService + { + public UpdateService(SimpleUserSchema.SimpleTable ti) + { + super(ti, ti.getRealTable()); + } + + // This checks permission before any data modification occurs + @Override + public boolean hasPermission(@NotNull UserPrincipal user, Class perm) + { + boolean hasPermission = super.hasPermission(user, perm); + boolean isEditPerm = perm == InsertPermission.class || perm == UpdatePermission.class || perm == DeletePermission.class; + + if (isEditPerm){ + return super.hasPermission(user, CageUITemplateCreatorPermission.class); // Besides normal folder permissions check for CageUILayoutEditorPermission + } + + return hasPermission; + } + + + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, @Nullable Map extraScriptContext) throws DuplicateKeyException, QueryUpdateServiceException, SQLException + { + List> result = null; + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + result = super._insertRowsUsingDIB(user, container, rows, getDataIteratorContext(errors, InsertOption.INSERT, configParameters), extraScriptContext); + } + afterInsertUpdate(result == null ? 0 : result.size(), errors); + return result; + } + + @Override + public List> updateRows(User user, Container container, List> rows, List> oldKeys, + BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + List> result = null; + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + result = super.updateRows(user, container, rows, oldKeys, errors, configParameters, extraScriptContext); + } + afterInsertUpdate(result == null ? 0 : result.size(), errors); + return result; + } + + @Override + public List> deleteRows(User user, Container container, List> keys, @Nullable Map configParameters, @Nullable Map extraScriptContext) + throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException + { + if(hasPermission(user, CageUITemplateCreatorPermission.class)){ + return super.deleteRows(user, container, keys, configParameters, extraScriptContext); + } + return null; + } + } +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalEditorPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalEditorPermission.java new file mode 100644 index 000000000..48b4ab1dd --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalEditorPermission.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIAnimalEditorPermission extends AbstractPermission +{ + public CageUIAnimalEditorPermission() + { + super("Cage UI Animal Editor", + "This permission allows the user to add/move animals between locations"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalReviewerPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalReviewerPermission.java new file mode 100644 index 000000000..d6d19186d --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAnimalReviewerPermission.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIAnimalReviewerPermission extends AbstractPermission +{ + public CageUIAnimalReviewerPermission() + { + super("Cage UI Animal Reviewer", + "This permission allows the user to approve animal moves"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUILayoutEditorAccessPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUILayoutEditorAccessPermission.java new file mode 100644 index 000000000..7a4b990ce --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUILayoutEditorAccessPermission.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUILayoutEditorAccessPermission extends AbstractPermission +{ + public CageUILayoutEditorAccessPermission() + { + super("Cage UI Layout Editor", + "This permission allows the user to visit the layout editor page"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIModificationEditorPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIModificationEditorPermission.java new file mode 100644 index 000000000..9a8c3da2f --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIModificationEditorPermission.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIModificationEditorPermission extends AbstractPermission +{ + public CageUIModificationEditorPermission() + { + super("Cage UI Modification Editor", + "This permission allows the user to create/update/delete cage modifications"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUINotesEditorPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUINotesEditorPermission.java new file mode 100644 index 000000000..0a528b2ab --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUINotesEditorPermission.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUINotesEditorPermission extends AbstractPermission +{ + public CageUINotesEditorPermission() + { + super("Cage UI Notes Editor", + "This permission allows the user to create/update/delete animal notes"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomCreatorPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomCreatorPermission.java new file mode 100644 index 000000000..3dd1420c5 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomCreatorPermission.java @@ -0,0 +1,32 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + + +public class CageUIRoomCreatorPermission extends AbstractPermission +{ + public CageUIRoomCreatorPermission() + { + super("Cage UI Layout Editor Room Creator", + "This permission allows the user to load template rooms and save as real rooms"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomModifierPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomModifierPermission.java new file mode 100644 index 000000000..36826e2b3 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIRoomModifierPermission.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIRoomModifierPermission extends AbstractPermission +{ + public CageUIRoomModifierPermission() + { + super("Cage UI Layout Editor Room Modifier", + "This permission allows the user to load and modify non template rooms in the layout editor."); + } +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUITemplateCreatorPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUITemplateCreatorPermission.java new file mode 100644 index 000000000..e3fad2a8a --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUITemplateCreatorPermission.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUITemplateCreatorPermission extends AbstractPermission +{ + public CageUITemplateCreatorPermission() + { + super("Cage UI Layout Editor Template Creator", + "This permission allows the user to access the full room creation process and make templates/rooms"); + } +} diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIUserPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIUserPermission.java new file mode 100644 index 000000000..1199130a2 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIUserPermission.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIUserPermission extends AbstractPermission +{ + public CageUIUserPermission() + { + super("Cage UI User", + "This permission allows the user to access the cage UI"); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/roles/CageUIAdminRole.java b/CageUI/src/org/labkey/cageui/security/roles/CageUIAdminRole.java new file mode 100644 index 000000000..ed3c162b4 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/roles/CageUIAdminRole.java @@ -0,0 +1,54 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.roles; + +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.AbstractRole; +import org.labkey.cageui.CageUIModule; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; +import org.labkey.cageui.security.permissions.CageUIAnimalReviewerPermission; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; +import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; +import org.labkey.cageui.security.permissions.CageUINotesEditorPermission; +import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; +import org.labkey.cageui.security.permissions.CageUIRoomModifierPermission; +import org.labkey.cageui.security.permissions.CageUITemplateCreatorPermission; + +public class CageUIAdminRole extends AbstractRole +{ + + public CageUIAdminRole(){ + this("Cage UI Admin", + "Administrator role for Cage UI", + CageUITemplateCreatorPermission.class, + CageUILayoutEditorAccessPermission.class, + CageUIRoomModifierPermission.class, + CageUIRoomCreatorPermission.class, + CageUIAnimalEditorPermission.class, + CageUIAnimalReviewerPermission.class, + CageUIModificationEditorPermission.class, + CageUINotesEditorPermission.class + ); + } + + protected CageUIAdminRole(String name, String description, Class... perms) { + super(name, description, CageUIModule.class, perms); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/roles/CageUIModificationEditorRole.java b/CageUI/src/org/labkey/cageui/security/roles/CageUIModificationEditorRole.java new file mode 100644 index 000000000..b35e46d4d --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/roles/CageUIModificationEditorRole.java @@ -0,0 +1,42 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.roles; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.AbstractRole; +import org.labkey.cageui.CageUIModule; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; +import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; + +public class CageUIModificationEditorRole extends AbstractRole +{ + + public CageUIModificationEditorRole(){ + this("Cage UI Cage Modification Editor", + "Cage modification editor role for Cage UI", + CageUIAnimalEditorPermission.class, + CageUIModificationEditorPermission.class + ); + } + + protected CageUIModificationEditorRole(String name, String description, Class... perms) { + super(name, description, CageUIModule.class, perms); + } + +} + diff --git a/CageUI/src/org/labkey/cageui/security/roles/CageUIRoomCreatorRole.java b/CageUI/src/org/labkey/cageui/security/roles/CageUIRoomCreatorRole.java new file mode 100644 index 000000000..7e177decf --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/roles/CageUIRoomCreatorRole.java @@ -0,0 +1,52 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.roles; + +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.AbstractRole; +import org.labkey.cageui.CageUIModule; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; +import org.labkey.cageui.security.permissions.CageUIAnimalReviewerPermission; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; +import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; +import org.labkey.cageui.security.permissions.CageUINotesEditorPermission; +import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; +import org.labkey.cageui.security.permissions.CageUIRoomModifierPermission; + +public class CageUIRoomCreatorRole extends AbstractRole +{ + + public CageUIRoomCreatorRole(){ + this("Cage UI Room Creator", + "Room creator role for Cage UI", + CageUIRoomModifierPermission.class, + CageUIRoomCreatorPermission.class, + CageUIAnimalEditorPermission.class, + CageUIAnimalReviewerPermission.class, + CageUIModificationEditorPermission.class, + CageUILayoutEditorAccessPermission.class, + CageUINotesEditorPermission.class + ); + } + + protected CageUIRoomCreatorRole(String name, String description, Class... perms) { + super(name, description, CageUIModule.class, perms); + } + +} diff --git a/CageUI/src/org/labkey/cageui/security/roles/CageUIRoomModifierRole.java b/CageUI/src/org/labkey/cageui/security/roles/CageUIRoomModifierRole.java new file mode 100644 index 000000000..613457031 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/roles/CageUIRoomModifierRole.java @@ -0,0 +1,47 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.roles; + +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.AbstractRole; +import org.labkey.cageui.CageUIModule; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; +import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; +import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; +import org.labkey.cageui.security.permissions.CageUINotesEditorPermission; +import org.labkey.cageui.security.permissions.CageUIRoomModifierPermission; + +public class CageUIRoomModifierRole extends AbstractRole +{ + public CageUIRoomModifierRole(){ + this("Cage UI Room Modifier Role", + "Room modifier role for Cage UI", + CageUIRoomModifierPermission.class, + CageUIAnimalEditorPermission.class, + CageUIModificationEditorPermission.class, + CageUILayoutEditorAccessPermission.class, + CageUINotesEditorPermission.class + ); + } + + protected CageUIRoomModifierRole(String name, String description, Class... perms) { + super(name, description, CageUIModule.class, perms); + } + +} diff --git a/CageUI/src/org/labkey/cageui/table/CageUICustomizer.java b/CageUI/src/org/labkey/cageui/table/CageUICustomizer.java new file mode 100644 index 000000000..067fccc31 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/table/CageUICustomizer.java @@ -0,0 +1,57 @@ +/* + * + * * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.table; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.labkey.api.data.AbstractTableInfo; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; +import org.labkey.api.ldk.table.AbstractTableCustomizer; +import org.labkey.api.query.ExprColumn; + +public class CageUICustomizer extends AbstractTableCustomizer +{ + + protected static final Logger _log = LogManager.getLogger(CageUICustomizer.class); + public CageUICustomizer() + { + + } + + @Override + public void customize(TableInfo table) + { + if (table instanceof AbstractTableInfo) + { + if (table.getName().equalsIgnoreCase("rack_types") && table.getSchema().getName().equalsIgnoreCase("cageui")) + customizeRackTypesTable((AbstractTableInfo) table); + } + } + + private void customizeRackTypesTable(AbstractTableInfo ti) + { + SQLFragment sql = new SQLFragment("(SELECT (length * width) as sqft)"); + ExprColumn newCol = new ExprColumn(ti, "sqft", sql, JdbcType.VARCHAR); + newCol.setLabel("Square Feet"); + newCol.setDescription("Square footage of the cages in the rack type"); + ti.addColumn(newCol); + } +} diff --git a/CageUI/src/org/labkey/cageui/view/hello.jsp b/CageUI/src/org/labkey/cageui/view/hello.jsp new file mode 100644 index 000000000..709814c19 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/view/hello.jsp @@ -0,0 +1,43 @@ +<%-- + ~ /* + ~ * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + ~ * + ~ * Licensed under the Apache License, Version 2.0 (the "License"); + ~ * you may not use this file except in compliance with the License. + ~ * You may obtain a copy of the License at + ~ * + ~ * http://www.apache.org/licenses/LICENSE-2.0 + ~ * + ~ * Unless required by applicable law or agreed to in writing, software + ~ * distributed under the License is distributed on an "AS IS" BASIS, + ~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ * See the License for the specific language governing permissions and + ~ * limitations under the License. + ~ */ + --%> + +<% +/* + * Copyright (c) 2025 Board of Regents of the University of Wisconsin System + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.security.User" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + Container c = getContainer(); + User user = getUser(); +%> +
Hello, and welcome to the CageUI module.
diff --git a/CageUI/tsconfig.json b/CageUI/tsconfig.json new file mode 100644 index 000000000..30a37626f --- /dev/null +++ b/CageUI/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES5", + "jsx": "react", + "lib": ["dom","es2017", "dom.iterable"], + "sourceMap": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "downlevelIteration": true + }, + "exclude": [ + "../CageUI/resources" + ] +} diff --git a/WNPRC_EHR/resources/data/lookup_sets.tsv b/WNPRC_EHR/resources/data/lookup_sets.tsv index 5a3a7852a..f733a010f 100644 --- a/WNPRC_EHR/resources/data/lookup_sets.tsv +++ b/WNPRC_EHR/resources/data/lookup_sets.tsv @@ -12,7 +12,9 @@ birth_condition Birth Condition Field Values value value birth_type Birth Type Field Values value title blood_billed_by Blood Billed By Field Values value title blood_code_prefixes Blood Code Prefix Field Values value -chemistry_method Chemistry Method Field Values value +cageui_item_types Room Item Type Field Values value title +cageui_rack_manufacturers Rack Manufacturer Field Values value title +chemistry_method Chemistry Method Field Values value chow_types Chow Types Field Values value clinpath_collection_method Clinpath Collection Method Field Values value clinpath_sampletype Clinpath Sample Type Field Values value diff --git a/WNPRC_EHR/resources/queries/ehr_lookups/rooms/.qview.xml b/WNPRC_EHR/resources/queries/ehr_lookups/rooms/.qview.xml new file mode 100644 index 000000000..d0f677a45 --- /dev/null +++ b/WNPRC_EHR/resources/queries/ehr_lookups/rooms/.qview.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/WNPRC_EHR/resources/views/ehrAdmin.html b/WNPRC_EHR/resources/views/ehrAdmin.html index 082dd39c9..bdcc617e3 100644 --- a/WNPRC_EHR/resources/views/ehrAdmin.html +++ b/WNPRC_EHR/resources/views/ehrAdmin.html @@ -27,7 +27,8 @@ //{name: 'Generate Schema XML', url: '<%=contextPath%>' + '/admin-getSchemaXmlDoc.view?full=true&dbSchema=core'}, //{name: 'View Site Errors', url: '<%=contextPath%>' + '/Shared/query-executeQuery.view?schemaName=auditlog&query.queryName=Client%20API%20Actions'} {name: 'UnSchedule BC Reports', url: '<%=contextPath%><%=containerPath%>/wnprc_ehr-UnscheduleBCReports.view'}, - {name: 'Schedule BC Reports', url: '<%=contextPath%><%=containerPath%>/wnprc_ehr-ScheduleBCReports.view'} + {name: 'Schedule BC Reports', url: '<%=contextPath%><%=containerPath%>/wnprc_ehr-ScheduleBCReports.view'}, + {name: 'EHR Extensible Columns', url: '<%=contextPath%><%=containerPath%>/ehr-ehrTemplates.view'}, ] }, {header: 'Documentation', diff --git a/WNPRC_EHR/resources/web/wnprc_ehr/wnprcOverRides.js b/WNPRC_EHR/resources/web/wnprc_ehr/wnprcOverRides.js index e220fbc05..aee736c2a 100644 --- a/WNPRC_EHR/resources/web/wnprc_ehr/wnprcOverRides.js +++ b/WNPRC_EHR/resources/web/wnprc_ehr/wnprcOverRides.js @@ -749,6 +749,7 @@ Ext4.override(EHR.form.field.ProjectEntryField, { Ext.Msg.alert('Error', "Must supply both year and prefix"); return } + console.log("SQL: ", sqlQueryString); LABKEY.Query.executeSql({ schemaName: 'study', diff --git a/WNPRC_EHR/src/client/query/helpers.ts b/WNPRC_EHR/src/client/query/helpers.ts index 513aecfbc..467d16044 100644 --- a/WNPRC_EHR/src/client/query/helpers.ts +++ b/WNPRC_EHR/src/client/query/helpers.ts @@ -164,7 +164,6 @@ export const lookupAnimalInfo = (id:string) => { .catch((data) => { reject(data); }); - }); }; diff --git a/WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/rackTypes.tsv b/WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/rackTypes.tsv new file mode 100644 index 000000000..eac8f29e3 --- /dev/null +++ b/WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/rackTypes.tsv @@ -0,0 +1,3 @@ +rowid name type manufacturer length width height sqft supportsTunnel description container createdby created modifiedby modified +1 cage-uk-0.0 0 Unknown false Default Unknown Rack EHR lcameron3 2025-02-07 09:31 lcameron3 2025-02-07 09:31 +2 pen-uk-0.0 1 Unknown false Default Unknown Pen EHR lcameron3 2025-02-07 09:31 lcameron3 2025-02-07 09:31 diff --git a/WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/racks.tsv b/WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/racks.tsv new file mode 100644 index 000000000..48b8c6c46 --- /dev/null +++ b/WNPRC_EHR/test/sampledata/wnprc_ehr/cageui/racks.tsv @@ -0,0 +1,2 @@ +rowid rackid rack_type container createdby created modifiedby modified +1 0 cage-uk-0.0 EHR lcameron3 2025-02-07 16:06 lcameron3 2025-02-07 16:06 diff --git a/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java b/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java index 1ca307754..1035afda2 100644 --- a/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java +++ b/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java @@ -234,7 +234,7 @@ public static void doSetup() throws Exception initTest.clickFolder(initTest.getProjectName()); initTest._containerHelper.enableModules(Arrays.asList("WNPRC_EHR", "EHR_Billing", "WNPRC_Billing", "WNPRC_BillingPublic")); initTest.clickFolder("EHR"); - initTest._containerHelper.enableModules(Arrays.asList("WNPRC_EHR", "EHR_Billing", "WNPRC_Billing", "WNPRC_BillingPublic", "PrimateId")); + initTest._containerHelper.enableModules(Arrays.asList("WNPRC_EHR", "EHR_Billing", "WNPRC_Billing", "WNPRC_BillingPublic", "PrimateId", "CageUI")); initTest.setModuleProperties(Arrays.asList(new ModulePropertyValue("EHR_Billing", "/" + initTest.getProjectName(), "BillingContainer", PRIVATE_FOLDER_PATH))); initTest.setModuleProperties(Arrays.asList(new ModulePropertyValue("EHR_Billing", "/" + @@ -4694,4 +4694,90 @@ public void activateDumbster(WebDriverWrapper myDriver) { } + // Imports rack data for cage ui testing + private void importRacks() throws IOException, CommandException + { + Connection connection = createDefaultConnection(); + Map responseMap = new HashMap<>(); + + List> tsv = loadTsv(TestFileUtils.getSampleData("wnprc_ehr/cageui/racks.tsv")); + insertTsvData(connection, "cageUI", "racks", tsv, EHR_FOLDER_PATH) + .forEach(row -> responseMap.put(row.get("rowid").toString(),row)); + } + // Imports rack types data for cage ui testing + private void importRackTypes() throws IOException, CommandException + { + Connection connection = createDefaultConnection(); + Map responseMap = new HashMap<>(); + + List> tsv = loadTsv(TestFileUtils.getSampleData("wnprc_ehr/cageui/rackTypes.tsv")); + insertTsvData(connection, "cageUI", "rack_types", tsv, EHR_FOLDER_PATH) + .forEach(row -> responseMap.put(row.get("rowid").toString(),row)); + } + + // Adds required starting data to cage UI tables + public void cageUISetup() throws Exception { + importRackTypes(); + importRacks(); + } + + /* + Tests the basic functionality, existence, saving, and loading for a basic cage UI editor layout. + */ + @Test + public void testCageUIBasic() throws Exception { + cageUISetup(); + beginAt(buildURL("cageui", getContainerPath(), "editLayout")); + + // Ensure room size options exist and click the first option. + WebElement sizeSelector = getDriver().findElement(By.className("room-size-selector-content")); + Assert.assertTrue(sizeSelector.isDisplayed()); + List sizes = sizeSelector.findElements(By.className("room-size-selector-option-card")); + Assert.assertFalse(sizes.isEmpty()); + sizes.get(0).click(); + WebElement sizeEnter = getDriver().findElement(By.xpath("//*[@id=\"app\"]/div/div[3]/div/div[2]/button")); + sizeEnter.click(); + + // Ensures the layout exists + WebElement layoutSvg = waitForElement(Locator.id("layout-svg"), 20000); + Assert.assertTrue(layoutSvg.isDisplayed()); + + // Puts a cage on the layout and ensures it exists + getDriver().findElement(By.id("cage_template_wrapper")).click(); + WebElement cage = waitForElement(Locator.id("default-rack-1"), 10000); + assertTrue(cage.isDisplayed()); + + // Puts a door on the layout and ensures it exists + getDriver().findElement(By.id("door_template_wrapper")).click(); + WebElement door = getDriver().findElement(By.id("door-1")); + assertTrue(door.isDisplayed()); + + // Click save and select room + getDriver().findElement(By.id("saveLayoutBtn")).click(); + getDriver().findElement(By.xpath("//div[contains(@class, '-control')]")).click(); + getDriver().findElement(By.id("react-select-2-option-0")).click(); + // Click confirm button + WebElement btnDiv = getDriver().findElement(By.className("popup-buttons")); + List btns = btnDiv.findElements(By.tagName("button")); + btns.get(0).click(); + // Click yes in confirmation window + btnDiv = getDriver().findElement(By.className("popup-buttons")); + btns = btnDiv.findElements(By.tagName("button")); + btns.get(0).click(); + + // Click close on success + btnDiv = waitForElement(Locator.byClass("popup-buttons"), 10000); + btns = btnDiv.findElements(By.tagName("button")); + btns.get(0).click(); + + // Wait for redirect and ensure page is loaded + layoutSvg = waitForElement(Locator.id("layout-svg"), 10000); + Assert.assertTrue(layoutSvg.isDisplayed()); + + // Ensure cage and door loaded in correctly + cage = waitForElement(Locator.id("default-rack-1"), 10000); + assertTrue(cage.isDisplayed()); + door = getDriver().findElement(By.id("door-1")); + assertTrue(door.isDisplayed()); + } } \ No newline at end of file From f86b5788390a012f02f2cf08302ec4f4738e8ad6 Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Fri, 6 Jun 2025 14:20:51 -0700 Subject: [PATCH 11/13] Mark CageUI as only supporting Postgres; dependency on EHR (#830) * Mark CageUI as only supporting Postgres * Declare explicit dependency; add missing column --- CageUI/build.gradle | 1 + CageUI/module.properties | 1 + CageUI/resources/schemas/cageui.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/CageUI/build.gradle b/CageUI/build.gradle index 8e4a647f5..2424dde9d 100644 --- a/CageUI/build.gradle +++ b/CageUI/build.gradle @@ -24,4 +24,5 @@ plugins { dependencies { BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: 'published', depExtension: 'module') + BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: ":server:modules:ehrModules:ehr", depProjectConfig: 'published', depExtension: 'module') } \ No newline at end of file diff --git a/CageUI/module.properties b/CageUI/module.properties index e11f4c105..7572f671c 100644 --- a/CageUI/module.properties +++ b/CageUI/module.properties @@ -23,3 +23,4 @@ URL: WNPRC/EHR/cageui-editLayout.view Name: CageUI License: Apache 2.0 LicenseURL: http://www.apache.org/licenses/LICENSE-2.0 +SupportedDatabases: pgsql diff --git a/CageUI/resources/schemas/cageui.xml b/CageUI/resources/schemas/cageui.xml index 91ffeccf1..bed3ab0da 100644 --- a/CageUI/resources/schemas/cageui.xml +++ b/CageUI/resources/schemas/cageui.xml @@ -34,6 +34,7 @@ + From 6651dc54f2722984e49842de3eb10b4c62a36421 Mon Sep 17 00:00:00 2001 From: LeviCameron1 <86750204+LeviCameron1@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:36:37 -0500 Subject: [PATCH 12/13] Fixes for CageUI layout editor (#836) * Fix for incorrect grid snapping due to chrome version update 137 * Fix for objects to jump away from the cursor in larger scaled rooms * Fix for clear layout not removing everything correctly * Fix for the large amount of empty space in the object toolbar --- CageUI/src/client/cageui.scss | 11 +++++ .../client/components/layoutEditor/Editor.tsx | 3 +- .../layoutEditor/RoomItemTemplate.tsx | 41 +++++++++++++++++-- .../context/LayoutEditorContextManager.tsx | 2 + .../src/client/utils/LayoutEditorHelpers.ts | 7 ++-- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index 4214c2bc5..98f03e273 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -578,6 +578,17 @@ margin-bottom: 10px; #utils { grid-area: room-utils; + display: grid; + grid-template-areas: "room-objects cage-templates ."; +} + +.util-svg-template-wrapper { + overflow: visible; +} + + +.layout-tooltip-container > * { + padding: 10px; } #layout-grid { diff --git a/CageUI/src/client/components/layoutEditor/Editor.tsx b/CageUI/src/client/components/layoutEditor/Editor.tsx index 7c0b0214e..dff6a8e93 100644 --- a/CageUI/src/client/components/layoutEditor/Editor.tsx +++ b/CageUI/src/client/components/layoutEditor/Editor.tsx @@ -273,7 +273,8 @@ const Editor: FC = ({roomSize}) => { // Drag end for dragging from the utilities to the layout const dragEnded = useCallback(async (event) => { - const draggedShape: d3.Selection = d3.select('.dragging'); + // clear transform attribute to prevent it from applying the transform while in groups. This was a change from Chrome 136 -> 137 + const draggedShape: d3.Selection = d3.select('.dragging').attr('transform', ''); if(draggedShape.empty()) { dragLockRef.current = false; return; diff --git a/CageUI/src/client/components/layoutEditor/RoomItemTemplate.tsx b/CageUI/src/client/components/layoutEditor/RoomItemTemplate.tsx index 07a34bf8a..1d20b6238 100644 --- a/CageUI/src/client/components/layoutEditor/RoomItemTemplate.tsx +++ b/CageUI/src/client/components/layoutEditor/RoomItemTemplate.tsx @@ -17,7 +17,7 @@ */ import * as React from 'react'; -import { FC } from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; import { ActionURL } from '@labkey/api'; import { RoomItemStringType } from '../../types/typings'; import { ReactSVG } from 'react-svg'; @@ -28,6 +28,38 @@ interface RoomItemTemplateProps { } export const RoomItemTemplate: FC = (props) => { const {fileName, className} = props; + const imgRef = useRef(null); + const [width, setWidth] = useState("100%"); + const [height, setHeight] = useState("100%"); + + // Effect reloads the svg to change the height and width of the wrapper for the requested svg after it is injected. + // This ensures that it doesn't have a lot of empty space in between the svgs + useEffect(() => { + if (!imgRef.current) return; + // Wait briefly for the nested SVG to render (adjust delay if needed) + const timer = setTimeout(() => { + const nestedSvg = imgRef.current?.reactWrapper.children[0].children[0]; + if (!nestedSvg) return; + + // Method 1: Use explicit width/height (if nested SVG has them) + const tempWidth = nestedSvg.getAttribute("width"); + const tempHeight = nestedSvg.getAttribute("height"); + + if (tempWidth && tempHeight) { + setWidth(tempWidth); + setHeight(tempHeight); + } + // Method 2: Fallback to rendered dimensions + else { + const rect = nestedSvg.getBoundingClientRect(); + setWidth(rect.width.toString()); + setHeight(rect.height.toString()); + } + }, 100); // Short delay to ensure rendering + + return () => clearTimeout(timer); + }, []); // Empty dependency array = runs once after mount + return (
@@ -35,9 +67,10 @@ export const RoomItemTemplate: FC = (props) => { src={`${ActionURL.getContextPath()}/cageui/static/${fileName}.svg`} id={`${fileName}_template_wrapper`} wrapper={'svg'} - className={className} - width={'250'} - height={'250'} + ref={imgRef} + height={height} + width={width} + className={className + " util-svg-template-wrapper"} />
); diff --git a/CageUI/src/client/context/LayoutEditorContextManager.tsx b/CageUI/src/client/context/LayoutEditorContextManager.tsx index 2fb14d77a..3e069c80e 100644 --- a/CageUI/src/client/context/LayoutEditorContextManager.tsx +++ b/CageUI/src/client/context/LayoutEditorContextManager.tsx @@ -1113,6 +1113,8 @@ export const LayoutEditorContextProvider: FC = ({children, p objects: [] } }); + setUnitLocs(createEmptyUnitLoc()); + setNextAvailGroup('rack-group-1'); } const saveRoom = async (): Promise => { diff --git a/CageUI/src/client/utils/LayoutEditorHelpers.ts b/CageUI/src/client/utils/LayoutEditorHelpers.ts index 4ef2078ba..28ef42c07 100644 --- a/CageUI/src/client/utils/LayoutEditorHelpers.ts +++ b/CageUI/src/client/utils/LayoutEditorHelpers.ts @@ -150,8 +150,8 @@ export const updateGrid = (transform, width, height, gridSize) => { const yMax = Math.ceil((height - transform.y) / transform.k / gridSize) * gridSize; // Draw the grid within the current visible area - for (let x = xMin; x <= xMax; x += gridSize) { - for (let y = yMin; y <= yMax; y += gridSize) { + for (let x = xMin; x < xMax; x += gridSize) { + for (let y = yMin; y < yMax; y += gridSize) { g.append("rect") .attr("x", x) .attr("y", y) @@ -734,8 +734,9 @@ export function createDragInLayout() { const element = d3.select(this); const transform = d3.zoomTransform(layoutSvg.node()); const scale = transform.k; + const [newX, newY] = d3.pointer(event.sourceEvent, this.parentNode); - element.attr('transform', `translate(${event.x},${event.y}) scale(${scale})`); + element.attr('transform', `translate(${newX},${newY}) scale(${scale})`); } ) } From 9bfcf8bb1c8bf10365ece63b98b3bcdf8f4c7e7e Mon Sep 17 00:00:00 2001 From: LeviCameron1 <86750204+LeviCameron1@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:04:13 -0500 Subject: [PATCH 13/13] CageUI Test Fix (#838) * Fix for incorrect grid snapping due to chrome version update 137 * Fix for objects to jump away from the cursor in larger scaled rooms * Fix for clear layout not removing everything correctly * Fix for the large amount of empty space in the object toolbar * Fix for test failing due to lookup tables not being populated * Fix for templates not being able to be saved while renamed --- .../src/client/components/layoutEditor/Editor.tsx | 5 ++++- .../components/layoutEditor/RoomSelectorPopup.tsx | 6 ++++-- .../client/context/LayoutEditorContextManager.tsx | 6 ++++-- .../src/client/types/layoutEditorContextTypes.ts | 2 +- WNPRC_EHR/resources/data/cageui_item_types.tsv | 13 +++++++++++++ .../resources/data/cageui_rack_manufacturers.tsv | 6 ++++++ WNPRC_EHR/resources/data/lookup_sets.tsv | 6 +++--- WNPRC_EHR/resources/views/populateInitialData.html | 14 ++++++++++++++ .../labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java | 4 ++-- 9 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 WNPRC_EHR/resources/data/cageui_item_types.tsv create mode 100644 WNPRC_EHR/resources/data/cageui_rack_manufacturers.tsv diff --git a/CageUI/src/client/components/layoutEditor/Editor.tsx b/CageUI/src/client/components/layoutEditor/Editor.tsx index dff6a8e93..5168db6d7 100644 --- a/CageUI/src/client/components/layoutEditor/Editor.tsx +++ b/CageUI/src/client/components/layoutEditor/Editor.tsx @@ -107,6 +107,7 @@ const Editor: FC = ({roomSize}) => { const [showRoomSelectorTemplateLoad, setShowRoomSelectorTemplateLoad] = useState(false); const [showSaveResult, setShowSaveResult] = useState(null); const [templateOptions, setTemplateOptions] = useState(false); + const [templateRename, setTemplateRename] = useState(null); // number of cells in grid width/height, based off scale const gridWidth = Math.ceil(SVG_WIDTH / roomSize.scale / CELL_SIZE); @@ -762,7 +763,8 @@ const Editor: FC = ({roomSize}) => { } const handleSave = async () => { - const result = await saveRoom(); + + const result = await saveRoom(templateRename); setShowSaveResult(result); } @@ -937,6 +939,7 @@ const Editor: FC = ({roomSize}) => { {setShowRoomSelector(false);setShowSaveConfirm(true);}} onCancel={() => {setTemplateOptions(false);setShowRoomSelector(false);}} /> diff --git a/CageUI/src/client/components/layoutEditor/RoomSelectorPopup.tsx b/CageUI/src/client/components/layoutEditor/RoomSelectorPopup.tsx index d1cbc3a06..53558d12b 100644 --- a/CageUI/src/client/components/layoutEditor/RoomSelectorPopup.tsx +++ b/CageUI/src/client/components/layoutEditor/RoomSelectorPopup.tsx @@ -32,11 +32,12 @@ interface RoomSelectorPopup { setRoom: React.Dispatch>; template: boolean; templateLoad?: boolean; + templateRename?: React.Dispatch>; } // For saving and loading in the layout editor, this is a room selector component export const RoomSelectorPopup: FC = (props) => { - const { onConfirm, onCancel, setRoom, template,templateLoad } = props; + const { onConfirm, onCancel, setRoom, template,templateLoad, templateRename } = props; const [selectedRoom, setSelectedRoom] = useState(null); const [options, setOptions] = useState[]>(null); const [templateName, setTemplateName] = useState(''); @@ -76,11 +77,12 @@ export const RoomSelectorPopup: FC = (props) => { onCancel(); return; } - // if template, save old name and new name together to parse later in submission + // if template, save old template name for later setRoom(prevState => ({ ...prevState, name: templateName })); + templateRename(selectedRoom); }else{ setRoom(prevState => ({ ...prevState, diff --git a/CageUI/src/client/context/LayoutEditorContextManager.tsx b/CageUI/src/client/context/LayoutEditorContextManager.tsx index 3e069c80e..9113ec69d 100644 --- a/CageUI/src/client/context/LayoutEditorContextManager.tsx +++ b/CageUI/src/client/context/LayoutEditorContextManager.tsx @@ -1117,12 +1117,14 @@ export const LayoutEditorContextProvider: FC = ({children, p setNextAvailGroup('rack-group-1'); } - const saveRoom = async (): Promise => { + const saveRoom = async (oldTemplateName?: string): Promise => { const commands: Command[] = []; const dataToSave: LayoutHistoryData[] = []; + // if template parse room name, 1 is the new name, 0 is the old name + const roomName = localRoom.name; - const oldRoomName: string = ActionURL.getParameter('room'); + const oldRoomName: string = oldTemplateName ? oldTemplateName : ActionURL.getParameter('room'); const savingTemplate: boolean = roomName.toLowerCase().includes("template"); const newEndDate = new Date(); const newStartDate = new Date(); diff --git a/CageUI/src/client/types/layoutEditorContextTypes.ts b/CageUI/src/client/types/layoutEditorContextTypes.ts index 525afb432..d0ad58736 100644 --- a/CageUI/src/client/types/layoutEditorContextTypes.ts +++ b/CageUI/src/client/types/layoutEditorContextTypes.ts @@ -47,7 +47,7 @@ export interface LayoutContextProps { export interface LayoutContextType { room: Room; setRoom: React.Dispatch>; - saveRoom: () => Promise; + saveRoom: (oldTemplateName?: string) => Promise; layoutSvg: d3.Selection; setLayoutSvg: React.Dispatch>>; unitLocs: UnitLocations; diff --git a/WNPRC_EHR/resources/data/cageui_item_types.tsv b/WNPRC_EHR/resources/data/cageui_item_types.tsv new file mode 100644 index 000000000..0adb98982 --- /dev/null +++ b/WNPRC_EHR/resources/data/cageui_item_types.tsv @@ -0,0 +1,13 @@ +Value Title Category Description Sort Order Date Disabled +0 Default Cage Caging 4 +1 Default Pen Caging 8 +2 Default Temp Cage Caging 4 +3 Default Play Cage Caging 8 +4 Cage Caging 4 +5 Pen Caging 8 +6 Temp Cage Caging 4 +7 Play Cage Caging 8 +100 Room Divider Room Object +101 Drain Room Object +102 Door Room Object +103 Gate Room Object diff --git a/WNPRC_EHR/resources/data/cageui_rack_manufacturers.tsv b/WNPRC_EHR/resources/data/cageui_rack_manufacturers.tsv new file mode 100644 index 000000000..2631b7eed --- /dev/null +++ b/WNPRC_EHR/resources/data/cageui_rack_manufacturers.tsv @@ -0,0 +1,6 @@ +Value Title Category Description Sort Order Date Disabled +at Allentown +sb Suburban +lk Lenderking +wnprc WNPRC +uk Unknown \ No newline at end of file diff --git a/WNPRC_EHR/resources/data/lookup_sets.tsv b/WNPRC_EHR/resources/data/lookup_sets.tsv index f733a010f..207704850 100644 --- a/WNPRC_EHR/resources/data/lookup_sets.tsv +++ b/WNPRC_EHR/resources/data/lookup_sets.tsv @@ -11,9 +11,9 @@ biopsy_type Biopsy Type Field Values value birth_condition Birth Condition Field Values value value birth_type Birth Type Field Values value title blood_billed_by Blood Billed By Field Values value title -blood_code_prefixes Blood Code Prefix Field Values value -cageui_item_types Room Item Type Field Values value title -cageui_rack_manufacturers Rack Manufacturer Field Values value title +blood_code_prefixes Blood Code Prefix Field Values value +cageui_item_types Room Item Type Field Values value +cageui_rack_manufacturers Rack Manufacturer Field Values value chemistry_method Chemistry Method Field Values value chow_types Chow Types Field Values value clinpath_collection_method Clinpath Collection Method Field Values value diff --git a/WNPRC_EHR/resources/views/populateInitialData.html b/WNPRC_EHR/resources/views/populateInitialData.html index ca97102b0..db850fad9 100644 --- a/WNPRC_EHR/resources/views/populateInitialData.html +++ b/WNPRC_EHR/resources/views/populateInitialData.html @@ -269,6 +269,20 @@ queryName: 'husbandry_assigned', module: 'wnprc_ehr', pk: 'rowid' + },{ + label: 'Room Item Type Field Values', + populateFn: 'populateFromFile', + schemaName: 'ehr_lookups', + queryName: 'cageui_item_types', + module: 'wnprc_ehr', + pk: 'value' + },{ + label: 'Rack Manufacturer Field Values', + populateFn: 'populateFromFile', + schemaName: 'ehr_lookups', + queryName: 'cageui_rack_manufacturers', + module: 'wnprc_ehr', + pk: 'value' }]; tables.sort(function(a, b) { diff --git a/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java b/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java index 1035afda2..15726050e 100644 --- a/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java +++ b/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java @@ -4701,7 +4701,7 @@ private void importRacks() throws IOException, CommandException Map responseMap = new HashMap<>(); List> tsv = loadTsv(TestFileUtils.getSampleData("wnprc_ehr/cageui/racks.tsv")); - insertTsvData(connection, "cageUI", "racks", tsv, EHR_FOLDER_PATH) + insertTsvData(connection, "cageui", "racks", tsv, EHR_FOLDER_PATH) .forEach(row -> responseMap.put(row.get("rowid").toString(),row)); } // Imports rack types data for cage ui testing @@ -4711,7 +4711,7 @@ private void importRackTypes() throws IOException, CommandException Map responseMap = new HashMap<>(); List> tsv = loadTsv(TestFileUtils.getSampleData("wnprc_ehr/cageui/rackTypes.tsv")); - insertTsvData(connection, "cageUI", "rack_types", tsv, EHR_FOLDER_PATH) + insertTsvData(connection, "cageui", "rack_types", tsv, EHR_FOLDER_PATH) .forEach(row -> responseMap.put(row.get("rowid").toString(),row)); }