From b0ca7b8cc1f0490781b33576fe9f5acecfbd2c5c Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 24 Apr 2026 20:49:04 -0700 Subject: [PATCH 01/18] Switch from GWT to React --- .../PlateTemplateDesigner.scss | 608 ++++++++++++++++++ .../PlateTemplateDesigner.tsx | 393 +++++++++++ .../src/client/PlateTemplateDesigner/app.tsx | 20 + .../components/GroupTypesPanel.tsx | 280 ++++++++ .../components/ShiftPanel.tsx | 28 + .../components/StatusBar.tsx | 32 + .../components/TemplateGrid.tsx | 125 ++++ .../components/WarningPanel.tsx | 31 + .../components/WellGroupProperties.tsx | 107 +++ .../src/client/PlateTemplateDesigner/dev.tsx | 21 + .../client/PlateTemplateDesigner/models.ts | 67 ++ .../PlateTemplateDesigner/typings/main.d.ts | 14 + assay/src/client/entryPoints.js | 8 + .../src/org/labkey/assay/PlateController.java | 375 ++++++++++- .../src/org/labkey/assay/plate/PlateImpl.java | 2 +- 15 files changed, 2107 insertions(+), 4 deletions(-) create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/app.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/dev.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/models.ts create mode 100644 assay/src/client/PlateTemplateDesigner/typings/main.d.ts diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss new file mode 100644 index 00000000000..3a790af9729 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -0,0 +1,608 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +.plate-template-designer { + padding: 12px 16px; + font-family: Arial, Helvetica, sans-serif; + font-size: 13px; + + &__error { + color: #c00; + padding: 16px; + } + + &__loading { + padding: 16px; + color: #666; + } + + &__header { + margin: 10px 0; + } + + &__name-label { + font-weight: bold; + } + + &__name-input { + margin-left: 8px; + padding: 4px 6px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 13px; + width: 280px; + } + + &__body { + display: flex; + gap: 16px; + align-items: flex-start; + } + + &__left { + flex: 0 0 auto; + } + + &__right { + flex: 1; + min-width: 200px; + } +} + +// StatusBar +.status-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0 10px; + border-bottom: 1px solid #ddd; + margin-bottom: 8px; + + &__btn { + padding: 5px 14px; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + font-size: 13px; + + &:hover:not(:disabled) { + background: #e8e8e8; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + + &--primary { + background: #337ab7; + border-color: #2e6da4; + color: #fff; + + &:hover:not(:disabled) { + background: #286090; + } + } + } + + &__dirty { + color: #c80; + font-style: italic; + } + + &__status { + color: #555; + } +} + +// GroupTypesPanel +.group-types-panel { + border: 1px solid #ccc; + border-radius: 3px; + + &__tabs { + display: flex; + flex-wrap: wrap; + border-bottom: 1px solid #ccc; + background: #f0f0f0; + } + + &__tab { + padding: 5px 10px; + border: none; + background: transparent; + cursor: pointer; + font-size: 12px; + border-right: 1px solid #ddd; + + &:hover { + background: #e0e0e0; + } + + &--active { + background: #fff; + font-weight: bold; + border-bottom: 2px solid #337ab7; + } + } + + &__tab-body { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px; + } + + &__groups { + flex: 0 0 160px; + min-width: 280px; + min-height: 60px; + } + + &__group { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + margin-bottom: 3px; + border-radius: 3px; + cursor: pointer; + border: 1px solid transparent; + min-width: 0; + + &:hover { + background: #f0f0f0; + } + + &--active { + border-color: #337ab7; + background: #e8f0fb; + } + } + + &__color-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + border: 1px solid rgba(0, 0, 0, 0.2); + flex-shrink: 0; + } + + &__group-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + } + + &__rename-input { + flex: 1; + padding: 1px 4px; + border: 1px solid #337ab7; + border-radius: 3px; + font-size: 12px; + min-width: 0; + } + + &__group-actions { + display: flex; + gap: 2px; + margin-left: auto; + flex-shrink: 0; + } + + &__action-btn { + padding: 2px 5px; + border: 1px solid #ccc; + border-radius: 2px; + background: transparent; + cursor: pointer; + font-size: 11px; + line-height: 1; + color: #555; + + &:hover { + background: #e0e0e0; + } + + &--delete { + color: #c00; + + &:hover { + background: #ffe0e0; + } + } + } + + &__create-row { + display: flex; + align-items: center; + gap: 4px; + margin-top: 6px; + } + + &__new-name-input { + flex: 1; + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + min-width: 0; + } + + &__add-btn { + padding: 3px 8px; + font-size: 12px; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + color: #333; + white-space: nowrap; + + &:hover:not(:disabled) { + background: #e8e8e8; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + + &--primary { + background: #337ab7; + border-color: #2e6da4; + color: #fff; + + &:hover:not(:disabled) { + background: #286090; + } + } + } +} + +// Multi-create dialog +.multi-create-dialog { + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + padding: 20px; + min-width: 300px; + + &__overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + &__title { + font-weight: bold; + font-size: 14px; + margin-bottom: 14px; + } + + &__table { + width: 100%; + border-collapse: collapse; + + td { + padding-bottom: 10px; + } + } + + &__label { + padding: 6px 16px 6px 0; + white-space: nowrap; + font-size: 13px; + vertical-align: middle; + } + + &__input { + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 13px; + width: 100%; + box-sizing: border-box; + + &--count { + width: 80px; + } + } + + &__error { + color: #c00; + font-size: 12px; + margin-top: 2px; + } + + &__buttons { + padding-top: 10px; + display: flex; + gap: 6px; + justify-content: flex-end; + } +} + +// Wrapper for grid + shift panel +.plate-grid-area { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +// ShiftPanel +.shift-panel { + &__grid { + display: grid; + grid-template-columns: repeat(3, 26px); + grid-template-rows: repeat(3, 26px); + gap: 2px; + align-items: center; + justify-items: center; + } + + &__btn { + width: 26px; + height: 26px; + padding: 0; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + font-size: 14px; + line-height: 1; + + &:hover { + background: #e8e8e8; + } + } + + &__label { + font-size: 10px; + color: #666; + text-align: center; + line-height: 1; + } +} + +// TemplateGrid +.template-grid { + user-select: none; + + &__table { + border-collapse: collapse; + border: 1px solid #aaa; + } + + &__corner { + width: 24px; + height: 24px; + } + + &__col-header { + text-align: center; + font-size: 11px; + font-weight: bold; + width: 28px; + height: 22px; + background: #f0f0f0; + border: 1px solid #ccc; + } + + &__row-header { + text-align: center; + font-size: 11px; + font-weight: bold; + width: 22px; + background: #f0f0f0; + border: 1px solid #ccc; + } + + &__cell { + width: 28px; + height: 28px; + border: 1px solid #ccc; + cursor: crosshair; + transition: filter 0.1s; + + &:hover { + filter: brightness(0.88); + } + + &--active { + outline: 2px solid #333; + outline-offset: -2px; + } + } +} + +// Right panel tabs +.right-panel-tabs { + display: flex; + border-bottom: 1px solid #ccc; + margin-bottom: 8px; + + &__tab { + padding: 5px 10px; + border: none; + background: transparent; + cursor: pointer; + font-size: 12px; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + + &:hover { + background: #f0f0f0; + } + + &--active { + font-weight: bold; + border-bottom-color: #337ab7; + background: #fff; + } + + &--warn { + color: #c80; + } + + &--active#{&}--warn { + border-bottom-color: #c80; + } + } +} + +// WellGroupProperties +.well-group-properties { + border: 1px solid #ccc; + border-radius: 3px; + padding: 10px; + min-height: 60px; + + &--empty { + color: #888; + font-style: italic; + } + + &__title { + font-weight: bold; + margin-bottom: 8px; + } + + &__no-props { + color: #888; + font-style: italic; + font-size: 12px; + } + + &__table { + width: 100%; + border-collapse: collapse; + } + + &__key { + font-weight: bold; + padding: 3px 6px 3px 0; + white-space: nowrap; + width: 40%; + } + + &__value-cell { + padding: 2px 4px 2px 0; + } + + &__action-cell { + padding: 2px 0 2px 6px; + white-space: nowrap; + width: 1%; + } + + &__value { + padding: 3px 4px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + width: 100%; + box-sizing: border-box; + } + + &__delete-btn { + padding: 2px 5px; + border: 1px solid #ccc; + border-radius: 2px; + background: transparent; + cursor: pointer; + font-size: 11px; + line-height: 1; + color: #c00; + + &:hover { + background: #ffe0e0; + } + } + + &__add-row td { + border-top: 1px solid #eee; + padding-top: 6px; + } + + &__new-key { + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + width: 100%; + box-sizing: border-box; + } + + &__new-value { + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + width: 100%; + box-sizing: border-box; + } + + &__add-btn { + padding: 3px 8px; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + font-size: 12px; + white-space: nowrap; + + &:hover:not(:disabled) { + background: #e8e8e8; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + } +} + +// WarningPanel +.warning-panel { + border: 1px solid #e8a000; + border-radius: 3px; + padding: 10px; + background: #fffbe6; + + &__title { + font-weight: bold; + color: #c80; + margin-bottom: 6px; + } + + &__none { + color: #888; + font-style: italic; + font-size: 12px; + } + + &__list { + margin: 0; + padding-left: 18px; + } + + &__item { + color: #7a5800; + font-size: 12px; + margin-bottom: 3px; + } +} diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx new file mode 100644 index 00000000000..16297b36f3e --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ActionURL, Ajax, Utils } from '@labkey/api'; + +import { PlateTemplate, WellGroup, computeWarnings } from './models'; +import { StatusBar } from './components/StatusBar'; +import { GroupTypesPanel } from './components/GroupTypesPanel'; +import { ShiftPanel } from './components/ShiftPanel'; +import { TemplateGrid } from './components/TemplateGrid'; +import { WellGroupProperties } from './components/WellGroupProperties'; +import { WarningPanel } from './components/WarningPanel'; + +import './PlateTemplateDesigner.scss'; + +const COLORS = [ + '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', + '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac', + '#6ba3be', '#ffbe7d', '#ff9d9a', '#86bcb6', '#8cd17d', + '#f1ce63', '#d4a6c8', '#ffb7c5', '#c7a97e', '#d7d5cf', +]; + +function assignColors(groups: WellGroup[]): Map { + const map = new Map(); + groups.forEach((g, i) => { + map.set(g.rowId, COLORS[i % COLORS.length]); + }); + return map; +} + +export function PlateTemplateDesigner(): JSX.Element { + const [plate, setPlate] = useState(null); + const [activeGroup, setActiveGroup] = useState(null); + const [activeTab, setActiveTab] = useState(''); + const [rightTab, setRightTab] = useState<'properties' | 'warnings'>('properties'); + const [isDirty, setIsDirty] = useState(false); + const [status, setStatus] = useState(''); + const [colorMap, setColorMap] = useState>(new Map()); + const [error, setError] = useState(null); + const plateNameRef = useRef(''); + const statusTimerRef = useRef | null>(null); + const nextGroupIdRef = useRef(-1); + + useEffect(() => { + const templateName = ActionURL.getParameter('templateName'); + const plateIdStr = ActionURL.getParameter('plateId'); + const assayType = ActionURL.getParameter('assayType'); + const templateType = ActionURL.getParameter('templateType'); + const rowCountStr = ActionURL.getParameter('rowCount'); + const colCountStr = ActionURL.getParameter('colCount'); + const copy = ActionURL.getParameter('copy') === 'true' || ActionURL.getParameter('copyTemplate') === 'true'; + + const params: Record = {}; + if (templateName) params.templateName = templateName; + if (plateIdStr) params.plateId = parseInt(plateIdStr, 10); + if (assayType) params.assayType = assayType; + if (templateType) params.templateType = templateType; + if (rowCountStr) params.rowCount = parseInt(rowCountStr, 10); + if (colCountStr) params.colCount = parseInt(colCountStr, 10); + params.copy = copy; + + Ajax.request({ + url: ActionURL.buildURL('plate', 'getTemplateDefinition.api'), + method: 'GET', + params, + success: Utils.getCallbackWrapper((response: { data: PlateTemplate }) => { + const plate = response.data; + plateNameRef.current = plate.defaultPlateName || plate.name || ''; + setPlate({ ...plate, name: plateNameRef.current }); + setColorMap(assignColors(plate.groups)); + setActiveTab(plate.groupTypes[0] ?? ''); + if (plate.copyMode) setIsDirty(true); + }), + failure: Utils.getCallbackWrapper((response: any) => { + setError(response?.exception ?? 'Failed to load plate template.'); + }, null, true), + }); + }, []); + + const handleNameChange = useCallback((name: string) => { + plateNameRef.current = name; + setPlate(prev => prev ? { ...prev, name } : null); + setIsDirty(true); + }, []); + + const handleGroupSelect = useCallback((group: WellGroup) => { + setActiveGroup(group); + }, []); + + const handleCellAssign = useCallback((row: number, col: number) => { + if (!activeGroup || !plate) return; + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => { + if (g.rowId === activeGroup.rowId) { + const alreadyHas = g.positions.some(p => p.row === row && p.col === col); + if (alreadyHas) return g; + return { ...g, positions: [...g.positions, { row, col }] }; + } + if (g.type === activeGroup.type) { + // Remove from other groups of the same type to avoid conflicts + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return g; + }); + return { ...prev, groups: updatedGroups }; + }); + setIsDirty(true); + }, [activeGroup, plate]); + + const handleCellToggle = useCallback((row: number, col: number) => { + if (!activeGroup || !plate) return; + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => { + if (g.rowId === activeGroup.rowId) { + const hasCell = g.positions.some(p => p.row === row && p.col === col); + if (hasCell) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return { ...g, positions: [...g.positions, { row, col }] }; + } + if (g.type === activeGroup.type) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return g; + }); + return { ...prev, groups: updatedGroups }; + }); + setIsDirty(true); + }, [activeGroup, plate]); + + const handleAddGroup = useCallback((type: string, name: string) => { + if (!plate) return; + const rowId = nextGroupIdRef.current--; + const newGroup: WellGroup = { + rowId, + type, + name, + positions: [], + properties: {}, + allowNewGroups: plate.canCreateGroupsByType?.[type] ?? false, + }; + setPlate(prev => prev ? { ...prev, groups: [...prev.groups, newGroup] } : null); + setColorMap(prev => { + const next = new Map(prev); + next.set(rowId, COLORS[prev.size % COLORS.length]); + return next; + }); + setActiveGroup(newGroup); + setIsDirty(true); + }, [plate]); + + const handleShift = useCallback((verticalShift: number, horizontalShift: number) => { + setPlate(prev => { + if (!prev) return null; + const { rows, cols } = prev; + const updatedGroups = prev.groups.map(g => { + if (g.type !== activeTab) return g; + return { + ...g, + positions: g.positions.map(p => ({ + row: ((p.row - verticalShift) % rows + rows) % rows, + col: ((p.col - horizontalShift) % cols + cols) % cols, + })), + }; + }); + return { ...prev, groups: updatedGroups }; + }); + setIsDirty(true); + }, [activeTab]); + + const handleDeleteGroup = useCallback((rowId: number) => { + setPlate(prev => prev ? { ...prev, groups: prev.groups.filter(g => g.rowId !== rowId) } : null); + setColorMap(prev => { const next = new Map(prev); next.delete(rowId); return next; }); + setActiveGroup(prev => prev?.rowId === rowId ? null : prev); + setIsDirty(true); + }, []); + + const handleRenameGroup = useCallback((rowId: number, newName: string) => { + setPlate(prev => prev ? { ...prev, groups: prev.groups.map(g => g.rowId === rowId ? { ...g, name: newName } : g) } : null); + setActiveGroup(prev => prev?.rowId === rowId ? { ...prev, name: newName } : prev); + setIsDirty(true); + }, []); + + const handlePropertyChange = useCallback((groupRowId: number, key: string, value: string) => { + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => + g.rowId === groupRowId ? { ...g, properties: { ...g.properties, [key]: value } } : g + ); + return { ...prev, groups: updatedGroups }; + }); + setActiveGroup(prev => prev?.rowId === groupRowId ? { ...prev, properties: { ...prev.properties, [key]: value } } : prev); + setIsDirty(true); + }, []); + + const handleDeleteProperty = useCallback((groupRowId: number, key: string) => { + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => { + if (g.rowId !== groupRowId) return g; + const { [key]: _removed, ...rest } = g.properties; + return { ...g, properties: rest }; + }); + return { ...prev, groups: updatedGroups }; + }); + setActiveGroup(prev => { + if (prev?.rowId !== groupRowId) return prev; + const { [key]: _removed, ...rest } = prev.properties; + return { ...prev, properties: rest }; + }); + setIsDirty(true); + }, []); + + const warningCount = useMemo(() => { + if (!plate?.showWarningPanel) return 0; + return computeWarnings(plate).length; + }, [plate]); + + const navigateAway = useCallback(() => { + const returnURL = ActionURL.getParameter('returnURL') || ActionURL.getParameter('returnUrl'); + const isSameOrigin = (url: string) => { + try { + return new URL(url, window.location.origin).origin === window.location.origin; + } catch { + return false; + } + }; + window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); + }, []); + + const handleSave = useCallback(() => { + if (!plate) return; + setStatus('Saving...'); + Ajax.request({ + url: ActionURL.buildURL('plate', 'saveTemplate.api'), + method: 'POST', + jsonData: plate, + success: Utils.getCallbackWrapper((response: { data: { rowId: number } }) => { + const rowId = response.data.rowId; + setIsDirty(false); + setPlate(prev => prev ? { ...prev, rowId } : null); + // Update URL to canonical form so a refresh reloads this plate + const url = new URL(window.location.href); + url.search = ''; + url.searchParams.set('templateName', plate.name); + url.searchParams.set('plateId', String(rowId)); + window.history.replaceState(null, '', url.toString()); + setStatus('Saved.'); + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + statusTimerRef.current = setTimeout(() => setStatus(''), 5000); + }), + failure: Utils.getCallbackWrapper((response: any) => { + setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); + }, null, true), + }); + }, [plate]); + + const handleSaveAndClose = useCallback(() => { + if (!plate) return; + if (!isDirty) { + navigateAway(); + return; + } + setStatus('Saving...'); + Ajax.request({ + url: ActionURL.buildURL('plate', 'saveTemplate.api'), + method: 'POST', + jsonData: plate, + success: Utils.getCallbackWrapper(() => { + setIsDirty(false); + navigateAway(); + }), + failure: Utils.getCallbackWrapper((response: any) => { + setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); + }, null, true), + }); + }, [plate, isDirty, navigateAway]); + + const handleCancel = useCallback(() => { + navigateAway(); + }, [navigateAway]); + + // Warn on unsaved navigation + useEffect(() => { + const handler = (e: BeforeUnloadEvent) => { + if (isDirty) { + e.preventDefault(); + e.returnValue = ''; + } + }; + window.addEventListener('beforeunload', handler); + return () => window.removeEventListener('beforeunload', handler); + }, [isDirty]); + + // Keep activeGroup in sync when plate changes + useEffect(() => { + if (activeGroup && plate) { + const updated = plate.groups.find(g => g.rowId === activeGroup.rowId); + if (updated) setActiveGroup(updated); + } + }, [plate]); // eslint-disable-line react-hooks/exhaustive-deps + + if (error) { + return
{error}
; + } + + if (!plate) { + return
Loading...
; + } + + return ( +
+ +
+ +
+
+
+ { setActiveTab(tab); setActiveGroup(null); }} + onAddGroup={handleAddGroup} + onDeleteGroup={handleDeleteGroup} + onRenameGroup={handleRenameGroup} + > +
+ + +
+
+
+
+ {plate.showWarningPanel && ( +
+ + +
+ )} + {(!plate.showWarningPanel || rightTab === 'properties') && ( + + )} + {plate.showWarningPanel && rightTab === 'warnings' && ( + + )} +
+
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/app.tsx b/assay/src/client/PlateTemplateDesigner/app.tsx new file mode 100644 index 00000000000..03276e83aa1 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/app.tsx @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { getServerContext } from '@labkey/api'; +import { ServerContextProvider, withAppUser } from '@labkey/components'; + +import { PlateTemplateDesigner } from './PlateTemplateDesigner'; + +// Need to wait for container element to be available in labkey wrapper before render +window.addEventListener('DOMContentLoaded', () => { + createRoot(document.getElementById('app')).render( + + + + ); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx new file mode 100644 index 00000000000..fa4d0a21275 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +import { PlateTemplate, WellGroup } from '../models'; + +interface Props { + plate: PlateTemplate; + activeGroup: WellGroup | null; + activeTab: string; + colorMap: Map; + onGroupSelect: (group: WellGroup) => void; + onTabChange: (tab: string) => void; + onAddGroup: (type: string, name: string) => void; + onDeleteGroup: (rowId: number) => void; + onRenameGroup: (rowId: number, newName: string) => void; + children?: React.ReactNode; +} + +export function GroupTypesPanel({ + plate, + activeGroup, + activeTab, + colorMap, + onGroupSelect, + onTabChange, + onAddGroup, + onDeleteGroup, + onRenameGroup, + children, +}: Props): JSX.Element { + const [newGroupName, setNewGroupName] = useState(''); + const [renamingId, setRenamingId] = useState(null); + const [renameValue, setRenameValue] = useState(''); + const [multiCreateOpen, setMultiCreateOpen] = useState(false); + const [multiBaseName, setMultiBaseName] = useState(''); + const [multiCount, setMultiCount] = useState('2'); + const [multiCountError, setMultiCountError] = useState(''); + const multiBaseNameRef = useRef(null); + + const groupsOfType = plate.groups.filter(g => g.type === activeTab); + const canAdd = plate.canCreateGroupsByType?.[activeTab] ?? false; + + const unusedDefaults = useMemo(() => { + const defaults = plate.typesToDefaultGroups[activeTab] ?? []; + return defaults.filter(d => !groupsOfType.some(g => g.name === d)); + }, [plate, activeTab, groupsOfType]); + + // Reset create-input when tab changes + useEffect(() => { + setNewGroupName(unusedDefaults[0] ?? ''); + setRenamingId(null); + }, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps + + // Advance to next unused default when the current one gets used + useEffect(() => { + if (unusedDefaults.length > 0 && !unusedDefaults.includes(newGroupName)) { + setNewGroupName(unusedDefaults[0]); + } + }, [unusedDefaults]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleCreate = () => { + const trimmed = newGroupName.trim(); + if (!trimmed) return; + onAddGroup(activeTab, trimmed); + }; + + const openMultiCreate = () => { + setMultiBaseName(newGroupName.trim()); + setMultiCount('2'); + setMultiCountError(''); + setMultiCreateOpen(true); + // Focus the base name input after the modal renders + setTimeout(() => multiBaseNameRef.current?.select(), 0); + }; + + const handleMultiCreate = () => { + const count = parseInt(multiCount, 10); + if (isNaN(count) || count < 1) { + setMultiCountError(`"${multiCount}" is not a valid count.`); + return; + } + const baseName = multiBaseName.trim(); + if (!baseName) return; + for (let i = 1; i <= count; i++) { + onAddGroup(activeTab, `${baseName} ${i}`); + } + setMultiCreateOpen(false); + }; + + const handleDeleteClick = (e: React.MouseEvent, group: WellGroup) => { + e.stopPropagation(); + if (window.confirm(`Delete well group "${group.name}"?`)) { + onDeleteGroup(group.rowId); + } + }; + + const handleRenameClick = (e: React.MouseEvent, group: WellGroup) => { + e.stopPropagation(); + setRenamingId(group.rowId); + setRenameValue(group.name); + }; + + const handleRenameCommit = (rowId: number) => { + const trimmed = renameValue.trim(); + if (trimmed) onRenameGroup(rowId, trimmed); + setRenamingId(null); + }; + + return ( +
+
+ {plate.groupTypes.map(type => ( + + ))} +
+
+
+ {groupsOfType.map(group => { + const color = colorMap.get(group.rowId); + const isActive = activeGroup?.rowId === group.rowId; + const isRenaming = renamingId === group.rowId; + return ( +
{ if (!isRenaming) onGroupSelect(group); }} + > + + {isRenaming ? ( + setRenameValue(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameCommit(group.rowId); + if (e.key === 'Escape') setRenamingId(null); + }} + onBlur={() => handleRenameCommit(group.rowId)} + onClick={e => e.stopPropagation()} + /> + ) : ( + {group.name} + )} + {isActive && !isRenaming && group.allowNewGroups && ( + + + + + )} +
+ ); + })} + {canAdd && ( +
+ {unusedDefaults.length > 0 ? ( + + ) : ( + setNewGroupName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newGroupName.trim()) handleCreate(); }} + /> + )} + + +
+ )} +
+ {children} +
+ {multiCreateOpen && ( +
setMultiCreateOpen(false)}> +
e.stopPropagation()}> +
Create Multiple Groups
+ + + + + + + + + + + + + + +
Base Name + setMultiBaseName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} + /> +
Count + { setMultiCount(e.target.value); setMultiCountError(''); }} + onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} + /> + {multiCountError &&
{multiCountError}
} +
+ + + +
+
+
+ )} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx new file mode 100644 index 00000000000..889c24f7127 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; + +interface Props { + onShift: (verticalShift: number, horizontalShift: number) => void; +} + +export function ShiftPanel({ onShift }: Props): JSX.Element { + return ( +
+
+ + + + + Shift + + + + +
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx new file mode 100644 index 00000000000..c68b54e9684 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; + +interface Props { + isDirty: boolean; + status: string; + onSaveAndClose: () => void; + onSave: () => void; + onCancel: () => void; +} + +export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: Props): JSX.Element { + return ( +
+ + + + {isDirty && Unsaved changes} + {status && {status}} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx new file mode 100644 index 00000000000..b4cf732fece --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useCallback, useRef } from 'react'; + +import { PlateTemplate, WellGroup } from '../models'; + +interface Props { + plate: PlateTemplate; + activeGroup: WellGroup | null; + activeTab: string; + colorMap: Map; + onCellAssign: (row: number, col: number) => void; + onCellToggle: (row: number, col: number) => void; +} + +function getRowLabel(row: number): string { + return String.fromCharCode(65 + row); +} + +function getCellColor(row: number, col: number, activeTab: string, plate: PlateTemplate, colorMap: Map): string | undefined { + // Only color cells belonging to groups of the currently active tab type, + // matching the GWT behavior of showing one type's layout at a time. + let color: string | undefined; + for (const group of plate.groups) { + if (group.type === activeTab && group.positions.some(p => p.row === row && p.col === col)) { + color = colorMap.get(group.rowId); + } + } + return color; +} + +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAssign, onCellToggle }: Props): JSX.Element { + const isDragging = useRef(false); + const hasMoved = useRef(false); + const startCell = useRef<{ row: number; col: number } | null>(null); + const dragCells = useRef>(new Set()); + + const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { + if (e.button !== 0) return; + isDragging.current = true; + hasMoved.current = false; + startCell.current = { row, col }; + dragCells.current = new Set([`${row},${col}`]); + e.preventDefault(); + }, []); + + const handleMouseEnter = useCallback((row: number, col: number) => { + if (!isDragging.current) return; + if (!hasMoved.current) { + hasMoved.current = true; + // Deferred: assign the mousedown cell now that we know it's a drag + if (startCell.current) { + onCellAssign(startCell.current.row, startCell.current.col); + } + } + const key = `${row},${col}`; + if (!dragCells.current.has(key)) { + dragCells.current.add(key); + onCellAssign(row, col); + } + }, [onCellAssign]); + + // Called on mouseup over a specific cell — handles click-toggle + const handleCellMouseUp = useCallback((row: number, col: number) => { + if (isDragging.current && !hasMoved.current) { + onCellToggle(row, col); + } + }, [onCellToggle]); + + // Called on the wrapper div — cleans up drag state + const handleDragEnd = useCallback(() => { + isDragging.current = false; + hasMoved.current = false; + startCell.current = null; + dragCells.current = new Set(); + }, []); + + return ( +
+ + + + + ))} + + + + {Array.from({ length: plate.rows }, (_, row) => ( + + + {Array.from({ length: plate.cols }, (_, col) => { + const color = getCellColor(row, col, activeTab, plate, colorMap); + const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); + const location = `${getRowLabel(row)}${col + 1}`; + const groupForCell = plate.groups.find( + g => g.type === activeTab && g.positions.some(p => p.row === row && p.col === col) + ); + const tooltip = groupForCell ? `${location}: ${groupForCell.name}` : location; + return ( + + ))} + +
+ {Array.from({ length: plate.cols }, (_, col) => ( + {col + 1}
{getRowLabel(row)} handleMouseDown(row, col, e)} + onMouseEnter={() => handleMouseEnter(row, col)} + onMouseUp={() => handleCellMouseUp(row, col)} + /> + ); + })} +
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx new file mode 100644 index 00000000000..d297bc1b125 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; + +import { PlateTemplate, computeWarnings } from '../models'; + +interface Props { + plate: PlateTemplate; +} + +export function WarningPanel({ plate }: Props): JSX.Element { + const warnings = computeWarnings(plate); + + return ( +
+
Warnings
+ {warnings.length === 0 ? ( +
No warnings.
+ ) : ( +
    + {warnings.map((w, i) => ( +
  • {w}
  • + ))} +
+ )} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx new file mode 100644 index 00000000000..e31158345b7 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useState } from 'react'; + +import { WellGroup } from '../models'; + +interface Props { + activeGroup: WellGroup | null; + onPropertyChange: (groupRowId: number, key: string, value: string) => void; + onDeleteProperty: (groupRowId: number, key: string) => void; +} + +export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: Props): JSX.Element { + const [newKey, setNewKey] = useState(''); + const [newValue, setNewValue] = useState(''); + + if (!activeGroup) { + return ( +
+ Select a well group to view its properties. +
+ ); + } + + const propEntries = Object.entries(activeGroup.properties); + + const handleAdd = () => { + const key = newKey.trim(); + if (!key) return; + onPropertyChange(activeGroup.rowId, key, newValue); + setNewKey(''); + setNewValue(''); + }; + + return ( +
+
{activeGroup.name}
+ + + {propEntries.length === 0 && ( + + + + )} + {propEntries.map(([key, value]) => ( + + + + + + ))} + + + + + + + + +
No properties defined.
{key} + onPropertyChange(activeGroup.rowId, key, e.target.value)} + /> + + +
+ setNewKey(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} + /> + + setNewValue(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} + /> + + +
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/dev.tsx b/assay/src/client/PlateTemplateDesigner/dev.tsx new file mode 100644 index 00000000000..e0208c5e08a --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/dev.tsx @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { getServerContext } from '@labkey/api'; +import { ServerContextProvider, withAppUser } from '@labkey/components'; + +import { PlateTemplateDesigner } from './PlateTemplateDesigner'; + +const render = () => { + createRoot(document.getElementById('app')).render( + + + + ); +}; + +render(); diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts new file mode 100644 index 00000000000..94e157b72e5 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface Position { + row: number; + col: number; +} + +export interface WellGroup { + rowId: number; + type: string; + name: string; + positions: Position[]; + properties: Record; + allowNewGroups: boolean; +} + +export interface PlateTemplate { + rowId: number; + name: string; + type: string; + rows: number; + cols: number; + groupTypes: string[]; + canCreateGroupsByType: Record; + groups: WellGroup[]; + plateProperties: Record; + typesToDefaultGroups: Record; + showWarningPanel: boolean; + existingTemplateNames: string[]; + copyMode: boolean; + defaultPlateName: string; +} + +export interface SaveTemplateResponse { + rowId: number; +} + +// Matches GWT TemplateGridCell.getWarnings() logic exactly. +export function computeWarnings(plate: PlateTemplate): string[] { + const cellTypes = new Map>(); + for (const group of plate.groups) { + for (const pos of group.positions) { + const key = `${pos.row},${pos.col}`; + if (!cellTypes.has(key)) cellTypes.set(key, new Set()); + cellTypes.get(key).add(group.type); + } + } + const warnings: string[] = []; + for (const [key, types] of cellTypes.entries()) { + const [row, col] = key.split(',').map(Number); + const cellLabel = `${String.fromCharCode(65 + row)}${col + 1}`; + const hasReplicate = types.has('REPLICATE'); + const hasSpecimen = types.has('SPECIMEN'); + const hasControl = types.has('CONTROL'); + if (hasReplicate && !(hasSpecimen || hasControl)) { + warnings.push(`${cellLabel}: Well is a replicate, but is not part of a specimen or control group.`); + } + if (hasControl && hasSpecimen) { + warnings.push(`${cellLabel}: Well is in both a specimen and a control group.`); + } + } + return warnings; +} diff --git a/assay/src/client/PlateTemplateDesigner/typings/main.d.ts b/assay/src/client/PlateTemplateDesigner/typings/main.d.ts new file mode 100644 index 00000000000..b8a4a9133e6 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/typings/main.d.ts @@ -0,0 +1,14 @@ +/** + * @deprecated Use getServerContext() from @labkey/api instead + */ +declare const LABKEY: import('@labkey/api').LabKey; + +/** + * Needed so we can use process.env.NODE_ENV, which is injected by webpack, but not included in the types declared in + * the browser environments. + */ +declare const process: { + env: { + NODE_ENV: string; + }; +}; diff --git a/assay/src/client/entryPoints.js b/assay/src/client/entryPoints.js index b11f0af9722..e0522e96d3c 100644 --- a/assay/src/client/entryPoints.js +++ b/assay/src/client/entryPoints.js @@ -9,5 +9,13 @@ module.exports = { title: 'New Assay Design', permissionClasses: ['org.labkey.api.assay.security.DesignAssayPermission'], path: './src/client/AssayTypeSelect' + }, { + name: 'plateTemplateDesigner', + title: 'Plate Template Designer', + permissionClasses: [ + 'org.labkey.api.security.permissions.InsertPermission', + 'org.labkey.api.assay.security.DesignAssayPermission' + ], + path: './src/client/PlateTemplateDesigner' }] }; \ No newline at end of file diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index e33db16cdd4..d35a2655dc3 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -15,11 +15,15 @@ */ package org.labkey.assay; +import org.apache.commons.io.input.BoundedInputStream; import org.apache.logging.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; import org.labkey.api.action.ApiJsonForm; +import org.labkey.api.action.ApiUsageException; +import org.labkey.api.action.BaseApiAction; import org.labkey.api.action.FormHandlerAction; +import org.labkey.api.action.NullSafeBindException; import org.labkey.api.action.FormViewAction; import org.labkey.api.action.GWTServiceAction; import org.labkey.api.action.Marshal; @@ -32,9 +36,12 @@ import org.labkey.api.action.SpringActionController; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateCustomField; +import org.labkey.api.assay.plate.PlateLayoutHandler; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.PlateSet; import org.labkey.api.assay.plate.PlateType; +import org.labkey.api.assay.plate.Position; +import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.collections.RowMapFactory; import org.labkey.api.data.Container; @@ -62,6 +69,8 @@ import org.labkey.api.util.JsonUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.URLHelper; +import org.labkey.api.module.ModuleHtmlView; +import org.labkey.api.module.ModuleLoader; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ActionURL; import org.labkey.api.view.DataViewSnapshotSelectionForm; @@ -76,6 +85,7 @@ import org.labkey.assay.plate.PlateSetExport; import org.labkey.assay.plate.PlateUrls; import org.labkey.assay.plate.TsvPlateLayoutHandler; +import org.labkey.assay.plate.WellGroupImpl; import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.ReformatOptions; import org.labkey.assay.view.AssayGWTView; @@ -91,6 +101,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -122,7 +133,7 @@ public ActionURL getPlateDetailsURL(Container c) } @RequiresPermission(ReadPermission.class) - public static class BeginAction extends SimpleRedirectAction + public static class BeginAction extends SimpleRedirectAction { @Override public ActionURL getRedirectURL(Object o) @@ -211,7 +222,365 @@ public ActionURL getRedirectURL(RowIdForm form) } @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public class DesignerAction extends SimpleViewAction + public class GetTemplateDefinitionAction extends ReadOnlyApiAction + { + @Override + public Object execute(DesignerForm form, BindException errors) throws Exception + { + String templateName = form.getTemplateName(); + Long plateId = form.getPlateId(); + boolean copyTemplate = form.isCopy(); + + if (templateName == null && plateId != null) + { + Plate plate = PlateManager.get().getPlate(getContainer(), plateId); + if (plate != null) + { + templateName = plate.getName(); + } + } + + Plate template; + PlateLayoutHandler handler; + + if (templateName != null) + { + if (plateId == null) + throw new Exception("plateId is required when templateName is specified."); + template = PlateService.get().getPlate(getContainer(), plateId); + if (template == null) + throw new NotFoundException("Plate '" + templateName + "' does not exist."); + handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); + if (handler == null) + throw new Exception("Plate template type '" + template.getAssayType() + "' does not exist."); + } + else + { + String assayTypeName = form.getAssayType(); + String templateTypeName = form.getTemplateType(); + int rowCount = form.getRowCount(); + int colCount = form.getColCount(); + + handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); + if (handler == null) + throw new Exception("Plate template type '" + assayTypeName + "' does not exist."); + PlateType plateType = PlateService.get().getPlateType(rowCount, colCount); + if (plateType == null) + throw new Exception("The plate type (" + rowCount + " x " + colCount + ") does not exist."); + template = handler.createPlate(templateTypeName, getContainer(), plateType); + } + + // Build groups list + List groups = template.getWellGroups(); + List> groupList = new ArrayList<>(); + for (int i = 0; i < groups.size(); i++) + { + WellGroup group = groups.get(i); + List> positions = new ArrayList<>(); + for (Position position : group.getPositions()) + { + Map pos = new HashMap<>(); + pos.put("row", position.getRow()); + pos.put("col", position.getColumn()); + positions.add(pos); + } + Map groupProps = new HashMap<>(); + for (String propName : group.getPropertyNames()) + { + Object propValue = group.getProperty(propName); + groupProps.put(propName, (propValue == null || propValue == JSONObject.NULL) ? null : propValue.toString()); + } + + int wellGroupId = copyTemplate || group.getRowId() == null ? -1 * (i + 1) : group.getRowId(); + Map g = new HashMap<>(); + g.put("rowId", wellGroupId); + g.put("type", group.getType().name()); + g.put("name", group.getName()); + g.put("positions", positions); + g.put("properties", groupProps); + g.put("allowNewGroups", handler.canCreateNewGroups(group.getType())); + groupList.add(g); + } + + Map templateProperties = new HashMap<>(); + for (String propName : template.getPropertyNames()) + templateProperties.put(propName, template.getProperty(propName) == null ? null : template.getProperty(propName).toString()); + + // Build type list + List typeList = new ArrayList<>(); + for (WellGroup.Type type : handler.getWellGroupTypes()) + typeList.add(type.name()); + + // Build canCreateGroupsByType + Map canCreateGroupsByType = new LinkedHashMap<>(); + for (WellGroup.Type type : handler.getWellGroupTypes()) + canCreateGroupsByType.put(type.name(), handler.canCreateNewGroups(type)); + + // Build typesToDefaultGroups + Map> typesToDefaultGroups = handler.getDefaultGroupsForTypes(); + + // Existing template names in container + List existingTemplateNames = new ArrayList<>(); + for (Plate p : PlateService.get().getPlates(getContainer())) + existingTemplateNames.add(p.getName()); + + long responseRowId = copyTemplate || template.getRowId() == null ? -1 : template.getRowId(); + String defaultPlateName; + if (templateName != null) + { + defaultPlateName = copyTemplate ? getUniqueName(getContainer(), templateName) : templateName; + } + else + { + defaultPlateName = ""; + } + + Map result = new HashMap<>(); + result.put("rowId", responseRowId); + result.put("name", template.getName()); + result.put("type", template.getAssayType()); + result.put("rows", template.getRows()); + result.put("cols", template.getColumns()); + result.put("groupTypes", typeList); + result.put("canCreateGroupsByType", canCreateGroupsByType); + result.put("groups", groupList); + result.put("plateProperties", templateProperties); + result.put("typesToDefaultGroups", typesToDefaultGroups); + result.put("showWarningPanel", handler.showEditorWarningPanel()); + result.put("existingTemplateNames", existingTemplateNames); + result.put("copyMode", copyTemplate); + result.put("defaultPlateName", defaultPlateName); + + return success(result); + } + } + + public static class SaveTemplateForm implements ApiJsonForm + { + private JSONObject _json; + + @Override + public void bindJson(JSONObject json) + { + _json = json; + } + + public JSONObject getJson() + { + return _json != null ? _json : new JSONObject(); + } + } + + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + public static class SaveTemplateAction extends MutatingApiAction + { + private static final int MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB + + @Override + protected BaseApiAction.FormAndErrors populateJacksonForm() throws Exception + { + byte[] bytes; + try (BoundedInputStream bounded = BoundedInputStream.builder() + .setInputStream(getViewContext().getRequest().getInputStream()) + .setMaxCount((long) MAX_BODY_BYTES + 1) + .get()) + { + bytes = bounded.readAllBytes(); + } + if (bytes.length > MAX_BODY_BYTES) + throw new ApiUsageException("Request body exceeds maximum allowed size of 10 MB."); + String body = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + JSONObject jsonObj = body.isEmpty() ? new JSONObject() : new JSONObject(body); + SaveTemplateForm form = new SaveTemplateForm(); + form.bindJson(jsonObj); + return new BaseApiAction.FormAndErrors<>(form, new NullSafeBindException(form, "form")); + } + + @Override + public Object execute(SaveTemplateForm form, BindException errors) throws Exception + { + JSONObject json = form.getJson(); + + long rowId = json.optLong("rowId", -1); + String name = json.getString("name"); + String type = json.getString("type"); + int rows = json.getInt("rows"); + int cols = json.getInt("cols"); + JSONArray groupsJson = json.optJSONArray("groups"); + JSONObject platePropsJson = json.optJSONObject("plateProperties"); + + Map plateProperties = new HashMap<>(); + if (platePropsJson != null) + { + for (String key : platePropsJson.keySet()) + plateProperties.put(key, platePropsJson.get(key)); + } + + boolean updateExisting = false; + Plate plate; + if (rowId > 0) + { + plate = PlateManager.get().getPlate(getContainer(), rowId); + if (plate == null) + throw new NotFoundException("Plate template not found: " + rowId); + // Check for a conflicting name from a different plate + Plate conflict = PlateManager.get().getPlateByName(getContainer(), name); + if (conflict != null && !conflict.getRowId().equals(plate.getRowId())) + throw new ApiUsageException("A plate template with name '" + name + "' already exists."); + if (!plate.getAssayType().equals(type)) + throw new ApiUsageException("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + name + "'"); + if (plate.getRows() != rows || plate.getColumns() != cols) + throw new ApiUsageException("Plate template dimensions cannot be changed for '" + name + "'"); + updateExisting = true; + } + else + { + if (PlateManager.get().getPlateByName(getContainer(), name) != null) + throw new ApiUsageException("A plate template with name '" + name + "' already exists."); + PlateType plateType = PlateService.get().getPlateType(rows, cols); + if (plateType == null) + throw new NotFoundException("The plate type (" + rows + " x " + cols + ") does not exist."); + plate = PlateManager.get().createPlate(getContainer(), type, plateType); + } + + plate.setName(name); + plate.setProperties(plateProperties); + + // Parse groups from JSON + List> submittedGroups = new ArrayList<>(); + Set submittedGroupIds = new HashSet<>(); + if (groupsJson != null) + { + for (int i = 0; i < groupsJson.length(); i++) + { + JSONObject g = groupsJson.getJSONObject(i); + Map gm = new HashMap<>(); + gm.put("rowId", g.optInt("rowId", -1)); + gm.put("type", g.getString("type")); + gm.put("name", g.getString("name")); + JSONArray posArr = g.optJSONArray("positions"); + List positions = new ArrayList<>(); + if (posArr != null) + { + for (int j = 0; j < posArr.length(); j++) + { + JSONObject p = posArr.getJSONObject(j); + positions.add(new int[]{p.getInt("row"), p.getInt("col")}); + } + } + gm.put("positions", positions); + JSONObject propsObj = g.optJSONObject("properties"); + Map props = new HashMap<>(); + if (propsObj != null) + { + for (String key : propsObj.keySet()) + { + Object val = propsObj.get(key); + props.put(key, val == JSONObject.NULL ? null : val); + } + } + gm.put("properties", props); + submittedGroups.add(gm); + if ((int) gm.get("rowId") > 0) + submittedGroupIds.add((int) gm.get("rowId")); + } + } + + // Mark well groups not in submission for deletion + List existingWellGroups = plate.getWellGroups(); + for (WellGroup existingGroup : existingWellGroups) + { + if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) + ((PlateImpl) plate).markWellGroupForDeletion(existingGroup); + } + + // Update or create well groups + for (Map gm : submittedGroups) + { + int gRowId = (int) gm.get("rowId"); + String groupTypeName = (String) gm.get("type"); + WellGroup.Type groupType; + try + { + groupType = WellGroup.Type.valueOf(groupTypeName); + } + catch (IllegalArgumentException e) + { + throw new ApiUsageException("Unknown well group type: '" + groupTypeName + "'"); + } + @SuppressWarnings("unchecked") + List posList = (List) gm.get("positions"); + List positions = new ArrayList<>(); + for (int[] p : posList) + positions.add(plate.getPosition(p[0], p[1])); + + @SuppressWarnings("unchecked") + Map props = (Map) gm.get("properties"); + + WellGroupImpl group; + if (updateExisting && gRowId > 0) + { + WellGroupImpl existing = findExistingWellGroup(existingWellGroups, gRowId); + if (existing == null) + throw new Exception("Well group " + gRowId + " was not found."); + if (existing.getType() != groupType) + throw new Exception("Well group type cannot be changed: " + gm.get("name")); + existing.setName((String) gm.get("name")); + existing.setPositions(positions); + ((PlateImpl) plate).storeWellGroup(existing); + group = existing; + } + else + { + group = (WellGroupImpl) plate.addWellGroup((String) gm.get("name"), groupType, positions); + } + group.setProperties(props); + } + + PlateLayoutHandler plateLayoutHandler = PlateManager.get().getPlateLayoutHandler(plate.getAssayType()); + + if (plateLayoutHandler == null) + { + throw new NotFoundException("Invalid assay type"); + } + plateLayoutHandler.validatePlate(getContainer(), getUser(), plate); + long savedRowId = PlateService.get().save(getContainer(), getUser(), plate); + return success(Map.of("rowId", savedRowId)); + } + + private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) + { + for (WellGroup wg : wellGroups) + { + if (wg.getRowId() != null && wg.getRowId() == rowId) + return (WellGroupImpl) wg; + } + return null; + } + } + + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + public static class DesignerAction extends SimpleViewAction + { + @Override + public ModelAndView getView(DesignerForm form, BindException errors) + { + return ModuleHtmlView.get( + ModuleLoader.getInstance().getModule("assay"), + ModuleHtmlView.getGeneratedViewPath("plateTemplateDesigner") + ); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("editPlateTemplate"); + root.addChild("Plate Editor"); + } + } + + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + public class DesignerGwtAction extends SimpleViewAction { @Override public ModelAndView getView(DesignerForm form, BindException errors) @@ -262,7 +631,7 @@ else if (form.getPlateId() != null) public void addNavTrail(NavTree root) { setHelpTopic("editPlateTemplate"); - root.addChild("Plate Editor"); + root.addChild("Plate Editor (GWT)"); } } diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index 5a255cf82ca..9934c2921de 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -237,7 +237,7 @@ public WellGroup addWellGroup(WellGroupImpl group) } @JsonIgnore - protected WellGroupImpl storeWellGroup(WellGroupImpl group) + public WellGroupImpl storeWellGroup(WellGroupImpl group) { group.setPlate(this); From ca99f5ed0e8ac3cfa68f890d1216aacd71d188e9 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 25 Apr 2026 08:46:49 -0700 Subject: [PATCH 02/18] Auto code review and comments --- .../PlateTemplateDesigner.scss | 100 +++++++++-- .../PlateTemplateDesigner.tsx | 162 +++++++++++++----- .../components/GroupTypesPanel.tsx | 148 +++++++++++++--- .../components/ShiftPanel.tsx | 22 ++- .../components/StatusBar.tsx | 19 +- .../components/TemplateGrid.tsx | 87 +++++++--- .../components/WarningPanel.tsx | 16 +- .../components/WellGroupProperties.tsx | 20 ++- .../client/PlateTemplateDesigner/models.ts | 32 +++- 9 files changed, 479 insertions(+), 127 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 3a790af9729..073aa16695b 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -4,6 +4,34 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ +/* + * ────────────────────────────────────────────────────────────────────────────── + * Overall layout (vertical stack): + * + * ┌──────────────────────────────────────────────────────────┐ + * │ .status-bar (Save & Close | Save | Cancel | status) │ + * ├──────────────────────────────────────────────────────────┤ + * │ .plate-template-designer__header (Plate Name input) │ + * ├──────────────────────────────────────────────────────────┤ + * │ .plate-template-designer__body (horizontal flex) │ + * │ ┌──────────────────────────────┐ ┌───────────────────┐ │ + * │ │ __left (flex: 0 0 auto) │ │ __right (flex:1) │ │ + * │ │ .group-types-panel │ │ right-panel-tabs │ │ + * │ │ ├─ tab strip │ │ + WellGroupProps │ │ + * │ │ └─ tab-body (flex row) │ │ or WarningPanel│ │ + * │ │ ├─ group list │ │ │ │ + * │ │ └─ .plate-grid-area │ │ │ │ + * │ │ ├─ TemplateGrid │ │ │ │ + * │ │ └─ ShiftPanel │ │ │ │ + * │ └──────────────────────────────┘ └───────────────────┘ │ + * └──────────────────────────────────────────────────────────┘ + * + * The left column is sized to its content (fixed width); the right column grows + * to fill remaining space with a minimum width so it doesn't collapse. + * ────────────────────────────────────────────────────────────────────────────── + */ + +// Root container .plate-template-designer { padding: 12px 16px; font-family: Arial, Helvetica, sans-serif; @@ -36,23 +64,27 @@ width: 280px; } + // Horizontal flex: left (group panel + grid) | right (properties / warnings) &__body { display: flex; gap: 16px; align-items: flex-start; } + // Shrinks/grows to fit GroupTypesPanel content; does not flex &__left { flex: 0 0 auto; } + // Fills remaining width; min-width prevents collapse when the window is narrow &__right { flex: 1; min-width: 200px; } } -// StatusBar +// ── StatusBar ───────────────────────────────────────────────────────────────── +// Pinned above the plate header; uses flex to space buttons and status text. .status-bar { display: flex; align-items: center; @@ -89,17 +121,22 @@ } } + // Unsaved-changes indicator — uses role="status" in JSX for screen reader announcements &__dirty { - color: #c80; + color: #7a5800; font-style: italic; } + // Transient save status ("Saving…", "Saved.") — auto-clears after 5 s &__status { color: #555; } } -// GroupTypesPanel +// ── GroupTypesPanel ─────────────────────────────────────────────────────────── +// Outer panel with a tab strip and a two-column flex body: +// left column: scrollable group list + create controls +// right column: TemplateGrid + ShiftPanel (passed as children) .group-types-panel { border: 1px solid #ccc; border-radius: 3px; @@ -130,6 +167,7 @@ } } + // Flex row: [group list] [grid area] — keeps the grid visually docked to the panel &__tab-body { display: flex; align-items: flex-start; @@ -137,12 +175,14 @@ padding: 8px; } + // Group list column — fixed width with a minimum so short lists don't collapse &__groups { flex: 0 0 160px; min-width: 280px; min-height: 60px; } + // Individual group row — clickable to select; keyboard-accessible via tabIndex + onKeyDown &__group { display: flex; align-items: center; @@ -164,6 +204,7 @@ } } + // Colour indicator matching the group's colour on the grid &__color-swatch { display: inline-block; width: 12px; @@ -181,6 +222,7 @@ min-width: 0; } + // In-place rename input (replaces group-name span when renaming is active) &__rename-input { flex: 1; padding: 1px 4px; @@ -190,6 +232,7 @@ min-width: 0; } + // Rename + delete buttons, visible only for the active group row &__group-actions { display: flex; gap: 2px; @@ -220,6 +263,7 @@ } } + // Row that holds the new-group name input + Create / Create multiple buttons &__create-row { display: flex; align-items: center; @@ -227,6 +271,7 @@ margin-top: 6px; } + // Shared by both the (free-text) variants &__new-name-input { flex: 1; padding: 3px 5px; @@ -267,7 +312,10 @@ } } -// Multi-create dialog +// ── Multi-create dialog ─────────────────────────────────────────────────────── +// Modal overlay + dialog for batch-creating N numbered groups. +// The overlay captures click-outside-to-close; the inner dialog stops propagation. +// Focus is trapped inside the dialog while open (see GroupTypesPanel focus-trap effect). .multi-create-dialog { background: #fff; border: 1px solid #ccc; @@ -301,6 +349,7 @@ } } + // Label cells carry id attributes and are referenced via aria-labelledby on the inputs &__label { padding: 6px 16px 6px 0; white-space: nowrap; @@ -335,7 +384,8 @@ } } -// Wrapper for grid + shift panel +// ── Plate grid area ─────────────────────────────────────────────────────────── +// Centres the TemplateGrid and ShiftPanel as a vertical column inside GroupTypesPanel. .plate-grid-area { display: flex; flex-direction: column; @@ -343,7 +393,9 @@ gap: 8px; } -// ShiftPanel +// ── ShiftPanel ──────────────────────────────────────────────────────────────── +// 3×3 CSS grid with arrow buttons at compass positions and a label in the centre. +// Empty corners use placeholders to maintain grid alignment. .shift-panel { &__grid { display: grid; @@ -378,9 +430,12 @@ } } -// TemplateGrid +// ── TemplateGrid ────────────────────────────────────────────────────────────── +// HTML table where each is a well. The user paints groups by clicking or dragging. +// Background colour comes from the colorMap for the active tab's groups (inline style). +// The --active modifier draws an outline around wells belonging to the selected group. .template-grid { - user-select: none; + user-select: none; // Prevents text selection during drag painting &__table { border-collapse: collapse; @@ -422,6 +477,7 @@ filter: brightness(0.88); } + // Indicates cells belonging to the currently active group &--active { outline: 2px solid #333; outline-offset: -2px; @@ -429,7 +485,9 @@ } } -// Right panel tabs +// ── Right panel tabs ────────────────────────────────────────────────────────── +// Tab strip shown in the right column when showWarningPanel is true. +// --warn colours the Warnings tab amber; --active+--warn shifts the indicator colour too. .right-panel-tabs { display: flex; border-bottom: 1px solid #ccc; @@ -455,16 +513,18 @@ } &--warn { - color: #c80; + color: #7a5800; } &--active#{&}--warn { - border-bottom-color: #c80; + border-bottom-color: #7a5800; } } } -// WellGroupProperties +// ── WellGroupProperties ─────────────────────────────────────────────────────── +// Table-based key/value editor for the active group's property bag. +// The --empty modifier styles the "no group selected" placeholder. .well-group-properties { border: 1px solid #ccc; border-radius: 3px; @@ -472,7 +532,7 @@ min-height: 60px; &--empty { - color: #888; + color: #767676; font-style: italic; } @@ -482,7 +542,7 @@ } &__no-props { - color: #888; + color: #767676; font-style: italic; font-size: 12px; } @@ -492,6 +552,7 @@ border-collapse: collapse; } + // Key column — fixed at 40% width, non-wrapping &__key { font-weight: bold; padding: 3px 6px 3px 0; @@ -506,7 +567,7 @@ &__action-cell { padding: 2px 0 2px 6px; white-space: nowrap; - width: 1%; + width: 1%; // Shrink-wraps to button content } &__value { @@ -533,6 +594,7 @@ } } + // Footer row — Add new property; separated visually from existing entries &__add-row td { border-top: 1px solid #eee; padding-top: 6px; @@ -576,7 +638,9 @@ } } -// WarningPanel +// ── WarningPanel ────────────────────────────────────────────────────────────── +// Amber-tinted panel listing validation warnings from computeWarnings(). +// Only rendered when plate.showWarningPanel is true (assay-type controlled). .warning-panel { border: 1px solid #e8a000; border-radius: 3px; @@ -585,12 +649,12 @@ &__title { font-weight: bold; - color: #c80; + color: #7a5800; margin-bottom: 6px; } &__none { - color: #888; + color: #767676; font-style: italic; font-size: 12px; } diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 16297b36f3e..9840731dbb4 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -4,6 +4,7 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; import { ActionURL, Ajax, Utils } from '@labkey/api'; import { PlateTemplate, WellGroup, computeWarnings } from './models'; @@ -16,6 +17,47 @@ import { WarningPanel } from './components/WarningPanel'; import './PlateTemplateDesigner.scss'; +/** + * Root component of the Plate Template Designer. + * + * ─── User workflow ────────────────────────────────────────────────────────────── + * 1. On mount, URL parameters are read (templateName, plateId, assayType, rowCount, + * colCount, copy) and the plate definition is fetched from the server. + * 2. The user selects a group type tab (e.g. CONTROL, SPECIMEN, REPLICATE). + * 3. Within that type, the user selects or creates a named group. + * 4. The user clicks or drags wells on the grid to paint them onto the active group. + * 5. The user optionally edits well group properties in the right panel. + * 6. "Save" persists without leaving; "Save & Close" saves then navigates to returnURL + * (or the plate list). "Cancel" navigates away without saving. + * + * ─── State architecture ───────────────────────────────────────────────────────── + * `plate` is the single source of truth for all template data. All mutations go + * through `setPlate` with functional updaters to avoid stale-closure bugs. + * + * `activeGroup` is a denormalized mirror of the currently selected group, kept in + * sync with `plate` via the sync effect below. It exists separately because: + * - Callbacks that use `setPlate(prev => ...)` don't have access to the current + * group data inside the updater; they use `activeGroup` from their closure. + * - Components that show the active group (WellGroupProperties, TemplateGrid + * cell highlighting) need a stable reference that doesn't require traversing + * `plate.groups` on every access. + * + * ─── ID conventions ───────────────────────────────────────────────────────────── + * Server-assigned group IDs are positive integers. Client-side created groups + * receive temporary negative IDs (nextGroupIdRef counts down from -1). This ensures + * new groups never collide with existing ones before the first save. The server + * replaces all IDs with permanent values on save; the client does not update + * individual group IDs — only the top-level `plate.rowId` is updated after save. + * + * ─── Cell interaction ─────────────────────────────────────────────────────────── + * Two cell callbacks are distinguished: + * `handleCellAssign` — idempotent add; also evicts the cell from any other group + * of the same type (one cell can only belong to one group per type). Used during + * drag operations. + * `handleCellToggle` — pure on/off; does not steal from siblings. Used for + * single-click (no drag movement). + */ + const COLORS = [ '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac', @@ -40,9 +82,9 @@ export function PlateTemplateDesigner(): JSX.Element { const [status, setStatus] = useState(''); const [colorMap, setColorMap] = useState>(new Map()); const [error, setError] = useState(null); - const plateNameRef = useRef(''); + const plateNameRef = useRef(''); // Mirrors plate.name; used in save-success to update URL without stale closure const statusTimerRef = useRef | null>(null); - const nextGroupIdRef = useRef(-1); + const nextGroupIdRef = useRef(-1); // Temporary negative IDs for client-created groups (see ID conventions above) useEffect(() => { const templateName = ActionURL.getParameter('templateName'); @@ -233,32 +275,42 @@ export function PlateTemplateDesigner(): JSX.Element { window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); }, []); - const handleSave = useCallback(() => { - if (!plate) return; + /** + * Shared Ajax save logic. Takes the plate snapshot and a success callback to avoid + * duplicating the request setup and failure handler in handleSave / handleSaveAndClose. + * The plate is passed as a parameter (rather than closed over) so callers can pass the + * latest snapshot without worrying about stale state. + */ + const requestSave = useCallback((currentPlate: PlateTemplate, onSuccess: (response: { data: { rowId: number } }) => void) => { setStatus('Saving...'); Ajax.request({ url: ActionURL.buildURL('plate', 'saveTemplate.api'), method: 'POST', - jsonData: plate, - success: Utils.getCallbackWrapper((response: { data: { rowId: number } }) => { - const rowId = response.data.rowId; - setIsDirty(false); - setPlate(prev => prev ? { ...prev, rowId } : null); - // Update URL to canonical form so a refresh reloads this plate - const url = new URL(window.location.href); - url.search = ''; - url.searchParams.set('templateName', plate.name); - url.searchParams.set('plateId', String(rowId)); - window.history.replaceState(null, '', url.toString()); - setStatus('Saved.'); - if (statusTimerRef.current) clearTimeout(statusTimerRef.current); - statusTimerRef.current = setTimeout(() => setStatus(''), 5000); - }), + jsonData: currentPlate, + success: Utils.getCallbackWrapper(onSuccess), failure: Utils.getCallbackWrapper((response: any) => { setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); }, null, true), }); - }, [plate]); + }, []); + + const handleSave = useCallback(() => { + if (!plate) return; + requestSave(plate, (response) => { + const rowId = response.data.rowId; + setIsDirty(false); + setPlate(prev => prev ? { ...prev, rowId } : null); + // Update URL to canonical form so a refresh reloads this plate + const url = new URL(window.location.href); + url.search = ''; + url.searchParams.set('templateName', plateNameRef.current); + url.searchParams.set('plateId', String(rowId)); + window.history.replaceState(null, '', url.toString()); + setStatus('Saved.'); + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + statusTimerRef.current = setTimeout(() => setStatus(''), 5000); + }); + }, [plate, requestSave]); const handleSaveAndClose = useCallback(() => { if (!plate) return; @@ -266,20 +318,11 @@ export function PlateTemplateDesigner(): JSX.Element { navigateAway(); return; } - setStatus('Saving...'); - Ajax.request({ - url: ActionURL.buildURL('plate', 'saveTemplate.api'), - method: 'POST', - jsonData: plate, - success: Utils.getCallbackWrapper(() => { - setIsDirty(false); - navigateAway(); - }), - failure: Utils.getCallbackWrapper((response: any) => { - setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); - }, null, true), + requestSave(plate, () => { + setIsDirty(false); + navigateAway(); }); - }, [plate, isDirty, navigateAway]); + }, [plate, isDirty, navigateAway, requestSave]); const handleCancel = useCallback(() => { navigateAway(); @@ -297,7 +340,17 @@ export function PlateTemplateDesigner(): JSX.Element { return () => window.removeEventListener('beforeunload', handler); }, [isDirty]); - // Keep activeGroup in sync when plate changes + // Keep activeGroup in sync when plate changes. + // + // Most plate mutations go through setPlate(prev => ...) updaters which don't have + // access to the current activeGroup. After each plate update, this effect finds the + // matching group by rowId and refreshes activeGroup so downstream components (e.g. + // WellGroupProperties, TemplateGrid cell highlight) see the latest data. + // + // activeGroup is intentionally excluded from the deps array: adding it would cause + // an infinite loop (effect sets activeGroup → triggers effect → sets activeGroup …). + // handleDeleteGroup handles the "group no longer exists" case by explicitly setting + // activeGroup to null before this effect can run. useEffect(() => { if (activeGroup && plate) { const updated = plate.groups.find(g => g.rowId === activeGroup.rowId); @@ -346,6 +399,8 @@ export function PlateTemplateDesigner(): JSX.Element { onDeleteGroup={handleDeleteGroup} onRenameGroup={handleRenameGroup} > + {/* The grid and shift panel are passed as children so they render + inside GroupTypesPanel's flex row, visually adjacent to the group list. */}
+ {/* Right panel: WellGroupProperties and (if enabled) a Warnings tab. + The tab strip only renders when showWarningPanel is true; otherwise + WellGroupProperties fills the full right column without tabs. */}
{plate.showWarningPanel && ( -
+
)} {(!plate.showWarningPanel || rightTab === 'properties') && ( - +
+ +
)} {plate.showWarningPanel && rightTab === 'warnings' && ( - +
+ +
)}
diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index fa4d0a21275..19f05b1ea36 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -4,6 +4,7 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React, { useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; @@ -20,6 +21,48 @@ interface Props { children?: React.ReactNode; } +/** + * Left-hand panel that manages group types (tabs) and individual well groups. + * + * ─── Layout ──────────────────────────────────────────────────────────────────── + * The panel is split into two side-by-side areas via a flex row: + * Left column – the group list + create controls (fixed width) + * Right area – children (the TemplateGrid + ShiftPanel), passed in from the parent + * + * This composition pattern keeps the grid visually anchored inside the panel boundary + * while letting the tab strip and group list scroll independently. + * + * ─── Tab switching ───────────────────────────────────────────────────────────── + * Each tab corresponds to a group type key (e.g. "CONTROL", "SPECIMEN", "REPLICATE"). + * Switching tabs: + * - Clears the active group selection (the parent sets activeGroup to null). + * - Updates the grid to show only that type's colour layout. + * - Resets the create-name input to the first unused default for the new type. + * + * ─── Group selection ─────────────────────────────────────────────────────────── + * Clicking a group row makes it the "active group". Once active, clicking or + * dragging cells on the TemplateGrid paints them onto that group. The active group + * is highlighted with a blue border and shows inline rename/delete actions. + * + * ─── Creating groups ─────────────────────────────────────────────────────────── + * Some group types come with predefined slot names (`typesToDefaultGroups`), e.g. + * "Virus" and "Cell Control" for certain assay types. While unused defaults remain, + * a + * appears for custom names. + * + * "Create multiple…" opens a modal dialog that batch-creates N numbered groups + * (e.g. "Sample 1" through "Sample 8") from a base name and count. Useful for + * assays with many specimens or replicates. + * + * ─── Renaming ────────────────────────────────────────────────────────────────── + * The pencil button activates an inline rename input in place of the group name. + * Blur or Enter commits the change; Escape discards it. + * + * ─── Modal focus trap ────────────────────────────────────────────────────────── + * When the multi-create dialog opens, a useEffect traps Tab/Shift-Tab focus inside + * the dialog and moves initial focus to the first focusable element. Escape closes + * the dialog from anywhere within it. + */ export function GroupTypesPanel({ plate, activeGroup, @@ -40,10 +83,14 @@ export function GroupTypesPanel({ const [multiCount, setMultiCount] = useState('2'); const [multiCountError, setMultiCountError] = useState(''); const multiBaseNameRef = useRef(null); + const dialogRef = useRef(null); - const groupsOfType = plate.groups.filter(g => g.type === activeTab); + // Stable derived list — memoized so useMemo and useEffect deps are stable. + const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); const canAdd = plate.canCreateGroupsByType?.[activeTab] ?? false; + // Predefined slot names not yet occupied by an existing group of this type. + // Drives the toggle in the create row. const unusedDefaults = useMemo(() => { const defaults = plate.typesToDefaultGroups[activeTab] ?? []; return defaults.filter(d => !groupsOfType.some(g => g.name === d)); @@ -62,6 +109,34 @@ export function GroupTypesPanel({ } }, [unusedDefaults]); // eslint-disable-line react-hooks/exhaustive-deps + // Focus trap for multi-create dialog + useEffect(() => { + if (!multiCreateOpen || !dialogRef.current) return; + const dialog = dialogRef.current; + const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setMultiCreateOpen(false); + return; + } + if (e.key !== 'Tab') return; + const focusable = getFocusable(); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last?.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first?.focus(); } + } + }; + + dialog.addEventListener('keydown', handleKeyDown); + getFocusable()[0]?.focus(); + return () => dialog.removeEventListener('keydown', handleKeyDown); + }, [multiCreateOpen]); + const handleCreate = () => { const trimmed = newGroupName.trim(); if (!trimmed) return; @@ -73,8 +148,7 @@ export function GroupTypesPanel({ setMultiCount('2'); setMultiCountError(''); setMultiCreateOpen(true); - // Focus the base name input after the modal renders - setTimeout(() => multiBaseNameRef.current?.select(), 0); + // Focus is handled by the focus-trap effect above }; const handleMultiCreate = () => { @@ -112,21 +186,27 @@ export function GroupTypesPanel({ return (
-
+
{plate.groupTypes.map(type => ( ))}
-
+
{groupsOfType.map(group => { const color = colorMap.get(group.rowId); @@ -135,11 +215,17 @@ export function GroupTypesPanel({ return (
{ if (!isRenaming) onGroupSelect(group); }} + onKeyDown={e => { + if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onGroupSelect(group); + } + }} > setRenameValue(e.target.value)} @@ -161,21 +248,24 @@ export function GroupTypesPanel({ ) : ( {group.name} )} + {/* Rename/delete actions appear only on the active group row */} {isActive && !isRenaming && group.allowNewGroups && ( )} @@ -184,8 +274,14 @@ export function GroupTypesPanel({ })} {canAdd && (
+ {/* + * Show a once all + * defaults are consumed or if there are none defined for this type. + */} {unusedDefaults.length > 0 ? ( {multiCreateOpen && (
setMultiCreateOpen(false)}> -
e.stopPropagation()}> -
Create Multiple Groups
+
e.stopPropagation()} + > +
Create Multiple Groups
- + - + diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx index 889c24f7127..43c3d3cb2a0 100644 --- a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx @@ -9,18 +9,32 @@ interface Props { onShift: (verticalShift: number, horizontalShift: number) => void; } +/** + * A compass-rose control that shifts all wells of the currently active group type one step in any + * cardinal direction. The shift wraps around plate edges (toroidal), so wells that fall off the + * bottom reappear at the top, etc. + * + * Sign convention (matches the modular arithmetic in PlateTemplateDesigner.handleShift): + * verticalShift > 0 → cells move UP (row index decreases: row = (row - shift + rows) % rows) + * verticalShift < 0 → cells move DOWN + * horizontalShift > 0 → cells move LEFT (col index decreases) + * horizontalShift < 0 → cells move RIGHT + * + * Shifts apply to every group of the active type simultaneously, preserving relative layout + * between groups. Only the active tab's type is affected; other types are unchanged. + */ export function ShiftPanel({ onShift }: Props): JSX.Element { return (
- + - + Shift - + - +
diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index c68b54e9684..9afc2600133 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -13,6 +13,21 @@ interface Props { onCancel: () => void; } +/** + * Persistent action bar pinned to the top of the designer. + * + * Button behavior: + * - "Save & Close": saves if dirty, then navigates to the returnURL (or plate list). + * Always enabled so users can leave even when clean. + * - "Save": persists the current state and updates the page URL to the canonical + * ?templateName=...&plateId=... form so a browser refresh reloads the same plate. + * Disabled when the plate is clean to prevent redundant requests. + * - "Cancel": navigates away without saving. The browser's beforeunload handler + * will prompt if there are unsaved changes. + * + * The "Unsaved changes" indicator and transient status text ("Saving…", "Saved.") + * use `role="status"` so screen readers announce them as they appear. + */ export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: Props): JSX.Element { return (
@@ -25,8 +40,8 @@ export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: - {isDirty && Unsaved changes} - {status && {status}} + {isDirty ? 'Unsaved changes' : ''} + {status}
); } diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index b4cf732fece..49f03c5e388 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -3,7 +3,8 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; +import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; @@ -20,24 +21,61 @@ function getRowLabel(row: number): string { return String.fromCharCode(65 + row); } -function getCellColor(row: number, col: number, activeTab: string, plate: PlateTemplate, colorMap: Map): string | undefined { - // Only color cells belonging to groups of the currently active tab type, - // matching the GWT behavior of showing one type's layout at a time. - let color: string | undefined; - for (const group of plate.groups) { - if (group.type === activeTab && group.positions.some(p => p.row === row && p.col === col)) { - color = colorMap.get(group.rowId); - } - } - return color; -} - +/** + * A scrollable well grid that lets the user paint cells onto the active well group. + * + * ─── Coloring ────────────────────────────────────────────────────────────────── + * Only wells belonging to groups of the *active tab type* are coloured. Wells from + * other types are invisible in the current view. This matches the original GWT + * behaviour of presenting one group type at a time. + * + * ─── Drag / click interaction ────────────────────────────────────────────────── + * Cell assignment uses a three-phase state machine tracked entirely via refs + * (no re-renders on drag): + * + * Phase 1 – mousedown on a cell: + * Enter drag mode. Record the start cell. Do NOT assign it yet — we first need + * to know whether the user is clicking (toggle) or dragging (assign-only). + * + * Phase 2 – mouseenter a *different* cell while dragging: + * We now know it's a drag. Retroactively assign the original start cell + * (deferred assign), then assign each subsequently entered cell. + * `dragCells` deduplicates entries so fast mouse movement can't assign the + * same cell twice. + * + * Phase 3 – mouseup: + * If the pointer never left the start cell (hasMoved === false), treat the + * interaction as a click and toggle that cell (add if absent, remove if present). + * Either way, reset all drag state. + * + * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck + * drag state when the pointer exits the grid. + * + * `onCellAssign` is idempotent (ignores duplicates) and also removes the assigned + * cell from any other group of the same type, enforcing the one-group-per-cell-per- + * type constraint. `onCellToggle` does a pure add/remove with no stealing. + */ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAssign, onCellToggle }: Props): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); const dragCells = useRef>(new Set()); + // Pre-compute a "row,col" → {color, groupName} map for the active tab type. + // This lets each cell do an O(1) lookup rather than scanning all groups and + // positions on every render (which would be O(groups × positions) per cell). + const positionMap = useMemo(() => { + const map = new Map(); + for (const group of plate.groups) { + if (group.type !== activeTab) continue; + const color = colorMap.get(group.rowId) ?? '#f5f5f5'; + for (const p of group.positions) { + map.set(`${p.row},${p.col}`, { color, groupName: group.name }); + } + } + return map; + }, [plate, activeTab, colorMap]); + const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { if (e.button !== 0) return; isDragging.current = true; @@ -51,7 +89,8 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs if (!isDragging.current) return; if (!hasMoved.current) { hasMoved.current = true; - // Deferred: assign the mousedown cell now that we know it's a drag + // Deferred assign: now that we know this is a drag, assign the cell + // the user originally pressed down on. if (startCell.current) { onCellAssign(startCell.current.row, startCell.current.col); } @@ -85,30 +124,26 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs + ))} {Array.from({ length: plate.rows }, (_, row) => ( - + {Array.from({ length: plate.cols }, (_, col) => { - const color = getCellColor(row, col, activeTab, plate, colorMap); + const entry = positionMap.get(`${row},${col}`); const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); const location = `${getRowLabel(row)}${col + 1}`; - const groupForCell = plate.groups.find( - g => g.type === activeTab && g.positions.some(p => p.row === row && p.col === col) - ); - const tooltip = groupForCell ? `${location}: ${groupForCell.name}` : location; + const tooltip = entry ? `${location}: ${entry.groupName}` : location; return ( @@ -75,6 +91,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro className="well-group-properties__new-key" type="text" placeholder="Property name" + aria-label="Property name" value={newKey} onChange={e => setNewKey(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} @@ -85,6 +102,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro className="well-group-properties__new-value" type="text" placeholder="Value" + aria-label="Property value" value={newValue} onChange={e => setNewValue(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts index 94e157b72e5..71036b33950 100644 --- a/assay/src/client/PlateTemplateDesigner/models.ts +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -10,12 +10,12 @@ export interface Position { } export interface WellGroup { - rowId: number; - type: string; + rowId: number; // Positive = server-assigned; negative = client-side temp ID (see nextGroupIdRef) + type: string; // Group type key, e.g. "CONTROL", "SPECIMEN", "REPLICATE" name: string; positions: Position[]; properties: Record; - allowNewGroups: boolean; + allowNewGroups: boolean; // Whether the user can create/rename/delete groups of this type } export interface PlateTemplate { @@ -24,14 +24,14 @@ export interface PlateTemplate { type: string; rows: number; cols: number; - groupTypes: string[]; - canCreateGroupsByType: Record; + groupTypes: string[]; // Ordered list of type keys; drives the tab strip + canCreateGroupsByType: Record; // Which types expose the create-group UI groups: WellGroup[]; plateProperties: Record; - typesToDefaultGroups: Record; - showWarningPanel: boolean; + typesToDefaultGroups: Record; // Predefined slot names per type (e.g. "Virus", "Cell Control") + showWarningPanel: boolean; // Set by the server based on assay type config existingTemplateNames: string[]; - copyMode: boolean; + copyMode: boolean; // True when the plate was loaded as a copy; starts the editor in dirty state defaultPlateName: string; } @@ -39,8 +39,22 @@ export interface SaveTemplateResponse { rowId: number; } -// Matches GWT TemplateGridCell.getWarnings() logic exactly. +/** + * Replicates the GWT TemplateGridCell.getWarnings() logic exactly. + * + * Two conditions produce warnings: + * 1. A REPLICATE well that belongs to neither a SPECIMEN nor a CONTROL group is almost certainly + * a configuration error — replicates are only meaningful relative to a specimen or control. + * 2. A well assigned to both a SPECIMEN and a CONTROL group is contradictory; those roles are + * mutually exclusive in LabKey assay semantics. + * + * Notes: + * - Warnings are per-cell, not per-group. + * - A cell can appear in multiple groups of different types (e.g. SPECIMEN + REPLICATE together is fine). + * - Cell labels use spreadsheet notation: row → letter (A=0, B=1, …), col → 1-based number. + */ export function computeWarnings(plate: PlateTemplate): string[] { + // Build a map from cell position → set of group types that include it. const cellTypes = new Map>(); for (const group of plate.groups) { for (const pos of group.positions) { From a4b284f3b5a96c6243434389a43e9cf905bbc175 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 25 Apr 2026 12:06:16 -0700 Subject: [PATCH 03/18] Test fixes --- .../PlateTemplateDesigner.scss | 11 + .../PlateTemplateDesigner.tsx | 90 +++++-- .../components/GroupTypesPanel.tsx | 236 ++++++++++-------- .../components/TemplateGrid.tsx | 52 ++-- 4 files changed, 234 insertions(+), 155 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 073aa16695b..5de546210cf 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -230,6 +230,17 @@ border-radius: 3px; font-size: 12px; min-width: 0; + + &--error { + border-color: #c00; + } + } + + // Validation error shown below the create row or the rename input + &__name-error { + color: #c00; + font-size: 12px; + margin-top: 2px; } // Rename + delete buttons, visible only for the active group row diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 9840731dbb4..8d6ad0ffe8c 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames'; import { ActionURL, Ajax, Utils } from '@labkey/api'; -import { PlateTemplate, WellGroup, computeWarnings } from './models'; +import { PlateTemplate, Position, WellGroup, computeWarnings } from './models'; import { StatusBar } from './components/StatusBar'; import { GroupTypesPanel } from './components/GroupTypesPanel'; import { ShiftPanel } from './components/ShiftPanel'; @@ -85,6 +85,10 @@ export function PlateTemplateDesigner(): JSX.Element { const plateNameRef = useRef(''); // Mirrors plate.name; used in save-success to update URL without stale closure const statusTimerRef = useRef | null>(null); const nextGroupIdRef = useRef(-1); // Temporary negative IDs for client-created groups (see ID conventions above) + // Always-current ref so callbacks can read the latest activeGroup without stale-closure bugs. + const activeGroupRef = useRef(null); + activeGroupRef.current = activeGroup; + const nextColorIndexRef = useRef(0); // Monotonically increasing; never decrements on delete so colors stay unique useEffect(() => { const templateName = ActionURL.getParameter('templateName'); @@ -113,6 +117,11 @@ export function PlateTemplateDesigner(): JSX.Element { plateNameRef.current = plate.defaultPlateName || plate.name || ''; setPlate({ ...plate, name: plateNameRef.current }); setColorMap(assignColors(plate.groups)); + nextColorIndexRef.current = plate.groups.length; + // Initialize below the minimum server rowId to avoid collisions. + // Server IDs should be positive, but guard against zero or negative values. + const minRowId = plate.groups.reduce((min, g) => Math.min(min, g.rowId), 0); + nextGroupIdRef.current = Math.min(-1, minRowId - 1); setActiveTab(plate.groupTypes[0] ?? ''); if (plate.copyMode) setIsDirty(true); }), @@ -132,48 +141,75 @@ export function PlateTemplateDesigner(): JSX.Element { setActiveGroup(group); }, []); - const handleCellAssign = useCallback((row: number, col: number) => { - if (!activeGroup || !plate) return; + // Called on every mouseenter during a drag with the rectangle defined by the + // mousedown cell and the current cell. preDragPositions is the snapshot of the + // active group's positions taken at mousedown (in TemplateGrid), before any drag + // events can modify state. + // + // Select mode (drag started on an empty cell): adds the rectangle to the group's + // pre-drag positions, so existing wells outside the rectangle are preserved. + // Also evicts rectangle cells from sibling groups of the same type. + // + // Unselect mode (drag started on a cell already in the group): removes all + // rectangle cells from the pre-drag positions without affecting other groups. + const handleDragRect = useCallback((r1: number, c1: number, r2: number, c2: number, isUnselect: boolean, preDragPositions: Position[]) => { + const activeGroup = activeGroupRef.current; + if (!activeGroup) return; + const minRow = Math.min(r1, r2); + const maxRow = Math.max(r1, r2); + const minCol = Math.min(c1, c2); + const maxCol = Math.max(c1, c2); + const rectPositions: Position[] = []; + for (let r = minRow; r <= maxRow; r++) { + for (let c = minCol; c <= maxCol; c++) { + rectPositions.push({ row: r, col: c }); + } + } + const rectKeys = new Set(rectPositions.map(p => `${p.row},${p.col}`)); setPlate(prev => { if (!prev) return null; + // Look up the active group's current type from prev to avoid stale-closure issues. + const currentType = prev.groups.find(g => g.rowId === activeGroup.rowId)?.type; const updatedGroups = prev.groups.map(g => { if (g.rowId === activeGroup.rowId) { - const alreadyHas = g.positions.some(p => p.row === row && p.col === col); - if (alreadyHas) return g; - return { ...g, positions: [...g.positions, { row, col }] }; + if (isUnselect) { + // Remove rect from pre-drag snapshot + return { ...g, positions: preDragPositions.filter(p => !rectKeys.has(`${p.row},${p.col}`)) }; + } + // Add rect to pre-drag snapshot (union, deduped) + const preDragKeys = new Set(preDragPositions.map(p => `${p.row},${p.col}`)); + const added = rectPositions.filter(p => !preDragKeys.has(`${p.row},${p.col}`)); + return { ...g, positions: [...preDragPositions, ...added] }; } - if (g.type === activeGroup.type) { - // Remove from other groups of the same type to avoid conflicts - return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + if (!isUnselect && currentType !== undefined && g.type === currentType) { + // Evict rectangle cells from sibling groups of the same type + return { ...g, positions: g.positions.filter(p => !rectKeys.has(`${p.row},${p.col}`)) }; } return g; }); return { ...prev, groups: updatedGroups }; }); setIsDirty(true); - }, [activeGroup, plate]); + }, []); + // Pure toggle: add the cell if absent, remove it if present const handleCellToggle = useCallback((row: number, col: number) => { - if (!activeGroup || !plate) return; + const activeGroup = activeGroupRef.current; + if (!activeGroup) return; setPlate(prev => { if (!prev) return null; const updatedGroups = prev.groups.map(g => { - if (g.rowId === activeGroup.rowId) { - const hasCell = g.positions.some(p => p.row === row && p.col === col); - if (hasCell) { - return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; - } - return { ...g, positions: [...g.positions, { row, col }] }; - } - if (g.type === activeGroup.type) { + if (g.rowId !== activeGroup.rowId) return g; + const hasCell = g.positions.some(p => p.row === row && p.col === col); + if (hasCell) { return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; } - return g; + return { ...g, positions: [...g.positions, { row, col }] }; }); return { ...prev, groups: updatedGroups }; }); setIsDirty(true); - }, [activeGroup, plate]); + }, []); const handleAddGroup = useCallback((type: string, name: string) => { if (!plate) return; @@ -187,9 +223,10 @@ export function PlateTemplateDesigner(): JSX.Element { allowNewGroups: plate.canCreateGroupsByType?.[type] ?? false, }; setPlate(prev => prev ? { ...prev, groups: [...prev.groups, newGroup] } : null); + const colorIndex = nextColorIndexRef.current++; setColorMap(prev => { const next = new Map(prev); - next.set(rowId, COLORS[prev.size % COLORS.length]); + next.set(rowId, COLORS[colorIndex % COLORS.length]); return next; }); setActiveGroup(newGroup); @@ -352,9 +389,12 @@ export function PlateTemplateDesigner(): JSX.Element { // handleDeleteGroup handles the "group no longer exists" case by explicitly setting // activeGroup to null before this effect can run. useEffect(() => { - if (activeGroup && plate) { + if (!plate) return; + if (activeGroup) { const updated = plate.groups.find(g => g.rowId === activeGroup.rowId); - if (updated) setActiveGroup(updated); + if (updated) { + setActiveGroup(updated); + } } }, [plate]); // eslint-disable-line react-hooks/exhaustive-deps @@ -407,7 +447,7 @@ export function PlateTemplateDesigner(): JSX.Element { activeGroup={activeGroup} activeTab={activeTab} colorMap={colorMap} - onCellAssign={handleCellAssign} + onDragRect={handleDragRect} onCellToggle={handleCellToggle} /> diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 19f05b1ea36..8b3aae7ae00 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -78,6 +78,7 @@ export function GroupTypesPanel({ const [newGroupName, setNewGroupName] = useState(''); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); + const [renameError, setRenameError] = useState(null); const [multiCreateOpen, setMultiCreateOpen] = useState(false); const [multiBaseName, setMultiBaseName] = useState(''); const [multiCount, setMultiCount] = useState('2'); @@ -89,6 +90,9 @@ export function GroupTypesPanel({ const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); const canAdd = plate.canCreateGroupsByType?.[activeTab] ?? false; + // True when the current create-input value is already taken by a group of this type. + const createNameConflicts = newGroupName.trim() !== '' && groupsOfType.some(g => g.name === newGroupName.trim()); + // Predefined slot names not yet occupied by an existing group of this type. // Drives the toggle in the create row. const unusedDefaults = useMemo(() => { @@ -139,8 +143,9 @@ export function GroupTypesPanel({ const handleCreate = () => { const trimmed = newGroupName.trim(); - if (!trimmed) return; + if (!trimmed || createNameConflicts) return; onAddGroup(activeTab, trimmed); + setNewGroupName(''); }; const openMultiCreate = () => { @@ -159,9 +164,14 @@ export function GroupTypesPanel({ } const baseName = multiBaseName.trim(); if (!baseName) return; - for (let i = 1; i <= count; i++) { - onAddGroup(activeTab, `${baseName} ${i}`); + const existingNames = new Set(groupsOfType.map(g => g.name)); + const namesToCreate = Array.from({ length: count }, (_, i) => `${baseName} ${i + 1}`) + .filter(name => !existingNames.has(name)); + if (namesToCreate.length === 0) { + setMultiCountError(`All ${count} generated name${count === 1 ? '' : 's'} already exist in this type.`); + return; } + namesToCreate.forEach(name => onAddGroup(activeTab, name)); setMultiCreateOpen(false); }; @@ -176,12 +186,25 @@ export function GroupTypesPanel({ e.stopPropagation(); setRenamingId(group.rowId); setRenameValue(group.name); + setRenameError(null); }; - const handleRenameCommit = (rowId: number) => { + // revertOnConflict=true: silently discard (used on blur so moving focus away doesn't leave the input frozen). + // revertOnConflict=false: show an inline error and keep the input open (used on Enter so the user sees feedback). + const handleRenameCommit = (rowId: number, revertOnConflict: boolean) => { const trimmed = renameValue.trim(); + if (trimmed && groupsOfType.some(g => g.rowId !== rowId && g.name === trimmed)) { + if (revertOnConflict) { + setRenamingId(null); + setRenameError(null); + } else { + setRenameError(`"${trimmed}" is already used by another group of this type.`); + } + return; + } if (trimmed) onRenameGroup(rowId, trimmed); setRenamingId(null); + setRenameError(null); }; return ( @@ -213,108 +236,125 @@ export function GroupTypesPanel({ const isActive = activeGroup?.rowId === group.rowId; const isRenaming = renamingId === group.rowId; return ( -
{ if (!isRenaming) onGroupSelect(group); }} - onKeyDown={e => { - if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - onGroupSelect(group); - } - }} - > - - {isRenaming ? ( - setRenameValue(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') handleRenameCommit(group.rowId); - if (e.key === 'Escape') setRenamingId(null); - }} - onBlur={() => handleRenameCommit(group.rowId)} - onClick={e => e.stopPropagation()} + +
{ if (!isRenaming) onGroupSelect(group); }} + onKeyDown={e => { + if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onGroupSelect(group); + } + }} + > + - ) : ( - {group.name} - )} - {/* Rename/delete actions appear only on the active group row */} - {isActive && !isRenaming && group.allowNewGroups && ( - - - - + aria-describedby={renameError ? 'rename-error' : undefined} + aria-invalid={!!renameError} + className={classNames('group-types-panel__rename-input', { + 'group-types-panel__rename-input--error': !!renameError, + })} + value={renameValue} + onChange={e => { setRenameValue(e.target.value); setRenameError(null); }} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameCommit(group.rowId, false); + if (e.key === 'Escape') { setRenamingId(null); setRenameError(null); } + }} + onBlur={() => handleRenameCommit(group.rowId, true)} + onClick={e => e.stopPropagation()} + /> + ) : ( + {group.name} + )} + {/* Rename/delete actions appear only on the active group row */} + {isActive && !isRenaming && group.allowNewGroups && ( + + + + + )} +
+ {isRenaming && renameError && ( +
{renameError}
)} -
+ ); })} {canAdd && ( -
- {/* - * Show a once all - * defaults are consumed or if there are none defined for this type. - */} - {unusedDefaults.length > 0 ? ( - while predefined defaults remain (prevents typos and + * ensures canonical names). Switch to a free-text once all + * defaults are consumed or if there are none defined for this type. + */} + {unusedDefaults.length > 0 ? ( + + ) : ( + setNewGroupName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newGroupName.trim() && !createNameConflicts) handleCreate(); }} + /> + )} + + +
+ {createNameConflicts && ( +
+ A group named "{newGroupName.trim()}" already exists in this type. +
)} - - - + )} {children} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index 49f03c5e388..6056bd1f949 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -6,14 +6,14 @@ import React, { useCallback, useMemo, useRef } from 'react'; import classNames from 'classnames'; -import { PlateTemplate, WellGroup } from '../models'; +import { PlateTemplate, Position, WellGroup } from '../models'; interface Props { plate: PlateTemplate; activeGroup: WellGroup | null; activeTab: string; colorMap: Map; - onCellAssign: (row: number, col: number) => void; + onDragRect: (r1: number, c1: number, r2: number, c2: number, isUnselect: boolean, preDragPositions: Position[]) => void; onCellToggle: (row: number, col: number) => void; } @@ -34,14 +34,14 @@ function getRowLabel(row: number): string { * (no re-renders on drag): * * Phase 1 – mousedown on a cell: - * Enter drag mode. Record the start cell. Do NOT assign it yet — we first need - * to know whether the user is clicking (toggle) or dragging (assign-only). + * Enter drag mode. Record the start cell. Do NOT assign anything yet — we + * first need to know whether the user is clicking (toggle) or dragging (rect). * * Phase 2 – mouseenter a *different* cell while dragging: - * We now know it's a drag. Retroactively assign the original start cell - * (deferred assign), then assign each subsequently entered cell. - * `dragCells` deduplicates entries so fast mouse movement can't assign the - * same cell twice. + * We now know it's a drag. Call onDragRect with the axis-aligned rectangle + * defined by the mousedown cell and the current cell, plus the drag mode + * (select vs unselect) determined at mousedown. The parent replaces or removes + * cells on every call, so the selection dynamically resizes as the mouse moves. * * Phase 3 – mouseup: * If the pointer never left the start cell (hasMoved === false), treat the @@ -50,16 +50,13 @@ function getRowLabel(row: number): string { * * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck * drag state when the pointer exits the grid. - * - * `onCellAssign` is idempotent (ignores duplicates) and also removes the assigned - * cell from any other group of the same type, enforcing the one-group-per-cell-per- - * type constraint. `onCellToggle` does a pure add/remove with no stealing. */ -export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAssign, onCellToggle }: Props): JSX.Element { +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: Props): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); - const dragCells = useRef>(new Set()); + const dragIsUnselect = useRef(false); // true when the drag started on a cell already in the active group + const preDragPositions = useRef([]); // snapshot of activeGroup.positions at mousedown // Pre-compute a "row,col" → {color, groupName} map for the active tab type. // This lets each cell do an O(1) lookup rather than scanning all groups and @@ -81,26 +78,17 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs isDragging.current = true; hasMoved.current = false; startCell.current = { row, col }; - dragCells.current = new Set([`${row},${col}`]); + dragIsUnselect.current = activeGroup?.positions.some(p => p.row === row && p.col === col) ?? false; + // Snapshot the current positions NOW, from the prop, before any drag events can modify state. + preDragPositions.current = activeGroup?.positions ?? []; e.preventDefault(); - }, []); + }, [activeGroup]); const handleMouseEnter = useCallback((row: number, col: number) => { - if (!isDragging.current) return; - if (!hasMoved.current) { - hasMoved.current = true; - // Deferred assign: now that we know this is a drag, assign the cell - // the user originally pressed down on. - if (startCell.current) { - onCellAssign(startCell.current.row, startCell.current.col); - } - } - const key = `${row},${col}`; - if (!dragCells.current.has(key)) { - dragCells.current.add(key); - onCellAssign(row, col); - } - }, [onCellAssign]); + if (!isDragging.current || !startCell.current) return; + hasMoved.current = true; + onDragRect(startCell.current.row, startCell.current.col, row, col, dragIsUnselect.current, preDragPositions.current); + }, [onDragRect]); // Called on mouseup over a specific cell — handles click-toggle const handleCellMouseUp = useCallback((row: number, col: number) => { @@ -114,7 +102,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs isDragging.current = false; hasMoved.current = false; startCell.current = null; - dragCells.current = new Set(); + dragIsUnselect.current = false; }, []); return ( From 062240583f630f9c20257bd175495b2a32c9a5a7 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 25 Apr 2026 13:14:45 -0700 Subject: [PATCH 04/18] Other cleanup --- .../PlateTemplateDesigner.scss | 1 - .../PlateTemplateDesigner.tsx | 5 +- .../components/GroupTypesPanel.tsx | 5 +- .../components/TemplateGrid.tsx | 61 +++++++++++++++++-- .../client/PlateTemplateDesigner/models.ts | 6 -- 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 5de546210cf..46162a3fde6 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -34,7 +34,6 @@ // Root container .plate-template-designer { padding: 12px 16px; - font-family: Arial, Helvetica, sans-serif; font-size: 13px; &__error { diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 8d6ad0ffe8c..d9459e6a23e 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -463,6 +463,7 @@ export function PlateTemplateDesigner(): JSX.Element {
Base NameBase Name setMultiBaseName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} @@ -242,17 +347,20 @@ export function GroupTypesPanel({
CountCount { setMultiCount(e.target.value); setMultiCountError(''); }} onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} /> - {multiCountError &&
{multiCountError}
} + {multiCountError &&
{multiCountError}
}
{Array.from({ length: plate.cols }, (_, col) => ( - {col + 1}{col + 1}
{getRowLabel(row)}{getRowLabel(row)} handleMouseDown(row, col, e)} onMouseEnter={() => handleMouseEnter(row, col)} diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx index d297bc1b125..ca49a6ed61c 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx @@ -3,7 +3,7 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { PlateTemplate, computeWarnings } from '../models'; @@ -11,8 +11,16 @@ interface Props { plate: PlateTemplate; } +/** + * Displays the list of validation warnings for the current plate layout. + * + * Warnings are recomputed synchronously from the latest plate state on each render. + * The panel is only shown when `plate.showWarningPanel` is true, which is controlled by the + * server-side assay type configuration (not all assay types use the REPLICATE/SPECIMEN/CONTROL + * group semantics that produce warnings). + */ export function WarningPanel({ plate }: Props): JSX.Element { - const warnings = computeWarnings(plate); + const warnings = useMemo(() => computeWarnings(plate), [plate]); return (
@@ -21,8 +29,8 @@ export function WarningPanel({ plate }: Props): JSX.Element {
No warnings.
) : (
    - {warnings.map((w, i) => ( -
  • {w}
  • + {warnings.map((w) => ( +
  • {w}
  • ))}
)} diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx index e31158345b7..67e242a2d68 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -13,6 +13,20 @@ interface Props { onDeleteProperty: (groupRowId: number, key: string) => void; } +/** + * Shows and edits the key/value property bag for the currently selected well group. + * + * Properties are assay-type-specific metadata attached to a group (e.g. concentration, + * dilution factor, sample ID). They are stored as plain strings and round-tripped through + * the server without interpretation by the designer. + * + * Interaction pattern: + * - Existing properties: each row has an inline text input for the value; changes propagate + * immediately to the parent (no separate submit step) via onPropertyChange. + * - Deleting: the trash button removes a property key entirely. + * - Adding: the footer row accepts a new key + value; "Add" (or Enter) commits the pair. + * The new-key input is the gate — the Add button stays disabled until a key is typed. + */ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: Props): JSX.Element { const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); @@ -52,6 +66,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro onPropertyChange(activeGroup.rowId, key, e.target.value)} /> @@ -60,9 +75,10 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro
([]); // snapshot of activeGroup.positions at mousedown + // Roving-tabindex state: tracks which cell holds tabIndex=0. Null means no cell has been + // focused yet, in which case (0,0) is the tab entry point. + const [focusedCell, setFocusedCell] = useState<{ row: number; col: number } | null>(null); + const cellRefs = useRef>(new Map()); + // Pre-compute a "row,col" → {color, groupName} map for the active tab type. // This lets each cell do an O(1) lookup rather than scanning all groups and // positions on every render (which would be O(groups × positions) per cell). @@ -105,9 +109,44 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe dragIsUnselect.current = false; }, []); + const handleCellFocus = useCallback((row: number, col: number) => { + setFocusedCell({ row, col }); + }, []); + + // Keyboard interaction for grid cells: + // Space / Enter → toggle the cell (same as a click with no drag) + // Arrow keys → move focus to the adjacent cell (wraps are intentionally prevented + // at plate edges to avoid confusing wrap-around focus jumps) + const handleCellKeyDown = useCallback((row: number, col: number, e: React.KeyboardEvent) => { + const moveFocus = (r: number, c: number) => { + e.preventDefault(); + setFocusedCell({ row: r, col: c }); + cellRefs.current.get(`${r},${c}`)?.focus(); + }; + switch (e.key) { + case ' ': + case 'Enter': + e.preventDefault(); + onCellToggle(row, col); + break; + case 'ArrowUp': + if (row > 0) moveFocus(row - 1, col); + break; + case 'ArrowDown': + if (row < plate.rows - 1) moveFocus(row + 1, col); + break; + case 'ArrowLeft': + if (col > 0) moveFocus(row, col - 1); + break; + case 'ArrowRight': + if (col < plate.cols - 1) moveFocus(row, col + 1); + break; + } + }, [onCellToggle, plate.rows, plate.cols]); + return (
- +
@@ -125,17 +164,29 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); const location = `${getRowLabel(row)}${col + 1}`; const tooltip = entry ? `${location}: ${entry.groupName}` : location; + const isTabStop = focusedCell + ? focusedCell.row === row && focusedCell.col === col + : row === 0 && col === 0; return ( { + const key = `${row},${col}`; + if (el) cellRefs.current.set(key, el); + else cellRefs.current.delete(key); + }} + tabIndex={isTabStop ? 0 : -1} className={classNames('template-grid__cell', { 'template-grid__cell--active': isActiveGroupCell, })} style={{ backgroundColor: entry?.color ?? '#f5f5f5' }} title={tooltip} + aria-label={tooltip} onMouseDown={e => handleMouseDown(row, col, e)} onMouseEnter={() => handleMouseEnter(row, col)} onMouseUp={() => handleCellMouseUp(row, col)} + onFocus={() => handleCellFocus(row, col)} + onKeyDown={e => handleCellKeyDown(row, col, e)} /> ); })} diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts index 71036b33950..7d8dd6c431f 100644 --- a/assay/src/client/PlateTemplateDesigner/models.ts +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -35,13 +35,7 @@ export interface PlateTemplate { defaultPlateName: string; } -export interface SaveTemplateResponse { - rowId: number; -} - /** - * Replicates the GWT TemplateGridCell.getWarnings() logic exactly. - * * Two conditions produce warnings: * 1. A REPLICATE well that belongs to neither a SPECIMEN nor a CONTROL group is almost certainly * a configuration error — replicates are only meaningful relative to a specimen or control. From 19e61e82ae6f321686338ae22f6872841c9d007d Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sun, 26 Apr 2026 19:08:34 -0700 Subject: [PATCH 05/18] Improve generics for plate code --- .../labkey/api/assay/plate/PlateService.java | 2 +- .../org/labkey/api/assay/plate/PlateSet.java | 2 +- .../src/org/labkey/assay/PlateController.java | 113 ++++++++++-------- .../plate/AssayPlateMetadataServiceImpl.java | 20 ++-- .../org/labkey/assay/plate/PlateCache.java | 24 ++-- .../org/labkey/assay/plate/PlateManager.java | 48 ++++---- .../labkey/assay/plate/PlateManagerTest.java | 60 +++++----- .../org/labkey/assay/plate/PlateSetImpl.java | 2 +- .../assay/plate/layout/LayoutEngine.java | 4 +- .../assay/plate/layout/LayoutOperation.java | 2 +- .../assay/plate/query/PlateSetTable.java | 4 +- 11 files changed, 147 insertions(+), 134 deletions(-) diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateService.java b/assay/api-src/org/labkey/api/assay/plate/PlateService.java index 5021f606c33..b55634aa859 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateService.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateService.java @@ -156,7 +156,7 @@ static PlateService get() */ @Nullable Plate getPlate(ContainerFilter cf, Long plateSetId, Object plateIdentifier); - @NotNull List getPlates(Container container); + @NotNull List getPlates(Container container); /** * Gets the plate set by ID diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateSet.java b/assay/api-src/org/labkey/api/assay/plate/PlateSet.java index 8e06e1c4c77..8dc659afa4d 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateSet.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateSet.java @@ -26,7 +26,7 @@ public interface PlateSet extends Identifiable boolean isTemplate(); - List getPlates(); + List getPlates(); PlateSetType getType(); diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index d35a2655dc3..dffcee52849 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -112,6 +112,45 @@ public class PlateController extends SpringActionController private static final SpringActionController.DefaultActionResolver _actionResolver = new DefaultActionResolver(PlateController.class); private static final Logger LOG = LogHelper.getLogger(PlateController.class, "Controller for plate related actions"); + record SubmittedGroup(int rowId, String type, String name, List positions, Map properties) + { + public static SubmittedGroup from(JSONObject g) + { + int rowId = g.optInt("rowId", -1); + String type = g.getString("type"); + String name = g.getString("name"); + JSONArray posArr = g.optJSONArray("positions"); + List positions = new ArrayList<>(); + if (posArr != null) + { + for (int j = 0; j < posArr.length(); j++) + { + JSONObject p = posArr.getJSONObject(j); + positions.add(PlatePosition.from(p)); + } + } + JSONObject propsObj = g.optJSONObject("properties"); + Map props = new HashMap<>(); + if (propsObj != null) + { + for (String key : propsObj.keySet()) + { + Object val = propsObj.get(key); + props.put(key, val == JSONObject.NULL ? null : val); + } + } + return new SubmittedGroup(rowId, type, name, positions, props); + } + } + + record PlatePosition(int row, int col) + { + public static PlatePosition from(JSONObject p) + { + return new PlatePosition(p.getInt("row"), p.getInt("col")); + } + } + public PlateController() { setActionResolver(_actionResolver); @@ -165,7 +204,7 @@ public static class PlateListAction extends SimpleViewAction public ModelAndView getView(ReturnUrlForm form, BindException errors) { setHelpTopic("editPlateTemplate"); - List plateTemplates = PlateService.get().getPlates(getContainer()) + List plateTemplates = PlateService.get().getPlates(getContainer()) .stream() .filter(p -> !TsvPlateLayoutHandler.TYPE.equalsIgnoreCase(p.getAssayType())) .toList(); @@ -180,6 +219,7 @@ public void addNavTrail(NavTree root) } } + /** Delete soon! */ @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public static class DesignerServiceAction extends GWTServiceAction { @@ -417,7 +457,7 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except } boolean updateExisting = false; - Plate plate; + PlateImpl plate; if (rowId > 0) { plate = PlateManager.get().getPlate(getContainer(), rowId); @@ -447,42 +487,16 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except plate.setProperties(plateProperties); // Parse groups from JSON - List> submittedGroups = new ArrayList<>(); + List submittedGroups = new ArrayList<>(); Set submittedGroupIds = new HashSet<>(); if (groupsJson != null) { for (int i = 0; i < groupsJson.length(); i++) { - JSONObject g = groupsJson.getJSONObject(i); - Map gm = new HashMap<>(); - gm.put("rowId", g.optInt("rowId", -1)); - gm.put("type", g.getString("type")); - gm.put("name", g.getString("name")); - JSONArray posArr = g.optJSONArray("positions"); - List positions = new ArrayList<>(); - if (posArr != null) - { - for (int j = 0; j < posArr.length(); j++) - { - JSONObject p = posArr.getJSONObject(j); - positions.add(new int[]{p.getInt("row"), p.getInt("col")}); - } - } - gm.put("positions", positions); - JSONObject propsObj = g.optJSONObject("properties"); - Map props = new HashMap<>(); - if (propsObj != null) - { - for (String key : propsObj.keySet()) - { - Object val = propsObj.get(key); - props.put(key, val == JSONObject.NULL ? null : val); - } - } - gm.put("properties", props); - submittedGroups.add(gm); - if ((int) gm.get("rowId") > 0) - submittedGroupIds.add((int) gm.get("rowId")); + SubmittedGroup g = SubmittedGroup.from(groupsJson.getJSONObject(i)); + submittedGroups.add(g); + if (g.rowId > 0) + submittedGroupIds.add(g.rowId); } } @@ -491,14 +505,14 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except for (WellGroup existingGroup : existingWellGroups) { if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) - ((PlateImpl) plate).markWellGroupForDeletion(existingGroup); + plate.markWellGroupForDeletion(existingGroup); } // Update or create well groups - for (Map gm : submittedGroups) + for (SubmittedGroup gm : submittedGroups) { - int gRowId = (int) gm.get("rowId"); - String groupTypeName = (String) gm.get("type"); + int gRowId = gm.rowId(); + String groupTypeName = gm.type(); WellGroup.Type groupType; try { @@ -508,14 +522,12 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except { throw new ApiUsageException("Unknown well group type: '" + groupTypeName + "'"); } - @SuppressWarnings("unchecked") - List posList = (List) gm.get("positions"); + List posList = gm.positions(); List positions = new ArrayList<>(); - for (int[] p : posList) - positions.add(plate.getPosition(p[0], p[1])); + for (PlatePosition p : posList) + positions.add(plate.getPosition(p.row, p.col)); - @SuppressWarnings("unchecked") - Map props = (Map) gm.get("properties"); + Map props = gm.properties(); WellGroupImpl group; if (updateExisting && gRowId > 0) @@ -524,15 +536,15 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except if (existing == null) throw new Exception("Well group " + gRowId + " was not found."); if (existing.getType() != groupType) - throw new Exception("Well group type cannot be changed: " + gm.get("name")); - existing.setName((String) gm.get("name")); + throw new Exception("Well group type cannot be changed: " + gm.name()); + existing.setName(gm.name); existing.setPositions(positions); - ((PlateImpl) plate).storeWellGroup(existing); + plate.storeWellGroup(existing); group = existing; } else { - group = (WellGroupImpl) plate.addWellGroup((String) gm.get("name"), groupType, positions); + group = plate.addWellGroup(gm.name, groupType, positions); } group.setProperties(props); } @@ -579,6 +591,7 @@ public void addNavTrail(NavTree root) } } + /** Delete soon! */ @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public class DesignerGwtAction extends SimpleViewAction { @@ -664,7 +677,7 @@ public static class CopyTemplateBean private HtmlString _treeHtml; private Plate _plate; private String _selectedDestination; - private List _destinationTemplates; + private List _destinationTemplates; public CopyTemplateBean(final Container container, final User user, final Integer plateId, final String selectedDestination) { @@ -1083,7 +1096,7 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti PlateImpl newPlate = new PlateImpl(getContainer(), form.getName(), form.getBarcode(), form.getAssayType(), _plateType); if (form.getData() == null && form.getTemplateId() != null && TsvPlateLayoutHandler.TYPE.equalsIgnoreCase(newPlate.getAssayType())) { - newPlate = (PlateImpl) PlateManager.get().copyPlate( + newPlate = PlateManager.get().copyPlate( getContainer(), getUser(), form.getTemplateId(), @@ -1104,7 +1117,7 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti if (form.isTemplate() && data == null) data = PlateManager.get().prepareEmptyPlateTemplateData(getContainer(), _plateType); - newPlate = (PlateImpl) PlateManager.get().createAndSavePlate(getContainer(), getUser(), newPlate, form.getPlateSetId(), data); + newPlate = PlateManager.get().createAndSavePlate(getContainer(), getUser(), newPlate, form.getPlateSetId(), data); } return success(newPlate); diff --git a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java index b4d7635f972..1dca54939e2 100644 --- a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java @@ -238,7 +238,7 @@ public Map apply(Map row) }); } - private List getPlatesForPlateSet( + private List getPlatesForPlateSet( Container container, User user, Long plateSetId, @@ -270,7 +270,7 @@ public DataIteratorBuilder parsePlateData( ) throws ExperimentException { // get the ordered list of plates for the plate set - List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); + List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); if (plates.isEmpty()) throw new ExperimentException("No plates were found for the plate set (" + plateSetId + ")."); PlateSet plateSet = plates.get(0).getPlateSet(); @@ -297,7 +297,7 @@ private List> _parsePlateData( AssayProvider provider, ExpProtocol protocol, PlateSet plateSet, - List plates, + List plates, FileLike dataFile, DataLoaderSettings settings ) throws ExperimentException @@ -356,7 +356,7 @@ public DataIteratorBuilder mergeReRunData( ) throws ExperimentException { Long plateSetId = getPlateSetId(context, provider, protocol); - List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); + List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); if (plates.isEmpty()) throw new ExperimentException("No plates were found for the plate set (" + plateSetId + ")."); @@ -540,7 +540,7 @@ private boolean isGridFormat(List> data) private List> parsePlateRows( AssayProvider provider, ExpProtocol protocol, - List plates, + List plates, List> data ) throws ExperimentException { @@ -604,7 +604,7 @@ private List> parsePlateRows( } // Resolves a pre-calculated "plateIdField" to a plate rowId and furnishes new "data" rows with the plate rowId. - private List> resolvePlateIdentifier(List plates, List> data, String plateIdField) + private List> resolvePlateIdentifier(List plates, List> data, String plateIdField) { var newData = new ArrayList>(); var plateIdentifiers = new HashMap(); @@ -664,7 +664,7 @@ public PlateGridInfo(PlateUtils.GridInfo info, PlateSet plateSet, Set me // locate the plate in the plate set this grid is associated with plus an optional // measure name - List plates = PlateManager.get().getPlatesForPlateSet(plateSet); + List plates = PlateManager.get().getPlatesForPlateSet(plateSet); List annotations = getAnnotations(); // if the plate set only has one plate, then treat a single annotation as the measure @@ -694,7 +694,7 @@ public PlateGridInfo(PlateUtils.GridInfo info, PlateSet plateSet, Set me } } - private @NotNull Plate getPlateForId(String annotation, List platesetPlates) throws ExperimentException + private @NotNull Plate getPlateForId(String annotation, List platesetPlates) throws ExperimentException { Plate plate = platesetPlates.stream().filter(p -> p.isIdentifierMatch(annotation)).findFirst().orElse(null); if (plate == null) @@ -734,7 +734,7 @@ private List> parsePlateGrids( AssayProvider provider, ExpProtocol protocol, PlateSet plateSet, - List plates, + List plates, FileLike dataFile ) throws ExperimentException { @@ -1754,7 +1754,7 @@ public void testGridAnnotations() throws Exception ); PlateSet plateSet = PlateManager.get().createPlateSet(container, user, new PlateSetImpl(), plates, null, null); - List plateSetPlates = PlateManager.get().getPlatesForPlateSet(plateSet); + List plateSetPlates = PlateManager.get().getPlatesForPlateSet(plateSet); assertEquals("Expected two plates to be created.", 2, plateSetPlates.size()); Plate plate = plateSetPlates.get(0); diff --git a/assay/src/org/labkey/assay/plate/PlateCache.java b/assay/src/org/labkey/assay/plate/PlateCache.java index 9827c7cfb5c..828a9674ccf 100644 --- a/assay/src/org/labkey/assay/plate/PlateCache.java +++ b/assay/src/org/labkey/assay/plate/PlateCache.java @@ -32,15 +32,15 @@ public class PlateCache { private static final PlateLoader _loader = new PlateLoader(); - private static final Cache PLATE_CACHE = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Plate Cache", _loader); + private static final Cache PLATE_CACHE = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Plate Cache", _loader); private static final Logger LOG = LogManager.getLogger(PlateCache.class); - private static class PlateLoader implements CacheLoader + private static class PlateLoader implements CacheLoader { private final Map> _containerPlateMap = new HashMap<>(); // internal collection to help un-cache all plates for a container @Override - public Plate load(@NotNull String key, @Nullable Object argument) + public PlateImpl load(@NotNull String key, @Nullable Object argument) { // parse the cache key PlateCacheKey cacheKey = new PlateCacheKey(key); @@ -55,7 +55,7 @@ public Plate load(@NotNull String key, @Nullable Object argument) { PlateBean bean = plates.get(0); - Plate plate = PlateManager.get().populatePlate(bean); + PlateImpl plate = PlateManager.get().populatePlate(bean); LOG.debug(String.format("Caching plate \"%s\" for folder %s", plate.getName(), cacheKey._container.getPath())); // add all cache keys for this plate @@ -65,7 +65,7 @@ public Plate load(@NotNull String key, @Nullable Object argument) return null; } - private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) + private void addCacheKeys(PlateCacheKey cacheKey, PlateImpl plate) { if (plate != null) { @@ -84,14 +84,14 @@ private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) if (cacheKey._type != PlateCacheKey.Type.plateId) PLATE_CACHE.put(PlateCacheKey.getCacheKey(plate.getContainer(), plate.getPlateId()), plate); - _containerPlateMap.computeIfAbsent(cacheKey._container, k -> new HashSet<>()).add(plate.getRowId()); + _containerPlateMap.computeIfAbsent(cacheKey._container, _ -> new HashSet<>()).add(plate.getRowId()); } } } - public static @Nullable Plate getPlate(Container c, long rowId) + public static @Nullable PlateImpl getPlate(Container c, long rowId) { - Plate plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, rowId)); + PlateImpl plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, rowId)); // We allow plates to be mutated, return a copy of the cached object which still references the // original wells and well groups return plate != null ? plate.copy() : null; @@ -150,23 +150,23 @@ private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) ).getArrayList(Long.class); } - private static @NotNull List getPlates(Container c, @Nullable SimpleFilter filter) + private static @NotNull List getPlates(Container c, @Nullable SimpleFilter filter) { List ids = getPlateIDs(c, filter); return ids.stream().map(id -> PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, id))).toList(); } - public static @NotNull List getPlates(Container c) + public static @NotNull List getPlates(Container c) { return getPlates(c, null); } - public static @NotNull List getPlatesForPlateSet(Container c, Long plateSetRowId) + public static @NotNull List getPlatesForPlateSet(Container c, Long plateSetRowId) { return getPlates(c, new SimpleFilter(FieldKey.fromParts(PlateTable.Column.PlateSet.name()), plateSetRowId)); } - public static @NotNull List getPlateTemplates(Container c) + public static @NotNull List getPlateTemplates(Container c) { return getPlates(c, new SimpleFilter(FieldKey.fromParts(PlateTable.Column.Template.name()), true)); } diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 2512cdb4ea9..c12203b1708 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -300,15 +300,15 @@ public List getWellGroupTypes() } @Override - public @NotNull Plate createPlate(Container container, String assayType, @NotNull PlateType plateType) + public @NotNull PlateImpl createPlate(Container container, String assayType, @NotNull PlateType plateType) { return new PlateImpl(container, null, null, assayType, plateType); } - public @NotNull Plate createAndSavePlate( + public @NotNull PlateImpl createAndSavePlate( @NotNull Container container, @NotNull User user, - @NotNull Plate plate, + @NotNull PlateImpl plate, @Nullable Long plateSetId, @Nullable List> data ) throws Exception @@ -316,10 +316,10 @@ public List getWellGroupTypes() return createAndSavePlate(container, user, plate, plateSetId, data, false); } - private @NotNull Plate createAndSavePlate( + private @NotNull PlateImpl createAndSavePlate( @NotNull Container container, @NotNull User user, - @NotNull Plate plate, + @NotNull PlateImpl plate, @Nullable Long plateSetId, @Nullable List> data, boolean skipAudit @@ -346,7 +346,7 @@ public List getWellGroupTypes() throw new ValidationException(String.format("Failed to create plate. Plate set \"%s\" is not a template plate set.", plateSet.getName())); if (!plate.isTemplate() && plateSet.isTemplate()) throw new ValidationException(String.format("Failed to create plate. Plate set \"%s\" is a template plate set.", plateSet.getName())); - ((PlateImpl) plate).setPlateSet(plateSet); + plate.setPlateSet(plateSet); } // Intentionally passing skipAudit=true, and not the passed in value for skipAudit, @@ -479,7 +479,7 @@ public Position createPosition(Container container, int row, int column) List plates = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), filter, null).getArrayList(PlateBean.class); // this should be 1 or 0, but don't blow up if there are more than one if (!plates.isEmpty()) - return populatePlate(plates.get(0)); + return populatePlate(plates.getFirst()); return null; } @@ -512,7 +512,7 @@ public List getMetadataColumns(@NotNull PlateSet plateSet, Container c } @NotNull - public List getPlateTemplates(Container container) + public List getPlateTemplates(Container container) { return PlateCache.getPlateTemplates(container); } @@ -557,7 +557,7 @@ public int getRunCountUsingPlate(@NotNull Container c, @NotNull User user, @NotN * @return A map of plate rowId to total number of runs across all plate-based assay runs in the * container/user scope for the specified plates. */ - public Map getPlateRunCounts(@NotNull Container c, @NotNull User user, @NotNull Collection plates) + public Map getPlateRunCounts(@NotNull Container c, @NotNull User user, @NotNull Collection plates) { if (plates.isEmpty()) return emptyMap(); @@ -742,7 +742,7 @@ private int getRunCountUsingPlateInResults(@NotNull Container c, @NotNull User u } @Override - public @Nullable Plate getPlate(Container container, long rowId) + public @Nullable PlateImpl getPlate(Container container, long rowId) { return PlateCache.getPlate(container, rowId); } @@ -786,10 +786,10 @@ private int getRunCountUsingPlateInResults(@NotNull Container c, @NotNull User u Plate plate = null; if (plateIdentifier != null) { - List plates = getPlatesForPlateSet(plateSet); - List matchingPlates = plates.stream().filter(p -> p.isIdentifierMatch(plateIdentifier.toString())).toList(); + List plates = getPlatesForPlateSet(plateSet); + List matchingPlates = plates.stream().filter(p -> p.isIdentifierMatch(plateIdentifier.toString())).toList(); if (matchingPlates.size() == 1) - plate = matchingPlates.get(0); + plate = matchingPlates.getFirst(); else if (matchingPlates.isEmpty()) throw new IllegalArgumentException("The plate identifier \"" + plateIdentifier + "\" does not match any plate in the plate set \"" + plateSet.getName() + "\"."); else @@ -820,7 +820,7 @@ else if (matchingPlates.isEmpty()) throw new IllegalStateException("More than one " + tableInfo.getName() + " found that matches the filter."); if (containers.size() == 1) - return ContainerManager.getForId(containers.get(0)); + return ContainerManager.getForId(containers.getFirst()); return null; } @@ -952,7 +952,7 @@ public boolean isDuplicatePlateTemplateName(Container container, String name) } @Override - public @NotNull List getPlates(Container c) + public @NotNull List getPlates(Container c) { return PlateCache.getPlates(c); } @@ -962,7 +962,7 @@ public boolean isDuplicatePlateTemplateName(Container container, String name) return PlateSetCache.getPlateSets(c); } - public List getPlatesForPlateSet(PlateSet plateSet) + public List getPlatesForPlateSet(PlateSet plateSet) { return PlateCache.getPlatesForPlateSet(plateSet.getContainer(), plateSet.getRowId()); } @@ -1028,7 +1028,7 @@ private long save(Container container, User user, Plate plate, @Nullable List wellGroupIds = wellToWellGroups.computeIfAbsent(wellId, k -> new HashSet<>()); + Set wellGroupIds = wellToWellGroups.computeIfAbsent(wellId, _ -> new HashSet<>()); wellGroupIds.add(wellGroupId); } @@ -1070,7 +1070,7 @@ protected Plate populatePlate(PlateBean bean) { for (Integer wellGroupId : wellGroupIds) { - List groupPositions = groupIdToPositions.computeIfAbsent(wellGroupId, k -> new ArrayList<>()); + List groupPositions = groupIdToPositions.computeIfAbsent(wellGroupId, _ -> new ArrayList<>()); groupPositions.add(well); } } @@ -1274,7 +1274,7 @@ private long savePlateImpl( if (wellDataMap.containsKey(position.getDescription())) { wellDataMap.get(position.getDescription()).forEach( - (key, value) -> wellRow.merge(key, value, (v1, v2) -> v1) + (key, value) -> wellRow.merge(key, value, (v1, _) -> v1) ); } @@ -1956,7 +1956,7 @@ private void copyWellGroups(@NotNull Plate source, @NotNull Plate copy) } } - public Plate copyPlate( + public PlateImpl copyPlate( Container container, User user, Long sourcePlateRowId, @@ -4541,7 +4541,7 @@ public record ReformatResult( Long plateSetRowId; String plateSetName; - List newPlates; + List newPlates; if (targetPlateSet.isNew()) { @@ -4846,7 +4846,7 @@ else if (!Objects.equals(sourcePlateSet.getRowId(), plateSet.getRowId())) return Pair.of(sourcePlateSet, sourcePlates); } - private @NotNull List getReformatTargetPlates(@NotNull PlateSetImpl targetPlateSet) + private @NotNull List getReformatTargetPlates(@NotNull PlateSetImpl targetPlateSet) { if (targetPlateSet.isNew()) return emptyList(); @@ -5079,7 +5079,7 @@ private record HydratedResult(List plateData, @Nullable Integer plate List sourcedWells = Arrays.stream(wellLayout.getWells()).filter(well -> well != null && well.sourcePlateId() > 0).toList(); if (!sourcedWells.isEmpty()) { - Long sourcePlateId = sourcedWells.get(0).sourcePlateId(); + Long sourcePlateId = sourcedWells.getFirst().sourcePlateId(); if (sourcedWells.stream().allMatch(w -> sourcePlateId.equals(w.sourcePlateId()))) templateId = sourcePlateId; } diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index b0df652dbb9..a60e5aacb43 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -239,7 +239,7 @@ public void testCreatePlateTemplate() throws Exception List sampleWellGroups = savedTemplate.getWellGroups(WellGroup.Type.SAMPLE); assertEquals(1, sampleWellGroups.size()); - WellGroup savedWg1 = sampleWellGroups.get(0); + WellGroup savedWg1 = sampleWellGroups.getFirst(); assertEquals("wg1", savedWg1.getName()); assertEquals("100", savedWg1.getProperty("score")); @@ -296,7 +296,7 @@ public void testCreatePlateTemplate() throws Exception assertEquals(1, updatedControlWellGroups.size()); // verify added positions - assertEquals(2, updatedControlWellGroups.get(0).getPositions().size()); + assertEquals(2, updatedControlWellGroups.getFirst().getPositions().size()); // verify plate type information assertEquals(plateType.getRows().intValue(), updatedTemplate.getRows()); @@ -353,7 +353,7 @@ public void testAccessPlateByIdentifiers() throws Exception // Assert assertTrue("Expected plateSet to have been persisted and provided with a rowId", plateSet.getRowId() > 0); - List plates = plateSet.getPlates(); + List plates = plateSet.getPlates(); assertEquals("Expected plateSet to have 3 plates", 3, plates.size()); // verify access via plate rowId @@ -394,7 +394,7 @@ public void testCreatePlateTemplates() throws Exception createPlate(PLATE_TYPE_96_WELLS); // Verify only plate templates are returned - List templates = PlateManager.get().getPlateTemplates(container); + List templates = PlateManager.get().getPlateTemplates(container); assertFalse("Expected there to be a plate template", templates.isEmpty()); for (Plate t : templates) assertTrue("Expected saved plate to have the template field set to true", t.isTemplate()); @@ -704,7 +704,7 @@ public void testGetWorklistSingleSampleManyToMany() throws Exception { // Arrange ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(container, user); - ExpMaterial sample = createSamples(1).get(0); + ExpMaterial sample = createSamples(1).getFirst(); List> rows1 = List.of( wellWithMetdata(createWellRow("A1", "SAMPLE", sample.getRowId()), 2.25, "B1234"), @@ -732,7 +732,7 @@ public void testGetWorklistSingleSampleOneToOne() throws Exception { // Arrange ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(container, user); - ExpMaterial sample = createSamples(3).get(0); + ExpMaterial sample = createSamples(3).getFirst(); List> rows1 = List.of( wellWithMetdata(createWellRow("A1", "SAMPLE", sample.getRowId()), 2.25, "B1234"), @@ -769,7 +769,7 @@ public void testGetWorklistSingleSampleOneToMany() throws Exception { // Arrange ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(container, user); - ExpMaterial sample = createSamples(3).get(0); + ExpMaterial sample = createSamples(3).getFirst(); List> rows1 = List.of( wellWithMetdata(createWellRow("A1", "SAMPLE", sample.getRowId()), 2.25, "B1234") @@ -927,7 +927,7 @@ public void testReformatQuadrant() throws Exception assertNotNull(result.previewData()); assertEquals("Expected quadrant operation on 3 plates to generate 1 plate.", 1, result.previewData().size()); - var previewPlate = result.previewData().get(0); + var previewPlate = result.previewData().getFirst(); var wellData = previewPlate.data(); assertEquals("Expected 12 wells to have data", 12, wellData.size()); @@ -961,7 +961,7 @@ public void testReformatQuadrant() throws Exception assertTrue("Expected a new plate set to be created", result.plateSetRowId() > 0); assertEquals(1, result.plateRowIds().size()); - var newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + var newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_384_WELLS, newPlate.getPlateType()); @@ -1087,7 +1087,7 @@ public void testReformatCompressByColumn() throws Exception assertNotNull(result.previewData()); assertEquals("Expected column compress operation on a 384-well plate to generate 1 12-well plates.", 1, result.previewData().size()); - List> plateData = result.previewData().get(0).data(); + List> plateData = result.previewData().getFirst().data(); assertEquals("Expected well P12 to be dropped as it does not include a sample.", sourcePlateData.size() - 1, plateData.size()); assertEquals(sampleRowIds.get(0), plateData.get(0).get("sampleId")); @@ -1111,7 +1111,7 @@ public void testReformatCompressByColumn() throws Exception assertEquals("Expected target plate set to be used", targetPlateSetId, result.plateSetRowId()); assertEquals(1, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); @@ -1168,7 +1168,7 @@ public void testReformatCompressByRow() throws Exception assertNotNull(result.previewData()); assertEquals("Expected row compress operation on a 384-well plate to generate 1 12-well plates.", 1, result.previewData().size()); - List> plateData = result.previewData().get(0).data(); + List> plateData = result.previewData().getFirst().data(); assertEquals("Expected well P12 to be dropped as it does not include a sample.", sourcePlateData.size() - 1, plateData.size()); assertEquals(sampleRowIds.get(0), plateData.get(0).get("sampleId")); @@ -1192,7 +1192,7 @@ public void testReformatCompressByRow() throws Exception assertEquals("Expected target plate set to be used", targetPlateSetId, result.plateSetRowId()); assertEquals(1, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); @@ -1302,7 +1302,7 @@ public void testReformatArrayByColumn() throws Exception assertEquals("Expected target plate set to be used", context.targetPlateSetId, result.plateSetRowId()); assertEquals(2, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); List sampleRowIds = context.sampleRowIds; @@ -1368,7 +1368,7 @@ public void testReformatArrayByRow() throws Exception assertEquals("Expected target plate set to be used", context.targetPlateSetId, result.plateSetRowId()); assertEquals(2, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); List sampleRowIds = context.sampleRowIds; @@ -1454,7 +1454,7 @@ public void testReformatArrayFromTemplate() throws Exception assertEquals("Expected target plate set to be used", context.targetPlateSetId, result.plateSetRowId()); assertEquals(3, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); List sampleRowIds = context.sampleRowIds; @@ -1505,7 +1505,7 @@ public void testReformatArrayFromTemplate() throws Exception switch (wellPosition) { - case "A1" -> assertEquals(sampleRowIds.get(0).intValue(), sampleId); // Group "S1" + case "A1" -> assertEquals(sampleRowIds.getFirst().intValue(), sampleId); // Group "S1" case "A2" -> assertEquals(sampleRowIds.get(11).intValue(), sampleId); case "A3" -> assertEquals(sampleRowIds.get(12).intValue(), sampleId); case "A4" -> assertEquals(0, sampleId); @@ -1516,7 +1516,7 @@ public void testReformatArrayFromTemplate() throws Exception case "C1" -> assertEquals(0, sampleId); // Control case "C2" -> assertEquals(0, sampleId); case "C3" -> assertEquals(0, sampleId); // Control - case "C4" -> assertEquals(sampleRowIds.get(0).intValue(), sampleId); // Group "S1" + case "C4" -> assertEquals(sampleRowIds.getFirst().intValue(), sampleId); // Group "S1" } var barcode = r.getString(FieldKey.fromParts(PlateMetadataFields.barcode.name())); @@ -1612,7 +1612,7 @@ public void testReplicateWellValidation() throws Exception assertCreatePlateThrows(expectedMessage, PLATE_TYPE_96_WELLS, plateName, null, sourcePlateData); // Fixup rows by making all rows the same and resubmit - sourcePlateData.forEach(row -> row.put("sampleId", sampleRowIds.get(0))); + sourcePlateData.forEach(row -> row.put("sampleId", sampleRowIds.getFirst())); // Act var newPlate = createPlate(PLATE_TYPE_96_WELLS, plateName, null, sourcePlateData); @@ -1668,8 +1668,8 @@ public void testReplicateCrossPlateValidation() throws Exception List sampleRowIds = createSamples(2).stream().map(ExpObject::getRowId).sorted().toList(); List> plate1Data = new ArrayList<>(); - plate1Data.add(createWellRow("A1", "SAMPLE", sampleRowIds.get(0), null, "R1")); - plate1Data.add(createWellRow("A2", "SAMPLE", sampleRowIds.get(0), null, "R1")); + plate1Data.add(createWellRow("A1", "SAMPLE", sampleRowIds.getFirst(), null, "R1")); + plate1Data.add(createWellRow("A2", "SAMPLE", sampleRowIds.getFirst(), null, "R1")); plate1Data.add(createWellRow("A3", "SAMPLE", sampleRowIds.get(0), null, "R1")); List> plate2Data = new ArrayList<>(); @@ -1679,8 +1679,8 @@ public void testReplicateCrossPlateValidation() throws Exception List> plate3Data = new ArrayList<>(); plate2Data.add(createWellRow("C1", "SAMPLE", sampleRowIds.get(0), null, "R2")); - plate2Data.add(createWellRow("C2", "SAMPLE", sampleRowIds.get(0), null, "R2")); - plate2Data.add(createWellRow("C3", "SAMPLE", sampleRowIds.get(0), null, "R2")); + plate2Data.add(createWellRow("C2", "SAMPLE", sampleRowIds.getFirst(), null, "R2")); + plate2Data.add(createWellRow("C3", "SAMPLE", sampleRowIds.getFirst(), null, "R2")); var plateData = List.of( new PlateManager.PlateData(null, plateType.getRowId(), null, null, plate1Data), @@ -1694,7 +1694,7 @@ public void testReplicateCrossPlateValidation() throws Exception assertCreatePlateSetThrows(expectedMessage, plateSetImpl, plateData, null); // Fixup rows by making all rows the same and resubmit - plate2Data.forEach(row -> row.put("sampleId", sampleRowIds.get(0))); + plate2Data.forEach(row -> row.put("sampleId", sampleRowIds.getFirst())); // Assert (expect no errors) createPlateSet(plateSetImpl, plateData, null); @@ -1721,12 +1721,12 @@ public void testControlValidation() throws Exception var plateData1 = List.of(new PlateManager.PlateData("PS1", plateType.getRowId(), null, null, PS1Data)); PlateSet plateSet1 = createPlateSet(plateSetImpl, plateData1, null); - List> dataPS2 = Arrays.asList(createWellRow("A1", "POSITIVE_CONTROL", sampleRowIds.get(0))); + List> dataPS2 = Arrays.asList(createWellRow("A1", "POSITIVE_CONTROL", sampleRowIds.getFirst())); var plateData2 = List.of(new PlateManager.PlateData("PS2", plateType.getRowId(), null, null, dataPS2)); // Act / Assert // Since the sample of index 0 is on PS1's plate, it is not a valid control for PS2's plate - String errorMsg = String.format("The sample \"%s\" is not a valid control.", sampleNames.get(0)); + String errorMsg = String.format("The sample \"%s\" is not a valid control.", sampleNames.getFirst()); assertCreatePlateSetThrows(errorMsg, plateSetImpl, plateData2, plateSet1.getRowId()); // Assert (expect no errors) @@ -1758,7 +1758,7 @@ public void testBuiltInColumns() throws Exception // Assert assertEquals(1, PPSPlateFields.size()); - assertEquals("SampleID", PPSPlateFields.get(0).getName()); + assertEquals("SampleID", PPSPlateFields.getFirst().getName()); assertEquals(4, APSPlateFields.size()); assertEquals("Type", APSPlateFields.get(0).getName()); @@ -1808,7 +1808,7 @@ public void testEnsureSampleWellTypeTriggerRespectsType() throws Exception List sampleRowIds = samples.stream().map(ExpObject::getRowId).sorted().toList(); List> data = List.of( - createWellRow("A1", "CONTROL", sampleRowIds.get(0)) + createWellRow("A1", "CONTROL", sampleRowIds.getFirst()) ); // Act @@ -1919,12 +1919,12 @@ public void testDeleteSampleWellReferencesUponSampleDelete() throws Exception var plateData = List.of(new PlateManager.PlateData(null, PLATE_TYPE_12_WELLS.getRowId(), null, null, wellData)); var PPS = createPlateSet(pps, plateData, null); - var ppsPlateRowId = PPS.getPlates().get(0).getRowId(); + var ppsPlateRowId = PPS.getPlates().getFirst().getRowId(); var aps = new PlateSetImpl(); aps.setType(PlateSetType.assay); var APS = createPlateSet(aps, plateData, PPS.getRowId()); - var apsPlateRowId = APS.getPlates().get(0).getRowId(); + var apsPlateRowId = APS.getPlates().getFirst().getRowId(); // Act // Formerly, this would result in a foreign key violation on the assay.well table diff --git a/assay/src/org/labkey/assay/plate/PlateSetImpl.java b/assay/src/org/labkey/assay/plate/PlateSetImpl.java index e17b57a09a7..7fe5db23cea 100644 --- a/assay/src/org/labkey/assay/plate/PlateSetImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateSetImpl.java @@ -147,7 +147,7 @@ public boolean isStandalone() } @Override - public List getPlates() + public List getPlates() { if (isNew()) return Collections.emptyList(); diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 75bced3f604..96bca37324f 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -19,7 +19,7 @@ public class LayoutEngine private final ReformatOptions _options; private Collection _sampleIds; private List _sourcePlates; - private List _targetPlates; + private List _targetPlates; private List _targetPlateData; private PlateType _targetPlateType; private Plate _targetTemplate; @@ -93,7 +93,7 @@ public void setSourcePlates(List sourcePlates) _sourcePlates = sourcePlates; } - public void setTargetPlates(List targetPlates) + public void setTargetPlates(List targetPlates) { _targetPlates = targetPlates; } diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index 2809df13514..cef436ec356 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -59,7 +59,7 @@ record ExecutionContext( PlateType targetPlateType, List sourcePlates, Plate targetTemplate, - List targetPlates, + List targetPlates, List targetPlateData, Collection sampleIds, WellData.Cache wellDataCache diff --git a/assay/src/org/labkey/assay/plate/query/PlateSetTable.java b/assay/src/org/labkey/assay/plate/query/PlateSetTable.java index dc5cd4927d7..42499ff0ab4 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateSetTable.java +++ b/assay/src/org/labkey/assay/plate/query/PlateSetTable.java @@ -194,7 +194,7 @@ public DataIteratorBuilder createImportDIB(User user, Container container, DataI // generate a value for the lsid final TableInfo plateSetTable = getQueryTable(); lsidGenerator.addColumn(plateSetTable.getColumn(PlateTable.Column.Lsid.name()), - (Supplier) () -> PlateManager.get().getLsid(PlateSet.class, container)); + (Supplier) () -> PlateManager.get().getLsid(PlateSet.class, container)); SimpleTranslator nameExpressionTranslator = new SimpleTranslator(lsidGenerator, context); nameExpressionTranslator.setDebugName("nameExpressionTranslator"); @@ -252,7 +252,7 @@ protected Map deleteRow( if (plateSet == null) throw new QueryUpdateServiceException(String.format("Plate set could not be found for ID : %d", rowId)); - List plates = plateSet.getPlates(); + List plates = plateSet.getPlates(); if (!plates.isEmpty()) throw new QueryUpdateServiceException(String.format("Plate set has %d plates associated with it and cannot be deleted.", plates.size())); From a912417528fdd4550aed113b96e5d0431da3f3e7 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Mon, 27 Apr 2026 19:09:35 -0700 Subject: [PATCH 06/18] Unit tests and other improvements --- assay/package-lock.json | 10629 +++++++++++----- assay/package.json | 44 +- .../PlateTemplateDesigner.scss | 139 +- .../PlateTemplateDesigner.tsx | 177 +- .../PlateTemplateDesigner.utils.test.ts | 152 + .../components/GroupTypesPanel.test.tsx | 338 + .../components/GroupTypesPanel.tsx | 406 +- .../components/MultiCreateDialog.test.tsx | 219 + .../components/MultiCreateDialog.tsx | 119 + .../components/RightPanel.test.tsx | 116 + .../components/RightPanel.tsx | 82 + .../components/ShiftPanel.test.tsx | 55 + .../components/ShiftPanel.tsx | 4 +- .../components/StatusBar.test.tsx | 133 + .../components/StatusBar.tsx | 32 +- .../components/TemplateGrid.test.tsx | 286 + .../components/TemplateGrid.tsx | 21 +- .../components/WarningPanel.test.tsx | 41 + .../components/WarningPanel.tsx | 16 +- .../components/WellGroupProperties.test.tsx | 168 + .../components/WellGroupProperties.tsx | 13 +- .../src/client/PlateTemplateDesigner/dev.tsx | 14 +- .../PlateTemplateDesigner/models.test.ts | 153 + .../client/PlateTemplateDesigner/models.ts | 10 +- assay/test/js/fileMock.js | 1 + assay/test/js/setup.ts | 1 + 26 files changed, 9585 insertions(+), 3784 deletions(-) create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts create mode 100644 assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/models.test.ts create mode 100644 assay/test/js/fileMock.js create mode 100644 assay/test/js/setup.ts diff --git a/assay/package-lock.json b/assay/package-lock.json index 3833d904a1c..f501dd2d16f 100644 --- a/assay/package-lock.json +++ b/assay/package-lock.json @@ -14,7 +14,12 @@ "@labkey/build": "9.1.1", "@types/jest": "30.0.0", "@types/react": "18.3.27", - "@types/react-dom": "18.3.7" + "@types/react-dom": "18.3.7", + "jest": "30.3.0", + "jest-cli": "30.3.0", + "jest-environment-jsdom": "30.3.0", + "jest-teamcity-reporter": "0.9.0", + "ts-jest": "29.4.6" } }, "node_modules/@adobe/css-tools": { @@ -23,6 +28,27 @@ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "license": "MIT" }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/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, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -477,6 +503,61 @@ "@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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-import-assertions": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", @@ -509,6 +590,32 @@ "@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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", @@ -525,6 +632,116 @@ "@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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", @@ -1698,6 +1915,128 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -1708,6 +2047,40 @@ "node": ">=14.17.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2155,485 +2528,797 @@ "react": "*" } }, - "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "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, - "license": "MIT", + "license": "ISC", + "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": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" } }, - "node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "node_modules/@isaacs/cliui/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, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/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, "license": "MIT", "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "node_modules/@isaacs/cliui/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, "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "sprintf-js": "~1.0.2" } }, - "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==", + "node_modules/@istanbuljs/load-nyc-config/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, "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@istanbuljs/load-nyc-config/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, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/@istanbuljs/load-nyc-config/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, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=10.0" + "node": ">=6" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": ">=8" } }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "node_modules/@istanbuljs/load-nyc-config/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, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=8" } }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=8" } }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", - "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", - "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "tslib": "2" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", - "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", + "node_modules/@jest/core/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, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/fs-print": "4.57.2", - "@jsonjoy.com/fs-snapshot": "4.57.2", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", - "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", - "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", + "node_modules/@jest/core/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, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2" + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", - "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "tslib": "2" + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", - "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.2", - "tree-dump": "^1.1.0" + "expect": "30.3.0", + "jest-snapshot": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", - "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" + "@jest/get-type": "30.1.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", - "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", - "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", - "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "17.67.0", - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0", - "@jsonjoy.com/json-pointer": "17.67.0", - "@jsonjoy.com/util": "17.67.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", - "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/util": "17.67.0" + "@types/node": "*", + "jest-regex-util": "30.0.1" }, "engines": { - "node": ">=10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "tslib": "2" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", - "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "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" }, - "engines": { - "node": ">=10.0" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters/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, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/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, + "license": "BlueOak-1.0.0", + "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/@jest/reporters/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "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, + "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -2645,10 +3330,10 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2662,16 +3347,12 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, "engines": { "node": ">=10.0" }, @@ -2683,15 +3364,16 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" @@ -2704,12 +3386,18 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, "engines": { "node": ">=10.0" }, @@ -2721,227 +3409,1668 @@ "tslib": "2" } }, - "node_modules/@labkey/api": { - "version": "1.51.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.1.tgz", - "integrity": "sha512-RORsQpToUXkGsZMqMfqW+5d8g3r09s2Pojjz4z66hZR3nXw6K6U7xaXih/+96vFwbJ7BeqUsbv71+5dxX6Bmfg==", - "license": "Apache-2.0" - }, - "node_modules/@labkey/build": { - "version": "9.1.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/build/-/@labkey/build-9.1.1.tgz", - "integrity": "sha512-+AQnP+dLBiCu0V60DC8UQBtZWolr2L+r80Hvz7785Wvm3/FvHu01eEwQFKa9sv9VMNLRIvDxxPL/6nVLmySDEg==", + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/core": "~7.29.0", - "@babel/plugin-transform-class-properties": "~7.28.6", - "@babel/plugin-transform-object-rest-spread": "~7.28.6", - "@babel/preset-env": "~7.29.2", - "@babel/preset-react": "~7.28.5", - "@babel/preset-typescript": "~7.28.5", - "@pmmmwh/react-refresh-webpack-plugin": "~0.6.2", - "ajv": "~8.18.0", - "babel-loader": "~10.1.1", - "bootstrap-sass": "~3.4.3", - "copy-webpack-plugin": "~14.0.0", - "cross-env": "~10.1.0", - "css-loader": "~7.1.4", - "fork-ts-checker-webpack-plugin": "~9.1.0", - "html-webpack-plugin": "~5.6.7", - "mini-css-extract-plugin": "~2.10.1", - "react-refresh": "~0.18.0", - "resolve-url-loader": "~5.0.0", - "rimraf": "~6.1.3", - "sass": "~1.99.0", - "sass-loader": "~16.0.7", - "source-map-loader": "~5.0.0", - "style-loader": "~4.0.0", - "typescript": "~5.9.3", - "webpack": "~5.106.2", - "webpack-bundle-analyzer": "~5.3.0", - "webpack-cli": "~7.0.2", - "webpack-dev-server": "~5.2.3" - } - }, - "node_modules/@labkey/components": { - "version": "7.31.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.31.1.tgz", - "integrity": "sha512-AsyqBEPM2afKzNFicSZVljNBqqzZ5qJ2KKI7rNqBBbRyU7ut4mAZ8nUmTEdv31g/Uj8wvRlkI7RrnasE4TE3EQ==", - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.51.1", - "@testing-library/dom": "~10.4.1", - "@testing-library/jest-dom": "~6.9.1", - "@testing-library/react": "~16.3.2", - "@testing-library/user-event": "~14.6.1", - "bootstrap": "~3.4.1", - "classnames": "~2.5.1", - "date-fns": "~3.6.0", - "date-fns-tz": "~3.2.0", - "font-awesome": "~4.7.0", - "immer": "~10.1.3", - "immutable": "~3.8.3", - "normalizr": "~3.6.2", - "numeral": "~2.0.6", - "papaparse": "5.5.3", - "react": "~18.3.1", - "react-color": "~2.19.3", - "react-datepicker": "~7.6.0", - "react-dom": "~18.3.1", - "react-router-dom": "~6.30.1", - "react-select": "~5.10.2", - "react-treebeard": "~3.2.4", - "vis-data": "~8.0.3", - "vis-network": "~10.0.2" + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "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, - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 16" + "node": ">=10.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.57.2", + "tree-dump": "^1.1.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@labkey/api": { + "version": "1.51.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.1.tgz", + "integrity": "sha512-RORsQpToUXkGsZMqMfqW+5d8g3r09s2Pojjz4z66hZR3nXw6K6U7xaXih/+96vFwbJ7BeqUsbv71+5dxX6Bmfg==", + "license": "Apache-2.0" + }, + "node_modules/@labkey/build": { + "version": "9.1.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/build/-/@labkey/build-9.1.1.tgz", + "integrity": "sha512-+AQnP+dLBiCu0V60DC8UQBtZWolr2L+r80Hvz7785Wvm3/FvHu01eEwQFKa9sv9VMNLRIvDxxPL/6nVLmySDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/plugin-transform-class-properties": "~7.28.6", + "@babel/plugin-transform-object-rest-spread": "~7.28.6", + "@babel/preset-env": "~7.29.2", + "@babel/preset-react": "~7.28.5", + "@babel/preset-typescript": "~7.28.5", + "@pmmmwh/react-refresh-webpack-plugin": "~0.6.2", + "ajv": "~8.18.0", + "babel-loader": "~10.1.1", + "bootstrap-sass": "~3.4.3", + "copy-webpack-plugin": "~14.0.0", + "cross-env": "~10.1.0", + "css-loader": "~7.1.4", + "fork-ts-checker-webpack-plugin": "~9.1.0", + "html-webpack-plugin": "~5.6.7", + "mini-css-extract-plugin": "~2.10.1", + "react-refresh": "~0.18.0", + "resolve-url-loader": "~5.0.0", + "rimraf": "~6.1.3", + "sass": "~1.99.0", + "sass-loader": "~16.0.7", + "source-map-loader": "~5.0.0", + "style-loader": "~4.0.0", + "typescript": "~5.9.3", + "webpack": "~5.106.2", + "webpack-bundle-analyzer": "~5.3.0", + "webpack-cli": "~7.0.2", + "webpack-dev-server": "~5.2.3" + } + }, + "node_modules/@labkey/components": { + "version": "7.31.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.31.1.tgz", + "integrity": "sha512-AsyqBEPM2afKzNFicSZVljNBqqzZ5qJ2KKI7rNqBBbRyU7ut4mAZ8nUmTEdv31g/Uj8wvRlkI7RrnasE4TE3EQ==", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@hello-pangea/dnd": "18.0.1", + "@labkey/api": "1.51.1", + "@testing-library/dom": "~10.4.1", + "@testing-library/jest-dom": "~6.9.1", + "@testing-library/react": "~16.3.2", + "@testing-library/user-event": "~14.6.1", + "bootstrap": "~3.4.1", + "classnames": "~2.5.1", + "date-fns": "~3.6.0", + "date-fns-tz": "~3.2.0", + "font-awesome": "~4.7.0", + "immer": "~10.1.3", + "immutable": "~3.8.3", + "normalizr": "~3.6.2", + "numeral": "~2.0.6", + "papaparse": "5.5.3", + "react": "~18.3.1", + "react-color": "~2.19.3", + "react-datepicker": "~7.6.0", + "react-dom": "~18.3.1", + "react-router-dom": "~6.30.1", + "react-select": "~5.10.2", + "react-treebeard": "~3.2.4", + "vis-data": "~8.0.3", + "vis-network": "~10.0.2" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", + "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "anser": "^2.1.1", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "@types/webpack": "5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <6.0.0", + "webpack": "^5.0.0", + "webpack-dev-server": "^4.8.0 || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "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, + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/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, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "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, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", "cpu": [ "arm" ], @@ -2950,40 +5079,141 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", "cpu": [ - "arm" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", "cpu": [ "arm64" ], @@ -2991,1975 +5221,2579 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "win32" + ] }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "win32" + ] }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", "cpu": [ "x64" ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "optional": true, + "os": [ + "win32" + ] + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "Apache-2.0", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.6" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "bin": { + "acorn": "bin/acorn" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" + "node": ">=10.13.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "acorn": "^8.14.0" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { - "node": ">= 10.0.0" + "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, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=8.9" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 14" } }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "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/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "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, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "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, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "node_modules/anser": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", + "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "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" + ], + "license": "Apache-2.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==", "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "engines": { + "node": ">=8" } }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "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, - "license": "MIT", + "license": "ISC", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", - "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "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, - "license": "MIT", + "license": "Python-2.0" + }, + "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==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "dequal": "^2.0.3" } }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "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, - "license": "MIT", + "license": "MIT" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" } }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" }, "engines": { - "node": ">=20.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", - "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", + "node_modules/babel-loader": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", + "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", "dev": true, "license": "MIT", "dependencies": { - "anser": "^2.1.1", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "html-entities": "^2.1.0", - "schema-utils": "^4.2.0", - "source-map": "^0.7.3" + "find-up": "^5.0.0" }, "engines": { - "node": ">=18.12" + "node": "^18.20.0 || ^20.10.0 || >=22.0.0" }, "peerDependencies": { - "@types/webpack": "5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <6.0.0", - "webpack": "^5.0.0", - "webpack-dev-server": "^4.8.0 || 5.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "1.x" + "@babel/core": "^7.12.0 || ^8.0.0-beta.1", + "@rspack/core": "^1.0.0 || ^2.0.0-0", + "webpack": ">=5.61.0" }, "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { + "@rspack/core": { "optional": true }, - "webpack-plugin-serve": { + "webpack": { "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/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==", + "license": "MIT", + "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==", "license": "MIT" }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", "license": "MIT", "engines": { - "node": ">=14.0.0" + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" + "@types/babel__core": "^7.20.5" }, "engines": { - "node": ">=18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "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==", "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" }, "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" + "node": ">=10", + "npm": ">=6" } }, - "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==", - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "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==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" + "@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": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node": ">=10" } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" }, "peerDependencies": { - "@testing-library/dom": ">=7.21.4" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "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==", - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "dev": true, "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "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==", + "license": "MIT" + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@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" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, - "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==", + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, - "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==", + "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, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, - "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==", + "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, + "license": "MIT" + }, + "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==", "dev": true, "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "engines": { + "node": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "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, "license": "MIT", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "ms": "2.0.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==", + "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, - "license": "MIT" + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "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, "license": "MIT" }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "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, "license": "MIT", "dependencies": { - "@types/node": "*" + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" } }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "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, - "license": "MIT" + "license": "ISC" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "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==", + "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "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, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-report": "*" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/jest/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==", + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "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" + } + ], "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "fast-json-stable-stringify": "2.x" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 6" } }, - "node_modules/@types/jest/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, - "license": "MIT" - }, - "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==", + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } }, - "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==", + "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, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "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, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } }, - "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, - "license": "MIT" + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, + "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==", + "license": "MIT", + "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==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" + "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/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "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, "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" } }, - "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==", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*" + "engines": { + "node": ">=6" } }, - "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==", + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", "dev": true, - "license": "MIT" + "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" + } + ], + "license": "CC-BY-4.0" }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "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==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", - "dependencies": { - "@types/express": "*" + "engines": { + "node": ">=10" } }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "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, "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "engines": { + "node": ">=6.0" } }, - "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==", + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "license": "MIT", - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=8" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "dev": true, "license": "MIT" }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "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==", "license": "MIT" }, - "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==", + "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, "license": "MIT", "dependencies": { - "@types/node": "*" + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" } }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "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, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } }, - "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==", + "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, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "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==", + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, - "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==", + "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==", + "dev": true, + "license": "MIT", + "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==", "dev": true, "license": "MIT" }, - "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==", + "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, "license": "MIT" }, - "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==", + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" + "engines": { + "node": ">= 12" } }, - "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, - "license": "MIT" - }, - "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==", + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "license": "MIT", "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" + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" } }, - "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==", + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.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==", + "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, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@xtuc/long": "4.2.2" + "ms": "2.0.0" } }, - "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==", + "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, "license": "MIT" }, - "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==", + "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, - "license": "MIT", - "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" - } + "license": "MIT" }, - "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==", + "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, "license": "MIT", - "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" + "engines": { + "node": ">=0.8" } }, - "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==", + "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, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "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==", + "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==", "dev": true, "license": "MIT", - "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" + "engines": { + "node": ">= 0.6" } }, - "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==", + "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, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } + "license": "MIT" }, - "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==", + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "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==", + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "dev": true, - "license": "Apache-2.0" + "license": "MIT" }, - "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==", + "node_modules/copy-webpack-plugin": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", + "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^7.0.3", + "tinyglobby": "^0.2.12" }, "engines": { - "node": ">= 0.6" + "node": ">= 20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" } }, - "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==", + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/core-js-pure": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "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, + "license": "MIT" + }, + "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, "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, "engines": { - "node": ">=10.13.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "acorn": "^8.14.0" + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.0" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" }, "engines": { - "node": ">=0.4.0" + "node": ">=20" } }, - "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==", + "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, "license": "MIT", "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=8.9" + "node": ">= 8" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, + "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==", "license": "MIT", "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" + "tiny-invariant": "^1.0.6" } }, - "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==", + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "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.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "ajv": "^8.0.0" + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" }, "peerDependenciesMeta": { - "ajv": { + "@rspack/core": { + "optional": true + }, + "webpack": { "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, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/anser": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", - "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", - "dev": true, - "license": "MIT" - }, - "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==", + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", + "license": "ISC", "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==", - "license": "MIT", + "semver": "bin/semver.js" + }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/ansi-styles": { + "node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "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/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/fb55" } }, - "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==", + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 8" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "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==", + "license": "MIT" + }, + "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, "license": "MIT", - "engines": { - "node": ">=8.6" + "bin": { + "cssesc": "bin/cssesc" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, - "license": "Python-2.0" - }, - "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==", - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "dequal": "^2.0.3" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" } }, - "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" } }, - "node_modules/babel-loader": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", - "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "find-up": "^5.0.0" + "ms": "^2.1.3" }, "engines": { - "node": "^18.20.0 || ^20.10.0 || >=22.0.0" + "node": ">=6.0" }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", "peerDependencies": { - "@babel/core": "^7.12.0 || ^8.0.0-beta.1", - "@rspack/core": "^1.0.0 || ^2.0.0-0", - "webpack": ">=5.61.0" + "babel-plugin-macros": "^3.1.0" }, "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { + "babel-plugin-macros": { "optional": true } } }, - "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==", + "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==", "license": "MIT", "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" + "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/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==", - "license": "MIT" - }, - "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==", - "license": "MIT" + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "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==", + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, "license": "MIT", "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" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "license": "MIT" - }, - "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==", - "license": "MIT" + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "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==", + "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==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "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/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==", - "license": "MIT" + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "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==", + "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==", "license": "MIT", "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" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "license": "MIT" - }, - "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==", + "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, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">= 0.8" } }, - "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==", - "license": "BSD-3-Clause", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "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==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, "engines": { - "node": ">=10", - "npm": ">=6" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "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==", - "license": "MIT", - "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" - }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", - "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "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, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } + "license": "MIT" }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "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, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" + "@leichtgewicht/ip-codec": "^2.0.1" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=6" } }, - "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==", - "license": "MIT" - }, - "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/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==", "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "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, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "utila": "~0.4" } }, - "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, - "license": "MIT" + "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==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } }, - "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==", + "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, "license": "MIT", - "engines": { - "node": "*" + "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/binary-extensions": { + "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, "engines": { - "node": ">=8" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "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.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "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==", + "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, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "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, + "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==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "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==", + "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, "license": "MIT" }, - "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==", + "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, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } + "license": "MIT" }, - "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==", + "node_modules/electron-to-chromium": { + "version": "1.5.341", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.341.tgz", + "integrity": "sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==", "dev": true, "license": "ISC" }, - "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==", - "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "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==", + "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, "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 4" } }, - "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==", + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "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" - } - ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=10.13.0" } }, - "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==", + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, - "license": "MIT" + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "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==", + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", "dev": true, "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" + "bin": { + "envinfo": "dist/cli.js" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "is-arrayish": "^0.2.1" } }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "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, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "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==", "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "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==", + "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==", "license": "MIT", - "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==", + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "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==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "es-errors": "^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==", + "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, "license": "MIT", "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==", + "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, + "license": "MIT" + }, + "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==", "license": "MIT", + "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, + "license": "BSD-2-Clause", "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "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" - } - ], - "license": "CC-BY-4.0" + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "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, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=0.10.0" } }, - "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==", + "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, "license": "MIT", "engines": { - "node": ">=6.0" + "node": ">= 0.6" } }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.8.x" } }, - "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==", - "license": "MIT" - }, - "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==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { - "source-map": "~0.6.0" + "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.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "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==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.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==", + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": ">=6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, "license": "MIT", + "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.14.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": ">=6" + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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==", + "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, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "ms": "2.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/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, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "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==", "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "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, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 4.9.1" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "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, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": ">= 1.43.0 < 2" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8" } }, - "node_modules/compression/node_modules/debug": { + "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==", @@ -4969,240 +7803,188 @@ "ms": "2.0.0" } }, - "node_modules/compression/node_modules/ms": { + "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, "license": "MIT" }, - "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/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, - "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, - "license": "MIT", - "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==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" } }, - "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, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT" + "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==", + "license": "(OFL-1.1 AND MIT)", + "engines": { + "node": ">=0.10.3" + } }, - "node_modules/copy-webpack-plugin": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", - "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", + "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, - "license": "MIT", + "license": "ISC", "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^7.0.3", - "tinyglobby": "^0.2.12" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 20.9.0" + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "node_modules/foreground-child/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, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" + "license": "ISC", + "engines": { + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", - "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "url": "https://github.com/sponsors/isaacs" } }, - "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, - "license": "MIT" - }, - "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==", + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", "dev": true, "license": "MIT", "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "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": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" + "node": ">=14.21.3" }, "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">3.6.0", + "webpack": "^5.11.0" } }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "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==", - "license": "MIT", - "dependencies": { - "tiny-invariant": "^1.0.6" - } + "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, + "license": "MIT" }, - "node_modules/css-loader": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", - "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "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, "license": "MIT", "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.40", - "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.6.3" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } } }, - "node_modules/css-loader/node_modules/semver": { + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", @@ -5215,109 +7997,124 @@ "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==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, - "license": "BSD-2-Clause", - "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" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "node": ">= 0.6" } }, - "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==", - "license": "MIT" - }, - "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==", + "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, "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" }, - "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==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "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==", "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns-tz": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", - "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "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==", "license": "MIT", - "peerDependencies": { - "date-fns": "^3.0.0 || ^4.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6.9.0" } }, - "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==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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==", "license": "MIT", "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" + "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" @@ -5326,852 +8123,999 @@ "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==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=18" + "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, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, "engines": { - "node": ">=18" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", - "license": "MIT", + "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, + "license": "ISC", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10.13.0" } }, - "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==", + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "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==", + "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, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", - "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": "18 || 20 || >=22" } }, - "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==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, "engines": { - "node": ">= 0.8" + "node": "18 || 20 || >=22" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, "engines": { - "node": ">=6" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/destroy": { + "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "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==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "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==", + "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, "license": "MIT" }, - "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==", + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=6" + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "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==", - "license": "MIT" - }, - "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==", + "node_modules/handlebars/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, - "license": "MIT", - "dependencies": { - "utila": "~0.4" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "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==", + "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==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "engines": { + "node": ">=8" } }, - "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, + "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==", "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "es-define-property": "^1.0.0" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "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" - } - ], - "license": "BSD-2-Clause" - }, - "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, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, + "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==", + "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", + "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==", + "license": "MIT", "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "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, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, - "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==", + "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, + "license": "MIT", + "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==", + "license": "BSD-3-Clause", + "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==", "license": "MIT" }, - "node_modules/electron-to-chromium": { - "version": "1.5.341", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.341.tgz", - "integrity": "sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==", + "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, - "license": "ISC" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "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, "license": "MIT", - "engines": { - "node": ">= 4" + "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/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "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, + "license": "MIT" + }, + "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, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "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, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" }, - "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, + "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, "license": "MIT", + "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": { - "envinfo": "dist/cli.js" + "html-minifier-terser": "cli.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" + "node": ">=12" } }, - "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==", + "node_modules/html-webpack-plugin": { + "version": "5.6.7", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", "dev": true, "license": "MIT", "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==", - "license": "MIT", + "@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": ">= 0.4" + "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/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "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" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "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, "license": "MIT" }, - "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==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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, + "license": "MIT" + }, + "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, "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8.0.0" } }, - "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==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "license": "MIT" - }, - "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==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 14" } }, - "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==", + "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, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "@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": ">=8.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } } }, - "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==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=4.0" + "node": ">= 14" } }, - "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==", + "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, - "license": "BSD-2-Clause", + "license": "Apache-2.0", "engines": { - "node": ">=4.0" + "node": ">=10.17.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=10.18" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "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==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "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==", + "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, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">= 0.6" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, - "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, + "node_modules/immutable": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz", + "integrity": "sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==", "license": "MIT", "engines": { - "node": ">=0.8.x" + "node": ">=0.10.0" } }, - "node_modules/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", - "dev": true, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.3.0", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "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, "license": "MIT", "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.14.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" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">= 0.10.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=0.8.19" } }, - "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, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT" + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } }, - "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==", + "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, + "license": "ISC" + }, + "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, "license": "MIT", "engines": { - "node": ">= 4.9.1" + "node": ">=10.13.0" } }, - "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==", + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "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==", + "license": "MIT", "dependencies": { - "websocket-driver": ">=0.5.1" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=0.8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "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==", + "license": "MIT" + }, + "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, "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, "engines": { - "node": ">=12.0.0" + "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==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "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, "license": "MIT", - "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.2", - "unpipe": "~1.0.0" + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": ">= 0.8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=0.10.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==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "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==", - "license": "MIT" + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=10" + "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, + "license": "MIT", + "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/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "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==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=0.12.0" } }, - "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==", - "license": "(OFL-1.1 AND MIT)", + "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, + "license": "MIT", "engines": { - "node": ">=0.10.3" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", - "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "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, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, + "license": "MIT" + }, + "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==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.16.7", - "chalk": "^4.1.2", - "chokidar": "^4.0.1", - "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" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=14.21.3" + "node": ">= 0.4" }, - "peerDependencies": { - "typescript": ">3.6.0", - "webpack": "^5.11.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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, "license": "MIT", - "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" + "engines": { + "node": ">=8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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, "license": "MIT" }, - "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==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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, "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=10" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "node_modules/istanbul-lib-instrument/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", @@ -6184,929 +9128,1050 @@ "node": ">=10" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "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==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "node_modules/istanbul-reports/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, - "license": "Unlicense" + "license": "MIT" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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, - "hasInstallScript": true, - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "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==", + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", - "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==", - "license": "MIT", "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" + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "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==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "engines": { - "node": ">= 0.4" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" }, "engines": { - "node": "18 || 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==", + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">=10.13.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "node_modules/jest-circus/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, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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, - "license": "BSD-2-Clause" - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "node_modules/jest-circus/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, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.5" + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/jest-config/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, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", - "dev": true, - "license": "ISC" - }, - "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, - "license": "MIT" - }, - "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==", + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "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==", - "license": "MIT", + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", "dependencies": { - "es-define-property": "^1.0.0" + "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/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", - "license": "MIT", + "node_modules/jest-config/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, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", - "license": "MIT", + "node_modules/jest-config/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, + "license": "BlueOak-1.0.0", "dependencies": { - "has-symbols": "^1.0.3" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "node_modules/jest-config/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-config/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, - "license": "MIT", - "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==", - "license": "BSD-3-Clause", - "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==", "license": "MIT" }, - "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==", + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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==", + "node_modules/jest-diff/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "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" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-diff/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, "license": "MIT" }, - "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==", + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", - "dev": true, - "license": "MIT" - }, - "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==", + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", "dev": true, "license": "MIT", "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" + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/html-webpack-plugin": { - "version": "5.6.7", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", - "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", + "node_modules/jest-each/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, "license": "MIT", - "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" + "node": ">=10" }, "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 - } + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "node_modules/jest-each/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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==", + "node_modules/jest-each/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, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", "dev": true, "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "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, - "license": "MIT" - }, - "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==", + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", "dev": true, "license": "MIT", "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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==", + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", "dev": true, "license": "MIT", "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" + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, "engines": { - "node": ">=10.18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-haste-map/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, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "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==", + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" }, - "peerDependencies": { - "postcss": "^8.1.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "node_modules/jest-leak-detector/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, "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/immutable": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz", - "integrity": "sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==", + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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==", + "node_modules/jest-leak-detector/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, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-matcher-utils/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, "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/jest-matcher-utils/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, - "license": "ISC" + "license": "MIT" }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, "engines": { - "node": ">=10.13.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "node_modules/jest-message-util/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, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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/jest-message-util/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, "license": "MIT" }, - "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==", + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "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==", + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", "dev": true, "license": "MIT", - "bin": { - "is-docker": "cli.js" + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", "dev": true, "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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==", + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-network-error": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", - "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "node_modules/jest-runner/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, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "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==", + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.12.0" + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.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==", + "node_modules/jest-runner/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, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "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==", + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", "dev": true, "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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==", + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, "license": "MIT", "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" + "balanced-match": "^1.0.0" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-inside-container": "^1.0.0" + "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" }, - "engines": { - "node": ">=16" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/jest-runtime/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, - "license": "MIT" + "license": "ISC" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/jest-runtime/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, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-diff": { + "node_modules/jest-snapshot": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { + "node_modules/jest-snapshot/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==", @@ -7119,7 +10184,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-diff/node_modules/pretty-format": { + "node_modules/jest-snapshot/node_modules/pretty-format": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", @@ -7134,86 +10199,70 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/react-is": { + "node_modules/jest-snapshot/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, "license": "MIT" }, - "node_modules/jest-matcher-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", - "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.3.0", - "pretty-format": "30.3.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10" } }, - "node_modules/jest-matcher-utils/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==", + "node_modules/jest-teamcity-reporter": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/jest-teamcity-reporter/-/jest-teamcity-reporter-0.9.0.tgz", + "integrity": "sha512-q6W+ZaJSCIXmxC9wsY67zNn+vwG/EgKJygYJYH860jih5zS6mc2ZFc4v78gh6rgzgM9/siUtQm7SnRunYuWmVw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "node_modules/jest-util": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-matcher-utils/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, - "license": "MIT" - }, - "node_modules/jest-message-util": { + "node_modules/jest-validate": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", - "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", + "@jest/get-type": "30.1.0", "@jest/types": "30.3.0", - "@types/stack-utils": "^2.0.3", + "camelcase": "^6.3.0", "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3", - "pretty-format": "30.3.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "leven": "^3.1.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { + "node_modules/jest-validate/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==", @@ -7226,7 +10275,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-message-util/node_modules/pretty-format": { + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", @@ -7241,51 +10303,28 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/react-is": { + "node_modules/jest-validate/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, "license": "MIT" }, - "node_modules/jest-mock": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.3.0", - "@types/node": "*", - "jest-util": "30.3.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util": { + "node_modules/jest-watcher": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", "dev": true, "license": "MIT", "dependencies": { + "@jest/test-result": "30.3.0", "@jest/types": "30.3.0", "@types/node": "*", + "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7341,6 +10380,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7408,9 +10487,19 @@ "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", "dev": true, "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/lines-and-columns": { @@ -7483,6 +10572,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7524,6 +10620,52 @@ "lz-string": "bin/bin.js" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", @@ -7668,6 +10810,16 @@ "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -7718,6 +10870,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -7777,6 +10939,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -7820,6 +11005,13 @@ "license": "MIT", "optional": true }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -7843,6 +11035,19 @@ "integrity": "sha512-30qCybsBaCBciotorvuOZTCGEg2AXrJfADMT2Kk/lvpIAcipHdK0zc33nNtwKzyfQAqIJXAcqET6YgflYUgsoQ==", "license": "MIT" }, + "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, + "license": "MIT", + "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", @@ -7865,6 +11070,13 @@ "node": "*" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7942,6 +11154,32 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -8085,6 +11323,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8116,6 +11380,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8194,6 +11468,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8489,6 +11773,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -8948,6 +12249,16 @@ "strip-ansi": "^6.0.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -9082,6 +12393,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -9192,6 +12510,19 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -9567,6 +12898,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "license": "ISC" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -9698,6 +13036,13 @@ "wbuf": "^1.7.3" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9748,6 +13093,51 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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": { + "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9761,6 +13151,40 @@ "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -9773,6 +13197,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -9821,6 +13258,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", @@ -9899,7 +13359,44 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/thingies": { "version": "2.6.0", @@ -9954,6 +13451,33 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9987,6 +13511,32 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tree-dump": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", @@ -10004,6 +13554,85 @@ "tslib": "2" } }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10031,6 +13660,29 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -10059,6 +13711,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -10130,6 +13796,41 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -10228,6 +13929,21 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -10317,6 +14033,29 @@ "vis-util": ">=6.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -10341,6 +14080,16 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/webpack": { "version": "5.106.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", @@ -10747,6 +14496,44 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10770,6 +14557,84 @@ "dev": true, "license": "MIT" }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "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": { + "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, + "license": "MIT", + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -10808,6 +14673,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -10824,6 +14716,35 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/assay/package.json b/assay/package.json index 7e67a515ade..e4b0b40973e 100644 --- a/assay/package.json +++ b/assay/package.json @@ -9,7 +9,42 @@ "start-link": "cross-env LINK=true npm run start", "build-dev": "npm run clean && cross-env NODE_ENV=development webpack --config node_modules/@labkey/build/webpack/dev.config.js --color", "build-prod": "npm run clean && cross-env NODE_ENV=production PROD_SOURCE_MAP=source-map webpack --config node_modules/@labkey/build/webpack/prod.config.js --color --progress --profile", - "clean": "rimraf resources/web/assay/gen && rimraf resources/views/gen && rimraf resources/web/gen" + "clean": "rimraf resources/web/assay/gen && rimraf resources/views/gen && rimraf resources/web/gen", + "test": "cross-env NODE_ENV=test jest" + }, + "jest": { + "globals": { + "LABKEY": { + "container": { + "formats": { + "dateFormat": "yyyy-MM-dd" + } + }, + "user": { + "id": 1004 + }, + "project": {}, + "moduleContext": {} + } + }, + "moduleNameMapper": { + "\\.(scss|css)$": "/test/js/fileMock.js" + }, + "moduleFileExtensions": ["tsx", "ts", "js"], + "preset": "ts-jest", + "setupFilesAfterEnv": ["/test/js/setup.ts"], + "testEnvironment": "jsdom", + "testMatch": null, + "testRegex": "(\\.(test))\\.(ts|tsx)$", + "testResultsProcessor": "jest-teamcity-reporter", + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "tsconfig": "node_modules/@labkey/build/webpack/tsconfig.test.json" + } + ] + } }, "dependencies": { "@labkey/components": "7.31.1" @@ -18,6 +53,11 @@ "@labkey/build": "9.1.1", "@types/jest": "30.0.0", "@types/react": "18.3.27", - "@types/react-dom": "18.3.7" + "@types/react-dom": "18.3.7", + "jest": "30.3.0", + "jest-cli": "30.3.0", + "jest-environment-jsdom": "30.3.0", + "jest-teamcity-reporter": "0.9.0", + "ts-jest": "29.4.6" } } diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 46162a3fde6..015dfac6eab 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -31,10 +31,23 @@ * ────────────────────────────────────────────────────────────────────────────── */ +// Visually-hidden utility — renders only to assistive technology +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + // Root container .plate-template-designer { padding: 12px 16px; - font-size: 13px; + font-size: 0.8125rem; &__error { color: #c00; @@ -59,7 +72,7 @@ padding: 4px 6px; border: 1px solid #ccc; border-radius: 3px; - font-size: 13px; + font-size: 0.8125rem; width: 280px; } @@ -79,6 +92,12 @@ &__right { flex: 1; min-width: 200px; + + // Guarantee hidden tabpanels (properties / warnings) take no space even if + // the framework's CSS reset omits the [hidden]{display:none} UA rule. + [hidden] { + display: none; + } } } @@ -98,7 +117,7 @@ border-radius: 3px; background: #f5f5f5; cursor: pointer; - font-size: 13px; + font-size: 0.8125rem; &:hover:not(:disabled) { background: #e8e8e8; @@ -130,6 +149,13 @@ &__status { color: #555; } + + // Validation error message (e.g., empty plate name) — uses role="alert" in JSX + &__error { + color: #c00; + font-weight: bold; + margin-left: 12px; + } } // ── GroupTypesPanel ─────────────────────────────────────────────────────────── @@ -152,7 +178,7 @@ border: none; background: transparent; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; border-right: 1px solid #ddd; &:hover { @@ -172,11 +198,17 @@ align-items: flex-start; gap: 12px; padding: 8px; + + // Some CSS frameworks omit the UA-stylesheet [hidden]{display:none} rule. + // The higher specificity (class + attribute) ensures this wins over display:flex. + &[hidden] { + display: none; + } } - // Group list column — fixed width with a minimum so short lists don't collapse + // Group list column — min-width prevents short lists from collapsing &__groups { - flex: 0 0 160px; + flex-shrink: 0; min-width: 280px; min-height: 60px; } @@ -227,7 +259,7 @@ padding: 1px 4px; border: 1px solid #337ab7; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; min-width: 0; &--error { @@ -238,25 +270,34 @@ // Validation error shown below the create row or the rename input &__name-error { color: #c00; - font-size: 12px; + font-size: 0.75rem; margin-top: 2px; } - // Rename + delete buttons, visible only for the active group row + // Rename + delete buttons; always rendered to keep row height uniform. + // Inactive rows get --hidden: visibility:hidden preserves layout space while + // pointer-events:none blocks mouse interaction. &__group-actions { display: flex; gap: 2px; margin-left: auto; flex-shrink: 0; + + &--hidden { + visibility: hidden; + pointer-events: none; + } } &__action-btn { - padding: 2px 5px; + padding: 5px 7px; + min-height: 24px; + min-width: 24px; border: 1px solid #ccc; border-radius: 2px; background: transparent; cursor: pointer; - font-size: 11px; + font-size: 0.6875rem; line-height: 1; color: #555; @@ -287,13 +328,13 @@ padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; min-width: 0; } &__add-btn { padding: 3px 8px; - font-size: 12px; + font-size: 0.75rem; border: 1px solid #aaa; border-radius: 3px; background: #f5f5f5; @@ -346,32 +387,43 @@ &__title { font-weight: bold; - font-size: 14px; + font-size: 0.875rem; margin-bottom: 14px; } - &__table { - width: 100%; - border-collapse: collapse; + // Flex column container replacing the old layout table + &__form { + display: flex; + flex-direction: column; + gap: 10px; + } - td { - padding-bottom: 10px; - } + // Each label + input row + &__field { + display: flex; + align-items: flex-start; + gap: 16px; } // Label cells carry id attributes and are referenced via aria-labelledby on the inputs &__label { - padding: 6px 16px 6px 0; + padding-top: 5px; white-space: nowrap; - font-size: 13px; - vertical-align: middle; + font-size: 0.8125rem; + min-width: 80px; + } + + // Wrapper for count input + error message (stacked vertically) + &__count-area { + display: flex; + flex-direction: column; } &__input { padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 13px; + font-size: 0.8125rem; width: 100%; box-sizing: border-box; @@ -382,12 +434,11 @@ &__error { color: #c00; - font-size: 12px; + font-size: 0.75rem; margin-top: 2px; } &__buttons { - padding-top: 10px; display: flex; gap: 6px; justify-content: flex-end; @@ -424,7 +475,7 @@ border-radius: 3px; background: #f5f5f5; cursor: pointer; - font-size: 14px; + font-size: 0.875rem; line-height: 1; &:hover { @@ -433,7 +484,7 @@ } &__label { - font-size: 10px; + font-size: 0.625rem; color: #666; text-align: center; line-height: 1; @@ -459,7 +510,7 @@ &__col-header { text-align: center; - font-size: 11px; + font-size: 0.6875rem; font-weight: bold; width: 28px; height: 22px; @@ -469,7 +520,7 @@ &__row-header { text-align: center; - font-size: 11px; + font-size: 0.6875rem; font-weight: bold; width: 22px; background: #f0f0f0; @@ -508,7 +559,7 @@ border: none; background: transparent; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; border-bottom: 2px solid transparent; margin-bottom: -1px; @@ -554,7 +605,7 @@ &__no-props { color: #767676; font-style: italic; - font-size: 12px; + font-size: 0.75rem; } &__table { @@ -584,18 +635,20 @@ padding: 3px 4px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; width: 100%; box-sizing: border-box; } &__delete-btn { - padding: 2px 5px; + padding: 5px 7px; + min-height: 24px; + min-width: 24px; border: 1px solid #ccc; border-radius: 2px; background: transparent; cursor: pointer; - font-size: 11px; + font-size: 0.6875rem; line-height: 1; color: #c00; @@ -614,7 +667,7 @@ padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; width: 100%; box-sizing: border-box; } @@ -623,7 +676,7 @@ padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; width: 100%; box-sizing: border-box; } @@ -634,7 +687,7 @@ border-radius: 3px; background: #f5f5f5; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; white-space: nowrap; &:hover:not(:disabled) { @@ -657,16 +710,10 @@ padding: 10px; background: #fffbe6; - &__title { - font-weight: bold; - color: #7a5800; - margin-bottom: 6px; - } - &__none { color: #767676; font-style: italic; - font-size: 12px; + font-size: 0.75rem; } &__list { @@ -676,7 +723,7 @@ &__item { color: #7a5800; - font-size: 12px; + font-size: 0.75rem; margin-bottom: 3px; } } diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index d9459e6a23e..29a69e19167 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -4,16 +4,14 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import classNames from 'classnames'; import { ActionURL, Ajax, Utils } from '@labkey/api'; import { PlateTemplate, Position, WellGroup, computeWarnings } from './models'; import { StatusBar } from './components/StatusBar'; import { GroupTypesPanel } from './components/GroupTypesPanel'; +import { RightPanel } from './components/RightPanel'; import { ShiftPanel } from './components/ShiftPanel'; import { TemplateGrid } from './components/TemplateGrid'; -import { WellGroupProperties } from './components/WellGroupProperties'; -import { WarningPanel } from './components/WarningPanel'; import './PlateTemplateDesigner.scss'; @@ -51,21 +49,22 @@ import './PlateTemplateDesigner.scss'; * * ─── Cell interaction ─────────────────────────────────────────────────────────── * Two cell callbacks are distinguished: - * `handleCellAssign` — idempotent add; also evicts the cell from any other group - * of the same type (one cell can only belong to one group per type). Used during - * drag operations. - * `handleCellToggle` — pure on/off; does not steal from siblings. Used for - * single-click (no drag movement). + * `handleDragRect` — paints a rectangle; also evicts those cells from sibling + * groups of the same type (one cell can only belong to one group per type). + * Used during drag operations. + * `handleCellToggle` — toggle: if the cell is already in the active group, remove + * it; otherwise add it and evict it from any sibling group of the same type. + * Used for single-click (no drag movement). */ const COLORS = [ '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', - '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac', - '#6ba3be', '#ffbe7d', '#ff9d9a', '#86bcb6', '#8cd17d', - '#f1ce63', '#d4a6c8', '#ffb7c5', '#c7a97e', '#d7d5cf', + '#ecb830', '#9b59b6', '#e84878', '#7a4222', '#888888', + '#30c068', '#ccd828', '#4848cc', '#d04018', '#18a8c0', + '#c030a8', '#8caa28', '#583848', '#c8d8e8', '#204888', ]; -function assignColors(groups: WellGroup[]): Map { +export function assignColors(groups: WellGroup[]): Map { const map = new Map(); groups.forEach((g, i) => { map.set(g.rowId, COLORS[i % COLORS.length]); @@ -73,6 +72,40 @@ function assignColors(groups: WellGroup[]): Map { return map; } +/** + * Toggles a single cell in the active group: + * - If the cell is already in the active group → remove it (no sibling changes). + * - If the cell is absent → add it to the active group and evict it from any + * other group of the same type so a cell never belongs to two groups of one type. + */ +export function toggleCell(groups: WellGroup[], activeGroupRowId: number, row: number, col: number): WellGroup[] { + const activeGroup = groups.find(g => g.rowId === activeGroupRowId); + if (!activeGroup) return groups; + const isInActiveGroup = activeGroup.positions.some(p => p.row === row && p.col === col); + const activeType = activeGroup.type; + return groups.map(g => { + if (g.rowId === activeGroupRowId) { + if (isInActiveGroup) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return { ...g, positions: [...g.positions, { row, col }] }; + } + // When adding: evict the cell from every sibling group of the same type + if (!isInActiveGroup && g.type === activeType) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return g; + }); +} + +export function isSameOrigin(url: string): boolean { + try { + return new URL(url, window.location.origin).origin === window.location.origin; + } catch { + return false; + } +} + export function PlateTemplateDesigner(): JSX.Element { const [plate, setPlate] = useState(null); const [activeGroup, setActiveGroup] = useState(null); @@ -192,46 +225,37 @@ export function PlateTemplateDesigner(): JSX.Element { setIsDirty(true); }, []); - // Pure toggle: add the cell if absent, remove it if present const handleCellToggle = useCallback((row: number, col: number) => { const activeGroup = activeGroupRef.current; if (!activeGroup) return; - setPlate(prev => { - if (!prev) return null; - const updatedGroups = prev.groups.map(g => { - if (g.rowId !== activeGroup.rowId) return g; - const hasCell = g.positions.some(p => p.row === row && p.col === col); - if (hasCell) { - return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; - } - return { ...g, positions: [...g.positions, { row, col }] }; - }); - return { ...prev, groups: updatedGroups }; - }); + setPlate(prev => prev ? { ...prev, groups: toggleCell(prev.groups, activeGroup.rowId, row, col) } : null); setIsDirty(true); }, []); const handleAddGroup = useCallback((type: string, name: string) => { - if (!plate) return; const rowId = nextGroupIdRef.current--; - const newGroup: WellGroup = { - rowId, - type, - name, - positions: [], - properties: {}, - allowNewGroups: plate.canCreateGroupsByType?.[type] ?? false, - }; - setPlate(prev => prev ? { ...prev, groups: [...prev.groups, newGroup] } : null); const colorIndex = nextColorIndexRef.current++; + setPlate(prev => { + if (!prev) return null; + const newGroup: WellGroup = { + rowId, + type, + name, + positions: [], + properties: {}, + allowNewGroups: prev.canCreateGroupsByType?.[type] ?? false, + }; + return { ...prev, groups: [...prev.groups, newGroup] }; + }); setColorMap(prev => { const next = new Map(prev); next.set(rowId, COLORS[colorIndex % COLORS.length]); return next; }); - setActiveGroup(newGroup); + // allowNewGroups is a stub here; the plate-sync effect re-derives it from the updated plate. + setActiveGroup({ rowId, type, name, positions: [], properties: {}, allowNewGroups: false }); setIsDirty(true); - }, [plate]); + }, []); const handleShift = useCallback((verticalShift: number, horizontalShift: number) => { setPlate(prev => { @@ -295,20 +319,13 @@ export function PlateTemplateDesigner(): JSX.Element { setIsDirty(true); }, []); - const warningCount = useMemo(() => { - if (!plate?.showWarningPanel) return 0; - return computeWarnings(plate).length; + const warnings = useMemo(() => { + if (!plate?.showWarningPanel) return []; + return computeWarnings(plate); }, [plate]); const navigateAway = useCallback(() => { const returnURL = ActionURL.getParameter('returnURL') || ActionURL.getParameter('returnUrl'); - const isSameOrigin = (url: string) => { - try { - return new URL(url, window.location.origin).origin === window.location.origin; - } catch { - return false; - } - }; window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); }, []); @@ -365,6 +382,11 @@ export function PlateTemplateDesigner(): JSX.Element { navigateAway(); }, [navigateAway]); + const handleTabChange = useCallback((tab: string) => { + setActiveTab(tab); + setActiveGroup(null); + }, []); + // Warn on unsaved navigation useEffect(() => { const handler = (e: BeforeUnloadEvent) => { @@ -411,6 +433,7 @@ export function PlateTemplateDesigner(): JSX.Element { { setActiveTab(tab); setActiveGroup(null); }} + onTabChange={handleTabChange} onAddGroup={handleAddGroup} onDeleteGroup={handleDeleteGroup} onRenameGroup={handleRenameGroup} @@ -457,55 +480,15 @@ export function PlateTemplateDesigner(): JSX.Element { {/* Right panel: WellGroupProperties and (if enabled) a Warnings tab. The tab strip only renders when showWarningPanel is true; otherwise WellGroupProperties fills the full right column without tabs. */} -
- {plate.showWarningPanel && ( -
- - -
- )} - {(!plate.showWarningPanel || rightTab === 'properties') && ( -
- -
- )} - {plate.showWarningPanel && rightTab === 'warnings' && ( -
- -
- )} -
+ ); diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts new file mode 100644 index 00000000000..ca49ffc27dd --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import { WellGroup } from './models'; +import { assignColors, isSameOrigin, toggleCell } from './PlateTemplateDesigner'; + +function makeGroup(rowId: number): WellGroup { + return { rowId, type: 'SPECIMEN', name: `Group ${rowId}`, positions: [], properties: {}, allowNewGroups: false }; +} + +function makeGroupAt(rowId: number, type: string, coords: Array<[number, number]>): WellGroup { + return { rowId, type, name: `Group ${rowId}`, positions: coords.map(([row, col]) => ({ row, col })), properties: {}, allowNewGroups: false }; +} + +describe('assignColors', () => { + test('returns an empty map for an empty group list', () => { + expect(assignColors([])).toEqual(new Map()); + }); + + test('assigns a color entry for each group', () => { + const groups = [makeGroup(1), makeGroup(2), makeGroup(3)]; + const map = assignColors(groups); + expect(map.size).toBe(3); + expect(map.get(1)).toBeDefined(); + expect(map.get(2)).toBeDefined(); + expect(map.get(3)).toBeDefined(); + }); + + test('uses group rowId as the map key, not the array index', () => { + const groups = [makeGroup(10), makeGroup(20)]; + const map = assignColors(groups); + expect(map.has(10)).toBe(true); + expect(map.has(20)).toBe(true); + expect(map.has(0)).toBe(false); + }); + + test('assigns distinct colors to the first 20 groups', () => { + const groups = Array.from({ length: 20 }, (_, i) => makeGroup(i + 1)); + const map = assignColors(groups); + const colors = Array.from(map.values()); + expect(new Set(colors).size).toBe(20); + }); + + test('wraps color assignment after 20 groups (21st group gets same color as 1st)', () => { + const groups = Array.from({ length: 21 }, (_, i) => makeGroup(i + 1)); + const map = assignColors(groups); + expect(map.get(21)).toBe(map.get(1)); + }); + + test('assigns colors in array order, not by rowId value', () => { + // rowId 99 comes first in the array, so it gets the first color + const groups = [makeGroup(99), makeGroup(1)]; + const map = assignColors(groups); + const firstColor = assignColors([makeGroup(1)]).get(1); + expect(map.get(99)).toBe(firstColor); + }); +}); + +describe('toggleCell', () => { + test('adds cell to active group when the cell is absent', () => { + const groups = [makeGroupAt(1, 'SPECIMEN', [])]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); + }); + + test('removes cell from active group when the cell is already present', () => { + const groups = [makeGroupAt(1, 'SPECIMEN', [[0, 0]])]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([]); + }); + + test('evicts cell from a sibling group of the same type when adding', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', []), // active — does not have the cell + makeGroupAt(2, 'SPECIMEN', [[0, 0]]), // sibling — owns the cell + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); // added to active + expect(result[1].positions).toEqual([]); // evicted from sibling + }); + + test('does not evict from a group of a different type when adding', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', []), + makeGroupAt(2, 'CONTROL', [[0, 0]]), // different type — must be untouched + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); + expect(result[1].positions).toEqual([{ row: 0, col: 0 }]); // unchanged + }); + + test('does not evict from sibling groups when removing (toggle off)', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', [[0, 0]]), // active — owns the cell + makeGroupAt(2, 'SPECIMEN', [[0, 0]]), // sibling — also owns the cell (edge case) + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([]); // removed from active + expect(result[1].positions).toEqual([{ row: 0, col: 0 }]); // sibling unchanged + }); + + test('leaves unrelated cells in the sibling group intact', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', []), + makeGroupAt(2, 'SPECIMEN', [[0, 0], [1, 1]]), // sibling owns (0,0) and (1,1) + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); + expect(result[1].positions).toEqual([{ row: 1, col: 1 }]); // only (0,0) evicted + }); + + test('returns groups unchanged when activeGroupRowId is not found', () => { + const groups = [makeGroupAt(1, 'SPECIMEN', [])]; + expect(toggleCell(groups, 99, 0, 0)).toEqual(groups); + }); +}); + +describe('isSameOrigin', () => { + // jsdom sets window.location.origin to 'http://localhost' + + test('returns true for a URL on the same origin', () => { + expect(isSameOrigin('http://localhost/some/path')).toBe(true); + }); + + test('returns true for a root-relative URL (same origin by construction)', () => { + expect(isSameOrigin('/labkey/plate/plateList.view')).toBe(true); + }); + + test('returns false for a different hostname', () => { + expect(isSameOrigin('http://evil.com/path')).toBe(false); + }); + + test('returns false for a different scheme', () => { + expect(isSameOrigin('https://localhost/path')).toBe(false); + }); + + test('returns false for a different port', () => { + expect(isSameOrigin('http://localhost:8080/path')).toBe(false); + }); + + test('returns false for a javascript: URL (XSS guard)', () => { + expect(isSameOrigin('javascript:alert(1)')).toBe(false); + }); + + test('returns false for an absolute URL with an invalid host (throws during construction)', () => { + // 'http://a b' has a space in the hostname which is invalid; the URL constructor throws, + // and the catch block returns false. + expect(isSameOrigin('http://a b/path')).toBe(false); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx new file mode 100644 index 00000000000..03d1f6ba963 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { PlateTemplate, WellGroup } from '../models'; +import { GroupTypesPanel } from './GroupTypesPanel'; + +function makeGroup(overrides: Partial = {}): WellGroup { + return { + rowId: 1, + type: 'SPECIMEN', + name: 'Group A', + positions: [], + properties: {}, + allowNewGroups: true, + ...overrides, + }; +} + +function makePlate(overrides: Partial = {}): PlateTemplate { + return { + rowId: 0, + name: 'Test Plate', + type: 'assay', + rows: 8, + cols: 12, + groupTypes: ['SPECIMEN', 'CONTROL'], + canCreateGroupsByType: { SPECIMEN: true, CONTROL: true }, + groups: [], + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: false, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', + ...overrides, + }; +} + +function renderPanel(overrides: Partial> = {}) { + const props = { + plate: makePlate(), + activeGroup: null, + activeTab: 'SPECIMEN', + colorMap: new Map(), + onGroupSelect: jest.fn(), + onTabChange: jest.fn(), + onAddGroup: jest.fn(), + onDeleteGroup: jest.fn(), + onRenameGroup: jest.fn(), + ...overrides, + }; + const result = render(); + return { ...result, props }; +} + +describe('GroupTypesPanel — create row: select vs input', () => { + test('shows a contains only the unused defaults', () => { + renderPanel({ + plate: makePlate({ + typesToDefaultGroups: { SPECIMEN: ['Virus', 'Cell Control'] }, + groups: [makeGroup({ name: 'Virus' })], // 'Virus' already used + }), + }); + const select = screen.getByRole('combobox', { name: 'Group name' }); + const options = Array.from(select.querySelectorAll('option')).map(o => o.textContent); + expect(options).toEqual(['Cell Control']); + }); + + test('shows a text when all defaults are used', () => { + renderPanel({ + plate: makePlate({ + typesToDefaultGroups: { SPECIMEN: ['Virus'] }, + groups: [makeGroup({ name: 'Virus' })], + }), + }); + expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: 'Group name' })).toBeNull(); + }); + + test('shows a text when no defaults are configured for the type', () => { + renderPanel({ plate: makePlate({ typesToDefaultGroups: {} }) }); + expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); + }); +}); + +describe('GroupTypesPanel — create row: name conflict detection', () => { + test('Create button is enabled for a unique name', async () => { + renderPanel({ plate: makePlate({ groups: [makeGroup({ name: 'Existing' })] }) }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'New Group'); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + + test('Create button is disabled and error shown when name conflicts', async () => { + renderPanel({ plate: makePlate({ groups: [makeGroup({ name: 'Existing' })] }) }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'Existing'); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + expect(screen.getByText(/already exists in this type/i)).toBeInTheDocument(); + }); + + test('conflict only checks groups of the same type', async () => { + // A group named 'Shared Name' in CONTROL should not conflict with SPECIMEN create input + renderPanel({ + plate: makePlate({ + groups: [makeGroup({ rowId: 2, type: 'CONTROL', name: 'Shared Name' })], + }), + }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'Shared Name'); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + expect(screen.queryByText(/already exists/i)).toBeNull(); + }); + + test('clicking Create calls onAddGroup with the trimmed name', async () => { + const { props } = renderPanel(); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), ' My Group '); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'My Group'); + }); + + test('pressing Enter in the name input calls onAddGroup', async () => { + const { props } = renderPanel(); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'My Group{Enter}'); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'My Group'); + }); + + test('Create button is disabled when name is empty', () => { + renderPanel(); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + }); +}); + +describe('GroupTypesPanel — inline rename', () => { + function renderWithActiveGroup() { + const group1 = makeGroup({ rowId: 1, name: 'Group A' }); + const group2 = makeGroup({ rowId: 2, name: 'Group B' }); + return renderPanel({ + plate: makePlate({ groups: [group1, group2] }), + activeGroup: group1, + }); + } + + test('rename button is visible for the active group when allowNewGroups is true', () => { + renderWithActiveGroup(); + expect(screen.getByRole('button', { name: 'Rename Group A' })).toBeInTheDocument(); + }); + + test('clicking rename shows an inline input', async () => { + renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + expect(screen.getByRole('textbox', { name: 'Rename Group A' })).toBeInTheDocument(); + }); + + test('pressing Enter with a non-conflicting name calls onRenameGroup', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Group C{Enter}'); + expect(props.onRenameGroup).toHaveBeenCalledWith(1, 'Group C'); + }); + + test('pressing Enter with a conflicting name shows an error and does not rename', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Group B{Enter}'); + expect(props.onRenameGroup).not.toHaveBeenCalled(); + expect(screen.getByText(/"Group B" is already used/i)).toBeInTheDocument(); + }); + + test('blurring with a conflicting name reverts silently (no error, no rename)', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Group B'); + await userEvent.tab(); // blur the input + expect(props.onRenameGroup).not.toHaveBeenCalled(); + expect(screen.queryByText(/"Group B" is already used/i)).toBeNull(); + expect(screen.queryByRole('textbox', { name: 'Rename Group A' })).toBeNull(); + }); + + test('pressing Escape cancels rename without calling onRenameGroup', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Something{Escape}'); + expect(props.onRenameGroup).not.toHaveBeenCalled(); + expect(screen.queryByRole('textbox', { name: 'Rename Group A' })).toBeNull(); + }); +}); + +describe('GroupTypesPanel — tab switching resets create input', () => { + test('switching activeTab resets the create name to empty (no defaults)', async () => { + const { rerender, props } = renderPanel({ + plate: makePlate({ canCreateGroupsByType: { SPECIMEN: true, CONTROL: true } }), + activeTab: 'SPECIMEN', + }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'In Progress'); + expect(screen.getByRole('textbox', { name: 'Group name' })).toHaveValue('In Progress'); + + rerender( + + ); + expect(screen.getByRole('textbox', { name: 'Group name' })).toHaveValue(''); + }); + + test('switching activeTab resets to the first unused default of the new tab', () => { + const plate = makePlate({ + canCreateGroupsByType: { SPECIMEN: true, CONTROL: true }, + typesToDefaultGroups: { CONTROL: ['Positive', 'Negative'] }, + }); + const { rerender, props } = renderPanel({ plate, activeTab: 'SPECIMEN' }); + + rerender(); + + const select = screen.getByRole('combobox', { name: 'Group name' }); + expect(select).toHaveValue('Positive'); + }); +}); + +describe('GroupTypesPanel — tab click', () => { + test('clicking a tab calls onTabChange with the tab key', async () => { + const { props } = renderPanel({ activeTab: 'SPECIMEN' }); + await userEvent.click(screen.getByRole('tab', { name: 'CONTROL' })); + expect(props.onTabChange).toHaveBeenCalledWith('CONTROL'); + }); +}); + +describe('GroupTypesPanel — group selection', () => { + function renderWithGroup() { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + return renderPanel({ + plate: makePlate({ groups: [group] }), + activeGroup: null, + }); + } + + test('clicking a group row calls onGroupSelect with that group', async () => { + const { props } = renderWithGroup(); + await userEvent.click(screen.getByRole('option', { name: 'Group A' })); + expect(props.onGroupSelect).toHaveBeenCalledWith( + expect.objectContaining({ rowId: 1, name: 'Group A' }) + ); + }); + + test('pressing Enter on a group row calls onGroupSelect', () => { + const { props } = renderWithGroup(); + fireEvent.keyDown(screen.getByRole('option', { name: 'Group A' }), { key: 'Enter' }); + expect(props.onGroupSelect).toHaveBeenCalledWith( + expect.objectContaining({ rowId: 1, name: 'Group A' }) + ); + }); + + test('pressing Space on a group row calls onGroupSelect', () => { + const { props } = renderWithGroup(); + fireEvent.keyDown(screen.getByRole('option', { name: 'Group A' }), { key: ' ' }); + expect(props.onGroupSelect).toHaveBeenCalledWith( + expect.objectContaining({ rowId: 1, name: 'Group A' }) + ); + }); +}); + +describe('GroupTypesPanel — delete group', () => { + function renderWithActiveGroup() { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + return renderPanel({ + plate: makePlate({ groups: [group] }), + activeGroup: group, + }); + } + + test('delete button calls onDeleteGroup when user confirms', async () => { + const { props } = renderWithActiveGroup(); + jest.spyOn(window, 'confirm').mockReturnValue(true); + await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + expect(props.onDeleteGroup).toHaveBeenCalledWith(1); + }); + + test('delete button does not call onDeleteGroup when user cancels', async () => { + const { props } = renderWithActiveGroup(); + jest.spyOn(window, 'confirm').mockReturnValue(false); + await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + expect(props.onDeleteGroup).not.toHaveBeenCalled(); + }); +}); + +describe('GroupTypesPanel — multi-create dialog', () => { + // Give the SPECIMEN type a default name so newGroupName starts as 'Virus', + // which becomes the dialog's initialBaseName. + function renderWithDefault() { + return renderPanel({ + plate: makePlate({ typesToDefaultGroups: { SPECIMEN: ['Virus'] } }), + }); + } + + test('clicking "Create multiple..." opens the dialog', async () => { + renderWithDefault(); + await userEvent.click(screen.getByRole('button', { name: 'Create multiple...' })); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + test('confirming multi-create calls onAddGroup for each generated name', async () => { + const { props } = renderWithDefault(); + await userEvent.click(screen.getByRole('button', { name: 'Create multiple...' })); + // Dialog opens with initialBaseName='Virus', default count=2 + await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Create' })); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'Virus 1'); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'Virus 2'); + }); + + test('closing the dialog with Cancel hides it', async () => { + renderWithDefault(); + await userEvent.click(screen.getByRole('button', { name: 'Create multiple...' })); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Cancel' })); + expect(screen.queryByRole('dialog')).toBeNull(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 986d5b74d1c..851ec7072da 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -3,12 +3,13 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; +import { MultiCreateDialog } from './MultiCreateDialog'; -interface Props { +interface GroupTypesPanelProps { plate: PlateTemplate; activeGroup: WellGroup | null; activeTab: string; @@ -74,16 +75,12 @@ export function GroupTypesPanel({ onDeleteGroup, onRenameGroup, children, -}: Props): JSX.Element { +}: GroupTypesPanelProps): JSX.Element { const [newGroupName, setNewGroupName] = useState(''); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); const [renameError, setRenameError] = useState(null); const [multiCreateOpen, setMultiCreateOpen] = useState(false); - const [multiBaseName, setMultiBaseName] = useState(''); - const [multiCount, setMultiCount] = useState('2'); - const [multiCountError, setMultiCountError] = useState(''); - const dialogRef = useRef(null); // Stable derived list — memoized so useMemo and useEffect deps are stable. const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); @@ -112,34 +109,6 @@ export function GroupTypesPanel({ } }, [unusedDefaults]); // eslint-disable-line react-hooks/exhaustive-deps - // Focus trap for multi-create dialog - useEffect(() => { - if (!multiCreateOpen || !dialogRef.current) return; - const dialog = dialogRef.current; - const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; - const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - setMultiCreateOpen(false); - return; - } - if (e.key !== 'Tab') return; - const focusable = getFocusable(); - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey) { - if (document.activeElement === first) { e.preventDefault(); last?.focus(); } - } else { - if (document.activeElement === last) { e.preventDefault(); first?.focus(); } - } - }; - - dialog.addEventListener('keydown', handleKeyDown); - getFocusable()[0]?.focus(); - return () => dialog.removeEventListener('keydown', handleKeyDown); - }, [multiCreateOpen]); - const handleCreate = () => { const trimmed = newGroupName.trim(); if (!trimmed || createNameConflicts) return; @@ -147,33 +116,6 @@ export function GroupTypesPanel({ setNewGroupName(''); }; - const openMultiCreate = () => { - setMultiBaseName(newGroupName.trim()); - setMultiCount('2'); - setMultiCountError(''); - setMultiCreateOpen(true); - // Focus is handled by the focus-trap effect above - }; - - const handleMultiCreate = () => { - const count = parseInt(multiCount, 10); - if (isNaN(count) || count < 1) { - setMultiCountError(`"${multiCount}" is not a valid count.`); - return; - } - const baseName = multiBaseName.trim(); - if (!baseName) return; - const existingNames = new Set(groupsOfType.map(g => g.name)); - const namesToCreate = Array.from({ length: count }, (_, i) => `${baseName} ${i + 1}`) - .filter(name => !existingNames.has(name)); - if (namesToCreate.length === 0) { - setMultiCountError(`All ${count} generated name${count === 1 ? '' : 's'} already exist in this type.`); - return; - } - namesToCreate.forEach(name => onAddGroup(activeTab, name)); - setMultiCreateOpen(false); - }; - const handleDeleteClick = (e: React.MouseEvent, group: WellGroup) => { e.stopPropagation(); if (window.confirm(`Delete well group "${group.name}"?`)) { @@ -225,204 +167,172 @@ export function GroupTypesPanel({ ))} -
-
- {groupsOfType.map(group => { - const color = colorMap.get(group.rowId); - const isActive = activeGroup?.rowId === group.rowId; - const isRenaming = renamingId === group.rowId; - return ( - -
{ if (!isRenaming) onGroupSelect(group); }} - onKeyDown={e => { - if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - onGroupSelect(group); - } - }} - > -
+ ))} {multiCreateOpen && ( -
setMultiCreateOpen(false)}> -
e.stopPropagation()} - > -
Create Multiple Groups
- - - - - - - - - - - - - - -
Base Name - setMultiBaseName(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} - /> -
Count - { setMultiCount(e.target.value); setMultiCountError(''); }} - onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} - /> - {multiCountError &&
{multiCountError}
} -
- - - -
-
-
+ g.name))} + onClose={() => setMultiCreateOpen(false)} + onConfirm={names => { + names.forEach(name => onAddGroup(activeTab, name)); + setMultiCreateOpen(false); + }} + /> )}
); diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx new file mode 100644 index 00000000000..395716da0bd --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { MultiCreateDialog } from './MultiCreateDialog'; + +function renderDialog(overrides: Partial> = {}) { + const props = { + initialBaseName: 'Sample', + existingNames: new Set(), + onClose: jest.fn(), + onConfirm: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('MultiCreateDialog', () => { + describe('initial state', () => { + test('pre-populates base name from initialBaseName', () => { + renderDialog({ initialBaseName: 'Virus' }); + expect(screen.getByLabelText(/Base Name/i)).toHaveValue('Virus'); + }); + + test('defaults count to 2', () => { + renderDialog(); + expect(screen.getByLabelText(/Count/i)).toHaveValue(2); + }); + + test('Create button is disabled when base name is empty', () => { + renderDialog({ initialBaseName: '' }); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + }); + + test('Create button is enabled when base name is non-empty', () => { + renderDialog({ initialBaseName: 'Sample' }); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + }); + + describe('successful creation', () => { + test('calls onConfirm with generated names using base name and count', async () => { + const { onConfirm } = renderDialog({ initialBaseName: 'Sample', existingNames: new Set() }); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '3'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2', 'Sample 3']); + }); + + test('trims whitespace from base name before generating names', async () => { + const { onConfirm } = renderDialog({ initialBaseName: ' Sample ' }); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2']); + }); + + test('filters out names already in existingNames', async () => { + const { onConfirm } = renderDialog({ + initialBaseName: 'Sample', + existingNames: new Set(['Sample 1', 'Sample 3']), + }); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '3'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + // Sample 1 and Sample 3 are already taken; only Sample 2 is new + expect(onConfirm).toHaveBeenCalledWith(['Sample 2']); + }); + }); + + describe('validation errors', () => { + test('shows error and does not call onConfirm when count is not a number', async () => { + const { onConfirm } = renderDialog(); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), 'abc'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(screen.getByText(/not a valid count/i)).toBeInTheDocument(); + }); + + test('shows error when count is less than 1', async () => { + const { onConfirm } = renderDialog(); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '0'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(screen.getByText(/not a valid count/i)).toBeInTheDocument(); + }); + + test('shows error and does not call onConfirm when all generated names already exist', async () => { + const { onConfirm } = renderDialog({ + initialBaseName: 'Sample', + existingNames: new Set(['Sample 1', 'Sample 2']), + }); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(screen.getByText(/already exist/i)).toBeInTheDocument(); + }); + + test('count error clears when count input is changed', async () => { + renderDialog(); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '0'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(screen.getByText(/not a valid count/i)).toBeInTheDocument(); + + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '3'); + expect(screen.queryByText(/not a valid count/i)).toBeNull(); + }); + }); + + describe('cancel / close', () => { + test('clicking Cancel calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + test('clicking the overlay calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.click(document.querySelector('.multi-create-dialog__overlay')); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + test('clicking inside the dialog does not call onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.click(document.querySelector('.multi-create-dialog')); + expect(onClose).not.toHaveBeenCalled(); + }); + + test('pressing Escape on the base name input calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.type(screen.getByLabelText(/Base Name/i), '{Escape}'); + // Both the input's onKeyDown and the dialog's focus-trap listener fire on Escape + // (the event bubbles), so onClose is called twice. Both calls are idempotent. + expect(onClose).toHaveBeenCalled(); + }); + + test('pressing Escape on the count input calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.type(screen.getByLabelText(/Count/i), '{Escape}'); + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe('keyboard submit', () => { + test('pressing Enter in the base name input submits', async () => { + const { onConfirm } = renderDialog({ initialBaseName: 'Sample' }); + await userEvent.type(screen.getByLabelText(/Base Name/i), '{Enter}'); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2']); + }); + + test('pressing Enter in the count input submits', async () => { + const { onConfirm } = renderDialog({ initialBaseName: 'Sample' }); + await userEvent.type(screen.getByLabelText(/Count/i), '{Enter}'); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2']); + }); + }); + + describe('focus trap', () => { + // Focusable order: Base Name → Count → Cancel → Create. + // userEvent.tab() fires Tab keydown events that bubble to the dialog's native listener. + test('Tab from last focusable element (Create) wraps focus to first (Base Name)', async () => { + renderDialog({ initialBaseName: 'Sample' }); // Create button enabled + // After mount the useEffect focuses the first element (Base Name); tab forward to Create. + await userEvent.tab(); // → Count + await userEvent.tab(); // → Cancel + await userEvent.tab(); // → Create (last) + await userEvent.tab(); // → focus trap wraps back to Base Name + expect(document.activeElement).toBe(screen.getByLabelText(/Base Name/i)); + }); + + test('Shift+Tab from first focusable element (Base Name) wraps focus to last (Create)', async () => { + renderDialog({ initialBaseName: 'Sample' }); // Create button enabled + // After mount, Base Name is focused (first focusable). Shift+Tab wraps to Create (last). + await userEvent.keyboard('{Shift>}{Tab}{/Shift}'); + expect(document.activeElement).toBe( + within(screen.getByRole('dialog')).getByRole('button', { name: 'Create' }) + ); + }); + }); + + describe('base name input', () => { + test('typing in the base name input updates its value', async () => { + renderDialog({ initialBaseName: '' }); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + await userEvent.type(screen.getByLabelText(/Base Name/i), 'NewName'); + expect(screen.getByLabelText(/Base Name/i)).toHaveValue('NewName'); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + + test('pressing Enter in count input with empty base name does not submit', async () => { + // The count input's onKeyDown calls handleCreate() without guarding baseName. + // handleCreate's own guard (if (!trimmedBase) return) must prevent submission. + const { onConfirm } = renderDialog({ initialBaseName: '' }); + await userEvent.type(screen.getByLabelText(/Count/i), '{Enter}'); + expect(onConfirm).not.toHaveBeenCalled(); + }); + }); + + describe('singular error message', () => { + test('uses singular "name" when count is 1 and that name already exists', async () => { + // Exercises the parsedCount === 1 branch of the ternary in the "all names exist" error. + renderDialog({ + initialBaseName: 'Sample', + existingNames: new Set(['Sample 1']), + }); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '1'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(screen.getByText(/all 1 generated name already exist/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx new file mode 100644 index 00000000000..a86e2a127f4 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useEffect, useRef, useState } from 'react'; + +interface MultiCreateDialogProps { + initialBaseName: string; + existingNames: Set; + onClose: () => void; + onConfirm: (names: string[]) => void; +} + +export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onConfirm }: MultiCreateDialogProps): JSX.Element { + const [baseName, setBaseName] = useState(initialBaseName); + const [count, setCount] = useState('2'); + const [countError, setCountError] = useState(''); + const dialogRef = useRef(null); + + // Focus trap: runs once on mount since the component only renders when the dialog is open. + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { onClose(); return; } + if (e.key !== 'Tab') return; + const focusable = getFocusable(); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last?.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first?.focus(); } + } + }; + + dialog.addEventListener('keydown', handleKeyDown); + getFocusable()[0]?.focus(); + return () => dialog.removeEventListener('keydown', handleKeyDown); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleCreate = () => { + const parsedCount = parseInt(count, 10); + if (isNaN(parsedCount) || parsedCount < 1) { + setCountError(`"${count}" is not a valid count.`); + return; + } + const trimmedBase = baseName.trim(); + if (!trimmedBase) return; + const namesToCreate = Array.from({ length: parsedCount }, (_, i) => `${trimmedBase} ${i + 1}`) + .filter(name => !existingNames.has(name)); + if (namesToCreate.length === 0) { + setCountError(`All ${parsedCount} generated name${parsedCount === 1 ? '' : 's'} already exist in this type.`); + return; + } + onConfirm(namesToCreate); + }; + + return ( +
+
e.stopPropagation()} + > +
Create Multiple Groups
+
+
+ Base Name + setBaseName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> +
+
+ Count +
+ { setCount(e.target.value); setCountError(''); }} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> + {countError &&
{countError}
} +
+
+
+ + +
+
+
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx new file mode 100644 index 00000000000..ccd23a5188c --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { RightPanel } from './RightPanel'; + +function renderPanel(overrides: Partial> = {}) { + const props = { + showWarningPanel: false, + rightTab: 'properties' as const, + onRightTabChange: jest.fn(), + warnings: [], + activeGroup: null, + onPropertyChange: jest.fn(), + onDeleteProperty: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('RightPanel — without warning panel', () => { + test('renders WellGroupProperties with no tab strip', () => { + renderPanel({ showWarningPanel: false }); + expect(screen.queryByRole('tablist')).toBeNull(); + expect(screen.queryByRole('tabpanel')).toBeNull(); + // WellGroupProperties empty-state text is present + expect(screen.getByText(/select a well group/i)).toBeInTheDocument(); + }); + + test('properties div has no role or aria-labelledby when showWarningPanel is false', () => { + renderPanel({ showWarningPanel: false }); + // The wrapper div around WellGroupProperties should have no role + const tabpanels = document.querySelectorAll('[role="tabpanel"]'); + expect(tabpanels).toHaveLength(0); + }); +}); + +describe('RightPanel — with warning panel, properties tab', () => { + test('renders a tablist with two tabs', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + expect(screen.getByRole('tablist')).toBeInTheDocument(); + expect(screen.getAllByRole('tab')).toHaveLength(2); + }); + + test('properties tab is aria-selected, warnings tab is not', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'false'); + }); + + test('properties tabpanel is rendered with correct id and aria-labelledby', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + const panel = document.getElementById('right-panel-properties'); + expect(panel).toBeInTheDocument(); + expect(panel).toHaveAttribute('role', 'tabpanel'); + expect(panel).toHaveAttribute('aria-labelledby', 'right-tab-properties'); + }); + + test('warnings tabpanel is hidden when rightTab is "properties"', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + expect(document.getElementById('right-panel-warnings')).toHaveAttribute('hidden'); + }); + + test('warnings tab label shows count badge when warnings exist', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: ['w1', 'w2', 'w3'] }); + expect(screen.getByRole('tab', { name: 'Warnings (3)' })).toBeInTheDocument(); + }); + + test('warnings tab label shows plain "Warnings" when list is empty', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: [] }); + expect(screen.getByRole('tab', { name: 'Warnings' })).toBeInTheDocument(); + }); + + test('clicking warnings tab calls onRightTabChange with "warnings"', async () => { + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + await userEvent.click(screen.getByRole('tab', { name: 'Warnings' })); + expect(onRightTabChange).toHaveBeenCalledWith('warnings'); + }); + + test('clicking properties tab calls onRightTabChange with "properties"', async () => { + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + await userEvent.click(screen.getByRole('tab', { name: 'Well Group Properties' })); + expect(onRightTabChange).toHaveBeenCalledWith('properties'); + }); +}); + +describe('RightPanel — with warning panel, warnings tab', () => { + test('renders warnings tabpanel; properties tabpanel is hidden', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: ['A1: warning'] }); + expect(document.getElementById('right-panel-warnings')).toBeInTheDocument(); + expect(document.getElementById('right-panel-properties')).toHaveAttribute('hidden'); + }); + + test('warnings tab is aria-selected, properties tab is not', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'false'); + }); + + test('warnings tabpanel has correct aria-labelledby', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + const panel = document.getElementById('right-panel-warnings'); + expect(panel).toHaveAttribute('aria-labelledby', 'right-tab-warnings'); + }); + + test('WarningPanel content is visible in warnings tab', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: [] }); + expect(screen.getByText('No warnings.')).toBeInTheDocument(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx new file mode 100644 index 00000000000..4d24c11247f --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import classNames from 'classnames'; + +import { WellGroup } from '../models'; +import { WellGroupProperties } from './WellGroupProperties'; +import { WarningPanel } from './WarningPanel'; + +interface RightPanelProps { + showWarningPanel: boolean; + rightTab: 'properties' | 'warnings'; + onRightTabChange: (tab: 'properties' | 'warnings') => void; + warnings: string[]; + activeGroup: WellGroup | null; + onPropertyChange: (groupRowId: number, key: string, value: string) => void; + onDeleteProperty: (groupRowId: number, key: string) => void; +} + +export function RightPanel(props: RightPanelProps): JSX.Element { + const { showWarningPanel, rightTab, onRightTabChange, warnings, activeGroup, onPropertyChange, onDeleteProperty } = props; + const warningCount = warnings.length; + + return ( +
+ {showWarningPanel && ( +
+ + +
+ )} + + {showWarningPanel && ( + + )} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx new file mode 100644 index 00000000000..75b2c242f40 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { ShiftPanel } from './ShiftPanel'; + +describe('ShiftPanel', () => { + test('renders four directional buttons', () => { + render(); + expect(screen.getByRole('button', { name: 'Shift up' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Shift down' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Shift left' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Shift right' })).toBeInTheDocument(); + }); + + test('Shift up calls onShift(1, 0) — moves cells up (decreases row index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift up' })); + expect(onShift).toHaveBeenCalledWith(1, 0); + }); + + test('Shift down calls onShift(-1, 0) — moves cells down (increases row index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift down' })); + expect(onShift).toHaveBeenCalledWith(-1, 0); + }); + + test('Shift left calls onShift(0, 1) — moves cells left (decreases col index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift left' })); + expect(onShift).toHaveBeenCalledWith(0, 1); + }); + + test('Shift right calls onShift(0, -1) — moves cells right (increases col index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift right' })); + expect(onShift).toHaveBeenCalledWith(0, -1); + }); + + test('each button only fires once per click', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift up' })); + expect(onShift).toHaveBeenCalledTimes(1); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx index 43c3d3cb2a0..08b1d913eb3 100644 --- a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -interface Props { +interface ShiftPanelProps { onShift: (verticalShift: number, horizontalShift: number) => void; } @@ -23,7 +23,7 @@ interface Props { * Shifts apply to every group of the active type simultaneously, preserving relative layout * between groups. Only the active tab's type is affected; other types are unchanged. */ -export function ShiftPanel({ onShift }: Props): JSX.Element { +export function ShiftPanel({ onShift }: ShiftPanelProps): JSX.Element { return (
diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx new file mode 100644 index 00000000000..e4242b0eeb2 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { StatusBar } from './StatusBar'; + +function renderStatusBar(overrides: Partial> = {}) { + const props = { + isDirty: false, + status: '', + plateName: 'My Plate', + onSaveAndClose: jest.fn(), + onSave: jest.fn(), + onCancel: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('StatusBar', () => { + describe('button states', () => { + test('Save button is disabled when plate is not dirty', () => { + renderStatusBar({ isDirty: false }); + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + }); + + test('Save button is enabled when plate is dirty', () => { + renderStatusBar({ isDirty: true }); + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled(); + }); + + test('Save & Close and Cancel are always enabled', () => { + renderStatusBar({ isDirty: false }); + expect(screen.getByRole('button', { name: /Save & Close/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled(); + }); + }); + + describe('dirty indicator', () => { + test('shows "Unsaved changes" when dirty', () => { + renderStatusBar({ isDirty: true }); + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + }); + + test('shows nothing in dirty indicator when clean', () => { + renderStatusBar({ isDirty: false }); + // The element is present but empty + const statuses = document.querySelectorAll('.status-bar__dirty'); + expect(statuses).toHaveLength(1); + expect(statuses[0]).toHaveTextContent(''); + }); + + test('displays transient status text', () => { + renderStatusBar({ status: 'Saved.' }); + expect(screen.getByText('Saved.')).toBeInTheDocument(); + }); + }); + + describe('validation', () => { + test('clicking Save with empty plate name shows error and does not call onSave', async () => { + const { onSave } = renderStatusBar({ plateName: '', isDirty: true }); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).not.toHaveBeenCalled(); + expect(screen.getByRole('alert')).toHaveTextContent(/plate name/i); + }); + + test('clicking Save with whitespace-only plate name shows error', async () => { + const { onSave } = renderStatusBar({ plateName: ' ', isDirty: true }); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).not.toHaveBeenCalled(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + + test('clicking Save with valid plate name calls onSave and shows no error', async () => { + const { onSave } = renderStatusBar({ plateName: 'My Plate', isDirty: true }); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('alert')).toBeNull(); + }); + + test('clicking Save & Close with empty name shows error and does not call onSaveAndClose', async () => { + const { onSaveAndClose } = renderStatusBar({ plateName: '' }); + await userEvent.click(screen.getByRole('button', { name: /Save & Close/i })); + expect(onSaveAndClose).not.toHaveBeenCalled(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + + test('clicking Save & Close with valid name calls onSaveAndClose', async () => { + const { onSaveAndClose } = renderStatusBar({ plateName: 'My Plate' }); + await userEvent.click(screen.getByRole('button', { name: /Save & Close/i })); + expect(onSaveAndClose).toHaveBeenCalledTimes(1); + }); + + test('error clears on a subsequent successful save', async () => { + // First render with empty name to trigger error + const { rerender } = render( + + ); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByRole('alert')).toBeInTheDocument(); + + // Re-render with a valid name — the error clears on the next successful validate + const onSave = jest.fn(); + rerender( + + ); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('alert')).toBeNull(); + }); + }); + + describe('Cancel', () => { + test('clicking Cancel calls onCancel', async () => { + const { onCancel } = renderStatusBar(); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + test('Cancel does not call onSave or onSaveAndClose', async () => { + const { onSave, onSaveAndClose } = renderStatusBar(); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onSave).not.toHaveBeenCalled(); + expect(onSaveAndClose).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index 9afc2600133..2af5b740051 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -3,11 +3,12 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; -interface Props { +interface StatusBarProps { isDirty: boolean; status: string; + plateName: string; onSaveAndClose: () => void; onSave: () => void; onCancel: () => void; @@ -28,13 +29,33 @@ interface Props { * The "Unsaved changes" indicator and transient status text ("Saving…", "Saved.") * use `role="status"` so screen readers announce them as they appear. */ -export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: Props): JSX.Element { +export function StatusBar({ isDirty, status, plateName, onSaveAndClose, onSave, onCancel }: StatusBarProps): JSX.Element { + const [error, setError] = useState(null); + + // Clear stale validation error once the user has filled in the plate name + useEffect(() => { + if (plateName.trim()) setError(null); + }, [plateName]); + + const validate = (): boolean => { + if (!plateName.trim()) { + setError('Please enter a plate name before saving.'); + return false; + } + setError(null); + return true; + }; + + const validateAndSave = () => { if (validate()) onSave(); }; + + const validateAndSaveAndClose = () => { if (validate()) onSaveAndClose(); }; + return (
- - {isDirty ? 'Unsaved changes' : ''} {status} + {error && {error}}
); } diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx new file mode 100644 index 00000000000..79add5b0c0e --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import { PlateTemplate, WellGroup } from '../models'; +import { TemplateGrid } from './TemplateGrid'; + +function makePlate(rows = 4, cols = 4, groups: WellGroup[] = []): PlateTemplate { + return { + rowId: 1, + name: 'Test', + type: 'assay', + rows, + cols, + groupTypes: ['SPECIMEN'], + canCreateGroupsByType: {}, + groups, + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: false, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', + }; +} + +function renderGrid(overrides: Partial> = {}) { + const props = { + plate: makePlate(), + activeGroup: null, + activeTab: 'SPECIMEN', + colorMap: new Map(), + onDragRect: jest.fn(), + onCellToggle: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +// Helpers for getting cells by their aria-label (e.g. "A1", "B3") +function getCell(label: string) { + return screen.getByLabelText(label); +} + +describe('TemplateGrid — rendering', () => { + test('renders the correct number of data cells', () => { + renderGrid({ plate: makePlate(3, 4) }); + // 3 rows × 4 cols = 12 cells, all labeled A1..C4 + expect(screen.getByLabelText('A1')).toBeInTheDocument(); + expect(screen.getByLabelText('C4')).toBeInTheDocument(); + }); + + test('renders column headers 1..cols', () => { + renderGrid({ plate: makePlate(2, 3) }); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + }); + + test('renders row headers A..H for 8 rows', () => { + renderGrid({ plate: makePlate(8, 1) }); + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'].forEach(letter => { + expect(screen.getAllByText(letter)[0]).toBeInTheDocument(); + }); + }); + + test('cell aria-label includes group name when cell is assigned', () => { + const group: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[1, '#ff0000']]), + }); + expect(screen.getByLabelText('A1: Sample 1')).toBeInTheDocument(); + }); + + test('cell tooltip is just the location when unassigned', () => { + renderGrid({ plate: makePlate(2, 2) }); + expect(screen.getByLabelText('B2')).toBeInTheDocument(); + }); +}); + +describe('TemplateGrid — roving tabindex', () => { + test('cell (0,0) has tabIndex=0 before any focus interaction', () => { + renderGrid(); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + expect(getCell('A2')).toHaveAttribute('tabindex', '-1'); + }); + + test('all other cells have tabIndex=-1 initially', () => { + renderGrid({ plate: makePlate(2, 2) }); + ['A2', 'B1', 'B2'].forEach(label => { + expect(getCell(label)).toHaveAttribute('tabindex', '-1'); + }); + }); + + test('focusing a cell updates the tab stop to that cell', () => { + renderGrid({ plate: makePlate(2, 2) }); + fireEvent.focus(getCell('B2')); + expect(getCell('B2')).toHaveAttribute('tabindex', '0'); + expect(getCell('A1')).toHaveAttribute('tabindex', '-1'); + }); +}); + +describe('TemplateGrid — keyboard navigation', () => { + test('ArrowRight moves focus from A1 to A2', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowRight' }); + expect(getCell('A2')).toHaveAttribute('tabindex', '0'); + expect(getCell('A1')).toHaveAttribute('tabindex', '-1'); + }); + + test('ArrowDown moves focus from A1 to B1', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowDown' }); + expect(getCell('B1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowLeft moves focus from A2 to A1', () => { + renderGrid(); + fireEvent.focus(getCell('A2')); + fireEvent.keyDown(getCell('A2'), { key: 'ArrowLeft' }); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowUp moves focus from B1 to A1', () => { + renderGrid(); + fireEvent.focus(getCell('B1')); + fireEvent.keyDown(getCell('B1'), { key: 'ArrowUp' }); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowLeft at column 0 does nothing', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowLeft' }); + // A1 should remain the tab stop + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowUp at row 0 does nothing', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowUp' }); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowDown at last row does nothing', () => { + renderGrid({ plate: makePlate(4, 4) }); + fireEvent.focus(getCell('D1')); + fireEvent.keyDown(getCell('D1'), { key: 'ArrowDown' }); + expect(getCell('D1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowRight at last column does nothing', () => { + renderGrid({ plate: makePlate(4, 4) }); + fireEvent.focus(getCell('A4')); + fireEvent.keyDown(getCell('A4'), { key: 'ArrowRight' }); + expect(getCell('A4')).toHaveAttribute('tabindex', '0'); + }); +}); + +describe('TemplateGrid — keyboard cell toggle', () => { + test('Space key calls onCellToggle with the cell coordinates', () => { + const { onCellToggle } = renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: ' ' }); + expect(onCellToggle).toHaveBeenCalledWith(0, 0); + }); + + test('Enter key calls onCellToggle with the cell coordinates', () => { + const { onCellToggle } = renderGrid(); + fireEvent.keyDown(getCell('B3'), { key: 'Enter' }); + expect(onCellToggle).toHaveBeenCalledWith(1, 2); + }); + + test('other keys do not call onCellToggle', () => { + const { onCellToggle } = renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'Tab' }); + expect(onCellToggle).not.toHaveBeenCalled(); + }); +}); + +describe('TemplateGrid — mouse click (no drag)', () => { + test('mousedown + mouseup on the same cell calls onCellToggle', () => { + const { onCellToggle } = renderGrid(); + const cell = getCell('A1'); + fireEvent.mouseDown(cell, { button: 0 }); + fireEvent.mouseUp(cell); + expect(onCellToggle).toHaveBeenCalledWith(0, 0); + }); + + test('right-click (button !== 0) does not start a drag', () => { + const { onCellToggle, onDragRect } = renderGrid(); + const cell = getCell('A1'); + fireEvent.mouseDown(cell, { button: 2 }); + fireEvent.mouseUp(cell); + expect(onCellToggle).not.toHaveBeenCalled(); + expect(onDragRect).not.toHaveBeenCalled(); + }); +}); + +describe('TemplateGrid — mouse drag', () => { + test('mousedown then mouseenter a different cell calls onDragRect (not onCellToggle)', () => { + const { onDragRect, onCellToggle } = renderGrid(); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + expect(onDragRect).toHaveBeenCalledWith(0, 0, 1, 1, false, []); + expect(onCellToggle).not.toHaveBeenCalled(); + }); + + test('drag started on a cell already in the active group uses unselect mode', () => { + const activeGroup: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + const { onDragRect } = renderGrid({ activeGroup }); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + // isUnselect should be true since A1 is already in the active group + expect(onDragRect).toHaveBeenCalledWith(0, 0, 1, 1, true, activeGroup.positions); + }); + + test('drag started on an empty cell uses select mode', () => { + const activeGroup: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [], properties: {}, allowNewGroups: false, + }; + const { onDragRect } = renderGrid({ activeGroup }); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + expect(onDragRect).toHaveBeenCalledWith(0, 0, 1, 1, false, []); + }); + + test('mouseleave the grid resets drag state so subsequent mouseenter does not fire', () => { + const { onDragRect } = renderGrid(); + const grid = document.querySelector('.template-grid') as HTMLElement; + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseLeave(grid); // drag cancelled + fireEvent.mouseEnter(getCell('B2')); // should be ignored + expect(onDragRect).not.toHaveBeenCalled(); + }); + + test('mouseup after drag does not call onCellToggle', () => { + const { onCellToggle } = renderGrid(); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + fireEvent.mouseUp(getCell('B2')); + expect(onCellToggle).not.toHaveBeenCalled(); + }); +}); + +describe('TemplateGrid — colorMap fallback', () => { + test('cell gets #f5f5f5 background when its group rowId is not in colorMap', () => { + const group: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map(), // rowId 1 not in map → falls back to '#f5f5f5' + }); + const cell = screen.getByLabelText('A1: Sample 1'); + expect(cell).toBeInTheDocument(); + expect(cell).toHaveStyle({ backgroundColor: '#f5f5f5' }); + }); + + test('group of a type other than activeTab is excluded from position map', () => { + const controlGroup: WellGroup = { + rowId: 2, type: 'CONTROL', name: 'Virus', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [controlGroup]), + colorMap: new Map([[2, '#00ff00']]), + activeTab: 'SPECIMEN', // CONTROL !== SPECIMEN → group is skipped + }); + // Cell is not labeled with the CONTROL group name since that type is inactive + expect(screen.getByLabelText('A1')).toBeInTheDocument(); + expect(screen.queryByLabelText('A1: Virus')).toBeNull(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index 67600997651..dc7a2cc0558 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import { PlateTemplate, Position, WellGroup } from '../models'; -interface Props { +interface TemplateGridProps { plate: PlateTemplate; activeGroup: WellGroup | null; activeTab: string; @@ -50,7 +50,7 @@ function getRowLabel(row: number): string { * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck * drag state when the pointer exits the grid. */ -export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: Props): JSX.Element { +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: TemplateGridProps): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); @@ -77,6 +77,18 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return map; }, [plate, activeTab, colorMap]); + // Pre-compute a Set of "row,col" keys for the active group's positions so each cell + // can do an O(1) membership check instead of scanning the positions array on every render. + const activeGroupPositionSet = useMemo(() => { + const set = new Set(); + if (activeGroup) { + for (const p of activeGroup.positions) { + set.add(`${p.row},${p.col}`); + } + } + return set; + }, [activeGroup]); + const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { if (e.button !== 0) return; isDragging.current = true; @@ -146,7 +158,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return (
- +
{Array.from({ length: plate.cols }, (_, col) => { const entry = positionMap.get(`${row},${col}`); - const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); + const isActiveGroupCell = activeGroupPositionSet.has(`${row},${col}`); const location = `${getRowLabel(row)}${col + 1}`; const tooltip = entry ? `${location}: ${entry.groupName}` : location; const isTabStop = focusedCell @@ -180,7 +192,6 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe 'template-grid__cell--active': isActiveGroupCell, })} style={{ backgroundColor: entry?.color ?? '#f5f5f5' }} - title={tooltip} aria-label={tooltip} onMouseDown={e => handleMouseDown(row, col, e)} onMouseEnter={() => handleMouseEnter(row, col)} diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx new file mode 100644 index 00000000000..1a740ce48d5 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { WarningPanel } from './WarningPanel'; + +describe('WarningPanel', () => { + test('shows "No warnings." and no list when warnings is empty', () => { + render(); + expect(screen.getByText('No warnings.')).toBeInTheDocument(); + expect(screen.queryByRole('list')).toBeNull(); + }); + + test('renders a list item for each warning', () => { + const warnings = ['A1: replicate with no specimen', 'B3: in both specimen and control']; + render(); + expect(screen.queryByText('No warnings.')).toBeNull(); + expect(screen.getAllByRole('listitem')).toHaveLength(2); + expect(screen.getByText('A1: replicate with no specimen')).toBeInTheDocument(); + expect(screen.getByText('B3: in both specimen and control')).toBeInTheDocument(); + }); + + test('renders a single warning without "No warnings."', () => { + render(); + expect(screen.getAllByRole('listitem')).toHaveLength(1); + expect(screen.queryByText('No warnings.')).toBeNull(); + }); + + test('switches from "No warnings." to a list when warnings change', () => { + const { rerender } = render(); + expect(screen.getByText('No warnings.')).toBeInTheDocument(); + + rerender(); + expect(screen.queryByText('No warnings.')).toBeNull(); + expect(screen.getByText('A1: some warning')).toBeInTheDocument(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx index ca49a6ed61c..dabb40c5998 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx @@ -3,28 +3,24 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useMemo } from 'react'; +import React from 'react'; -import { PlateTemplate, computeWarnings } from '../models'; - -interface Props { - plate: PlateTemplate; +interface WarningPanelProps { + warnings: string[]; } /** * Displays the list of validation warnings for the current plate layout. * - * Warnings are recomputed synchronously from the latest plate state on each render. + * Warnings are computed in the parent (PlateTemplateDesigner) and passed as a prop so the + * computation is not duplicated between the count badge and this panel. * The panel is only shown when `plate.showWarningPanel` is true, which is controlled by the * server-side assay type configuration (not all assay types use the REPLICATE/SPECIMEN/CONTROL * group semantics that produce warnings). */ -export function WarningPanel({ plate }: Props): JSX.Element { - const warnings = useMemo(() => computeWarnings(plate), [plate]); - +export function WarningPanel({ warnings }: WarningPanelProps): JSX.Element { return (
-
Warnings
{warnings.length === 0 ? (
No warnings.
) : ( diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx new file mode 100644 index 00000000000..c3506d6e135 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { WellGroup } from '../models'; +import { WellGroupProperties } from './WellGroupProperties'; + +function makeGroup(overrides: Partial = {}): WellGroup { + return { + rowId: 1, + type: 'SPECIMEN', + name: 'Group A', + positions: [], + properties: {}, + allowNewGroups: true, + ...overrides, + }; +} + +function renderProps(activeGroup: WellGroup | null, overrides: Partial> = {}) { + const props = { + activeGroup, + onPropertyChange: jest.fn(), + onDeleteProperty: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('WellGroupProperties', () => { + describe('empty state', () => { + test('shows placeholder text when no group is selected', () => { + renderProps(null); + expect(screen.getByText(/select a well group/i)).toBeInTheDocument(); + }); + + test('does not render a table when no group is selected', () => { + renderProps(null); + expect(screen.queryByRole('table')).toBeNull(); + }); + }); + + describe('group display', () => { + test('renders the group name as a title', () => { + renderProps(makeGroup({ name: 'My Group' })); + expect(screen.getByText('My Group')).toBeInTheDocument(); + }); + + test('shows "No properties defined" when group has no properties', () => { + renderProps(makeGroup({ properties: {} })); + expect(screen.getByText(/No properties defined/i)).toBeInTheDocument(); + }); + + test('renders a row for each existing property', () => { + renderProps(makeGroup({ properties: { conc: '1.0', dilution: '2x' } })); + expect(screen.getByText('conc')).toBeInTheDocument(); + expect(screen.getByDisplayValue('1.0')).toBeInTheDocument(); + expect(screen.getByText('dilution')).toBeInTheDocument(); + expect(screen.getByDisplayValue('2x')).toBeInTheDocument(); + }); + + test('renders a delete button per property', () => { + renderProps(makeGroup({ properties: { a: '1', b: '2' } })); + expect(screen.getAllByRole('button', { name: /Delete property/i })).toHaveLength(2); + }); + }); + + describe('editing existing properties', () => { + test('changing a value input calls onPropertyChange with groupRowId, key, and new value', () => { + const group = makeGroup({ properties: { conc: '1.0' } }); + const { onPropertyChange } = renderProps(group); + // The input is controlled (value comes from activeGroup.properties), so use + // fireEvent.change to fire a single synthetic event without the re-render cycle + // that makes userEvent.type fight the controlled value. + fireEvent.change(screen.getByLabelText('conc'), { target: { value: '5.0' } }); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'conc', '5.0'); + }); + + test('clicking the delete button calls onDeleteProperty with groupRowId and key', async () => { + const group = makeGroup({ properties: { conc: '1.0' } }); + const { onDeleteProperty } = renderProps(group); + await userEvent.click(screen.getByRole('button', { name: 'Delete property conc' })); + expect(onDeleteProperty).toHaveBeenCalledWith(group.rowId, 'conc'); + }); + }); + + describe('adding a new property', () => { + test('Add button is disabled when the key input is empty', () => { + renderProps(makeGroup()); + expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled(); + }); + + test('Add button is enabled when a key is typed', async () => { + renderProps(makeGroup()); + await userEvent.type(screen.getByLabelText('Property name'), 'newKey'); + expect(screen.getByRole('button', { name: 'Add' })).toBeEnabled(); + }); + + test('clicking Add calls onPropertyChange with the new key and value then clears inputs', async () => { + const group = makeGroup(); + const { onPropertyChange } = renderProps(group); + await userEvent.type(screen.getByLabelText('Property name'), 'dose'); + await userEvent.type(screen.getByLabelText('Property value'), '10mg'); + await userEvent.click(screen.getByRole('button', { name: 'Add' })); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'dose', '10mg'); + expect(screen.getByLabelText('Property name')).toHaveValue(''); + expect(screen.getByLabelText('Property value')).toHaveValue(''); + }); + + test('pressing Enter in the key input calls onPropertyChange', async () => { + const group = makeGroup(); + const { onPropertyChange } = renderProps(group); + await userEvent.type(screen.getByLabelText('Property name'), 'dose'); + await userEvent.type(screen.getByLabelText('Property value'), '10mg'); + await userEvent.type(screen.getByLabelText('Property name'), '{Enter}'); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'dose', '10mg'); + }); + + test('pressing Enter in the value input calls onPropertyChange', async () => { + const group = makeGroup(); + const { onPropertyChange } = renderProps(group); + await userEvent.type(screen.getByLabelText('Property name'), 'dose'); + await userEvent.type(screen.getByLabelText('Property value'), '10mg'); + await userEvent.type(screen.getByLabelText('Property value'), '{Enter}'); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'dose', '10mg'); + }); + + test('Add does not fire when key is whitespace-only', async () => { + const { onPropertyChange } = renderProps(makeGroup()); + await userEvent.type(screen.getByLabelText('Property name'), ' '); + await userEvent.click(screen.getByRole('button', { name: 'Add' })); + expect(onPropertyChange).not.toHaveBeenCalled(); + }); + + test('pressing Enter with a whitespace-only key does not call onPropertyChange', async () => { + // The Add button is disabled for whitespace, but the Enter key handler on the + // input calls handleAdd() directly, which must guard against empty trimmed keys. + const { onPropertyChange } = renderProps(makeGroup()); + await userEvent.type(screen.getByLabelText('Property name'), ' {Enter}'); + expect(onPropertyChange).not.toHaveBeenCalled(); + }); + }); + + describe('known bug: inputs not reset when active group changes', () => { + // This documents the existing behavior where newKey/newValue are NOT reset + // when the active group prop changes (see review finding #15). + // The inputs retain their values across group switches until the component unmounts. + test('newKey input retains value when activeGroup prop changes', async () => { + const group1 = makeGroup({ rowId: 1, name: 'Group 1' }); + const group2 = makeGroup({ rowId: 2, name: 'Group 2' }); + const { rerender } = render( + + ); + await userEvent.type(screen.getByLabelText('Property name'), 'stale-key'); + rerender( + + ); + // Bug: input still shows the value from group1's editing session + expect(screen.getByLabelText('Property name')).toHaveValue('stale-key'); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx index 67e242a2d68..62061b6badd 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { WellGroup } from '../models'; -interface Props { +interface WellGroupPropertiesProps { activeGroup: WellGroup | null; onPropertyChange: (groupRowId: number, key: string, value: string) => void; onDeleteProperty: (groupRowId: number, key: string) => void; @@ -27,7 +27,7 @@ interface Props { * - Adding: the footer row accepts a new key + value; "Add" (or Enter) commits the pair. * The new-key input is the gate — the Add button stays disabled until a key is typed. */ -export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: Props): JSX.Element { +export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: WellGroupPropertiesProps): JSX.Element { const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); @@ -53,6 +53,13 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro
{activeGroup.name}
@@ -161,7 +173,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe {getRowLabel(row)}
+ + + + + + + {propEntries.length === 0 && ( @@ -67,7 +74,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro className="well-group-properties__value" type="text" aria-label={key} - value={value ?? ''} + value={value} onChange={e => onPropertyChange(activeGroup.rowId, key, e.target.value)} /> diff --git a/assay/src/client/PlateTemplateDesigner/dev.tsx b/assay/src/client/PlateTemplateDesigner/dev.tsx index e0208c5e08a..d75b4201270 100644 --- a/assay/src/client/PlateTemplateDesigner/dev.tsx +++ b/assay/src/client/PlateTemplateDesigner/dev.tsx @@ -10,12 +10,8 @@ import { ServerContextProvider, withAppUser } from '@labkey/components'; import { PlateTemplateDesigner } from './PlateTemplateDesigner'; -const render = () => { - createRoot(document.getElementById('app')).render( - - - - ); -}; - -render(); +createRoot(document.getElementById('app')).render( + + + +); diff --git a/assay/src/client/PlateTemplateDesigner/models.test.ts b/assay/src/client/PlateTemplateDesigner/models.test.ts new file mode 100644 index 00000000000..90872a9b1ae --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/models.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import { + PlateTemplate, + WellGroup, + computeWarnings, + GROUP_TYPE_CONTROL, + GROUP_TYPE_REPLICATE, + GROUP_TYPE_SPECIMEN, +} from './models'; + +function makePlate(groups: Partial[]): PlateTemplate { + return { + rowId: 1, + name: 'Test Plate', + type: 'assay', + rows: 8, + cols: 12, + groupTypes: [GROUP_TYPE_SPECIMEN, GROUP_TYPE_CONTROL, GROUP_TYPE_REPLICATE], + canCreateGroupsByType: {}, + groups: groups.map((g, i) => ({ + rowId: i + 1, + type: GROUP_TYPE_SPECIMEN, + name: `Group ${i + 1}`, + positions: [], + properties: {}, + allowNewGroups: false, + ...g, + })), + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: true, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', + }; +} + +describe('computeWarnings', () => { + test('returns no warnings for an empty plate', () => { + expect(computeWarnings(makePlate([]))).toEqual([]); + }); + + test('returns no warnings when no cells are assigned', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [] }, + { type: GROUP_TYPE_SPECIMEN, positions: [] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('warns when a REPLICATE cell has no SPECIMEN or CONTROL', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('A1'); + expect(warnings[0]).toContain('replicate'); + }); + + test('no warning when REPLICATE cell is also in a SPECIMEN group', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_SPECIMEN, positions: [{ row: 0, col: 0 }] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('no warning when REPLICATE cell is also in a CONTROL group', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_CONTROL, positions: [{ row: 0, col: 0 }] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('warns when a cell is in both SPECIMEN and CONTROL groups', () => { + const plate = makePlate([ + { type: GROUP_TYPE_SPECIMEN, positions: [{ row: 1, col: 2 }] }, + { type: GROUP_TYPE_CONTROL, positions: [{ row: 1, col: 2 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('B3'); + expect(warnings[0]).toContain('specimen'); + expect(warnings[0]).toContain('control'); + }); + + test('cell in SPECIMEN + CONTROL + REPLICATE produces only the specimen/control warning', () => { + // REPLICATE warning suppressed because CONTROL is present + const plate = makePlate([ + { type: GROUP_TYPE_SPECIMEN, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_CONTROL, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('specimen'); + expect(warnings[0]).toContain('control'); + }); + + test('cell in an unrelated group type produces no warning', () => { + const plate = makePlate([ + { type: 'UNKNOWN', positions: [{ row: 0, col: 0 }] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('produces separate warnings for multiple problem cells', () => { + const plate = makePlate([ + // Two orphan replicates + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }, { row: 0, col: 1 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(2); + }); + + test('uses correct spreadsheet cell labels', () => { + // row 0 col 0 → A1 + // row 1 col 11 → B12 + // row 7 col 11 → H12 + const plate = makePlate([ + { + type: GROUP_TYPE_REPLICATE, + positions: [ + { row: 0, col: 0 }, + { row: 1, col: 11 }, + { row: 7, col: 11 }, + ], + }, + ]); + const warnings = computeWarnings(plate); + const labels = warnings.map(w => w.split(':')[0]); + expect(labels).toContain('A1'); + expect(labels).toContain('B12'); + expect(labels).toContain('H12'); + }); + + test('a cell in the same group type twice (two groups, same type) still counts as one type', () => { + // Two REPLICATE groups covering the same cell — the cell is still only REPLICATE-typed + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + ]); + const warnings = computeWarnings(plate); + // Only one warning for the cell (not two) + expect(warnings).toHaveLength(1); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts index 7d8dd6c431f..b8e120e63cd 100644 --- a/assay/src/client/PlateTemplateDesigner/models.ts +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -47,6 +47,10 @@ export interface PlateTemplate { * - A cell can appear in multiple groups of different types (e.g. SPECIMEN + REPLICATE together is fine). * - Cell labels use spreadsheet notation: row → letter (A=0, B=1, …), col → 1-based number. */ +export const GROUP_TYPE_REPLICATE = 'REPLICATE'; +export const GROUP_TYPE_SPECIMEN = 'SPECIMEN'; +export const GROUP_TYPE_CONTROL = 'CONTROL'; + export function computeWarnings(plate: PlateTemplate): string[] { // Build a map from cell position → set of group types that include it. const cellTypes = new Map>(); @@ -61,9 +65,9 @@ export function computeWarnings(plate: PlateTemplate): string[] { for (const [key, types] of cellTypes.entries()) { const [row, col] = key.split(',').map(Number); const cellLabel = `${String.fromCharCode(65 + row)}${col + 1}`; - const hasReplicate = types.has('REPLICATE'); - const hasSpecimen = types.has('SPECIMEN'); - const hasControl = types.has('CONTROL'); + const hasReplicate = types.has(GROUP_TYPE_REPLICATE); + const hasSpecimen = types.has(GROUP_TYPE_SPECIMEN); + const hasControl = types.has(GROUP_TYPE_CONTROL); if (hasReplicate && !(hasSpecimen || hasControl)) { warnings.push(`${cellLabel}: Well is a replicate, but is not part of a specimen or control group.`); } diff --git a/assay/test/js/fileMock.js b/assay/test/js/fileMock.js new file mode 100644 index 00000000000..f053ebf7976 --- /dev/null +++ b/assay/test/js/fileMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/assay/test/js/setup.ts b/assay/test/js/setup.ts new file mode 100644 index 00000000000..7b0828bfa80 --- /dev/null +++ b/assay/test/js/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; From 6851d9c12c3665e3f712b4b246b30c7e4aa3df58 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 28 Apr 2026 09:45:06 -0700 Subject: [PATCH 07/18] Another checkpoint --- .../PlateTemplateDesigner.scss | 23 +-- .../PlateTemplateDesigner.test.tsx | 141 ++++++++++++++++++ .../PlateTemplateDesigner.tsx | 13 +- .../components/GroupTypesPanel.tsx | 126 +++++++++++----- .../components/MultiCreateDialog.tsx | 117 ++++++++------- .../components/RightPanel.test.tsx | 32 ++-- .../components/RightPanel.tsx | 65 ++++---- .../components/StatusBar.tsx | 3 +- .../components/TabButton.test.tsx | 88 +++++++++++ .../components/TabButton.tsx | 37 +++++ .../components/TemplateGrid.tsx | 10 +- .../components/WellGroupProperties.tsx | 9 +- 12 files changed, 513 insertions(+), 151 deletions(-) create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TabButton.tsx diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 015dfac6eab..f814107cf20 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -314,6 +314,14 @@ } } + // Inline "Delete?" prompt label shown before the Yes / No confirmation buttons. + &__confirm-text { + font-size: 0.75rem; + color: #333; + margin-right: 2px; + white-space: nowrap; + } + // Row that holds the new-group name input + Create / Create multiple buttons &__create-row { display: flex; @@ -364,9 +372,10 @@ } // ── Multi-create dialog ─────────────────────────────────────────────────────── -// Modal overlay + dialog for batch-creating N numbered groups. -// The overlay captures click-outside-to-close; the inner dialog stops propagation. -// Focus is trapped inside the dialog while open (see GroupTypesPanel focus-trap effect). +// Modal dialog for batch-creating N numbered groups. +// Uses the native element with showModal() for top-layer placement, focus +// restoration on close, and a browser-managed backdrop. Focus is trapped inside the +// dialog while open (see MultiCreateDialog focus-trap effect). .multi-create-dialog { background: #fff; border: 1px solid #ccc; @@ -375,14 +384,8 @@ padding: 20px; min-width: 300px; - &__overlay { - position: fixed; - inset: 0; + &::backdrop { background: rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; } &__title { diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx new file mode 100644 index 00000000000..eadbde173c6 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { Ajax } from '@labkey/api'; + +import { PlateTemplateDesigner } from './PlateTemplateDesigner'; +import { PlateTemplate } from './models'; + +jest.mock('@labkey/api', () => ({ + ActionURL: { + getParameter: jest.fn().mockReturnValue(null), + buildURL: jest.fn().mockReturnValue('/mock-url'), + }, + Ajax: { + request: jest.fn(), + }, + Utils: { + // Pass the callback through unchanged so tests can invoke it directly. + getCallbackWrapper: (fn: any) => fn, + }, +})); + +const mockPlate: PlateTemplate = { + rowId: 1, + name: 'Test Plate', + type: 'assay', + rows: 4, + cols: 4, + groupTypes: ['SPECIMEN', 'CONTROL'], + canCreateGroupsByType: { SPECIMEN: true, CONTROL: true }, + groups: [], + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: false, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', +}; + +describe('PlateTemplateDesigner', () => { + let successCallback: ((response: any) => void) | undefined; + let failureCallback: ((response: any) => void) | undefined; + + beforeEach(() => { + jest.clearAllMocks(); + (Ajax.request as jest.Mock).mockImplementation(({ success, failure }) => { + successCallback = success; + failureCallback = failure; + }); + }); + + afterEach(() => { + successCallback = undefined; + failureCallback = undefined; + }); + + describe('initial load', () => { + test('shows loading state before data arrives', () => { + render(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + test('calls Ajax.request on mount', () => { + render(); + expect(Ajax.request).toHaveBeenCalledTimes(1); + }); + + test('renders the plate name input after a successful load', () => { + render(); + act(() => successCallback?.({ data: mockPlate })); + expect(screen.getByDisplayValue('Test Plate')).toBeInTheDocument(); + }); + + test('renders group type tabs after a successful load', () => { + render(); + act(() => successCallback?.({ data: mockPlate })); + expect(screen.getByRole('tab', { name: 'SPECIMEN' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'CONTROL' })).toBeInTheDocument(); + }); + + test('uses defaultPlateName as the initial plate name when provided', () => { + render(); + act(() => successCallback?.({ + data: { ...mockPlate, defaultPlateName: 'Copy of Test Plate', name: 'Test Plate' }, + })); + expect(screen.getByDisplayValue('Copy of Test Plate')).toBeInTheDocument(); + }); + + test('starts in dirty state when copyMode is true', () => { + render(); + act(() => successCallback?.({ data: { ...mockPlate, copyMode: true } })); + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + }); + }); + + describe('load failure', () => { + test('renders the server exception message on failure', () => { + render(); + act(() => failureCallback?.({ exception: 'Server error' })); + expect(screen.getByText('Server error')).toBeInTheDocument(); + }); + + test('renders a fallback message when exception is missing', () => { + render(); + act(() => failureCallback?.({})); + expect(screen.getByText('Failed to load plate template.')).toBeInTheDocument(); + }); + }); + + describe('plate name editing', () => { + async function renderLoaded() { + render(); + act(() => successCallback?.({ data: mockPlate })); + return screen.getByDisplayValue('Test Plate'); + } + + test('changing the plate name marks the form dirty', async () => { + const input = await renderLoaded(); + await userEvent.clear(input); + await userEvent.type(input, 'My Plate'); + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + }); + + test('Save button is disabled when form is clean', async () => { + await renderLoaded(); + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + }); + + test('Save button is enabled after a name change', async () => { + const input = await renderLoaded(); + await userEvent.clear(input); + await userEvent.type(input, 'My Plate'); + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled(); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 29a69e19167..23a55c1c63b 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -9,7 +9,7 @@ import { ActionURL, Ajax, Utils } from '@labkey/api'; import { PlateTemplate, Position, WellGroup, computeWarnings } from './models'; import { StatusBar } from './components/StatusBar'; import { GroupTypesPanel } from './components/GroupTypesPanel'; -import { RightPanel } from './components/RightPanel'; +import { RIGHT_TAB_PROPERTIES, RightPanel, RightTab } from './components/RightPanel'; import { ShiftPanel } from './components/ShiftPanel'; import { TemplateGrid } from './components/TemplateGrid'; @@ -110,7 +110,7 @@ export function PlateTemplateDesigner(): JSX.Element { const [plate, setPlate] = useState(null); const [activeGroup, setActiveGroup] = useState(null); const [activeTab, setActiveTab] = useState(''); - const [rightTab, setRightTab] = useState<'properties' | 'warnings'>('properties'); + const [rightTab, setRightTab] = useState(RIGHT_TAB_PROPERTIES); const [isDirty, setIsDirty] = useState(false); const [status, setStatus] = useState(''); const [colorMap, setColorMap] = useState>(new Map()); @@ -387,6 +387,13 @@ export function PlateTemplateDesigner(): JSX.Element { setActiveGroup(null); }, []); + // Clear pending status timer on unmount to prevent setState on an unmounted component. + useEffect(() => { + return () => { + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + }; + }, []); + // Warn on unsaved navigation useEffect(() => { const handler = (e: BeforeUnloadEvent) => { @@ -425,7 +432,7 @@ export function PlateTemplateDesigner(): JSX.Element { } if (!plate) { - return
Loading...
; + return
Loading...
; } return ( diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 851ec7072da..449ceab4524 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; import { MultiCreateDialog } from './MultiCreateDialog'; +import { TabButton } from './TabButton'; interface GroupTypesPanelProps { plate: PlateTemplate; @@ -81,6 +82,8 @@ export function GroupTypesPanel({ const [renameValue, setRenameValue] = useState(''); const [renameError, setRenameError] = useState(null); const [multiCreateOpen, setMultiCreateOpen] = useState(false); + // rowId of the group awaiting inline delete confirmation; null when no confirmation is pending. + const [confirmDeleteId, setConfirmDeleteId] = useState(null); // Stable derived list — memoized so useMemo and useEffect deps are stable. const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); @@ -96,10 +99,11 @@ export function GroupTypesPanel({ return defaults.filter(d => !groupsOfType.some(g => g.name === d)); }, [plate, activeTab, groupsOfType]); - // Reset create-input when tab changes + // Reset create-input and transient UI state when tab changes useEffect(() => { setNewGroupName(unusedDefaults[0] ?? ''); setRenamingId(null); + setConfirmDeleteId(null); }, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps // Advance to next unused default when the current one gets used @@ -118,9 +122,18 @@ export function GroupTypesPanel({ const handleDeleteClick = (e: React.MouseEvent, group: WellGroup) => { e.stopPropagation(); - if (window.confirm(`Delete well group "${group.name}"?`)) { - onDeleteGroup(group.rowId); - } + setConfirmDeleteId(group.rowId); + }; + + const handleDeleteConfirm = (e: React.MouseEvent, rowId: number) => { + e.stopPropagation(); + onDeleteGroup(rowId); + setConfirmDeleteId(null); + }; + + const handleDeleteCancel = (e: React.MouseEvent) => { + e.stopPropagation(); + setConfirmDeleteId(null); }; const handleRenameClick = (e: React.MouseEvent, group: WellGroup) => { @@ -150,21 +163,33 @@ export function GroupTypesPanel({ return (
-
+
) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; + const tabs = Array.from(e.currentTarget.querySelectorAll('[role="tab"]')); + const currentIndex = tabs.findIndex(t => t === document.activeElement); + if (currentIndex === -1) return; + e.preventDefault(); + const next = e.key === 'ArrowLeft' + ? (currentIndex - 1 + tabs.length) % tabs.length + : (currentIndex + 1) % tabs.length; + tabs[next].click(); + tabs[next].focus(); + }} + > {plate.groupTypes.map(type => ( - + ))}
{plate.groupTypes.map(type => ( @@ -178,7 +203,7 @@ export function GroupTypesPanel({ > {type === activeTab && ( <> -
+
{groupsOfType.map(group => { const color = colorMap.get(group.rowId); const isActive = activeGroup?.rowId === group.rowId; @@ -189,8 +214,8 @@ export function GroupTypesPanel({ className={classNames('group-types-panel__group', { 'group-types-panel__group--active': isActive, })} - role="option" - aria-selected={isActive} + role="listitem" + aria-current={isActive ? true : undefined} tabIndex={0} onClick={() => { if (!isRenaming) onGroupSelect(group); }} onKeyDown={e => { @@ -231,31 +256,52 @@ export function GroupTypesPanel({ The --hidden modifier keeps them invisible and non-interactive on unselected / renaming rows. */} {group.allowNewGroups && ( - - - + + + ) : ( + - + + + + ) )}
{isRenaming && renameError && ( diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx index a86e2a127f4..bb27be5c028 100644 --- a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx @@ -16,17 +16,20 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC const [baseName, setBaseName] = useState(initialBaseName); const [count, setCount] = useState('2'); const [countError, setCountError] = useState(''); - const dialogRef = useRef(null); + const dialogRef = useRef(null); - // Focus trap: runs once on mount since the component only renders when the dialog is open. useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; + + // Open as a modal (top layer + native backdrop; also restores focus on close). + dialog.showModal(); + + // Focus trap: cycle Tab/Shift-Tab within the dialog. const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { onClose(); return; } if (e.key !== 'Tab') return; const focusable = getFocusable(); const first = focusable[0]; @@ -38,9 +41,24 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC } }; + // The native fires a `cancel` event on Escape before closing itself. + // Prevent the browser's default close so we control the unmounting via onClose. + const handleCancel = (e: Event) => { + e.preventDefault(); + onClose(); + }; + dialog.addEventListener('keydown', handleKeyDown); + dialog.addEventListener('cancel', handleCancel); + + // Move focus to the first focusable element inside the dialog. getFocusable()[0]?.focus(); - return () => dialog.removeEventListener('keydown', handleKeyDown); + + return () => { + dialog.removeEventListener('keydown', handleKeyDown); + dialog.removeEventListener('cancel', handleCancel); + if (dialog.open) dialog.close(); + }; }, []); // eslint-disable-line react-hooks/exhaustive-deps const handleCreate = () => { @@ -61,59 +79,54 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC }; return ( -
-
e.stopPropagation()} - > -
Create Multiple Groups
-
-
- Base Name + +
Create Multiple Groups
+
+
+ Base Name + setBaseName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> +
+
+ Count +
setBaseName(e.target.value)} + className="multi-create-dialog__input multi-create-dialog__input--count" + type="number" + min="1" + aria-labelledby="multi-create-count-label" + aria-describedby={countError ? 'multi-create-count-error' : undefined} + aria-invalid={!!countError} + value={count} + onChange={e => { setCount(e.target.value); setCountError(''); }} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} /> + {countError &&
{countError}
}
-
- Count -
- { setCount(e.target.value); setCountError(''); }} - onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} - /> - {countError &&
{countError}
} -
-
-
- - -
+
+
+ +
-
+
); } diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx index ccd23a5188c..c7cffcb5936 100644 --- a/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; -import { RightPanel } from './RightPanel'; +import { RIGHT_TAB_PROPERTIES, RIGHT_TAB_WARNINGS, RightPanel } from './RightPanel'; function renderPanel(overrides: Partial> = {}) { const props = { showWarningPanel: false, - rightTab: 'properties' as const, + rightTab: RIGHT_TAB_PROPERTIES, onRightTabChange: jest.fn(), warnings: [], activeGroup: null, @@ -43,19 +43,19 @@ describe('RightPanel — without warning panel', () => { describe('RightPanel — with warning panel, properties tab', () => { test('renders a tablist with two tabs', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); expect(screen.getByRole('tablist')).toBeInTheDocument(); expect(screen.getAllByRole('tab')).toHaveLength(2); }); test('properties tab is aria-selected, warnings tab is not', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'false'); }); test('properties tabpanel is rendered with correct id and aria-labelledby', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); const panel = document.getElementById('right-panel-properties'); expect(panel).toBeInTheDocument(); expect(panel).toHaveAttribute('role', 'tabpanel'); @@ -63,54 +63,54 @@ describe('RightPanel — with warning panel, properties tab', () => { }); test('warnings tabpanel is hidden when rightTab is "properties"', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); expect(document.getElementById('right-panel-warnings')).toHaveAttribute('hidden'); }); test('warnings tab label shows count badge when warnings exist', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: ['w1', 'w2', 'w3'] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES, warnings: ['w1', 'w2', 'w3'] }); expect(screen.getByRole('tab', { name: 'Warnings (3)' })).toBeInTheDocument(); }); test('warnings tab label shows plain "Warnings" when list is empty', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: [] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES, warnings: [] }); expect(screen.getByRole('tab', { name: 'Warnings' })).toBeInTheDocument(); }); test('clicking warnings tab calls onRightTabChange with "warnings"', async () => { - const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); await userEvent.click(screen.getByRole('tab', { name: 'Warnings' })); - expect(onRightTabChange).toHaveBeenCalledWith('warnings'); + expect(onRightTabChange).toHaveBeenCalledWith(RIGHT_TAB_WARNINGS); }); test('clicking properties tab calls onRightTabChange with "properties"', async () => { - const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS }); await userEvent.click(screen.getByRole('tab', { name: 'Well Group Properties' })); - expect(onRightTabChange).toHaveBeenCalledWith('properties'); + expect(onRightTabChange).toHaveBeenCalledWith(RIGHT_TAB_PROPERTIES); }); }); describe('RightPanel — with warning panel, warnings tab', () => { test('renders warnings tabpanel; properties tabpanel is hidden', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: ['A1: warning'] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS, warnings: ['A1: warning'] }); expect(document.getElementById('right-panel-warnings')).toBeInTheDocument(); expect(document.getElementById('right-panel-properties')).toHaveAttribute('hidden'); }); test('warnings tab is aria-selected, properties tab is not', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS }); expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'false'); }); test('warnings tabpanel has correct aria-labelledby', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS }); const panel = document.getElementById('right-panel-warnings'); expect(panel).toHaveAttribute('aria-labelledby', 'right-tab-warnings'); }); test('WarningPanel content is visible in warnings tab', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: [] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS, warnings: [] }); expect(screen.getByText('No warnings.')).toBeInTheDocument(); }); }); diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx index 4d24c11247f..9e8e55579eb 100644 --- a/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx @@ -4,16 +4,20 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React from 'react'; -import classNames from 'classnames'; import { WellGroup } from '../models'; +import { TabButton } from './TabButton'; import { WellGroupProperties } from './WellGroupProperties'; import { WarningPanel } from './WarningPanel'; +export const RIGHT_TAB_PROPERTIES = 'properties' as const; +export const RIGHT_TAB_WARNINGS = 'warnings' as const; +export type RightTab = typeof RIGHT_TAB_PROPERTIES | typeof RIGHT_TAB_WARNINGS; + interface RightPanelProps { showWarningPanel: boolean; - rightTab: 'properties' | 'warnings'; - onRightTabChange: (tab: 'properties' | 'warnings') => void; + rightTab: RightTab; + onRightTabChange: (tab: RightTab) => void; warnings: string[]; activeGroup: WellGroup | null; onPropertyChange: (groupRowId: number, key: string, value: string) => void; @@ -26,40 +30,49 @@ export function RightPanel(props: RightPanelProps): JSX.Element { return (
- {showWarningPanel && ( -
- - +
)} diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index 2af5b740051..80a539bd161 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -48,7 +48,8 @@ export function StatusBar({ isDirty, status, plateName, onSaveAndClose, onSave, const validateAndSave = () => { if (validate()) onSave(); }; - const validateAndSaveAndClose = () => { if (validate()) onSaveAndClose(); }; + // Skip validation when there is nothing to save — the parent will navigate away without writing. + const validateAndSaveAndClose = () => { if (!isDirty || validate()) onSaveAndClose(); }; return (
diff --git a/assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx b/assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx new file mode 100644 index 00000000000..c229a5ef014 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { TabButton } from './TabButton'; + +function renderTab(overrides: Partial> = {}) { + const props = { + id: 'my-tab', + panelId: 'my-panel', + isActive: false, + baseClass: 'my-tab', + onClick: jest.fn(), + children: 'Tab Label', + ...overrides, + }; + render(); + return props; +} + +describe('TabButton', () => { + test('renders with role="tab"', () => { + renderTab(); + expect(screen.getByRole('tab')).toBeInTheDocument(); + }); + + test('sets id on the button element', () => { + renderTab({ id: 'right-tab-properties' }); + expect(screen.getByRole('tab')).toHaveAttribute('id', 'right-tab-properties'); + }); + + test('sets aria-controls to panelId', () => { + renderTab({ panelId: 'right-panel-properties' }); + expect(screen.getByRole('tab')).toHaveAttribute('aria-controls', 'right-panel-properties'); + }); + + test('aria-selected is true when isActive is true', () => { + renderTab({ isActive: true }); + expect(screen.getByRole('tab', { selected: true })).toBeInTheDocument(); + }); + + test('aria-selected is false when isActive is false', () => { + renderTab({ isActive: false }); + expect(screen.getByRole('tab', { selected: false })).toBeInTheDocument(); + }); + + test('applies baseClass to the button', () => { + renderTab({ baseClass: 'my-tab' }); + expect(screen.getByRole('tab')).toHaveClass('my-tab'); + }); + + test('adds --active when isActive is true', () => { + renderTab({ baseClass: 'my-tab', isActive: true }); + expect(screen.getByRole('tab')).toHaveClass('my-tab--active'); + }); + + test('does not add --active when isActive is false', () => { + renderTab({ baseClass: 'my-tab', isActive: false }); + expect(screen.getByRole('tab')).not.toHaveClass('my-tab--active'); + }); + + test('applies extraClassName when provided', () => { + renderTab({ extraClassName: 'my-tab--warn' }); + expect(screen.getByRole('tab')).toHaveClass('my-tab--warn'); + }); + + test('does not add unexpected classes when extraClassName is omitted', () => { + renderTab({ baseClass: 'my-tab', isActive: false }); + expect(screen.getByRole('tab')).toHaveClass('my-tab'); + expect(screen.getByRole('tab').className.trim()).toBe('my-tab'); + }); + + test('renders children', () => { + renderTab({ children: 'Well Group Properties' }); + expect(screen.getByText('Well Group Properties')).toBeInTheDocument(); + }); + + test('calls onClick when clicked', async () => { + const { onClick } = renderTab(); + await userEvent.click(screen.getByRole('tab')); + expect(onClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/TabButton.tsx b/assay/src/client/PlateTemplateDesigner/components/TabButton.tsx new file mode 100644 index 00000000000..5809c4ea9f8 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TabButton.tsx @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import classNames from 'classnames'; + +interface TabButtonProps { + id: string; + panelId: string; + isActive: boolean; + baseClass: string; + extraClassName?: string; + onClick: () => void; + children: React.ReactNode; +} + +/** + * A single ARIA tab button. Handles `role="tab"`, `aria-controls`, `aria-selected`, + * and the BEM `--active` modifier. Use inside a `role="tablist"` container. + */ +export function TabButton({ id, panelId, isActive, baseClass, extraClassName, onClick, children }: TabButtonProps): JSX.Element { + return ( + + ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index dc7a2cc0558..7882dfd4771 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -97,7 +97,9 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe dragIsUnselect.current = activeGroup?.positions.some(p => p.row === row && p.col === col) ?? false; // Snapshot the current positions NOW, from the prop, before any drag events can modify state. preDragPositions.current = activeGroup?.positions ?? []; - e.preventDefault(); + // Note: text selection during drag is already prevented by `user-select: none` in CSS, + // so e.preventDefault() is not needed here and is intentionally omitted so the browser's + // default focus-on-mousedown behaviour is preserved. }, [activeGroup]); const handleMouseEnter = useCallback((row: number, col: number) => { @@ -109,6 +111,9 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe // Called on mouseup over a specific cell — handles click-toggle const handleCellMouseUp = useCallback((row: number, col: number) => { if (isDragging.current && !hasMoved.current) { + // Explicitly move focus to the clicked cell so arrow-key navigation + // picks up from the correct position after a mouse interaction. + cellRefs.current.get(`${row},${col}`)?.focus(); onCellToggle(row, col); } }, [onCellToggle]); @@ -158,7 +163,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return (
-
PropertyValueActions
+
@@ -182,6 +187,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return ( { const key = `${row},${col}`; if (el) cellRefs.current.set(key, el); diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx index 62061b6badd..1e520886c77 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -3,7 +3,7 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { WellGroup } from '../models'; @@ -31,6 +31,13 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); + // Reset draft inputs when the selected group changes so stale text from a + // previous group cannot accidentally be committed to the newly selected one. + useEffect(() => { + setNewKey(''); + setNewValue(''); + }, [activeGroup?.rowId]); + if (!activeGroup) { return (
From 28daef787f527a2acea922ee6cd173cb0e173fe7 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 28 Apr 2026 17:30:11 -0700 Subject: [PATCH 08/18] Code review improvements, generics updates --- .../org/labkey/api/assay/plate/Plate.java | 6 +- .../PlateTemplateDesigner.tsx | 4 +- .../src/org/labkey/assay/PlateController.java | 381 +++++++++--------- .../assay/plate/PlateDataServiceImpl.java | 4 +- .../src/org/labkey/assay/plate/PlateImpl.java | 15 +- .../org/labkey/assay/plate/PlateManager.java | 16 +- .../labkey/assay/plate/PlateManagerTest.java | 8 +- .../org/labkey/assay/plate/WellGroupImpl.java | 4 +- 8 files changed, 224 insertions(+), 214 deletions(-) diff --git a/assay/api-src/org/labkey/api/assay/plate/Plate.java b/assay/api-src/org/labkey/api/assay/plate/Plate.java index c754bdcd7e2..ea0c8973633 100644 --- a/assay/api-src/org/labkey/api/assay/plate/Plate.java +++ b/assay/api-src/org/labkey/api/assay/plate/Plate.java @@ -66,11 +66,11 @@ public interface Plate extends PropertySet, Identifiable @Nullable WellGroup getWellGroup(int rowId); - @NotNull List getWellGroups(); + @NotNull List getWellGroups(); - @NotNull List getWellGroups(Position position); + @NotNull List getWellGroups(Position position); - @NotNull List getWellGroups(WellGroup.Type type); + @NotNull List getWellGroups(WellGroup.Type type); @NotNull Map> getWellGroupMap(); diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 23a55c1c63b..6489b79f15c 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -142,7 +142,7 @@ export function PlateTemplateDesigner(): JSX.Element { params.copy = copy; Ajax.request({ - url: ActionURL.buildURL('plate', 'getTemplateDefinition.api'), + url: ActionURL.buildURL('plate', 'getDesignerTemplateDefinition.api'), method: 'GET', params, success: Utils.getCallbackWrapper((response: { data: PlateTemplate }) => { @@ -338,7 +338,7 @@ export function PlateTemplateDesigner(): JSX.Element { const requestSave = useCallback((currentPlate: PlateTemplate, onSuccess: (response: { data: { rowId: number } }) => void) => { setStatus('Saving...'); Ajax.request({ - url: ActionURL.buildURL('plate', 'saveTemplate.api'), + url: ActionURL.buildURL('plate', 'saveDesignerTemplate.api'), method: 'POST', jsonData: currentPlate, success: Utils.getCallbackWrapper(onSuccess), diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index dffcee52849..4469022ce8a 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -15,15 +15,12 @@ */ package org.labkey.assay; -import org.apache.commons.io.input.BoundedInputStream; import org.apache.logging.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; import org.labkey.api.action.ApiJsonForm; -import org.labkey.api.action.ApiUsageException; -import org.labkey.api.action.BaseApiAction; import org.labkey.api.action.FormHandlerAction; -import org.labkey.api.action.NullSafeBindException; +import org.labkey.api.action.JsonInputLimit; import org.labkey.api.action.FormViewAction; import org.labkey.api.action.GWTServiceAction; import org.labkey.api.action.Marshal; @@ -112,7 +109,7 @@ public class PlateController extends SpringActionController private static final SpringActionController.DefaultActionResolver _actionResolver = new DefaultActionResolver(PlateController.class); private static final Logger LOG = LogHelper.getLogger(PlateController.class, "Controller for plate related actions"); - record SubmittedGroup(int rowId, String type, String name, List positions, Map properties) + public record SubmittedGroup(int rowId, String type, String name, List positions, Map properties) { public static SubmittedGroup from(JSONObject g) { @@ -245,6 +242,130 @@ public void setRowId(int rowId) } } + @Marshal(Marshaller.JSONObject) + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + @JsonInputLimit(10 * 1024 * 1024) + public static class SaveDesignerTemplateAction extends MutatingApiAction + { + private PlateType _plateType; + + @Override + public Object execute(CreatePlateForm form, BindException errors) throws Exception + { + Long rowId = form.getRowId(); + String name = form.getName(); + boolean updateExisting = false; + PlateImpl plate; + + if (rowId != null && rowId > 0) + { + plate = PlateManager.get().getPlate(getContainer(), rowId); + if (plate == null) + throw new NotFoundException("Plate template not found: " + rowId); + Plate conflict = PlateManager.get().getPlateByName(getContainer(), name); + if (conflict != null && !conflict.getRowId().equals(plate.getRowId())) + throw new ValidationException("A plate template with name '" + name + "' already exists."); + if (!plate.getAssayType().equals(form.getAssayType())) + throw new ValidationException("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + name + "'"); + if (plate.getRows() != form.getRows() || plate.getColumns() != form.getCols()) + throw new ValidationException("Plate template dimensions cannot be changed for '" + name + "'"); + updateExisting = true; + } + else + { + if (PlateManager.get().getPlateByName(getContainer(), name) != null) + throw new ValidationException("A plate template with name '" + name + "' already exists."); + plate = PlateManager.get().createPlate(getContainer(), form.getAssayType(), _plateType); + plate.setTemplate(true); + } + + plate.setName(name); + plate.setProperties(form.getPlateProperties()); + + List submittedGroups = form.getGroups(); + Set submittedGroupIds = new HashSet<>(); + for (SubmittedGroup g : submittedGroups) + if (g.rowId() > 0) + submittedGroupIds.add(g.rowId()); + + // Mark well groups absent from the submission for deletion + List existingWellGroups = plate.getWellGroups(); + for (WellGroup existingGroup : existingWellGroups) + if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) + plate.markWellGroupForDeletion(existingGroup); + + // Update existing or create new well groups + for (SubmittedGroup gm : submittedGroups) + { + WellGroup.Type groupType; + try + { + groupType = WellGroup.Type.valueOf(gm.type()); + } + catch (IllegalArgumentException e) + { + throw new ValidationException("Unknown well group type: '" + gm.type() + "'"); + } + + List positions = new ArrayList<>(); + for (PlatePosition p : gm.positions()) + positions.add(plate.getPosition(p.row(), p.col())); + + WellGroupImpl group; + if (updateExisting && gm.rowId() > 0) + { + group = findExistingWellGroup(existingWellGroups, gm.rowId()); + if (group == null) + throw new ValidationException("Well group " + gm.rowId() + " was not found."); + if (group.getType() != groupType) + throw new ValidationException("Well group type cannot be changed: " + gm.name()); + group.setName(gm.name()); + group.setPositions(positions); + plate.storeWellGroup(group); + } + else + { + group = plate.addWellGroup(gm.name(), groupType, positions); + } + group.setProperties(gm.properties()); + } + + PlateLayoutHandler handler = PlateManager.get().getPlateLayoutHandler(plate.getAssayType()); + if (handler == null) + throw new NotFoundException("Invalid assay type"); + handler.validatePlate(getContainer(), getUser(), plate); + long savedRowId = PlateService.get().save(getContainer(), getUser(), plate); + return success(Map.of("rowId", savedRowId)); + } + + private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) + { + for (WellGroupImpl wg : wellGroups) + if (wg.getRowId() != null && wg.getRowId() == rowId) + return wg; + return null; + } + + @Override + public void validateForm(CreatePlateForm form, Errors errors) + { + if (form.getGroups() == null || form.getGroups().isEmpty()) + { + errors.reject(ERROR_REQUIRED, "At least one group is required."); + } + // Template designer (groups) path: plateType resolved by rowId lookup on update, + // or by rows×cols on create. + if (form.getRowId() == null || form.getRowId() <= 0) + { + _plateType = form.getPlateType() != null + ? PlateManager.get().getPlateType(form.getPlateType()) + : PlateService.get().getPlateType(form.getRows(), form.getCols()); + if (_plateType == null) + errors.reject(ERROR_REQUIRED, "The plate type (" + form.getRows() + " x " + form.getCols() + ") does not exist."); + } + } + } + @RequiresPermission(ReadPermission.class) public static class PlateDetailsAction extends SimpleRedirectAction { @@ -262,7 +383,7 @@ public ActionURL getRedirectURL(RowIdForm form) } @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public class GetTemplateDefinitionAction extends ReadOnlyApiAction + public class GetDesignerTemplateDefinitionAction extends ReadOnlyApiAction { @Override public Object execute(DesignerForm form, BindException errors) throws Exception @@ -286,13 +407,13 @@ public Object execute(DesignerForm form, BindException errors) throws Exception if (templateName != null) { if (plateId == null) - throw new Exception("plateId is required when templateName is specified."); + throw new NotFoundException("plateId is required when templateName is specified."); template = PlateService.get().getPlate(getContainer(), plateId); if (template == null) - throw new NotFoundException("Plate '" + templateName + "' does not exist."); + throw new NotFoundException("Plate referenced by plateId does not exist."); handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); if (handler == null) - throw new Exception("Plate template type '" + template.getAssayType() + "' does not exist."); + throw new ValidationException("Plate template type '" + template.getAssayType() + "' does not exist."); } else { @@ -303,10 +424,10 @@ public Object execute(DesignerForm form, BindException errors) throws Exception handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); if (handler == null) - throw new Exception("Plate template type '" + assayTypeName + "' does not exist."); + throw new ValidationException("Plate template type '" + assayTypeName + "' does not exist."); PlateType plateType = PlateService.get().getPlateType(rowCount, colCount); if (plateType == null) - throw new Exception("The plate type (" + rowCount + " x " + colCount + ") does not exist."); + throw new ValidationException("The plate type (" + rowCount + " x " + colCount + ") does not exist."); template = handler.createPlate(templateTypeName, getContainer(), plateType); } @@ -395,182 +516,6 @@ public Object execute(DesignerForm form, BindException errors) throws Exception } } - public static class SaveTemplateForm implements ApiJsonForm - { - private JSONObject _json; - - @Override - public void bindJson(JSONObject json) - { - _json = json; - } - - public JSONObject getJson() - { - return _json != null ? _json : new JSONObject(); - } - } - - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public static class SaveTemplateAction extends MutatingApiAction - { - private static final int MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB - - @Override - protected BaseApiAction.FormAndErrors populateJacksonForm() throws Exception - { - byte[] bytes; - try (BoundedInputStream bounded = BoundedInputStream.builder() - .setInputStream(getViewContext().getRequest().getInputStream()) - .setMaxCount((long) MAX_BODY_BYTES + 1) - .get()) - { - bytes = bounded.readAllBytes(); - } - if (bytes.length > MAX_BODY_BYTES) - throw new ApiUsageException("Request body exceeds maximum allowed size of 10 MB."); - String body = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); - JSONObject jsonObj = body.isEmpty() ? new JSONObject() : new JSONObject(body); - SaveTemplateForm form = new SaveTemplateForm(); - form.bindJson(jsonObj); - return new BaseApiAction.FormAndErrors<>(form, new NullSafeBindException(form, "form")); - } - - @Override - public Object execute(SaveTemplateForm form, BindException errors) throws Exception - { - JSONObject json = form.getJson(); - - long rowId = json.optLong("rowId", -1); - String name = json.getString("name"); - String type = json.getString("type"); - int rows = json.getInt("rows"); - int cols = json.getInt("cols"); - JSONArray groupsJson = json.optJSONArray("groups"); - JSONObject platePropsJson = json.optJSONObject("plateProperties"); - - Map plateProperties = new HashMap<>(); - if (platePropsJson != null) - { - for (String key : platePropsJson.keySet()) - plateProperties.put(key, platePropsJson.get(key)); - } - - boolean updateExisting = false; - PlateImpl plate; - if (rowId > 0) - { - plate = PlateManager.get().getPlate(getContainer(), rowId); - if (plate == null) - throw new NotFoundException("Plate template not found: " + rowId); - // Check for a conflicting name from a different plate - Plate conflict = PlateManager.get().getPlateByName(getContainer(), name); - if (conflict != null && !conflict.getRowId().equals(plate.getRowId())) - throw new ApiUsageException("A plate template with name '" + name + "' already exists."); - if (!plate.getAssayType().equals(type)) - throw new ApiUsageException("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + name + "'"); - if (plate.getRows() != rows || plate.getColumns() != cols) - throw new ApiUsageException("Plate template dimensions cannot be changed for '" + name + "'"); - updateExisting = true; - } - else - { - if (PlateManager.get().getPlateByName(getContainer(), name) != null) - throw new ApiUsageException("A plate template with name '" + name + "' already exists."); - PlateType plateType = PlateService.get().getPlateType(rows, cols); - if (plateType == null) - throw new NotFoundException("The plate type (" + rows + " x " + cols + ") does not exist."); - plate = PlateManager.get().createPlate(getContainer(), type, plateType); - } - - plate.setName(name); - plate.setProperties(plateProperties); - - // Parse groups from JSON - List submittedGroups = new ArrayList<>(); - Set submittedGroupIds = new HashSet<>(); - if (groupsJson != null) - { - for (int i = 0; i < groupsJson.length(); i++) - { - SubmittedGroup g = SubmittedGroup.from(groupsJson.getJSONObject(i)); - submittedGroups.add(g); - if (g.rowId > 0) - submittedGroupIds.add(g.rowId); - } - } - - // Mark well groups not in submission for deletion - List existingWellGroups = plate.getWellGroups(); - for (WellGroup existingGroup : existingWellGroups) - { - if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) - plate.markWellGroupForDeletion(existingGroup); - } - - // Update or create well groups - for (SubmittedGroup gm : submittedGroups) - { - int gRowId = gm.rowId(); - String groupTypeName = gm.type(); - WellGroup.Type groupType; - try - { - groupType = WellGroup.Type.valueOf(groupTypeName); - } - catch (IllegalArgumentException e) - { - throw new ApiUsageException("Unknown well group type: '" + groupTypeName + "'"); - } - List posList = gm.positions(); - List positions = new ArrayList<>(); - for (PlatePosition p : posList) - positions.add(plate.getPosition(p.row, p.col)); - - Map props = gm.properties(); - - WellGroupImpl group; - if (updateExisting && gRowId > 0) - { - WellGroupImpl existing = findExistingWellGroup(existingWellGroups, gRowId); - if (existing == null) - throw new Exception("Well group " + gRowId + " was not found."); - if (existing.getType() != groupType) - throw new Exception("Well group type cannot be changed: " + gm.name()); - existing.setName(gm.name); - existing.setPositions(positions); - plate.storeWellGroup(existing); - group = existing; - } - else - { - group = plate.addWellGroup(gm.name, groupType, positions); - } - group.setProperties(props); - } - - PlateLayoutHandler plateLayoutHandler = PlateManager.get().getPlateLayoutHandler(plate.getAssayType()); - - if (plateLayoutHandler == null) - { - throw new NotFoundException("Invalid assay type"); - } - plateLayoutHandler.validatePlate(getContainer(), getUser(), plate); - long savedRowId = PlateService.get().save(getContainer(), getUser(), plate); - return success(Map.of("rowId", savedRowId)); - } - - private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) - { - for (WellGroup wg : wellGroups) - { - if (wg.getRowId() != null && wg.getRowId() == rowId) - return (WellGroupImpl) wg; - } - return null; - } - } - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public static class DesignerAction extends SimpleViewAction { @@ -959,6 +904,13 @@ public static class CreatePlateForm implements ApiJsonForm private boolean _template; private Long _templateId; + // Template designer fields (groups path) + private Long _rowId; + private int _rows; + private int _cols; + private List _groups; // non-null activates the template-upsert path + private final Map _plateProperties = new HashMap<>(); + public String getDescription() { return _description; @@ -1004,6 +956,31 @@ public Long getTemplateId() return _templateId; } + public Long getRowId() + { + return _rowId; + } + + public int getRows() + { + return _rows; + } + + public int getCols() + { + return _cols; + } + + public List getGroups() + { + return _groups; + } + + public Map getPlateProperties() + { + return _plateProperties; + } + @Override public void bindJson(JSONObject json) { @@ -1031,6 +1008,33 @@ public void bindJson(JSONObject json) if (json.has("templateId")) _templateId = json.getLong("templateId"); + // Template designer fields + if (json.has("rowId")) + _rowId = json.getLong("rowId"); + // "type" is how the React designer sends assayType + if (json.has("type") && !json.has("assayType")) + _assayType = json.getString("type"); + if (json.has("rows")) + _rows = json.getInt("rows"); + if (json.has("cols")) + _cols = json.getInt("cols"); + if (json.has("plateProperties")) + { + JSONObject props = json.getJSONObject("plateProperties"); + for (String key : props.keySet()) + { + Object val = props.get(key); + _plateProperties.put(key, val == JSONObject.NULL ? null : val); + } + } + if (json.has("groups")) + { + _groups = new ArrayList<>(); + JSONArray arr = json.getJSONArray("groups"); + for (int i = 0; i < arr.length(); i++) + _groups.add(SubmittedGroup.from(arr.getJSONObject(i))); + } + if (json.has("data")) { _data = new ArrayList<>(); @@ -1052,6 +1056,7 @@ public void bindJson(JSONObject json) @Marshal(Marshaller.JSONObject) @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + @JsonInputLimit(10 * 1024 * 1024) public static class CreatePlateAction extends MutatingApiAction { private PlateType _plateType; @@ -1061,6 +1066,8 @@ public void validateForm(CreatePlateForm form, Errors errors) { if (form.getPlateType() == null) errors.reject(ERROR_REQUIRED, "Plate \"plateType\" is required."); + if (form.getGroups() != null) + errors.reject(ERROR_REQUIRED, "Group values are not supported."); _plateType = PlateManager.get().getPlateType(form.getPlateType()); if (_plateType == null) diff --git a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java index 9efbde2e7a1..af614c9465f 100644 --- a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java @@ -198,7 +198,7 @@ public long saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exc // first, mark well groups not submitted for saving as deleted Set groups = gwtPlate.getGroups(); - List existingWellGroups = plate.getWellGroups(); + List existingWellGroups = plate.getWellGroups(); for (WellGroup existingWellGroup : existingWellGroups) { if (groups.stream().noneMatch(g-> g.getRowId() == existingWellGroup.getRowId())) @@ -246,7 +246,7 @@ public long saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exc } } - private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) + private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) { for (WellGroup wellGroup : wellGroups) { diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index 9934c2921de..0cfbca7fb86 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -244,6 +244,9 @@ public WellGroupImpl storeWellGroup(WellGroupImpl group) if (_groups == null) _groups = new HashMap<>(); Map groupsByType = _groups.computeIfAbsent(group.getType(), k -> new LinkedHashMap<>()); + // If this group has a rowId, remove any stale entry keyed under the old name (rename case). + if (group.getRowId() != null) + groupsByType.values().removeIf(existing -> group.getRowId().equals(existing.getRowId())); groupsByType.put(group.getName(), group); if (!wellGroupsInOrder(groupsByType)) { @@ -281,14 +284,14 @@ private boolean wellGroupsInOrder(Map groups) @JsonIgnore @Override - public @NotNull List getWellGroups(Position position) + public @NotNull List getWellGroups(Position position) { - List wellGroups = getWellGroups(); + List wellGroups = getWellGroups(); if (wellGroups.isEmpty()) return Collections.emptyList(); - List groups = new ArrayList<>(); - for (WellGroup group : wellGroups) + List groups = new ArrayList<>(); + for (WellGroupImpl group : wellGroups) { if (group.contains(position)) groups.add(group); @@ -299,12 +302,12 @@ private boolean wellGroupsInOrder(Map groups) @JsonIgnore @Override - public @NotNull List getWellGroups() + public @NotNull List getWellGroups() { if (_groups == null) return Collections.emptyList(); - List allGroups = new ArrayList<>(); + List allGroups = new ArrayList<>(); for (Map groups : _groups.values()) allGroups.addAll(groups.values()); diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index c12203b1708..72138aeca1b 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -1213,9 +1213,9 @@ private long savePlateImpl( // create/update well groups QueryUpdateService wellGroupQus = getWellGroupUpdateService(container, user); - for (WellGroup group : plate.getWellGroups()) + for (WellGroupImpl wellgroup : plate.getWellGroups()) { - WellGroupImpl wellgroup = (WellGroupImpl) group; + assert !wellgroup._deleted; String wellGroupInstanceLsid = wellgroup.getLSID(); Map wellGroupRow; @@ -1239,8 +1239,8 @@ private long savePlateImpl( if (wellGroupErrors.hasErrors()) throw wellGroupErrors; - wellGroupInstanceLsid = (String) insertedRows.get(0).get(WellTable.Column.Lsid.name()); - wellgroup = ObjectFactory.Registry.getFactory(WellGroupImpl.class).fromMap(wellgroup, insertedRows.get(0)); + wellGroupInstanceLsid = (String) insertedRows.getFirst().get(WellTable.Column.Lsid.name()); + wellgroup = ObjectFactory.Registry.getFactory(WellGroupImpl.class).fromMap(wellgroup, insertedRows.getFirst()); savePropertyBag(container, user, wellGroupInstanceLsid, wellgroup.getProperties(), false); } } @@ -1398,7 +1398,7 @@ private long savePlateImpl( // return a list of wellId and wellGroupId pairs private List> getWellGroupPositions(Plate plate, Position position) { - List groups = plate.getWellGroups(position); + List groups = plate.getWellGroups(position); List> wellGroupPositions = new ArrayList<>(groups.size()); for (WellGroup group : groups) @@ -1583,7 +1583,7 @@ public void beforeDeleteWellGroup(Container container, Integer wellGroupId) DbScope scope = schema.getSchema().getScope(); assert scope.isTransactionActive(); - new SqlExecutor(scope).execute("" + + new SqlExecutor(scope).execute( "DELETE FROM " + schema.getTableInfoWellGroupPositions() + " WHERE wellGroupId = ?", wellGroupId); } @@ -1594,7 +1594,7 @@ private void deleteWellGroupPositions(Plate plate) DbScope scope = schema.getSchema().getScope(); assert scope.isTransactionActive(); - new SqlExecutor(scope).execute("" + + new SqlExecutor(scope).execute( "DELETE FROM " + schema.getTableInfoWellGroupPositions() + " WHERE wellId IN (SELECT rowId FROM " + schema.getTableInfoWell() + " WHERE plateId=?)", plate.getRowId()); } @@ -2025,7 +2025,7 @@ else if (isDuplicatePlateName(container, user, name, destinationPlateSet)) // Save the plate long plateId = savePlateImpl(container, user, newPlate, true, null, true); - newPlate = (PlateImpl) getPlate(container, plateId); + newPlate = getPlate(container, plateId); if (newPlate == null) throw new IllegalStateException("Unexpected failure. Failed to retrieve plate after save (pre-commit)."); diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index a60e5aacb43..77efd75ea70 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -230,14 +230,14 @@ public void testCreatePlateTemplate() throws Exception assertNotNull(savedTemplate.getLSID()); assertEquals(plateType.getRowId(), savedTemplate.getPlateType().getRowId()); - List wellGroups = savedTemplate.getWellGroups(); + List wellGroups = savedTemplate.getWellGroups(); assertEquals(3, wellGroups.size()); // TsvPlateTypeHandler creates two CONTROL well groups "Positive" and "Negative" - List controlWellGroups = savedTemplate.getWellGroups(WellGroup.Type.CONTROL); + List controlWellGroups = savedTemplate.getWellGroups(WellGroup.Type.CONTROL); assertEquals(2, controlWellGroups.size()); - List sampleWellGroups = savedTemplate.getWellGroups(WellGroup.Type.SAMPLE); + List sampleWellGroups = savedTemplate.getWellGroups(WellGroup.Type.SAMPLE); assertEquals(1, sampleWellGroups.size()); WellGroup savedWg1 = sampleWellGroups.getFirst(); assertEquals("wg1", savedWg1.getName()); @@ -292,7 +292,7 @@ public void testCreatePlateTemplate() throws Exception assertNotNull(updatedWg2); // verify deleted well group - List updatedControlWellGroups = updatedTemplate.getWellGroups(WellGroup.Type.CONTROL); + List updatedControlWellGroups = updatedTemplate.getWellGroups(WellGroup.Type.CONTROL); assertEquals(1, updatedControlWellGroups.size()); // verify added positions diff --git a/assay/src/org/labkey/assay/plate/WellGroupImpl.java b/assay/src/org/labkey/assay/plate/WellGroupImpl.java index 857c6fb47d9..19c7e530c8b 100644 --- a/assay/src/org/labkey/assay/plate/WellGroupImpl.java +++ b/assay/src/org/labkey/assay/plate/WellGroupImpl.java @@ -209,7 +209,7 @@ public synchronized Set getOverlappingGroups() _overlappingGroups = new LinkedHashSet<>(); for (Position position : getPositions()) { - List groups = _plate.getWellGroups(position); + List groups = _plate.getWellGroups(position); for (WellGroup group : groups) { if (group != this) @@ -313,7 +313,7 @@ public Plate getPlate() { if (_plate == null && _plateId != null) { - _plate = (PlateImpl) PlateCache.getPlate(getContainer(), _plateId); + _plate = PlateCache.getPlate(getContainer(), _plateId); } return _plate; } From 82f1f83b7abfd88560c26ab66b94e98bcb514957 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 28 Apr 2026 18:28:08 -0700 Subject: [PATCH 09/18] Unit test fixup, bounds validation --- .../PlateTemplateDesigner.tsx | 4 +- .../components/GroupTypesPanel.test.tsx | 6 +- .../components/GroupTypesPanel.tsx | 6 +- .../components/MultiCreateDialog.tsx | 79 ++++++++++--------- .../components/StatusBar.test.tsx | 2 +- .../components/WellGroupProperties.test.tsx | 10 +-- .../src/org/labkey/assay/PlateController.java | 4 + assay/test/js/setup.ts | 9 +++ 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 6489b79f15c..5025beb9a56 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -122,6 +122,8 @@ export function PlateTemplateDesigner(): JSX.Element { const activeGroupRef = useRef(null); activeGroupRef.current = activeGroup; const nextColorIndexRef = useRef(0); // Monotonically increasing; never decrements on delete so colors stay unique + // Capture returnURL once at mount; handleSave strips query params via replaceState, so reading from the URL later would return null. + const returnURLRef = useRef(ActionURL.getParameter('returnUrl')); useEffect(() => { const templateName = ActionURL.getParameter('templateName'); @@ -325,7 +327,7 @@ export function PlateTemplateDesigner(): JSX.Element { }, [plate]); const navigateAway = useCallback(() => { - const returnURL = ActionURL.getParameter('returnURL') || ActionURL.getParameter('returnUrl'); + const returnURL = returnURLRef.current; window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); }, []); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx index 03d1f6ba963..71f463a3db7 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx @@ -291,15 +291,17 @@ describe('GroupTypesPanel — delete group', () => { test('delete button calls onDeleteGroup when user confirms', async () => { const { props } = renderWithActiveGroup(); - jest.spyOn(window, 'confirm').mockReturnValue(true); await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + // First click shows inline confirmation; click Yes to confirm + await userEvent.click(screen.getByRole('button', { name: 'Confirm delete Group A' })); expect(props.onDeleteGroup).toHaveBeenCalledWith(1); }); test('delete button does not call onDeleteGroup when user cancels', async () => { const { props } = renderWithActiveGroup(); - jest.spyOn(window, 'confirm').mockReturnValue(false); await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + // First click shows inline confirmation; click No to cancel + await userEvent.click(screen.getByRole('button', { name: 'Cancel delete Group A' })); expect(props.onDeleteGroup).not.toHaveBeenCalled(); }); }); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 449ceab4524..b397e149783 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -203,7 +203,7 @@ export function GroupTypesPanel({ > {type === activeTab && ( <> -
+
{groupsOfType.map(group => { const color = colorMap.get(group.rowId); const isActive = activeGroup?.rowId === group.rowId; @@ -214,8 +214,8 @@ export function GroupTypesPanel({ className={classNames('group-types-panel__group', { 'group-types-panel__group--active': isActive, })} - role="listitem" - aria-current={isActive ? true : undefined} + role="option" + aria-selected={isActive} tabIndex={0} onClick={() => { if (!isRenaming) onGroupSelect(group); }} onKeyDown={e => { diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx index bb27be5c028..edb2b7456e6 100644 --- a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx @@ -81,50 +81,53 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC return ( -
Create Multiple Groups
-
-
- Base Name - setBaseName(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} - /> -
-
- Count -
+
e.stopPropagation()}> +
Create Multiple Groups
+
+
+ Base Name { setCount(e.target.value); setCountError(''); }} + className="multi-create-dialog__input" + type="text" + aria-labelledby="multi-create-base-name-label" + value={baseName} + onChange={e => setBaseName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} /> - {countError &&
{countError}
}
-
-
- - +
+ Count +
+ { setCount(e.target.value); setCountError(''); }} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> + {countError &&
{countError}
} +
+
+
+ + +
diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx index e4242b0eeb2..9ca66dba283 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx @@ -85,7 +85,7 @@ describe('StatusBar', () => { }); test('clicking Save & Close with empty name shows error and does not call onSaveAndClose', async () => { - const { onSaveAndClose } = renderStatusBar({ plateName: '' }); + const { onSaveAndClose } = renderStatusBar({ plateName: '', isDirty: true }); await userEvent.click(screen.getByRole('button', { name: /Save & Close/i })); expect(onSaveAndClose).not.toHaveBeenCalled(); expect(screen.getByRole('alert')).toBeInTheDocument(); diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx index c3506d6e135..879bcd515ab 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx @@ -147,11 +147,8 @@ describe('WellGroupProperties', () => { }); }); - describe('known bug: inputs not reset when active group changes', () => { - // This documents the existing behavior where newKey/newValue are NOT reset - // when the active group prop changes (see review finding #15). - // The inputs retain their values across group switches until the component unmounts. - test('newKey input retains value when activeGroup prop changes', async () => { + describe('inputs reset when active group changes', () => { + test('newKey input is cleared when activeGroup prop changes', async () => { const group1 = makeGroup({ rowId: 1, name: 'Group 1' }); const group2 = makeGroup({ rowId: 2, name: 'Group 2' }); const { rerender } = render( @@ -161,8 +158,7 @@ describe('WellGroupProperties', () => { rerender( ); - // Bug: input still shows the value from group1's editing session - expect(screen.getByLabelText('Property name')).toHaveValue('stale-key'); + expect(screen.getByLabelText('Property name')).toHaveValue(''); }); }); }); diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 4469022ce8a..0e7ed1cc1a0 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -309,7 +309,11 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti List positions = new ArrayList<>(); for (PlatePosition p : gm.positions()) + { + if (p.row() < 0 || p.row() >= plate.getRows() || p.col() < 0 || p.col() >= plate.getColumns()) + throw new ValidationException("Position (" + p.row() + ", " + p.col() + ") is out of bounds for a " + plate.getRows() + " x " + plate.getColumns() + " plate."); positions.add(plate.getPosition(p.row(), p.col())); + } WellGroupImpl group; if (updateExisting && gm.rowId() > 0) diff --git a/assay/test/js/setup.ts b/assay/test/js/setup.ts index 7b0828bfa80..95ccacbca05 100644 --- a/assay/test/js/setup.ts +++ b/assay/test/js/setup.ts @@ -1 +1,10 @@ import '@testing-library/jest-dom'; + +// jsdom does not implement HTMLDialogElement.showModal / .close. +// These stubs are enough for component tests to run. +HTMLDialogElement.prototype.showModal = function () { + this.setAttribute('open', ''); +}; +HTMLDialogElement.prototype.close = function () { + this.removeAttribute('open'); +}; From 3cdf006169ae999b6c2bd9dcf423da02c39fe2fc Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 29 Apr 2026 17:50:58 -0700 Subject: [PATCH 10/18] Manual test and code review feedback --- .../PlateTemplateDesigner.scss | 47 ++---- .../PlateTemplateDesigner.tsx | 18 ++- .../components/GroupTypesPanel.test.tsx | 69 +++++++++ .../components/GroupTypesPanel.tsx | 14 +- .../components/StatusBar.tsx | 6 +- .../components/TemplateGrid.test.tsx | 122 ++++++++++++++++ .../components/TemplateGrid.tsx | 63 +++++--- .../src/org/labkey/assay/PlateController.java | 137 +++++++++--------- 8 files changed, 345 insertions(+), 131 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index f814107cf20..af28e9f6019 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -111,34 +111,6 @@ border-bottom: 1px solid #ddd; margin-bottom: 8px; - &__btn { - padding: 5px 14px; - border: 1px solid #aaa; - border-radius: 3px; - background: #f5f5f5; - cursor: pointer; - font-size: 0.8125rem; - - &:hover:not(:disabled) { - background: #e8e8e8; - } - - &:disabled { - opacity: 0.5; - cursor: default; - } - - &--primary { - background: #337ab7; - border-color: #2e6da4; - color: #fff; - - &:hover:not(:disabled) { - background: #286090; - } - } - } - // Unsaved-changes indicator — uses role="status" in JSX for screen reader announcements &__dirty { color: #7a5800; @@ -233,6 +205,12 @@ border-color: #337ab7; background: #e8f0fb; } + + // Applied when a well belonging to this group is hovered or keyboard-focused + &--highlighted { + background: #eef4ff; + border-color: #9fbfdf; + } } // Colour indicator matching the group's colour on the grid @@ -344,7 +322,6 @@ padding: 3px 8px; font-size: 0.75rem; border: 1px solid #aaa; - border-radius: 3px; background: #f5f5f5; cursor: pointer; color: #333; @@ -475,7 +452,6 @@ height: 26px; padding: 0; border: 1px solid #aaa; - border-radius: 3px; background: #f5f5f5; cursor: pointer; font-size: 0.875rem; @@ -541,10 +517,11 @@ filter: brightness(0.88); } - // Indicates cells belonging to the currently active group + // Indicates cells belonging to the highlighted group (selected or hovered in the list). + // box-shadow inset is used instead of outline because outline is clipped by adjacent + // cells in border-collapse:collapse tables across several browsers. &--active { - outline: 2px solid #333; - outline-offset: -2px; + box-shadow: inset 0 0 0 2px #000; } } } @@ -618,7 +595,6 @@ // Key column — fixed at 40% width, non-wrapping &__key { - font-weight: bold; padding: 3px 6px 3px 0; white-space: nowrap; width: 40%; @@ -648,7 +624,6 @@ min-height: 24px; min-width: 24px; border: 1px solid #ccc; - border-radius: 2px; background: transparent; cursor: pointer; font-size: 0.6875rem; @@ -669,7 +644,6 @@ &__new-key { padding: 3px 5px; border: 1px solid #ccc; - border-radius: 3px; font-size: 0.75rem; width: 100%; box-sizing: border-box; @@ -687,7 +661,6 @@ &__add-btn { padding: 3px 8px; border: 1px solid #aaa; - border-radius: 3px; background: #f5f5f5; cursor: pointer; font-size: 0.75rem; diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 5025beb9a56..0eb46719b0c 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -115,6 +115,10 @@ export function PlateTemplateDesigner(): JSX.Element { const [status, setStatus] = useState(''); const [colorMap, setColorMap] = useState>(new Map()); const [error, setError] = useState(null); + // rowId of the group being hovered in the group list; null when no group is hovered. + const [hoveredGroupId, setHoveredGroupId] = useState(null); + // rowId of the group that owns the currently hovered/focused well; null when no such well. + const [hoveredWellGroupId, setHoveredWellGroupId] = useState(null); const plateNameRef = useRef(''); // Mirrors plate.name; used in save-success to update URL without stale closure const statusTimerRef = useRef | null>(null); const nextGroupIdRef = useRef(-1); // Temporary negative IDs for client-created groups (see ID conventions above) @@ -356,11 +360,9 @@ export function PlateTemplateDesigner(): JSX.Element { const rowId = response.data.rowId; setIsDirty(false); setPlate(prev => prev ? { ...prev, rowId } : null); - // Update URL to canonical form so a refresh reloads this plate + // Update URL to canonical form so a refresh reloads this plate. const url = new URL(window.location.href); - url.search = ''; - url.searchParams.set('templateName', plateNameRef.current); - url.searchParams.set('plateId', String(rowId)); + url.search = `?templateName=${encodeURIComponent(plateNameRef.current)}&plateId=${encodeURIComponent(rowId)}`; window.history.replaceState(null, '', url.toString()); setStatus('Saved.'); if (statusTimerRef.current) clearTimeout(statusTimerRef.current); @@ -429,6 +431,10 @@ export function PlateTemplateDesigner(): JSX.Element { } }, [plate]); // eslint-disable-line react-hooks/exhaustive-deps + // The group whose wells should be visually highlighted on the grid. + // Group hover from the list takes priority; falls back to the active group. + const highlightedGroupId = hoveredGroupId ?? activeGroup?.rowId ?? null; + if (error) { return
{error}
; } @@ -465,11 +471,13 @@ export function PlateTemplateDesigner(): JSX.Element { activeGroup={activeGroup} activeTab={activeTab} colorMap={colorMap} + hoveredWellGroupId={hoveredWellGroupId} onGroupSelect={handleGroupSelect} onTabChange={handleTabChange} onAddGroup={handleAddGroup} onDeleteGroup={handleDeleteGroup} onRenameGroup={handleRenameGroup} + onGroupHover={setHoveredGroupId} > {/* The grid and shift panel are passed as children so they render inside GroupTypesPanel's flex row, visually adjacent to the group list. */} @@ -479,8 +487,10 @@ export function PlateTemplateDesigner(): JSX.Element { activeGroup={activeGroup} activeTab={activeTab} colorMap={colorMap} + highlightedGroupId={highlightedGroupId} onDragRect={handleDragRect} onCellToggle={handleCellToggle} + onWellHover={setHoveredWellGroupId} />
diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx index 71f463a3db7..bce63d4c4e2 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx @@ -48,11 +48,13 @@ function renderPanel(overrides: Partial(), + hoveredWellGroupId: null as number | null, onGroupSelect: jest.fn(), onTabChange: jest.fn(), onAddGroup: jest.fn(), onDeleteGroup: jest.fn(), onRenameGroup: jest.fn(), + onGroupHover: jest.fn(), ...overrides, }; const result = render(); @@ -338,3 +340,70 @@ describe('GroupTypesPanel — multi-create dialog', () => { expect(screen.queryByRole('dialog')).toBeNull(); }); }); + +describe('GroupTypesPanel — group hover (onGroupHover)', () => { + test('mousing into a group row calls onGroupHover with that group rowId', () => { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + const { props } = renderPanel({ + plate: makePlate({ groups: [group] }), + }); + fireEvent.mouseEnter(screen.getByRole('option', { name: 'Group A' })); + expect(props.onGroupHover).toHaveBeenCalledWith(1); + }); + + test('mousing out of the group list calls onGroupHover with null', () => { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + const { props } = renderPanel({ + plate: makePlate({ groups: [group] }), + }); + fireEvent.mouseLeave(screen.getByRole('listbox', { name: 'Well groups' })); + expect(props.onGroupHover).toHaveBeenCalledWith(null); + }); +}); + +describe('GroupTypesPanel — well-hover highlighting (hoveredWellGroupId)', () => { + test('group row receives --highlighted class when hoveredWellGroupId matches its rowId', () => { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + renderPanel({ + plate: makePlate({ groups: [group] }), + hoveredWellGroupId: 1, + }); + expect(screen.getByRole('option', { name: 'Group A' })).toHaveClass( + 'group-types-panel__group--highlighted' + ); + }); + + test('group row does not receive --highlighted class when hoveredWellGroupId is null', () => { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + renderPanel({ + plate: makePlate({ groups: [group] }), + hoveredWellGroupId: null, + }); + expect(screen.getByRole('option', { name: 'Group A' })).not.toHaveClass( + 'group-types-panel__group--highlighted' + ); + }); + + test('group row does not receive --highlighted class when hoveredWellGroupId is a different rowId', () => { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + renderPanel({ + plate: makePlate({ groups: [group] }), + hoveredWellGroupId: 99, + }); + expect(screen.getByRole('option', { name: 'Group A' })).not.toHaveClass( + 'group-types-panel__group--highlighted' + ); + }); + + test('active group does not receive --highlighted class even when its rowId matches hoveredWellGroupId', () => { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + renderPanel({ + plate: makePlate({ groups: [group] }), + activeGroup: group, + hoveredWellGroupId: 1, + }); + const row = screen.getByRole('option', { name: 'Group A' }); + expect(row).toHaveClass('group-types-panel__group--active'); + expect(row).not.toHaveClass('group-types-panel__group--highlighted'); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index b397e149783..4cc2a71fb34 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -15,11 +15,13 @@ interface GroupTypesPanelProps { activeGroup: WellGroup | null; activeTab: string; colorMap: Map; + hoveredWellGroupId: number | null; onGroupSelect: (group: WellGroup) => void; onTabChange: (tab: string) => void; onAddGroup: (type: string, name: string) => void; onDeleteGroup: (rowId: number) => void; onRenameGroup: (rowId: number, newName: string) => void; + onGroupHover: (groupId: number | null) => void; children?: React.ReactNode; } @@ -70,11 +72,13 @@ export function GroupTypesPanel({ activeGroup, activeTab, colorMap, + hoveredWellGroupId, onGroupSelect, onTabChange, onAddGroup, onDeleteGroup, onRenameGroup, + onGroupHover, children, }: GroupTypesPanelProps): JSX.Element { const [newGroupName, setNewGroupName] = useState(''); @@ -203,17 +207,25 @@ export function GroupTypesPanel({ > {type === activeTab && ( <> -
+
onGroupHover(null)} + > {groupsOfType.map(group => { const color = colorMap.get(group.rowId); const isActive = activeGroup?.rowId === group.rowId; + const isHighlighted = hoveredWellGroupId === group.rowId; const isRenaming = renamingId === group.rowId; return (
onGroupHover(group.rowId)} role="option" aria-selected={isActive} tabIndex={0} diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index 80a539bd161..3430ec75b21 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -53,13 +53,13 @@ export function StatusBar({ isDirty, status, plateName, onSaveAndClose, onSave, return (
- - - {isDirty ? 'Unsaved changes' : ''} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx index 79add5b0c0e..7bff19fe5aa 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx @@ -34,8 +34,10 @@ function renderGrid(overrides: Partial activeGroup: null, activeTab: 'SPECIMEN', colorMap: new Map(), + highlightedGroupId: null as number | null, onDragRect: jest.fn(), onCellToggle: jest.fn(), + onWellHover: jest.fn(), ...overrides, }; render(); @@ -284,3 +286,123 @@ describe('TemplateGrid — colorMap fallback', () => { expect(screen.queryByLabelText('A1: Virus')).toBeNull(); }); }); + +describe('TemplateGrid — well highlighting (highlightedGroupId)', () => { + function makeGroupWithPositions(rowId: number): WellGroup { + return { + rowId, + type: 'SPECIMEN', + name: `Group ${rowId}`, + positions: [{ row: 0, col: 0 }, { row: 0, col: 1 }], + properties: {}, + allowNewGroups: false, + }; + } + + test('cells belonging to the highlighted group receive the --active class', () => { + const group = makeGroupWithPositions(1); + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[1, '#ff0000']]), + highlightedGroupId: 1, + }); + expect(getCell('A1: Group 1')).toHaveClass('template-grid__cell--active'); + expect(getCell('A2: Group 1')).toHaveClass('template-grid__cell--active'); + }); + + test('cells not in the highlighted group do not receive --active class', () => { + const group = makeGroupWithPositions(1); + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[1, '#ff0000']]), + highlightedGroupId: 1, + }); + // B1 and B2 are not in the group + expect(getCell('B1')).not.toHaveClass('template-grid__cell--active'); + expect(getCell('B2')).not.toHaveClass('template-grid__cell--active'); + }); + + test('no cells receive --active class when highlightedGroupId is null', () => { + const group = makeGroupWithPositions(1); + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[1, '#ff0000']]), + highlightedGroupId: null, + }); + expect(getCell('A1: Group 1')).not.toHaveClass('template-grid__cell--active'); + expect(getCell('A2: Group 1')).not.toHaveClass('template-grid__cell--active'); + }); + + test('only cells of the highlighted group are active when multiple groups exist', () => { + const group1 = makeGroupWithPositions(1); + const group2: WellGroup = { + rowId: 2, type: 'SPECIMEN', name: 'Group 2', + positions: [{ row: 1, col: 0 }, { row: 1, col: 1 }], + properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [group1, group2]), + colorMap: new Map([[1, '#ff0000'], [2, '#00ff00']]), + highlightedGroupId: 2, + }); + expect(getCell('B1: Group 2')).toHaveClass('template-grid__cell--active'); + expect(getCell('B2: Group 2')).toHaveClass('template-grid__cell--active'); + expect(getCell('A1: Group 1')).not.toHaveClass('template-grid__cell--active'); + expect(getCell('A2: Group 1')).not.toHaveClass('template-grid__cell--active'); + }); +}); + +describe('TemplateGrid — onWellHover', () => { + test('mousing into a cell belonging to a group calls onWellHover with that group rowId', () => { + const group: WellGroup = { + rowId: 5, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + const { onWellHover } = renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[5, '#ff0000']]), + }); + fireEvent.mouseEnter(getCell('A1: Sample 1')); + expect(onWellHover).toHaveBeenCalledWith(5); + }); + + test('mousing into an unassigned cell calls onWellHover with null', () => { + const { onWellHover } = renderGrid(); + fireEvent.mouseEnter(getCell('A1')); + expect(onWellHover).toHaveBeenCalledWith(null); + }); + + test('focusing a cell belonging to a group calls onWellHover with that group rowId', () => { + const group: WellGroup = { + rowId: 5, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + const { onWellHover } = renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[5, '#ff0000']]), + }); + fireEvent.focus(getCell('A1: Sample 1')); + expect(onWellHover).toHaveBeenCalledWith(5); + }); + + test('focusing an unassigned cell calls onWellHover with null', () => { + const { onWellHover } = renderGrid(); + fireEvent.focus(getCell('A1')); + expect(onWellHover).toHaveBeenCalledWith(null); + }); + + test('mouse leaving the grid calls onWellHover with null', () => { + const { onWellHover } = renderGrid(); + const grid = document.querySelector('.template-grid') as HTMLElement; + fireEvent.mouseLeave(grid); + expect(onWellHover).toHaveBeenCalledWith(null); + }); + + test('mousing into a cell during a drag does not call onWellHover', () => { + const { onWellHover } = renderGrid(); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + onWellHover.mockClear(); // ignore any calls from before the drag started + fireEvent.mouseEnter(getCell('B2')); + expect(onWellHover).not.toHaveBeenCalled(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index 7882dfd4771..e9d0246dc76 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -13,8 +13,10 @@ interface TemplateGridProps { activeGroup: WellGroup | null; activeTab: string; colorMap: Map; + highlightedGroupId: number | null; onDragRect: (r1: number, c1: number, r2: number, c2: number, isUnselect: boolean, preDragPositions: Position[]) => void; onCellToggle: (row: number, col: number) => void; + onWellHover: (groupRowId: number | null) => void; } function getRowLabel(row: number): string { @@ -50,7 +52,7 @@ function getRowLabel(row: number): string { * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck * drag state when the pointer exits the grid. */ -export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: TemplateGridProps): JSX.Element { +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, highlightedGroupId, onDragRect, onCellToggle, onWellHover }: TemplateGridProps): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); @@ -62,32 +64,35 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe const [focusedCell, setFocusedCell] = useState<{ row: number; col: number } | null>(null); const cellRefs = useRef>(new Map()); - // Pre-compute a "row,col" → {color, groupName} map for the active tab type. + // Pre-compute a "row,col" → {color, groupName, groupRowId} map for the active tab type. // This lets each cell do an O(1) lookup rather than scanning all groups and // positions on every render (which would be O(groups × positions) per cell). const positionMap = useMemo(() => { - const map = new Map(); + const map = new Map(); for (const group of plate.groups) { if (group.type !== activeTab) continue; const color = colorMap.get(group.rowId) ?? '#f5f5f5'; for (const p of group.positions) { - map.set(`${p.row},${p.col}`, { color, groupName: group.name }); + map.set(`${p.row},${p.col}`, { color, groupName: group.name, groupRowId: group.rowId }); } } return map; }, [plate, activeTab, colorMap]); - // Pre-compute a Set of "row,col" keys for the active group's positions so each cell - // can do an O(1) membership check instead of scanning the positions array on every render. - const activeGroupPositionSet = useMemo(() => { + // Pre-compute a Set of "row,col" keys for the highlighted group's positions. + // The highlighted group is either the one being hovered in the group list, or + // the active group when nothing is being hovered, so each cell does an O(1) check. + const highlightedGroupPositionSet = useMemo(() => { const set = new Set(); - if (activeGroup) { - for (const p of activeGroup.positions) { - set.add(`${p.row},${p.col}`); + if (highlightedGroupId === null) return set; + for (const group of plate.groups) { + if (group.rowId === highlightedGroupId) { + for (const p of group.positions) set.add(`${p.row},${p.col}`); + break; } } return set; - }, [activeGroup]); + }, [highlightedGroupId, plate.groups]); const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { if (e.button !== 0) return; @@ -103,10 +108,15 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe }, [activeGroup]); const handleMouseEnter = useCallback((row: number, col: number) => { - if (!isDragging.current || !startCell.current) return; - hasMoved.current = true; - onDragRect(startCell.current.row, startCell.current.col, row, col, dragIsUnselect.current, preDragPositions.current); - }, [onDragRect]); + if (isDragging.current && startCell.current) { + hasMoved.current = true; + onDragRect(startCell.current.row, startCell.current.col, row, col, dragIsUnselect.current, preDragPositions.current); + } else { + // Not dragging: report which group this well belongs to for list highlighting. + const entry = positionMap.get(`${row},${col}`); + onWellHover(entry?.groupRowId ?? null); + } + }, [onDragRect, onWellHover, positionMap]); // Called on mouseup over a specific cell — handles click-toggle const handleCellMouseUp = useCallback((row: number, col: number) => { @@ -118,17 +128,20 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe } }, [onCellToggle]); - // Called on the wrapper div — cleans up drag state + // Called on the wrapper div — cleans up drag state and clears well hover const handleDragEnd = useCallback(() => { isDragging.current = false; hasMoved.current = false; startCell.current = null; dragIsUnselect.current = false; - }, []); + onWellHover(null); + }, [onWellHover]); const handleCellFocus = useCallback((row: number, col: number) => { setFocusedCell({ row, col }); - }, []); + const entry = positionMap.get(`${row},${col}`); + onWellHover(entry?.groupRowId ?? null); + }, [positionMap, onWellHover]); // Keyboard interaction for grid cells: // Space / Enter → toggle the cell (same as a click with no drag) @@ -162,7 +175,15 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe }, [onCellToggle, plate.rows, plate.cols]); return ( -
+
{ + // Clear well hover when keyboard focus leaves the grid entirely + if (!e.currentTarget.contains(e.relatedTarget as Node)) onWellHover(null); + }} + > @@ -178,7 +199,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe {Array.from({ length: plate.cols }, (_, col) => { const entry = positionMap.get(`${row},${col}`); - const isActiveGroupCell = activeGroupPositionSet.has(`${row},${col}`); + const isHighlightedGroupCell = highlightedGroupPositionSet.has(`${row},${col}`); const location = `${getRowLabel(row)}${col + 1}`; const tooltip = entry ? `${location}: ${entry.groupName}` : location; const isTabStop = focusedCell @@ -195,7 +216,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe }} tabIndex={isTabStop ? 0 : -1} className={classNames('template-grid__cell', { - 'template-grid__cell--active': isActiveGroupCell, + 'template-grid__cell--active': isHighlightedGroupCell, })} style={{ backgroundColor: entry?.color ?? '#f5f5f5' }} aria-label={tooltip} diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 0e7ed1cc1a0..9f2f465697c 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -242,15 +242,83 @@ public void setRowId(int rowId) } } + public static class SaveTemplateForm extends CreatePlateForm + { + // Template designer fields (groups path) + private Long _rowId; + private int _rows; + private int _cols; + private List _groups; // non-null activates the template-upsert path + private final Map _plateProperties = new HashMap<>(); + + public Long getRowId() + { + return _rowId; + } + + public int getRows() + { + return _rows; + } + + public int getCols() + { + return _cols; + } + + public List getGroups() + { + return _groups; + } + + public Map getPlateProperties() + { + return _plateProperties; + } + + @Override + public void bindJson(JSONObject json) + { + super.bindJson(json); + + // Template designer fields + if (json.has("rowId")) + _rowId = json.getLong("rowId"); + // "type" is how the React designer sends assayType + if (json.has("type") && !json.has("assayType")) + _assayType = json.getString("type"); + if (json.has("rows")) + _rows = json.getInt("rows"); + if (json.has("cols")) + _cols = json.getInt("cols"); + if (json.has("plateProperties")) + { + JSONObject props = json.getJSONObject("plateProperties"); + for (String key : props.keySet()) + { + Object val = props.get(key); + _plateProperties.put(key, val == JSONObject.NULL ? null : val); + } + } + if (json.has("groups")) + { + _groups = new ArrayList<>(); + JSONArray arr = json.getJSONArray("groups"); + for (int i = 0; i < arr.length(); i++) + _groups.add(SubmittedGroup.from(arr.getJSONObject(i))); + } + } + } + @Marshal(Marshaller.JSONObject) @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) @JsonInputLimit(10 * 1024 * 1024) - public static class SaveDesignerTemplateAction extends MutatingApiAction + public static class SaveDesignerTemplateAction extends MutatingApiAction { private PlateType _plateType; @Override - public Object execute(CreatePlateForm form, BindException errors) throws Exception + public Object execute(SaveTemplateForm form, BindException errors) throws Exception { Long rowId = form.getRowId(); String name = form.getName(); @@ -351,7 +419,7 @@ private WellGroupImpl findExistingWellGroup(List wellGroups, int } @Override - public void validateForm(CreatePlateForm form, Errors errors) + public void validateForm(SaveTemplateForm form, Errors errors) { if (form.getGroups() == null || form.getGroups().isEmpty()) { @@ -898,7 +966,7 @@ public void setPlateId(Integer plateId) public static class CreatePlateForm implements ApiJsonForm { - private String _assayType = TsvPlateLayoutHandler.TYPE; + protected String _assayType = TsvPlateLayoutHandler.TYPE; private List> _data; private String _description; private String _name; @@ -908,13 +976,6 @@ public static class CreatePlateForm implements ApiJsonForm private boolean _template; private Long _templateId; - // Template designer fields (groups path) - private Long _rowId; - private int _rows; - private int _cols; - private List _groups; // non-null activates the template-upsert path - private final Map _plateProperties = new HashMap<>(); - public String getDescription() { return _description; @@ -960,31 +1021,6 @@ public Long getTemplateId() return _templateId; } - public Long getRowId() - { - return _rowId; - } - - public int getRows() - { - return _rows; - } - - public int getCols() - { - return _cols; - } - - public List getGroups() - { - return _groups; - } - - public Map getPlateProperties() - { - return _plateProperties; - } - @Override public void bindJson(JSONObject json) { @@ -1012,33 +1048,6 @@ public void bindJson(JSONObject json) if (json.has("templateId")) _templateId = json.getLong("templateId"); - // Template designer fields - if (json.has("rowId")) - _rowId = json.getLong("rowId"); - // "type" is how the React designer sends assayType - if (json.has("type") && !json.has("assayType")) - _assayType = json.getString("type"); - if (json.has("rows")) - _rows = json.getInt("rows"); - if (json.has("cols")) - _cols = json.getInt("cols"); - if (json.has("plateProperties")) - { - JSONObject props = json.getJSONObject("plateProperties"); - for (String key : props.keySet()) - { - Object val = props.get(key); - _plateProperties.put(key, val == JSONObject.NULL ? null : val); - } - } - if (json.has("groups")) - { - _groups = new ArrayList<>(); - JSONArray arr = json.getJSONArray("groups"); - for (int i = 0; i < arr.length(); i++) - _groups.add(SubmittedGroup.from(arr.getJSONObject(i))); - } - if (json.has("data")) { _data = new ArrayList<>(); @@ -1070,8 +1079,6 @@ public void validateForm(CreatePlateForm form, Errors errors) { if (form.getPlateType() == null) errors.reject(ERROR_REQUIRED, "Plate \"plateType\" is required."); - if (form.getGroups() != null) - errors.reject(ERROR_REQUIRED, "Group values are not supported."); _plateType = PlateManager.get().getPlateType(form.getPlateType()); if (_plateType == null) From ab53b99e7a4e75bf75d864e4016efe01884d7e0e Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 29 Apr 2026 19:59:20 -0700 Subject: [PATCH 11/18] Delete GWT --- api/build.gradle | 28 +- .../org/labkey/api/gwt/Internal.gwt.xml | 9 - .../api/gwt/client/assay/AssayException.java | 41 - .../api/gwt/client/ui/BoundTextBox.java | 188 --- .../api/gwt/client/ui/DirtyCallback.java | 26 - .../labkey/api/gwt/client/ui/HelpPopup.java | 66 - .../labkey/api/gwt/client/ui/ImageButton.java | 162 -- .../api/gwt/client/ui/SaveButtonBar.java | 97 -- .../labkey/api/gwt/client/ui/Saveable.java | 61 - .../api/gwt/client/ui/TextBoxDialogBox.java | 132 -- .../api/gwt/client/ui/WebPartPanel.java | 50 - .../api/gwt/client/ui/WidgetUpdatable.java | 27 - .../labkey/api/gwt/client/ui/WindowUtil.java | 144 -- .../ui/domain/CancellationException.java | 24 - .../client/ui/incubator/ResizableWidget.java | 45 - .../incubator/ResizableWidgetCollection.java | 329 ---- .../api/gwt/client/util/BooleanProperty.java | 93 -- .../api/gwt/client/util/ColorGenerator.java | 64 - .../client/util/ErrorDialogAsyncCallback.java | 124 -- .../api/gwt/client/util/IPropertyWrapper.java | 29 - .../api/gwt/client/util/IntegerProperty.java | 89 -- .../api/gwt/client/util/PropertyUtil.java | 137 -- .../api/gwt/client/util/ServiceUtil.java | 95 -- .../api/gwt/client/util/StringProperty.java | 67 - .../api/gwt/client/util/StringUtils.java | 152 -- .../labkey/api/action/GWTServiceAction.java | 76 - .../org/labkey/api/assay/AssayQCService.java | 12 +- .../labkey/api/data/RuntimeSQLException.java | 3 +- .../labkey/api/dataiterator/CopyConfig.java | 2 +- .../org/labkey/api/exp/OntologyManager.java | 5 +- .../api/gwt/client/AuditBehaviorType.java | 114 +- .../api/gwt/client/DefaultScaleType.java | 76 +- .../api/gwt/client/DefaultValueType.java | 94 +- .../api/gwt/client/FacetingBehaviorType.java | 138 +- .../api/gwt/client/LockedPropertyType.java | 0 .../model/GWTPropertyDescriptorMixin.java | 97 +- .../gwt/client/assay/model/GWTProtocol.java | 953 ++++++----- .../client/model/GWTConditionalFormat.java | 103 +- .../api/gwt/client/model/GWTContainer.java | 140 +- .../api/gwt/client/model/GWTDomain.java | 658 ++++---- .../gwt/client/model/GWTFilterCriteria.java | 3 +- .../labkey/api/gwt/client/model/GWTIndex.java | 3 +- .../client/model/GWTPropertyDescriptor.java | 1388 ++++++++--------- .../client/model/GWTPropertyValidator.java | 398 ++--- .../client/model/PropertyValidatorType.java | 252 ++- .../api/gwt/client/ui/PropertyType.java | 0 .../api/gwt/server/BaseRemoteService.java | 141 -- api/src/org/labkey/api/ontology/Ontology.java | 2 +- .../org/labkey/api/pipeline/PipelineJob.java | 13 +- .../labkey/api/query/CrosstabExcelWriter.java | 2 +- .../org/labkey/api/util/ExceptionUtil.java | 4 +- .../org/labkey/api/util/OptionBuilder.java | 2 +- api/src/org/labkey/api/view/GWTView.java | 118 -- api/src/org/labkey/api/view/GWTView.jsp | 35 - assay/gwtsrc/gwt/AssayApplication.gwt.xml | 11 - .../org/labkey/assay/AssayApplication.java | 122 -- .../designer/client/GroupChangeListener.java | 35 - .../client/GroupChangeListenerAdapter.java | 47 - .../plate/designer/client/GroupTypePanel.java | 314 ---- .../designer/client/GroupTypePanelRow.java | 164 -- .../designer/client/GroupTypesTabPanel.java | 136 -- .../designer/client/PlateDataService.java | 34 - .../client/PlateDataServiceAsync.java | 32 - .../designer/client/PlatePropertyPanel.java | 30 - .../client/PropertyCreationDialog.java | 46 - .../plate/designer/client/PropertyPanel.java | 204 --- .../plate/designer/client/ShiftPanel.java | 98 -- .../plate/designer/client/StatusBar.java | 147 -- .../designer/client/TemplateDesigner.java | 56 - .../plate/designer/client/TemplateGrid.java | 121 -- .../designer/client/TemplateGridCell.java | 197 --- .../plate/designer/client/TemplateView.java | 649 -------- .../plate/designer/client/WarningPanel.java | 67 - .../client/WellGroupPropertyPanel.java | 66 - .../plate/designer/client/model/GWTPlate.java | 205 --- .../designer/client/model/GWTPosition.java | 87 -- .../designer/client/model/GWTWellGroup.java | 128 -- .../src/org/labkey/assay/PlateController.java | 72 - .../data/generator/AssayDesignGenerator.java | 5 +- .../data/generator/AssayRunDataGenerator.java | 3 +- .../assay/plate/PlateDataServiceImpl.java | 258 --- .../org/labkey/assay/view/AssayGWTView.java | 50 - .../api/ExpDataClassDataTestCase.jsp | 4 +- .../experiment/api/ExpSampleTypeTestCase.jsp | 4 +- query/src/org/labkey/query/MultiValueTest.jsp | 4 +- .../query/model/MetadataTableJSONMixin.java | 3 +- .../study/controllers/CohortController.java | 5 +- .../study/query/CohortUpdateService.java | 9 +- .../org/labkey/study/view/participantAll.jsp | 4 +- 89 files changed, 2222 insertions(+), 8306 deletions(-) delete mode 100644 api/gwtsrc/org/labkey/api/gwt/Internal.gwt.xml delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/assay/AssayException.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/BoundTextBox.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/DirtyCallback.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/HelpPopup.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/ImageButton.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/SaveButtonBar.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/Saveable.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/TextBoxDialogBox.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/WebPartPanel.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/WidgetUpdatable.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/WindowUtil.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/domain/CancellationException.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidget.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidgetCollection.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/BooleanProperty.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/ColorGenerator.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/ErrorDialogAsyncCallback.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/IPropertyWrapper.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/IntegerProperty.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/PropertyUtil.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/ServiceUtil.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/StringProperty.java delete mode 100644 api/gwtsrc/org/labkey/api/gwt/client/util/StringUtils.java delete mode 100644 api/src/org/labkey/api/action/GWTServiceAction.java rename api/{gwtsrc => src}/org/labkey/api/gwt/client/AuditBehaviorType.java (96%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/DefaultScaleType.java (96%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/DefaultValueType.java (90%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/FacetingBehaviorType.java (96%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/LockedPropertyType.java (100%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java (85%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/assay/model/GWTProtocol.java (95%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTConditionalFormat.java (89%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTContainer.java (95%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTDomain.java (92%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTFilterCriteria.java (83%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTIndex.java (95%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java (57%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/GWTPropertyValidator.java (90%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/model/PropertyValidatorType.java (60%) rename api/{gwtsrc => src}/org/labkey/api/gwt/client/ui/PropertyType.java (100%) delete mode 100644 api/src/org/labkey/api/gwt/server/BaseRemoteService.java delete mode 100644 api/src/org/labkey/api/view/GWTView.java delete mode 100644 api/src/org/labkey/api/view/GWTView.jsp delete mode 100644 assay/gwtsrc/gwt/AssayApplication.gwt.xml delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/assay/AssayApplication.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListener.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListenerAdapter.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanelRow.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypesTabPanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataService.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataServiceAsync.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlatePropertyPanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyCreationDialog.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyPanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/ShiftPanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/StatusBar.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateDesigner.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGrid.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGridCell.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateView.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WarningPanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WellGroupPropertyPanel.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPlate.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPosition.java delete mode 100644 assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTWellGroup.java delete mode 100644 assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java delete mode 100644 assay/src/org/labkey/assay/view/AssayGWTView.java diff --git a/api/build.gradle b/api/build.gradle index ec41567f313..b9c8f700499 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -74,7 +74,7 @@ configurations { sourceSets { main { java { - srcDirs = ["src", "gwtsrc", "${BuildUtils.getBuildDirPath(project)}/xb"] + srcDirs = ["src", "${BuildUtils.getBuildDirPath(project)}/xb"] } // TODO move resources files into resources directory to avoid this overlap resources { @@ -420,19 +420,6 @@ dependencies { ) ) - BuildUtils.addExternalDependency( - project, - new ExternalDependency( - "org.gwtproject:gwt-servlet-jakarta:${gwtServletJakartaVersion}", - "Server Support for Google Web Toolkit", - "Google", - "http://code.google.com/webtoolkit/", - ExternalDependency.APACHE_2_LICENSE_NAME, - ExternalDependency.APACHE_2_LICENSE_URL, - "Support for rich web apps", - ) - ) - BuildUtils.addExternalDependency( project, new ExternalDependency( @@ -1023,19 +1010,6 @@ dependencies { ) ) - BuildUtils.addExternalDependency( - project, - new ExternalDependency( - "jakarta.validation:jakarta.validation-api:${validationJakartaApiVersion}", - "Bean Validation API (JSR 303)", - "JCP", - "http://jcp.org/en/jsr/detail?id=303", - ExternalDependency.APACHE_2_LICENSE_NAME, - ExternalDependency.APACHE_2_LICENSE_URL, - "Validation of objects, requirement of GWT", - ) - ) - BuildUtils.addExternalDependency( project, new ExternalDependency( diff --git a/api/gwtsrc/org/labkey/api/gwt/Internal.gwt.xml b/api/gwtsrc/org/labkey/api/gwt/Internal.gwt.xml deleted file mode 100644 index a67836e894d..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/Internal.gwt.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/assay/AssayException.java b/api/gwtsrc/org/labkey/api/gwt/client/assay/AssayException.java deleted file mode 100644 index f7d4f9cc498..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/assay/AssayException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.assay; - -import com.google.gwt.user.client.rpc.SerializableException; - -/** - * User: jeckels - * Date: Jul 17, 2007 - */ -public class AssayException extends SerializableException -{ - public AssayException() - { - super(); - } - - public AssayException(String message) - { - super(message); - } - - public AssayException(Throwable cause) - { - super(cause.toString()); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/BoundTextBox.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/BoundTextBox.java deleted file mode 100644 index 54c523a0d3d..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/BoundTextBox.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; -import com.google.gwt.event.dom.client.ChangeEvent; -import com.google.gwt.event.dom.client.ChangeHandler; -import com.google.gwt.event.dom.client.KeyPressEvent; -import com.google.gwt.event.dom.client.KeyPressHandler; -import com.google.gwt.event.dom.client.KeyUpEvent; -import com.google.gwt.event.dom.client.KeyUpHandler; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.TextBox; -import com.google.gwt.user.client.ui.Widget; -import org.labkey.api.gwt.client.util.StringProperty; -import org.labkey.api.gwt.client.util.StringUtils; - -/** - * User: jeckels -* Date: Sep 9, 2008 -*/ -public class BoundTextBox extends HorizontalPanel -{ - protected final String _initialValue; - private final WidgetUpdatable _updateable; - private final DirtyCallback _dirtyCallback; - protected TextBox _box; - protected String _caption; - private boolean _required; - - public BoundTextBox(String caption, String id, String initialValue, final WidgetUpdatable updatable) - { - this(caption, id, initialValue, updatable, null); - } - - public BoundTextBox(String caption, String id, StringProperty prop) - { - this(caption, id, prop, null); - } - - public BoundTextBox(String caption, String id, final StringProperty prop, DirtyCallback dirtyCallback) - { - this(caption, id, prop.getString(), new WidgetUpdatable(){ - @Override - public void update(Widget widget) - { - prop.set(((TextBox)widget).getText()); - } - }, dirtyCallback); - } - - public BoundTextBox(String caption, String id, String initialValue, WidgetUpdatable updatable, final DirtyCallback dirtyCallback) - { - _initialValue = null==initialValue ? "" : initialValue; - _updateable = updatable; - _dirtyCallback = dirtyCallback; - _caption = caption; - _box = new TextBox(); - DOM.setElementAttribute(_box.getElement(), "id", id); - DOM.setElementAttribute(_box.getElement(), "name", id); - _box.setText(StringUtils.trimToEmpty(initialValue)); - _box.addBlurHandler(new BlurHandler() - { - @Override - public void onBlur(BlurEvent event) - { - _update((Widget)event.getSource()); - } - }); - _box.addChangeHandler(new ChangeHandler() - { - @Override - public void onChange(ChangeEvent change) - { - _update((Widget)change.getSource()); - } - }); - _box.addKeyPressHandler(new KeyPressHandler() - { - @Override - public void onKeyPress(KeyPressEvent e) - { - _dirty(); - } - }); - _box.addKeyUpHandler(new KeyUpHandler() - { - @Override - public void onKeyUp(KeyUpEvent e) - { - _dirty(); - } - }); - add(_box); - } - - - public void setRequired(boolean required) - { - _required = required; - } - - - void _update(Widget sender) - { - if (checkValid()) - _updateable.update(sender); - } - - - void _dirty() - { - if (_dirtyCallback != null && !_initialValue.equals(getBox().getText())) - _dirtyCallback.setDirty(true); - } - - - public boolean checkValid() - { - String value = _box.getText(); - String msg = validateValue(value); - if (null == msg) - { - clearErrorFormat(getBox()); - return true; - } - else - { - setErrorFormat(getBox(), msg, false); - return false; - } - } - - public final String validate() - { - return validateValue(getBox().getText()); - } - - - protected String validateValue(String text) - { - text = text.trim(); - if (_required && (text == null || text.isEmpty())) - return "\"" + _caption + "\" is required."; - return null; - } - - - public TextBox getBox() - { - return _box; - } - - - - static void setErrorFormat(Widget w, String message, boolean alert) - { - if (null == message) - message = "illegal value"; - - w.addStyleName("labkey-textbox-error"); - w.setTitle(message); - if (alert) - Window.alert(message); - } - - static void clearErrorFormat(Widget w) - { - w.removeStyleName("labkey-textbox-error"); - w.setTitle(""); - } -} \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/DirtyCallback.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/DirtyCallback.java deleted file mode 100644 index 01d1d89c53e..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/DirtyCallback.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -/** - * Used for UI components to callback to let their container know that their value has changed - * User: jeckels - * Date: Sep 9, 2008 - */ -public interface DirtyCallback -{ - void setDirty(boolean dirty); -} \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/HelpPopup.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/HelpPopup.java deleted file mode 100644 index d23c009c174..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/HelpPopup.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Element; -import com.google.gwt.user.client.ui.InlineLabel; -import org.labkey.api.gwt.client.util.PropertyUtil; - -/** - * User: Karl Lum - * Date: Aug 22, 2007 - */ -public class HelpPopup extends InlineLabel -{ - private final Element _element; - private final String _title; - private String _body; - - public HelpPopup(String title, String body) - { - _element = getElement(); - _title = title; - _body = body; - - final String headerSize = PropertyUtil.getServerProperty("header1Size"); - String text = "?"; - DOM.setInnerHTML(_element, text); - - addMouseOverHandler(e -> showHelpDiv(_element, _title, _body)); - - addMouseOutHandler(e -> hideHelpDiv()); - } - - public void setBody(String body) - { - _body = body; - } - - /** - * JSNI method to call the underlying javascript function in util.js - */ - public static native void hideHelpDiv() /*-{ - $wnd.hideHelpDivDelay(); - }-*/; - - public static native void showHelpDiv(Element element, String title, String body) /*-{ - $wnd.showHelpDivDelay(element, title, body); - }-*/; -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/ImageButton.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/ImageButton.java deleted file mode 100644 index 1b160535f3b..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/ImageButton.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.KeyPressEvent; -import com.google.gwt.event.dom.client.KeyPressHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.ui.ButtonBase; -import com.google.gwt.user.client.ui.ClickListener; -import com.google.gwt.user.client.ui.ClickListenerCollection; -import com.google.gwt.user.client.ui.Widget; - -import java.util.ArrayList; -import java.util.List; - -public class ImageButton extends ButtonBase implements ClickListener -{ - private final ClickListenerCollection _clickListeners = new ClickListenerCollection(); - private final List _clickHandlers = new ArrayList<>(); - private String _text; - - public ImageButton(String text, ClickListener listener) - { - this(text); - addClickListener(listener); - } - - public ImageButton(String text, ClickHandler handler) - { - this(text); - addClickHandler(handler); - } - - public ImageButton(String text) - { - super(DOM.createSpan()); - addClickListener(this); - - _text = text; - DOM.setAttribute(getElement(), "id", "button_" + text); - - refreshState(); - - addKeyPressHandler(new KeyPressHandler() - { - @Override - public void onKeyPress(KeyPressEvent event) - { - if (event.getCharCode() == ' ' && !event.isAnyModifierKeyDown()) - { - fireEvent(new ClickEvent() - { - // Hack - subclass exists to make the constructor public - }); - } - } - }); - - super.addClickListener(new ClickListener() - { - @Override - public void onClick(Widget sender) - { - if (isEnabled()) - _clickListeners.fireClick(sender); - } - }); - super.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent event) - { - if (isEnabled()) - { - for (ClickHandler clickHandler : _clickHandlers) - { - clickHandler.onClick(event); - } - } - } - }); - } - - @Override - public HandlerRegistration addClickHandler(final ClickHandler handler) - { - _clickHandlers.add(handler); - return new HandlerRegistration() - { - @Override - public void removeHandler() - { - _clickHandlers.remove(handler); - } - }; - } - - @Override - public void addClickListener(ClickListener listener) - { - _clickListeners.add(listener); - } - - @Override - public void removeClickListener(ClickListener listener) - { - _clickListeners.remove(listener); - } - - @Override - public String getText() - { - return _text; - } - - public void refreshState() - { - // TODO: This should try to use Button.ButtonBuilder - setHTML("" + _text + ""); - } - - @Override - public void setText(String text) - { - if (_text.equals(text)) - return; - - _text = text; - refreshState(); - } - - @Override - public void setEnabled(boolean enabled) - { - super.setEnabled(enabled); - refreshState(); - } - - /** to make life simple, just override onClick instead of registering a listener */ - @Override - public void onClick(Widget sender) - { - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/SaveButtonBar.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/SaveButtonBar.java deleted file mode 100644 index abffa7378d4..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/SaveButtonBar.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.ui.ButtonBase; -import com.google.gwt.user.client.ui.HorizontalPanel; - -/** - * User: jgarms - * Date: Jun 2, 2008 - * Time: 1:46:00 PM - */ -public class SaveButtonBar extends HorizontalPanel -{ - private final Saveable owner; - - private final ButtonBase finishButton; - private final ButtonBase saveButton; - private final ButtonBase cancelButton; - - public SaveButtonBar(Saveable s) - { - super(); - owner = s; - - getTable().setClassName("gwt-ButtonBar"); - - finishButton = new ImageButton("Save & Close", new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - owner.finish(); - } - }); - - add(finishButton); - - saveButton = new ImageButton("Save", new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - owner.save(); - } - }); - add(saveButton); - - - cancelButton = new ImageButton("Cancel", new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - owner.cancel(); - } - }); - - add(cancelButton); - } - - public void disableAll() - { - saveButton.setEnabled(false); - finishButton.setEnabled(false); - cancelButton.setEnabled(false); - } - - public void setAllowSave(boolean dirty) - { - // For unknown reasons, GWT ignores the button click if an already-enabled button is re-enabled - // during its click handling. Specifically, this happens if the dirty state is set as part of a blur handler - // when a form element is losing focus because a user is clicking on the save or cancel buttons - if (saveButton.isEnabled() != dirty) - saveButton.setEnabled(dirty); - if (!cancelButton.isEnabled()) - cancelButton.setEnabled(true); - if (!finishButton.isEnabled()) - finishButton.setEnabled(true); - } - -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/Saveable.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/Saveable.java deleted file mode 100644 index 690d1c42c28..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/Saveable.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -/** - * Interface for widgets that need a Save Finish Cancel button bar - * - * User: jgarms - * Date: Jun 2, 2008 - * Time: 11:01:14 AM - */ -public interface Saveable -{ - /** - * @return the URL that should be considered the current URL. This is useful for apps that both create - * new objects and edit existing objects. The new URL typically doesn't have a RowId or other identifier, - * but the edit does. Thus, to return to the "same" page after saving a new object, you need to add - * the RowId or otherwise change it. - */ - String getCurrentURL(); - - interface SaveListener - { - void saveSuccessful(ObjectType result, String designerUrl); - } - - /** - * Save button clicked - */ - void save(); - - /** - * Save button clicked - */ - void save(SaveListener listener); - - /** - * Cancel button clicked - */ - void cancel(); - - /** - * Finish button clicked - */ - void finish(); - - boolean isDirty(); -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/TextBoxDialogBox.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/TextBoxDialogBox.java deleted file mode 100644 index b87bf216d06..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/TextBoxDialogBox.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.user.client.ui.DialogBox; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.KeyboardListenerAdapter; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.TextBox; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Widget; - -/** - * User: jeckels - * Date: Apr 24, 2007 - */ -public abstract class TextBoxDialogBox extends DialogBox -{ - private final TextBox _textBox; - - public TextBoxDialogBox(String title, String label) - { - super(false); - - setText(title); - - _textBox = new TextBox(); - _textBox.addKeyboardListener(new KeyboardListenerAdapter() - { - @Override - public void onKeyDown(Widget sender, char keyCode, int modifiers) - { - if (keyCode == KeyCodes.KEY_ENTER) - { - commit(); - } - else if (keyCode == KeyCodes.KEY_ESCAPE) - { - hide(); - } - } - }); - - VerticalPanel contentPanel = new VerticalPanel(); - contentPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER); - - HorizontalPanel inputPanel = new HorizontalPanel(); - inputPanel.setSpacing(5); - - inputPanel.add(new Label(label + ": ")); - inputPanel.add(_textBox); - - HorizontalPanel buttonPanel = new HorizontalPanel(); - buttonPanel.setSpacing(5); - - ImageButton okButton = new ImageButton("OK"); - buttonPanel.add(okButton); - okButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - commit(); - } - }); - - ImageButton cancelButton = new ImageButton("Cancel"); - cancelButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - hide(); - } - }); - buttonPanel.add(cancelButton); - - contentPanel.add(inputPanel); - contentPanel.add(buttonPanel); - - setWidget(contentPanel); - _textBox.setFocus(true); - } - - private void commit() - { - String propName = _textBox.getText().trim(); - - if (commit(propName)) - { - hide(); - } - } - - @Override - public void show() - { - WindowUtil.centerDialog(this); - super.show(); - _textBox.selectAll(); - _textBox.setFocus(true); - WindowUtil.centerDialog(this); - } - - public void show(String defaultValue) - { - _textBox.setText(defaultValue); - show(); - } - - /** Do something useful with the value. If it's not valid, return false and the dialog won't go away. - */ - protected abstract boolean commit(String value); - -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/WebPartPanel.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/WebPartPanel.java deleted file mode 100644 index 4c156181296..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/WebPartPanel.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.safehtml.shared.SafeHtmlUtils; -import com.google.gwt.user.client.ui.Composite; -import com.google.gwt.user.client.ui.HTMLPanel; -import com.google.gwt.user.client.ui.Widget; - -/** - * User: jeckels - * Date: Jun 13, 2007 - */ -public class WebPartPanel extends Composite -{ - public WebPartPanel(String title, Widget contents) - { - final HTMLPanel panel; - final String bodyId = HTMLPanel.createUniqueId(); - - panel = new HTMLPanel( - "
" + - "
" + - "

" + SafeHtmlUtils.htmlEscape(title) + "

" + - "
" + - "
" + - "
" + - "
" - ); - panel.getElement().setAttribute("name", "webpart"); - - panel.add(contents, bodyId); - - initWidget(panel); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/WidgetUpdatable.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/WidgetUpdatable.java deleted file mode 100644 index fe5afd7ea01..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/WidgetUpdatable.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.user.client.ui.Widget; - -/** - * User: jeckels -* Date: Sep 9, 2008 -*/ -public interface WidgetUpdatable -{ - void update(Widget widget); -} \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/WindowUtil.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/WindowUtil.java deleted file mode 100644 index 8b0452a22c1..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/WindowUtil.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.DialogBox; -import com.google.gwt.user.client.ui.Widget; - -/** - * User: Mark Igra - * Date: Feb 26, 2007 - * Time: 9:56:50 PM - */ -public class WindowUtil -{ - /** - * Gets the left scroll position. - * - * @return The left scroll position. - */ - public static native int getScrollLeft() /*-{ - var scrollLeft; - if ($wnd.innerHeight) - { - scrollLeft = $wnd.pageXOffset; - } - else if ($doc.documentElement && $doc.documentElement.scrollLeft) - { - scrollLeft = $doc.documentElement.scrollLeft; - } - else if ($doc.body) - { - scrollLeft = $doc.body.scrollLeft; - } - return scrollLeft; - }-*/; - - /** - * Gets the top scroll position. - * - * @return The top scroll position. - */ - public static native int getScrollTop() /*-{ - var scrollTop; - if ($wnd.innerHeight) - { - scrollTop = $wnd.pageYOffset; - } - else if ($doc.documentElement && $doc.documentElement.scrollTop) - { - scrollTop = $doc.documentElement.scrollTop; - } - else if ($doc.body) - { - scrollTop = $doc.body.scrollTop; - } - return scrollTop; - }-*/; - - public static native void scrollTo(int x, int y) /*-{ - $wnd.scrollTo(x, y); - }-*/; - - - public static void scrollIntoView(Widget w) - { - int widgetTop = w.getAbsoluteTop(); - int widgetLeft = w.getAbsoluteLeft(); - int widgetWidth = w.getOffsetWidth(); - int widgetHeight = w.getOffsetHeight(); - int widgetRight = widgetLeft + widgetWidth; - int widgetBottom = widgetTop + widgetHeight; - - int visTop = getScrollTop(); - int visLeft = getScrollLeft(); - int visWidth = Window.getClientWidth(); - int visHeight = Window.getClientHeight(); - int visRight = visLeft + visWidth; - int visBottom = visTop + visHeight; - - if (widgetTop >= visTop && widgetBottom <= visBottom && - widgetLeft >= visLeft && widgetRight <= visRight) - return; - - - int newTop = visTop; - int newLeft = visLeft; - if (widgetTop < visTop || widgetHeight > visHeight) - newTop = widgetTop; - else if (widgetBottom > visBottom) - newTop = widgetBottom - visHeight; - - if (widgetLeft < visLeft || widgetWidth > visWidth) - newLeft = widgetLeft; - else if (widgetRight > visRight) - newLeft = widgetRight - visWidth; - - scrollTo(newLeft, newTop); - } - - /** - * Navigates to a different URL (leaving this app) using - * window.location=loc - * @param loc - * - * Does NOT work with simple action names like "begin.view" (this breaks on IE). See PropertyUtil.getRelativeURL() - * and PropertyUtil.getContextPath(). - */ - public static void setLocation(String loc) - { - Window.Location.assign(loc); - } - - /** - * Navigates back one page - */ - public static native void back() /*-{ - $wnd.history.back(); - }-*/; - - public static void centerDialog(DialogBox dialogBox) - { - dialogBox.setPopupPosition((Window.getClientWidth() - dialogBox.getOffsetWidth()) / 2 + WindowUtil.getScrollLeft(), (Window.getClientHeight() - dialogBox.getOffsetHeight()) / 2 + WindowUtil.getScrollTop()); - } - - public static native String prompt(String prompt, String defaultValue) /*-{ - return $wnd.prompt(prompt, null == defaultValue ? "" : defaultValue); - }-*/; - -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/domain/CancellationException.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/domain/CancellationException.java deleted file mode 100644 index fc4c4c5b43e..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/domain/CancellationException.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui.domain; -/* - * User: adam - * Date: Dec 31, 2009 - * Time: 12:25:09 PM - */ -public class CancellationException extends RuntimeException -{ -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidget.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidget.java deleted file mode 100644 index 2e00d11051d..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidget.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * 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.api.gwt.client.ui.incubator; - -import com.google.gwt.user.client.Element; - -/** - * An interface that defines the methods required to support automatic resizing - * of the Widget element. - */ -public interface ResizableWidget { - /** - * Get the widget's element. - */ - Element getElement(); - - /** - * Check if this widget is attached to the page. - * - * @return true if the widget is attached to the page - */ - boolean isAttached(); - - /** - * This method is called when the dimensions of the parent element change. - * Subclasses should override this method as needed. - * - * @param width the new client width of the element - * @param height the new client height of the element - */ - void onResize(int width, int height); -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidgetCollection.java b/api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidgetCollection.java deleted file mode 100644 index 22d036c221a..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/ui/incubator/ResizableWidgetCollection.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * 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.api.gwt.client.ui.incubator; - -import com.google.gwt.event.logical.shared.ResizeEvent; -import com.google.gwt.event.logical.shared.ResizeHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.WindowResizeListener; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * A collection of {@link ResizableWidget} that periodically checks the outer - * dimensions of a widget and redraws it as necessary. Every - * {@link ResizableWidgetCollection} uses a timer, so consider the cost when - * adding one. - * - * Typically, a {@link ResizableWidgetCollection} is only needed if you expect - * your widgets to resize based on window resizing or other events. Fixed sized - * Widgets do not need to be added to a {@link ResizableWidgetCollection} as - * they cannot be resized. - */ -public class ResizableWidgetCollection implements WindowResizeListener, - Iterable { - /** - * Information about a widgets size. - */ - private static class ResizableWidgetInfo { - /** - * The current clientHeight. - */ - private int curHeight; - - /** - * The current clientWidth. - */ - private int curWidth; - - /** - * Constructor. - * - * @param widget the widget that will be monitored - */ - public ResizableWidgetInfo(ResizableWidget widget) { - curWidth = DOM.getElementPropertyInt(widget.getElement(), "clientWidth"); - curHeight = DOM.getElementPropertyInt(widget.getElement(), "clientHeight"); - } - - /** - * Set the new dimensions of the widget if they changed. - * - * @param width the new width - * @param height the new height - * @return true if the dimensions have changed - */ - public boolean setClientSize(int width, int height) { - if (width != curWidth || height != curHeight) { - this.curWidth = width; - this.curHeight = height; - return true; - } else { - return false; - } - } - } - - /** - * The default delay between resize checks in milliseconds. - */ - private static final int DEFAULT_RESIZE_CHECK_DELAY = 400; - - /** - * A static {@link ResizableWidgetCollection} that can be used in most cases. - */ - private static ResizableWidgetCollection staticCollection = null; - - /** - * Get the globally accessible {@link ResizableWidgetCollection}. In most - * cases, the global collection can be used for all {@link ResizableWidget}s. - * - * @return the global {@link ResizableWidgetCollection} - */ - public static ResizableWidgetCollection get() { - if (staticCollection == null) { - staticCollection = new ResizableWidgetCollection(); - } - return staticCollection; - } - - /** - * The timer used to periodically compare the dimensions of elements to their - * old dimensions. - */ - private final Timer resizeCheckTimer = new Timer() { - @Override - public void run() { - // Ignore changes that result from window resize events - if (windowHeight != Window.getClientHeight() - || windowWidth != Window.getClientWidth()) { - windowHeight = Window.getClientHeight(); - windowWidth = Window.getClientWidth(); - schedule(resizeCheckDelay); - return; - } - - // Look for elements that have new dimensions - checkWidgetSize(); - - // Start checking again - if (resizeCheckingEnabled) { - schedule(resizeCheckDelay); - } - } - }; - - /** - * A hash map of the resizable widgets this collection is checking. - */ - private final Map widgets = new HashMap<>(); - - /** - * The current window height. - */ - private int windowHeight = 0; - - /** - * The current window width. - */ - private int windowWidth = 0; - - /** - * The hook used to remove the window handler. - */ - private HandlerRegistration windowHandler; - - /** - * The delay between resize checks. - */ - private int resizeCheckDelay = DEFAULT_RESIZE_CHECK_DELAY; - - /** - * A boolean indicating that resize checking should run. - */ - private boolean resizeCheckingEnabled; - - /** - * Create a ResizableWidget. - */ - public ResizableWidgetCollection() { - this(DEFAULT_RESIZE_CHECK_DELAY); - } - - /** - * Constructor. - * - * @param resizeCheckingEnabled false to disable resize checking - */ - public ResizableWidgetCollection(boolean resizeCheckingEnabled) { - this(DEFAULT_RESIZE_CHECK_DELAY, resizeCheckingEnabled); - } - - /** - * Constructor. - * - * @param resizeCheckDelay the delay between checks in milliseconds - */ - public ResizableWidgetCollection(int resizeCheckDelay) { - this(resizeCheckDelay, true); - } - - /** - * Constructor. - */ - protected ResizableWidgetCollection(int resizeCheckDelay, - boolean resizeCheckingEnabled) { - setResizeCheckDelay(resizeCheckDelay); - setResizeCheckingEnabled(resizeCheckingEnabled); - } - - /** - * Add a resizable widget to the collection. - * - * @param widget the resizable widget to add - */ - public void add(ResizableWidget widget) { - widgets.put(widget, new ResizableWidgetInfo(widget)); - } - - /** - * Check to see if any Widgets have been resized and call their handlers - * appropriately. - */ - public void checkWidgetSize() { - for (Map.Entry entry : widgets.entrySet()) { - ResizableWidget widget = entry.getKey(); - ResizableWidgetInfo info = entry.getValue(); - int curWidth = widget.getElement().getPropertyInt("clientWidth"); - int curHeight = widget.getElement().getPropertyInt("clientHeight"); - - // Call the onResize method only if the widget is attached - if (info.setClientSize(curWidth, curHeight)) { - if (curWidth > 0 && curHeight > 0 && widget.isAttached()) { - widget.onResize(curWidth, curHeight); - } - } - } - } - - /** - * Get the delay between resize checks in milliseconds. - * - * @return the resize check delay - */ - public int getResizeCheckDelay() { - return resizeCheckDelay; - } - - /** - * Check whether or not resize checking is enabled. - * - * @return true is resize checking is enabled - */ - public boolean isResizeCheckingEnabled() { - return resizeCheckingEnabled; - } - - @Override - public Iterator iterator() { - return widgets.keySet().iterator(); - } - - /** - * Called when the browser window is resized. - * - * @param width the width of the window's client area. - * @param height the height of the window's client area. - */ - @Override - @Deprecated - public void onWindowResized(int width, int height) { - checkWidgetSize(); - } - - /** - * Remove a {@link ResizableWidget} from the collection. - * - * @param widget the widget to remove - */ - public void remove(ResizableWidget widget) { - widgets.remove(widget); - } - - /** - * Set the delay between resize checks in milliseconds. - * - * @param resizeCheckDelay the new delay - */ - public void setResizeCheckDelay(int resizeCheckDelay) { - this.resizeCheckDelay = resizeCheckDelay; - } - - /** - * Set whether or not resize checking is enabled. If disabled, elements will - * still be resized on window events, but the timer will not check their - * dimensions periodically. - * - * @param enabled true to enable the resize checking timer - */ - public void setResizeCheckingEnabled(boolean enabled) { - if (enabled && !resizeCheckingEnabled) { - resizeCheckingEnabled = true; - if (windowHandler == null) { - windowHandler = Window.addResizeHandler(new ResizeHandler() { - @Override - public void onResize(ResizeEvent event) { - onWindowResized(event.getWidth(), event.getHeight()); - } - }); - } - resizeCheckTimer.schedule(resizeCheckDelay); - } else if (!enabled && resizeCheckingEnabled) { - resizeCheckingEnabled = false; - if (windowHandler != null) { - windowHandler.removeHandler(); - windowHandler = null; - } - resizeCheckTimer.cancel(); - } - } - - /** - * Inform the {@link ResizableWidgetCollection} that the size of a widget has - * changed and already been redrawn. This will prevent the widget from being - * redrawn on the next loop. - * - * @param widget the widget's size that changed - */ - public void updateWidgetSize(ResizableWidget widget) { - if (!widget.isAttached()) { - return; - } - - ResizableWidgetInfo info = widgets.get(widget); - if (info != null) { - int curWidth = widget.getElement().getPropertyInt("clientWidth"); - int curHeight = widget.getElement().getPropertyInt("clientHeight"); - info.setClientSize(curWidth, curHeight); - } - } - -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/BooleanProperty.java b/api/gwtsrc/org/labkey/api/gwt/client/util/BooleanProperty.java deleted file mode 100644 index 3c68ed489af..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/BooleanProperty.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import com.google.gwt.user.client.rpc.IsSerializable; - -/** - * User: Matthew - * Date: Apr 25, 2007 - * Time: 4:43:06 PM - */ -public class BooleanProperty implements IPropertyWrapper, IsSerializable -{ - Boolean b; - - public BooleanProperty() - { - b = null; - } - - public BooleanProperty(boolean b) - { - setBool(b); - } - - public BooleanProperty(Boolean b) - { - set(b); - } - - @Override - public Object get() - { - return b; - } - - @Override - public void set(Object o) - { - if (o instanceof String) - b = Boolean.valueOf((String)o); - else if (o instanceof Boolean) - b = (Boolean)o; - else - throw new IllegalArgumentException(String.valueOf(o)); - } - - public Boolean getBoolean() - { - return b; - } - - public void setBool(boolean b) - { - this.b = Boolean.valueOf(b); - } - - public boolean booleanValue() - { - return b != null && b.booleanValue(); - } - - @Deprecated - public boolean getBool() - { - return b != null && b.booleanValue(); - } - - public String toString() - { - return String.valueOf(b); - } - - @Override - public int hashCode() - { - return null==b ? 0 : b.hashCode(); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/ColorGenerator.java b/api/gwtsrc/org/labkey/api/gwt/client/util/ColorGenerator.java deleted file mode 100644 index bb159f80d30..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/ColorGenerator.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -/** - * User: brittp -* Date: Feb 5, 2007 -* Time: 6:32:56 PM -*/ -public class ColorGenerator -{ - private static final String[] SUBCOLOR_STRINGS = new String[]{"66", "AA", "EE", "88", "CC"}; - private int _r = 0; - private int _g = 1; - private int _b = 2; - - public ColorGenerator() - { - - } - - public ColorGenerator(int startColor) - { - for (int i = 0; i < startColor; i++) - advance(); - } - - public String next() - { - advance(); - return SUBCOLOR_STRINGS[_r % SUBCOLOR_STRINGS.length] + - SUBCOLOR_STRINGS[_g % SUBCOLOR_STRINGS.length] + - SUBCOLOR_STRINGS[_b % SUBCOLOR_STRINGS.length]; - } - - private void advance() - { - do - { - _r = (_r + 1) % SUBCOLOR_STRINGS.length; - if (_r % 2 == 0) - { - _g = (_g + 1) % SUBCOLOR_STRINGS.length; - if (_g % 3 == 0) - _b = (_b + 1) % SUBCOLOR_STRINGS.length; - } - } - while (_r == _g && _g == _b); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/ErrorDialogAsyncCallback.java b/api/gwtsrc/org/labkey/api/gwt/client/util/ErrorDialogAsyncCallback.java deleted file mode 100644 index a5ba4f7b7d8..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/ErrorDialogAsyncCallback.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.rpc.StatusCodeException; - -/** - * User: jeckels - * Date: May 28, 2010 - */ -@SuppressWarnings("PatternVariableCanBeUsed") -public abstract class ErrorDialogAsyncCallback implements AsyncCallback -{ - private final String _description; - - public ErrorDialogAsyncCallback() - { - this(null); - } - - protected ErrorDialogAsyncCallback(String description) - { - _description = description; - } - - public static void showDialog(Throwable caught) - { - showDialog(caught, null); - } - - public static void showDialog(Throwable caught, String message) - { - ErrorDialogAsyncCallback dialog = new ErrorDialogAsyncCallback(message) - { - @Override - public void onSuccess(Object result) - { - } - }; - dialog.onFailure(caught); - } - - @Override - public final void onFailure(Throwable caught) - { - String message = null; - if (caught instanceof StatusCodeException) - { - StatusCodeException statusCodeException = (StatusCodeException)caught; - switch (statusCodeException.getStatusCode()) - { - case 0: - // Indicates the request was cancelled because the user navigated to another page - // Don't bother showing any dialog at all - return; - case 401: - message = "You do not have permission to perform this operation. Your session may have expired."; - break; - case 404: - message = "Not found."; - break; - case 500: - message = "The server encountered an error"; - if(statusCodeException.getMessage() != null) - message += ": " + statusCodeException.getMessage(); - break; - default: - message = "There was an error"; - if(statusCodeException.getMessage() != null) - message += ": " + statusCodeException.getMessage(); - message += " (" + statusCodeException.getStatusCode() + ")"; - break; - } - } - else - { - // We really want to be able to check for instances of - // com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure - // and treat them like StatusCodeExceptions, but it's a private class so just do a string check for the - // abort case - if ("HTTP download failed with status 0".equals(caught.getMessage())) - { - return; - } - } - if (message == null) - { - message = caught.getMessage() == null || caught.getMessage().trim().isEmpty() ? caught.toString() : caught.getMessage(); - } - if (_description != null) - { - message = _description + ": " + message; - } - reportFailure(message, caught); - handleFailure(message, caught); - } - - /** Shows the error message to the user in a dialog */ - protected void reportFailure(String message, Throwable caught) - { - Window.alert(message); - } - - /** Subclasses can override to provide additional error handling */ - protected void handleFailure(String message, Throwable caught) - { - - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/IPropertyWrapper.java b/api/gwtsrc/org/labkey/api/gwt/client/util/IPropertyWrapper.java deleted file mode 100644 index 7e75458d846..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/IPropertyWrapper.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -/** - * User: Matthew - * Date: Apr 25, 2007 - * Time: 4:42:14 PM - */ -public interface IPropertyWrapper -{ - Object get(); - void set(Object o); - int hashCode(); -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/IntegerProperty.java b/api/gwtsrc/org/labkey/api/gwt/client/util/IntegerProperty.java deleted file mode 100644 index 6c01aea6fbb..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/IntegerProperty.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import com.google.gwt.user.client.rpc.IsSerializable; - -/** - * User: Matthew - * Date: Apr 25, 2007 - * Time: 4:43:06 PM - */ -public class IntegerProperty implements IPropertyWrapper, IsSerializable -{ - Integer i; - - public IntegerProperty() - { - i = null; - } - - public IntegerProperty(int i) - { - setInt(i); - } - - public IntegerProperty(Integer i) - { - set(i); - } - - @Override - public Object get() - { - return i; - } - - @Override - public void set(Object o) - { - i = (Integer)o; - } - - public Integer getInteger() - { - return i; - } - - public void setInt(int i) - { - // this.i = Integer.valueOf(i); - this.i = Integer.valueOf(i); - } - - public int intValue() - { - return null==i ? 0 : i.intValue(); - } - - @Deprecated - public int getInt() - { - return i.intValue(); - } - - public String toString() - { - return String.valueOf(i); - } - - @Override - public int hashCode() - { - return null==i ? 0 : i.hashCode(); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/PropertyUtil.java b/api/gwtsrc/org/labkey/api/gwt/client/util/PropertyUtil.java deleted file mode 100644 index 186080c2fd4..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/PropertyUtil.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import com.google.gwt.http.client.URL; - -/** - * User: brittp - * Date: Feb 2, 2007 - * Time: 9:49:20 AM - */ -public class PropertyUtil -{ - public static native String getServerProperty(String propName) - /*-{ - var value = $wnd.LABKEY.GWTProperties[propName]; - if (!value) - return null; - else - return value; - }-*/; - - public static native String getCurrentURL() - /*-{ - var location = $wnd.location + ''; - // Safari returns current location with spaces, rather than %20. - // The "g" parameter creates a "global" regex, which will replace all instances. - location = location.replace(new RegExp(' ', 'g'), '%20'); - location = location.replace(new RegExp('\\+', 'g'), '%20'); - return location; - }-*/; - - public static String getContainerPath() - { - return getServerProperty("container"); - } - - // Is the current container a project? - public static boolean isProject() - { - String path = getContainerPath(); - // Currently in a project if container path is not the root and has no slashes after the first character - return !"/".equals(path) && (-1 == getContainerPath().indexOf('/', 1)); - } - - public static String getController() - { - return getServerProperty("controller"); - } - - public static String getAction() - { - return getServerProperty("action"); - } - - public static String getQueryString() - { - return getServerProperty("queryString"); - } - - public static String getReturnURL() - { - return getServerProperty("returnUrl"); - } - - public static String getRedirectURL() - { - return getServerProperty("redirectUrl"); - } - - public static String getCancelURL() - { - return getServerProperty("cancelUrl"); - } - - public static String getContextPath() - { - String ret = getServerProperty("contextPath"); - if (ret == null) - return ""; - return ret; - } - - // @Nullable (commented because GWT doesn't recognize) - public static String getMaxAllowedPhi() - { - return getServerProperty("maxAllowedPhi"); - } - - public static String getRelativeURL(String action) - { - return getRelativeURL(action, getController()); - } - - public static String getRelativeURL(String action, String pageFlow) - { - String[] pathParts = PropertyUtil.getContainerPath().split("/"); - StringBuilder encodedPath = new StringBuilder("/"); - for (String pathPart : pathParts) - { - if (!pathPart.isEmpty()) - { - //issue 14006: changed encodeComponent to encodePathSegment, b/c the former will convert spaces to '+' - String part = URL.encodePathSegment(pathPart); - encodedPath.append(part).append("/"); - } - } - if (!action.contains(".")) - action = action + ".view"; - return getContextPath() + encodedPath + pageFlow + "-" + action; - } - - /** @return true if the two arguments are both null, or are .equals() */ - public static boolean nullSafeEquals(Object o1, Object o2) - { - if (o1 == o2) - { - return true; - } - - return o1 != null && o1.equals(o2); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/ServiceUtil.java b/api/gwtsrc/org/labkey/api/gwt/client/util/ServiceUtil.java deleted file mode 100644 index f037245c112..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/ServiceUtil.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import com.google.gwt.http.client.RequestBuilder; -import com.google.gwt.http.client.URL; -import com.google.gwt.user.client.rpc.RpcRequestBuilder; -import com.google.gwt.user.client.rpc.ServiceDefTarget; -import com.google.gwt.user.client.ui.RootPanel; - -import java.util.Collections; -import java.util.Map; - -/** - * User: brittp - * Date: Feb 2, 2007 - * Time: 11:35:54 AM - */ -public class ServiceUtil -{ - public static Object configureEndpoint(Object remoteService, String actionName) - { - return configureEndpoint(remoteService, actionName, null); - } - - public static Object configureEndpoint(Object remoteService, String actionName, String controllerName) - { - return configureEndpoint(remoteService, actionName, controllerName, Collections.emptyMap()); - } - - public static Object configureEndpoint(Object remoteService, String actionName, String controllerName, Map urlParams) - { - ServiceDefTarget endpoint = (ServiceDefTarget) remoteService; - - String url; - if (controllerName == null) - { - url = PropertyUtil.getRelativeURL(actionName); - } - else - { - url = PropertyUtil.getRelativeURL(actionName, controllerName); - } - String separator = "?"; - for (String key : urlParams.keySet()) - { - url += separator; - separator = "&"; - //issue 14006: changed encodeComponent to encodePathSegment, b/c the former will convert spaces to '+' - url += URL.encodePathSegment(key) + "=" + URL.encodePathSegment(urlParams.get(key)); - } - endpoint.setServiceEntryPoint(url); - - RpcRequestBuilder rpc = new RpcRequestBuilder() - { - @Override - protected void doFinish(RequestBuilder rb) - { - rb.setHeader("X-LABKEY-CSRF",getCsrfToken()); - super.doFinish(rb); - } - }; - endpoint.setRpcRequestBuilder(rpc); - - return remoteService; - } - - public static RootPanel findRootPanel(String classname) - { - int index = classname.indexOf(".client."); - if (index != -1) - { - classname = classname.substring(0, index) + classname.substring(index + ".client.".length() - 1); - } - return RootPanel.get(classname + "-Root"); - } - - public static native String getCsrfToken() /*-{ - return $wnd.LABKEY.CSRF; - }-*/; -} \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/StringProperty.java b/api/gwtsrc/org/labkey/api/gwt/client/util/StringProperty.java deleted file mode 100644 index cbafe7986cb..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/StringProperty.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import com.google.gwt.user.client.rpc.IsSerializable; - -/** - * User: Matthew - * Date: Apr 25, 2007 - * Time: 4:44:09 PM - */ -public class StringProperty implements IPropertyWrapper, IsSerializable -{ - String s; - - public StringProperty() - { - s = null; - } - - public StringProperty(String s) - { - this.s = s; - } - - @Override - public Object get() - { - return s; - } - - @Override - public void set(Object o) - { - s = (String)o; - } - - public String getString() - { - return s; - } - - public String toString() - { - return s; - } - - @Override - public int hashCode() - { - return null==s ? 0 : s.hashCode(); - } -} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/util/StringUtils.java b/api/gwtsrc/org/labkey/api/gwt/client/util/StringUtils.java deleted file mode 100644 index 1af3b035e6e..00000000000 --- a/api/gwtsrc/org/labkey/api/gwt/client/util/StringUtils.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.util; - -import java.util.List; - -/** - * GWT client code is the only place we should be using these methods; use org.apache.commons.lang3.StringUtils instead. - * This class will be removed in a future release of LabKey. - * - * User: Mark Igra - * Date: Feb 15, 2007 - * Time: 10:20:35 PM - */ - -@Deprecated -public class StringUtils -{ - public static boolean isEmpty(String str) - { - return null == str || str.isEmpty(); - } - - public static String trimToNull(String str) - { - if (null == str) - return str; - - str = str.trim(); - return str.isEmpty() ? null : str; - } - - public static String trimToEmpty(String str) - { - if (null == str) - return ""; - - return str.trim(); - } - - public static boolean equals(String a, String b) - { - return (null == a && null == b) || (null != a && a.equals(b)); - } - - public static String join(List l, String join) - { - StringBuffer sb = new StringBuffer(); - String sep = ""; - for (int i = 0; i < l.size(); i++) - { - sb.append(sep); - sb.append(l.get(i).toString()); - sep = join; - } - - return sb.toString(); - } - - static public String filter(String s, boolean encodeSpace) - { - if (null == s || s.isEmpty()) - return ""; - - int len = s.length(); - int i = 0; - - StringBuffer sb = new StringBuffer(2 * len); - sb.append(s.substring(0, i)); - boolean newline = false; - - for (; i < len; ++i) - { - char c = s.charAt(i); - - //Character.isWhitespace() not supported in GWT - if (c != '\t' && c != ' ') - newline = false; - if ('\r' == c || '\n' == c) - newline = true; - - switch (c) - { - case '&': - sb.append("&"); - break; - case '"': - sb.append("""); - break; - case '<': - sb.append("<"); - break; - case '>': - sb.append(">"); - break; - case '\n': - if (encodeSpace) - sb.append("
\n"); - else - sb.append(c); - break; - case '\t': - if (!encodeSpace) - sb.append(c); - else if (newline) - sb.append("    "); - else - sb.append("    "); - break; - case ' ': - if (encodeSpace && newline) - sb.append(" "); - else - sb.append(' '); - break; - default: - sb.append(c); - break; - } - } - - return sb.toString(); - } - - - public static String filter(Object o) - { - return filter(o == null ? null : o.toString()); - } - - /** - * HTML encode a string - */ - public static String filter(String s) - { - return filter(s, false); - } -} diff --git a/api/src/org/labkey/api/action/GWTServiceAction.java b/api/src/org/labkey/api/action/GWTServiceAction.java deleted file mode 100644 index 5c94ad4de69..00000000000 --- a/api/src/org/labkey/api/action/GWTServiceAction.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2008-2018 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.api.action; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.jetbrains.annotations.NotNull; -import org.labkey.api.gwt.server.BaseRemoteService; -import org.labkey.api.usageMetrics.SimpleMetricsService; -import org.labkey.api.view.UnauthorizedException; -import org.springframework.validation.Errors; -import org.springframework.web.servlet.ModelAndView; - -public abstract class GWTServiceAction extends BaseViewAction -{ - protected GWTServiceAction() - { - super(Object.class); - setUnauthorizedType(UnauthorizedException.Type.sendUnauthorized); - } - - @Override - public ModelAndView handleRequest(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) - { - // Use Core as the catch-all to consolidate reporting, even though GWT uses are distributed across modules - SimpleMetricsService.get().increment("Core", "GWTService", getClass().getSimpleName()); - - BaseRemoteService service = createService(); - if (!isPost()) - { - // GWT service requests must be POSTs - httpServletResponse.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); - httpServletResponse.setHeader("Allow", "POST"); - } - else - { - service.doPost(httpServletRequest, httpServletResponse); - } - return null; - } - - protected abstract BaseRemoteService createService(); - - // methods we ignore, but have to implement since we extend BaseViewAction - @Override - protected String getCommandClassMethodName() - { - throw new UnsupportedOperationException(); - } - - @Override - public void validate(Object o, Errors errors) - { - throw new UnsupportedOperationException(); - } - - @Override - public ModelAndView handleRequest() - { - throw new UnsupportedOperationException(); - } -} diff --git a/api/src/org/labkey/api/assay/AssayQCService.java b/api/src/org/labkey/api/assay/AssayQCService.java index 4509603f535..11cc4834eb2 100644 --- a/api/src/org/labkey/api/assay/AssayQCService.java +++ b/api/src/org/labkey/api/assay/AssayQCService.java @@ -15,6 +15,7 @@ */ package org.labkey.api.assay; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; import org.labkey.api.data.SQLFragment; @@ -25,7 +26,6 @@ import org.labkey.api.security.User; import org.labkey.api.view.HttpView; -import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -55,7 +55,7 @@ static AssayQCService getProvider() { if (!_providers.isEmpty()) { - return _providers.get(0); + return _providers.getFirst(); } return _defaultProvider; } @@ -114,13 +114,13 @@ static AssayQCService getProvider() void setDefaultDataImportState(Container container, DataState state); /** - * Gets/sets whether or not a blank state should be interpreted as public data or not + * Gets/sets whether a blank state should be interpreted as public data or not */ boolean isBlankQCStatePublic(Container container); void setIsBlankQCStatePublic(Container container, boolean isPublic); /** - * Gets/sets whether or not a comment is required on a QC State change + * Gets/sets whether a comment is required on a QC State change */ boolean isRequireCommentOnQCStateChange(Container container); void setRequireCommentOnQCStateChange(Container container, boolean requireCommentOnQCStateChange); @@ -129,7 +129,7 @@ static AssayQCService getProvider() * Returns the warnings view if the specified run has a current QC state associated with it */ @Nullable - HttpView getAssayReImportWarningView(Container container, ExpRun run) throws ExperimentException; + HttpView getAssayReImportWarningView(Container container, ExpRun run) throws ExperimentException; class DefaultQCService implements AssayQCService { @@ -199,7 +199,7 @@ public void setDefaultDataImportState(Container container, DataState state) } @Override - public @Nullable HttpView getAssayReImportWarningView(Container container, ExpRun run) throws ExperimentException + public @Nullable HttpView getAssayReImportWarningView(Container container, ExpRun run) { return null; } diff --git a/api/src/org/labkey/api/data/RuntimeSQLException.java b/api/src/org/labkey/api/data/RuntimeSQLException.java index 0f38a6b5aba..1473f1dd564 100644 --- a/api/src/org/labkey/api/data/RuntimeSQLException.java +++ b/api/src/org/labkey/api/data/RuntimeSQLException.java @@ -16,7 +16,6 @@ package org.labkey.api.data; -import com.google.gwt.user.client.rpc.IsSerializable; import org.jetbrains.annotations.NotNull; import org.labkey.api.data.dialect.SqlDialect; @@ -31,7 +30,7 @@ * User: mbellew * Date: Mar 23, 2005 */ -public class RuntimeSQLException extends RuntimeException implements Serializable, IsSerializable +public class RuntimeSQLException extends RuntimeException implements Serializable { // don't want to use cause, I want to impersonate the cause SQLException sqlx; diff --git a/api/src/org/labkey/api/dataiterator/CopyConfig.java b/api/src/org/labkey/api/dataiterator/CopyConfig.java index 90db56c9df9..26b1c4c0275 100644 --- a/api/src/org/labkey/api/dataiterator/CopyConfig.java +++ b/api/src/org/labkey/api/dataiterator/CopyConfig.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; +import org.jetbrains.annotations.NotNull; import org.labkey.api.data.CompareType; import org.labkey.api.data.SimpleFilter; import org.labkey.api.query.FieldKey; @@ -24,7 +25,6 @@ import org.labkey.api.util.ConfigurationException; import org.labkey.remoteapi.query.Filter; -import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; diff --git a/api/src/org/labkey/api/exp/OntologyManager.java b/api/src/org/labkey/api/exp/OntologyManager.java index 469b025eecc..f509e343236 100644 --- a/api/src/org/labkey/api/exp/OntologyManager.java +++ b/api/src/org/labkey/api/exp/OntologyManager.java @@ -48,7 +48,6 @@ import org.labkey.api.exp.property.PropertyService; import org.labkey.api.exp.property.SystemProperty; import org.labkey.api.exp.property.ValidatorContext; -import org.labkey.api.gwt.client.ui.domain.CancellationException; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.FieldKey; import org.labkey.api.query.PropertyValidationError; @@ -377,7 +376,7 @@ public static void insertTabDelimited(Container c, Map map = rows.getMap(); // TODO: hack -- should exit and return cancellation status instead of throwing if (Thread.currentThread().isInterrupted()) - throw new CancellationException(); + throw new RuntimeException(); assert before.start(); @@ -597,7 +596,7 @@ private static void saveTabDelimited(TableInfo table, // TODO: hack -- should exit and return cancellation status instead of throwing if (Thread.currentThread().isInterrupted()) - throw new CancellationException(); + throw new RuntimeException(); parameterMap.clearParameters(); diff --git a/api/gwtsrc/org/labkey/api/gwt/client/AuditBehaviorType.java b/api/src/org/labkey/api/gwt/client/AuditBehaviorType.java similarity index 96% rename from api/gwtsrc/org/labkey/api/gwt/client/AuditBehaviorType.java rename to api/src/org/labkey/api/gwt/client/AuditBehaviorType.java index e369374e6d1..0c8b2bd758d 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/AuditBehaviorType.java +++ b/api/src/org/labkey/api/gwt/client/AuditBehaviorType.java @@ -1,57 +1,57 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * User: klum - * Date: 10/17/12 - */ -public enum AuditBehaviorType -{ - NONE("None", 1), - DETAILED("Detailed", 3), - SUMMARY("Summary", 2); - - private final String _label; - private final int _priority; - - AuditBehaviorType(String label, int priority) - { - _label = label; - _priority = priority; - } - - public String getLabel() - { - return _label; - } - - public int getPriority() - { - return _priority; - } - - public static AuditBehaviorType getEffectiveAuditLevel(@Nullable AuditBehaviorType apiOverride, @NotNull AuditBehaviorType tableAuditBehaviorType) - { - if (apiOverride == null || apiOverride._priority < tableAuditBehaviorType._priority) - return tableAuditBehaviorType; - - return apiOverride; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * User: klum + * Date: 10/17/12 + */ +public enum AuditBehaviorType +{ + NONE("None", 1), + DETAILED("Detailed", 3), + SUMMARY("Summary", 2); + + private final String _label; + private final int _priority; + + AuditBehaviorType(String label, int priority) + { + _label = label; + _priority = priority; + } + + public String getLabel() + { + return _label; + } + + public int getPriority() + { + return _priority; + } + + public static AuditBehaviorType getEffectiveAuditLevel(@Nullable AuditBehaviorType apiOverride, @NotNull AuditBehaviorType tableAuditBehaviorType) + { + if (apiOverride == null || apiOverride._priority < tableAuditBehaviorType._priority) + return tableAuditBehaviorType; + + return apiOverride; + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/DefaultScaleType.java b/api/src/org/labkey/api/gwt/client/DefaultScaleType.java similarity index 96% rename from api/gwtsrc/org/labkey/api/gwt/client/DefaultScaleType.java rename to api/src/org/labkey/api/gwt/client/DefaultScaleType.java index 187ef2755ab..929055e9d2b 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/DefaultScaleType.java +++ b/api/src/org/labkey/api/gwt/client/DefaultScaleType.java @@ -1,38 +1,38 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client; - -/** - * User: cnathe - * Date: 3/18/14 - */ -public enum DefaultScaleType -{ - LINEAR("Linear"), - LOG("Log"); - - private final String _label; - - DefaultScaleType(String label) - { - _label = label; - } - - public String getLabel() - { - return _label; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client; + +/** + * User: cnathe + * Date: 3/18/14 + */ +public enum DefaultScaleType +{ + LINEAR("Linear"), + LOG("Log"); + + private final String _label; + + DefaultScaleType(String label) + { + _label = label; + } + + public String getLabel() + { + return _label; + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/DefaultValueType.java b/api/src/org/labkey/api/gwt/client/DefaultValueType.java similarity index 90% rename from api/gwtsrc/org/labkey/api/gwt/client/DefaultValueType.java rename to api/src/org/labkey/api/gwt/client/DefaultValueType.java index 6e033567f14..259c59bc19a 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/DefaultValueType.java +++ b/api/src/org/labkey/api/gwt/client/DefaultValueType.java @@ -1,49 +1,47 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client; - -import com.google.gwt.user.client.rpc.IsSerializable; - -import java.io.Serializable; - -public enum DefaultValueType implements Serializable, IsSerializable -{ - FIXED_EDITABLE("Editable default", "An editable default value will be entered for the user. The default value will be the same for every user for every insert."), - FIXED_NON_EDITABLE("Fixed value", "Fixed values cannot be edited by the user. This option is used to save fixed data with each inserted data row."), - LAST_ENTERED("Last entered", "An editable default value will be entered for the user's first use of the form. During subsequent inserts, the user will see their last entered value as the default."); - - private String _label; - private String _helpText; - - // Needed for GWT serialization to work correctly, at least in dev mode - DefaultValueType() {} - - DefaultValueType(String label, String helpText) - { - _label = label; - _helpText = helpText; - } - - public String getLabel() - { - return _label; - } - - public String getHelpText() - { - return _helpText; - } +/* + * Copyright (c) 2018-2019 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.api.gwt.client; + +import java.io.Serializable; + +public enum DefaultValueType implements Serializable +{ + FIXED_EDITABLE("Editable default", "An editable default value will be entered for the user. The default value will be the same for every user for every insert."), + FIXED_NON_EDITABLE("Fixed value", "Fixed values cannot be edited by the user. This option is used to save fixed data with each inserted data row."), + LAST_ENTERED("Last entered", "An editable default value will be entered for the user's first use of the form. During subsequent inserts, the user will see their last entered value as the default."); + + private String _label; + private String _helpText; + + // Needed for GWT serialization to work correctly, at least in dev mode + DefaultValueType() {} + + DefaultValueType(String label, String helpText) + { + _label = label; + _helpText = helpText; + } + + public String getLabel() + { + return _label; + } + + public String getHelpText() + { + return _helpText; + } } \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/FacetingBehaviorType.java b/api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java similarity index 96% rename from api/gwtsrc/org/labkey/api/gwt/client/FacetingBehaviorType.java rename to api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java index e8884d38c47..5994d34761c 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/FacetingBehaviorType.java +++ b/api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java @@ -1,69 +1,69 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client; - -/** - * User: klum - * Date: Apr 11, 2012 - */ -public enum FacetingBehaviorType -{ - AUTOMATIC("Auto detect", - "The column will be examined to determine if the faceted filter panel can be shown. A column is a faceting candidate if it is a " + - "lookup, dimension or of type: (Boolean, Integer, Text, DateTime)."), - ALWAYS_ON("On", - "The faceted filter panel will be shown by default."), - ALWAYS_OFF("Off", - "The faceted filter panel will not be shown by default."); - - private final String _label; - private final String _helpText; - - private static String _helpPopupHtml; - - FacetingBehaviorType(String label, String helpText) - { - _label = label; - _helpText = helpText; - } - - public String getLabel() - { - return _label; - } - - public String getHelpText() - { - return _helpText; - } - - public static String getHelpPopupHtml() - { - if (_helpPopupHtml == null) - { - StringBuilder helpString = new StringBuilder(); - for (int i = 0; i < values().length; i++) - { - FacetingBehaviorType type = values()[i]; - helpString.append("").append(type.getLabel()).append(": ").append(type.getHelpText()); - if (i < values().length - 1) - helpString.append("

"); - } - _helpPopupHtml = helpString.toString(); - } - return _helpPopupHtml; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client; + +/** + * User: klum + * Date: Apr 11, 2012 + */ +public enum FacetingBehaviorType +{ + AUTOMATIC("Auto detect", + "The column will be examined to determine if the faceted filter panel can be shown. A column is a faceting candidate if it is a " + + "lookup, dimension or of type: (Boolean, Integer, Text, DateTime)."), + ALWAYS_ON("On", + "The faceted filter panel will be shown by default."), + ALWAYS_OFF("Off", + "The faceted filter panel will not be shown by default."); + + private final String _label; + private final String _helpText; + + private static String _helpPopupHtml; + + FacetingBehaviorType(String label, String helpText) + { + _label = label; + _helpText = helpText; + } + + public String getLabel() + { + return _label; + } + + public String getHelpText() + { + return _helpText; + } + + public static String getHelpPopupHtml() + { + if (_helpPopupHtml == null) + { + StringBuilder helpString = new StringBuilder(); + for (int i = 0; i < values().length; i++) + { + FacetingBehaviorType type = values()[i]; + helpString.append("").append(type.getLabel()).append(": ").append(type.getHelpText()); + if (i < values().length - 1) + helpString.append("

"); + } + _helpPopupHtml = helpString.toString(); + } + return _helpPopupHtml; + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/LockedPropertyType.java b/api/src/org/labkey/api/gwt/client/LockedPropertyType.java similarity index 100% rename from api/gwtsrc/org/labkey/api/gwt/client/LockedPropertyType.java rename to api/src/org/labkey/api/gwt/client/LockedPropertyType.java diff --git a/api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java b/api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java similarity index 85% rename from api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java rename to api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java index 88642a23a37..16dc20bf5ca 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java +++ b/api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java @@ -1,49 +1,48 @@ -/* - * Copyright (c) 2019 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.api.gwt.client.assay.model; - - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.labkey.api.gwt.client.util.StringProperty; - -/** - * Configures the fields that are not returned when serializing a GWTPropertyDescriptor. - * Ideally we would just add the @JsonIgnore annotations to GWTPropertyDescriptor directly, - * but the GWT compiler would need to have jackson on the classpath which isn't - * necessary. - */ -@JsonIgnoreProperties({ - "setMeasure", - "setDimension", - "setExcludeFromShifting", - "lookupDescription", - "fileType", - "updatedField", - "newField", - "renderUpdate" -}) -public abstract class GWTPropertyDescriptorMixin -{ - GWTPropertyDescriptorMixin(@JsonProperty("PHI") StringProperty phi, @JsonProperty("URL") StringProperty url, @JsonProperty("URLTarget") StringProperty URLTarget) - { } - @JsonProperty("PHI") - abstract void setPHI(String phi); // rename property on deserialize - @JsonProperty("URL") - abstract void setURL(String url); // rename property on deserialize - @JsonProperty("URLTarget") - abstract void setURLTarget(String urlTarget); // rename property on deserialize -} +/* + * Copyright (c) 2019 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.api.gwt.client.assay.model; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configures the fields that are not returned when serializing a GWTPropertyDescriptor. + * Ideally we would just add the @JsonIgnore annotations to GWTPropertyDescriptor directly, + * but the GWT compiler would need to have jackson on the classpath which isn't + * necessary. + */ +@JsonIgnoreProperties({ + "setMeasure", + "setDimension", + "setExcludeFromShifting", + "lookupDescription", + "fileType", + "updatedField", + "newField", + "renderUpdate" +}) +public abstract class GWTPropertyDescriptorMixin +{ + GWTPropertyDescriptorMixin(@JsonProperty("PHI") String phi, @JsonProperty("URL") String url, @JsonProperty("URLTarget") String URLTarget) + { } + @JsonProperty("PHI") + abstract void setPHI(String phi); // rename property on deserialize + @JsonProperty("URL") + abstract void setURL(String url); // rename property on deserialize + @JsonProperty("URLTarget") + abstract void setURLTarget(String urlTarget); // rename property on deserialize +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTProtocol.java b/api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java similarity index 95% rename from api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTProtocol.java rename to api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java index 6d79916061a..50ce1dd6a33 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTProtocol.java +++ b/api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java @@ -1,477 +1,476 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.assay.model; - -import com.google.gwt.user.client.rpc.IsSerializable; -import org.apache.commons.lang3.StringUtils; -import org.labkey.api.gwt.client.model.GWTContainer; -import org.labkey.api.gwt.client.model.GWTDomain; -import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * User: brittp - * Date: Jun 20, 2007 - * Time: 2:29:22 PM - */ -public class GWTProtocol implements IsSerializable -{ - private Long _protocolId; - private String _name; - private String _description; - private String _providerName; - private String domainKindName; - - private Map _protocolParameters; - - private List> _domains; - - private List _availablePlateTemplates; - private Map _availableMetadataInputFormats = new HashMap<>(); - private String _metadataInputFormatHelp; - - private String _selectedPlateTemplate; - private String _selectedMetadataInputFormat; - - /** Scripts defined in the module itself, associated with the assay provider */ - private List _moduleTransformScripts = new ArrayList<>(); - /** Scripts defined in the assay definition */ - private final List> _protocolTransformScripts = new ArrayList<>(); - - private List _availableDetectionMethods; - private String _selectedDetectionMethod; - - private boolean _allowBackgroundUpload; - private boolean _allowEditableResults; - private boolean _allowQCStates; - private boolean _allowTransformationScript; - private boolean _allowPlateMetadata; - - // UNDONE: update 'autoCopy' to 'autoLink' for the two members below and align ui-components and tests accordingly - private GWTContainer _autoCopyTargetContainer; - private String _autoCopyTargetContainerId; - private String _autoLinkCategory; - private boolean _saveScriptFiles; - private boolean _editableRuns; - private boolean _editableResults; - private boolean _backgroundUpload; - private boolean _qcEnabled; - private boolean _plateMetadata; - private String _status; - private List _excludedContainerIds; - private String _auditUserComment; - - public GWTProtocol() - { - } - - public Long getProtocolId() - { - return _protocolId; - } - - public void setProtocolId(Long protocolId) - { - _protocolId = protocolId; - } - - - public List> getDomains() - { - return _domains; - } - - public void setDomains(List> domains) - { - _domains = domains; - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public String getDescription() - { - return _description; - } - - public void setDescription(String description) - { - _description = description; - } - - public Map getProtocolParameters() - { - return _protocolParameters; - } - - public void setProtocolParameters(Map protocolParameters) - { - _protocolParameters = protocolParameters; - } - - public String getProviderName() - { - return _providerName; - } - - public void setProviderName(String providerName) - { - _providerName = providerName; - } - - public String getDomainKindName() - { - return domainKindName; - } - - public void setDomainKindName(String domainKindName) - { - this.domainKindName = domainKindName; - } - - public List getAvailablePlateTemplates() - { - return _availablePlateTemplates; - } - - public void setAvailablePlateTemplates(List availablePlateTemplates) - { - _availablePlateTemplates = availablePlateTemplates; - } - - public String getSelectedPlateTemplate() - { - return _selectedPlateTemplate; - } - - public void setSelectedPlateTemplate(String selectedPlateTemplate) - { - _selectedPlateTemplate = selectedPlateTemplate; - } - - public List getModuleTransformScripts() - { - return _moduleTransformScripts; - } - - public void setModuleTransformScripts(List moduleTransformScripts) - { - _moduleTransformScripts = moduleTransformScripts; - } - - public boolean isAllowTransformationScript() - { - return _allowTransformationScript; - } - - public void setAllowTransformationScript(boolean allowTransformationScript) - { - _allowTransformationScript = allowTransformationScript; - } - - public List> getProtocolTransformScripts() - { - return _protocolTransformScripts; - } - - private void handleMapTransformScripts(List> protocolTransformScripts) - { - for (Map map : protocolTransformScripts) - { - _protocolTransformScripts.add(Map.of( - "scriptPath", ((String) map.get("scriptPath")).trim(), - "runOnEdit", map.get("runOnEdit"), - "runOnImport", map.get("runOnImport") - )); - } - } - - private void handleStringTransformScripts(List protocolTransformScripts) - { - List> transformedScripts = new ArrayList<>(protocolTransformScripts.size()); - for (String script : protocolTransformScripts) - { - transformedScripts.add(Map.of( - "scriptPath", script.trim(), - "runOnEdit", false, - "runOnImport", true - )); - } - handleMapTransformScripts(transformedScripts); - } - - public void setProtocolTransformScripts(List protocolTransformScripts) - { - if (!protocolTransformScripts.isEmpty()) { - Object first = protocolTransformScripts.get(0); - if (first instanceof Map) { - handleMapTransformScripts((List>) protocolTransformScripts); - } else if (first instanceof String) { - handleStringTransformScripts((List) protocolTransformScripts); - } else { - throw new IllegalArgumentException("Unsupported type: " + first.getClass().getName()); - } - } - } - - public List getAvailableDetectionMethods() - { - return _availableDetectionMethods; - } - - public void setAvailableDetectionMethods(List availableDetectionMethods) - { - _availableDetectionMethods = availableDetectionMethods; - } - - public String getSelectedDetectionMethod() - { - return _selectedDetectionMethod; - } - - public void setSelectedDetectionMethod(String detectionMethod) - { - _selectedDetectionMethod = detectionMethod; - } - - public GWTContainer getAutoCopyTargetContainer() - { - return _autoCopyTargetContainer; - } - - public void setAutoCopyTargetContainer(GWTContainer autoCopyTargetContainer) - { - _autoCopyTargetContainer = autoCopyTargetContainer; - } - - public String getAutoCopyTargetContainerId() - { - return _autoCopyTargetContainerId; - } - - public void setAutoCopyTargetContainerId(String autoCopyTargetContainerId) - { - _autoCopyTargetContainerId = autoCopyTargetContainerId; - } - - public String getAutoLinkCategory() - { - return _autoLinkCategory; - } - - public void setAutoLinkCategory(String autoLinkCategory) - { - _autoLinkCategory = autoLinkCategory; - } - - public boolean isSaveScriptFiles() - { - return _saveScriptFiles; - } - - public void setSaveScriptFiles(boolean saveScriptFiles) - { - _saveScriptFiles = saveScriptFiles; - } - - public boolean isEditableRuns() - { - return _editableRuns; - } - - public void setEditableRuns(boolean editableRuns) - { - _editableRuns = editableRuns; - } - - public boolean isEditableResults() - { - return _editableResults; - } - - public void setEditableResults(boolean editableResults) - { - _editableResults = editableResults; - } - - public boolean isBackgroundUpload() - { - return _backgroundUpload; - } - - public void setBackgroundUpload(boolean backgroundUpload) - { - _backgroundUpload = backgroundUpload; - } - - public Map getAvailableMetadataInputFormats() - { - return _availableMetadataInputFormats; - } - - public void setAvailableMetadataInputFormats(Map availableMetadataInputFormats) - { - _availableMetadataInputFormats = availableMetadataInputFormats; - } - - public String getMetadataInputFormatHelp() - { - return _metadataInputFormatHelp; - } - - public void setMetadataInputFormatHelp(String metadataInputFormatHelp) - { - _metadataInputFormatHelp = metadataInputFormatHelp; - } - - public String getSelectedMetadataInputFormat() - { - return _selectedMetadataInputFormat; - } - - public void setSelectedMetadataInputFormat(String selectedMetadataInputFormat) - { - _selectedMetadataInputFormat = selectedMetadataInputFormat; - } - - public boolean isQcEnabled() - { - return _qcEnabled; - } - - public void setQcEnabled(boolean qcEnabled) - { - _qcEnabled = qcEnabled; - } - - public boolean isAllowQCStates() - { - return _allowQCStates; - } - - public void setAllowQCStates(boolean allowQCStates) - { - _allowQCStates = allowQCStates; - } - - public boolean isAllowEditableResults() - { - return _allowEditableResults; - } - - public void setAllowEditableResults(boolean allowEditableResults) - { - _allowEditableResults = allowEditableResults; - } - - public boolean isAllowBackgroundUpload() - { - return _allowBackgroundUpload; - } - - public void setAllowBackgroundUpload(boolean allowBackgroundUpload) - { - _allowBackgroundUpload = allowBackgroundUpload; - } - - public boolean isAllowPlateMetadata() - { - return _allowPlateMetadata; - } - - public void setAllowPlateMetadata(boolean allowPlateMetadata) - { - _allowPlateMetadata = allowPlateMetadata; - } - - public boolean isPlateMetadata() - { - return _plateMetadata; - } - - public void setPlateMetadata(boolean plateMetadata) - { - _plateMetadata = plateMetadata; - } - - public String getStatus() - { - return _status; - } - - public void setStatus(String status) - { - _status = status; - } - - public List getExcludedContainerIds() - { - return _excludedContainerIds; - } - - public void setExcludedContainerIds(List excludedContainerIds) - { - _excludedContainerIds = excludedContainerIds; - } - - public String getAuditUserComment() - { - return _auditUserComment; - } - - public void setAuditUserComment(String auditUserComment) - { - _auditUserComment = auditUserComment; - } - - public Map getAuditRecordMap() - { - Map map = new LinkedHashMap<>(); - map.put("Name", getName()); - if (!StringUtils.isEmpty(getDescription())) - map.put("Description", getDescription()); - if (!StringUtils.isEmpty(getStatus())) - map.put("Status", getStatus()); - String autoCopyTargetContainerId = getAutoCopyTargetContainer() != null ? getAutoCopyTargetContainer().getEntityId() : getAutoCopyTargetContainerId(); - if (!StringUtils.isEmpty(autoCopyTargetContainerId)) - map.put("AutoCopyTargetContainer", autoCopyTargetContainerId); - if (!StringUtils.isEmpty(getAutoLinkCategory())) - map.put("AutoLinkCategory", getAutoLinkCategory()); - map.put("SaveScriptFiles", isSaveScriptFiles()); - map.put("IsEditableResults", isEditableResults()); - map.put("IsEditableRuns", isEditableRuns()); - map.put("IsBackgroundUpload", isBackgroundUpload()); - map.put("IsQcEnabled", isQcEnabled()); - map.put("IsPlateMetadataEnabled", isPlateMetadata()); - - return map; - } - - -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.assay.model; + +import org.apache.commons.lang3.StringUtils; +import org.labkey.api.gwt.client.model.GWTContainer; +import org.labkey.api.gwt.client.model.GWTDomain; +import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * User: brittp + * Date: Jun 20, 2007 + * Time: 2:29:22 PM + */ +public class GWTProtocol +{ + private Long _protocolId; + private String _name; + private String _description; + private String _providerName; + private String domainKindName; + + private Map _protocolParameters; + + private List> _domains; + + private List _availablePlateTemplates; + private Map _availableMetadataInputFormats = new HashMap<>(); + private String _metadataInputFormatHelp; + + private String _selectedPlateTemplate; + private String _selectedMetadataInputFormat; + + /** Scripts defined in the module itself, associated with the assay provider */ + private List _moduleTransformScripts = new ArrayList<>(); + /** Scripts defined in the assay definition */ + private final List> _protocolTransformScripts = new ArrayList<>(); + + private List _availableDetectionMethods; + private String _selectedDetectionMethod; + + private boolean _allowBackgroundUpload; + private boolean _allowEditableResults; + private boolean _allowQCStates; + private boolean _allowTransformationScript; + private boolean _allowPlateMetadata; + + // UNDONE: update 'autoCopy' to 'autoLink' for the two members below and align ui-components and tests accordingly + private GWTContainer _autoCopyTargetContainer; + private String _autoCopyTargetContainerId; + private String _autoLinkCategory; + private boolean _saveScriptFiles; + private boolean _editableRuns; + private boolean _editableResults; + private boolean _backgroundUpload; + private boolean _qcEnabled; + private boolean _plateMetadata; + private String _status; + private List _excludedContainerIds; + private String _auditUserComment; + + public GWTProtocol() + { + } + + public Long getProtocolId() + { + return _protocolId; + } + + public void setProtocolId(Long protocolId) + { + _protocolId = protocolId; + } + + + public List> getDomains() + { + return _domains; + } + + public void setDomains(List> domains) + { + _domains = domains; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public Map getProtocolParameters() + { + return _protocolParameters; + } + + public void setProtocolParameters(Map protocolParameters) + { + _protocolParameters = protocolParameters; + } + + public String getProviderName() + { + return _providerName; + } + + public void setProviderName(String providerName) + { + _providerName = providerName; + } + + public String getDomainKindName() + { + return domainKindName; + } + + public void setDomainKindName(String domainKindName) + { + this.domainKindName = domainKindName; + } + + public List getAvailablePlateTemplates() + { + return _availablePlateTemplates; + } + + public void setAvailablePlateTemplates(List availablePlateTemplates) + { + _availablePlateTemplates = availablePlateTemplates; + } + + public String getSelectedPlateTemplate() + { + return _selectedPlateTemplate; + } + + public void setSelectedPlateTemplate(String selectedPlateTemplate) + { + _selectedPlateTemplate = selectedPlateTemplate; + } + + public List getModuleTransformScripts() + { + return _moduleTransformScripts; + } + + public void setModuleTransformScripts(List moduleTransformScripts) + { + _moduleTransformScripts = moduleTransformScripts; + } + + public boolean isAllowTransformationScript() + { + return _allowTransformationScript; + } + + public void setAllowTransformationScript(boolean allowTransformationScript) + { + _allowTransformationScript = allowTransformationScript; + } + + public List> getProtocolTransformScripts() + { + return _protocolTransformScripts; + } + + private void handleMapTransformScripts(List> protocolTransformScripts) + { + for (Map map : protocolTransformScripts) + { + _protocolTransformScripts.add(Map.of( + "scriptPath", ((String) map.get("scriptPath")).trim(), + "runOnEdit", map.get("runOnEdit"), + "runOnImport", map.get("runOnImport") + )); + } + } + + private void handleStringTransformScripts(List protocolTransformScripts) + { + List> transformedScripts = new ArrayList<>(protocolTransformScripts.size()); + for (String script : protocolTransformScripts) + { + transformedScripts.add(Map.of( + "scriptPath", script.trim(), + "runOnEdit", false, + "runOnImport", true + )); + } + handleMapTransformScripts(transformedScripts); + } + + public void setProtocolTransformScripts(List protocolTransformScripts) + { + if (!protocolTransformScripts.isEmpty()) { + Object first = protocolTransformScripts.get(0); + if (first instanceof Map) { + handleMapTransformScripts((List>) protocolTransformScripts); + } else if (first instanceof String) { + handleStringTransformScripts((List) protocolTransformScripts); + } else { + throw new IllegalArgumentException("Unsupported type: " + first.getClass().getName()); + } + } + } + + public List getAvailableDetectionMethods() + { + return _availableDetectionMethods; + } + + public void setAvailableDetectionMethods(List availableDetectionMethods) + { + _availableDetectionMethods = availableDetectionMethods; + } + + public String getSelectedDetectionMethod() + { + return _selectedDetectionMethod; + } + + public void setSelectedDetectionMethod(String detectionMethod) + { + _selectedDetectionMethod = detectionMethod; + } + + public GWTContainer getAutoCopyTargetContainer() + { + return _autoCopyTargetContainer; + } + + public void setAutoCopyTargetContainer(GWTContainer autoCopyTargetContainer) + { + _autoCopyTargetContainer = autoCopyTargetContainer; + } + + public String getAutoCopyTargetContainerId() + { + return _autoCopyTargetContainerId; + } + + public void setAutoCopyTargetContainerId(String autoCopyTargetContainerId) + { + _autoCopyTargetContainerId = autoCopyTargetContainerId; + } + + public String getAutoLinkCategory() + { + return _autoLinkCategory; + } + + public void setAutoLinkCategory(String autoLinkCategory) + { + _autoLinkCategory = autoLinkCategory; + } + + public boolean isSaveScriptFiles() + { + return _saveScriptFiles; + } + + public void setSaveScriptFiles(boolean saveScriptFiles) + { + _saveScriptFiles = saveScriptFiles; + } + + public boolean isEditableRuns() + { + return _editableRuns; + } + + public void setEditableRuns(boolean editableRuns) + { + _editableRuns = editableRuns; + } + + public boolean isEditableResults() + { + return _editableResults; + } + + public void setEditableResults(boolean editableResults) + { + _editableResults = editableResults; + } + + public boolean isBackgroundUpload() + { + return _backgroundUpload; + } + + public void setBackgroundUpload(boolean backgroundUpload) + { + _backgroundUpload = backgroundUpload; + } + + public Map getAvailableMetadataInputFormats() + { + return _availableMetadataInputFormats; + } + + public void setAvailableMetadataInputFormats(Map availableMetadataInputFormats) + { + _availableMetadataInputFormats = availableMetadataInputFormats; + } + + public String getMetadataInputFormatHelp() + { + return _metadataInputFormatHelp; + } + + public void setMetadataInputFormatHelp(String metadataInputFormatHelp) + { + _metadataInputFormatHelp = metadataInputFormatHelp; + } + + public String getSelectedMetadataInputFormat() + { + return _selectedMetadataInputFormat; + } + + public void setSelectedMetadataInputFormat(String selectedMetadataInputFormat) + { + _selectedMetadataInputFormat = selectedMetadataInputFormat; + } + + public boolean isQcEnabled() + { + return _qcEnabled; + } + + public void setQcEnabled(boolean qcEnabled) + { + _qcEnabled = qcEnabled; + } + + public boolean isAllowQCStates() + { + return _allowQCStates; + } + + public void setAllowQCStates(boolean allowQCStates) + { + _allowQCStates = allowQCStates; + } + + public boolean isAllowEditableResults() + { + return _allowEditableResults; + } + + public void setAllowEditableResults(boolean allowEditableResults) + { + _allowEditableResults = allowEditableResults; + } + + public boolean isAllowBackgroundUpload() + { + return _allowBackgroundUpload; + } + + public void setAllowBackgroundUpload(boolean allowBackgroundUpload) + { + _allowBackgroundUpload = allowBackgroundUpload; + } + + public boolean isAllowPlateMetadata() + { + return _allowPlateMetadata; + } + + public void setAllowPlateMetadata(boolean allowPlateMetadata) + { + _allowPlateMetadata = allowPlateMetadata; + } + + public boolean isPlateMetadata() + { + return _plateMetadata; + } + + public void setPlateMetadata(boolean plateMetadata) + { + _plateMetadata = plateMetadata; + } + + public String getStatus() + { + return _status; + } + + public void setStatus(String status) + { + _status = status; + } + + public List getExcludedContainerIds() + { + return _excludedContainerIds; + } + + public void setExcludedContainerIds(List excludedContainerIds) + { + _excludedContainerIds = excludedContainerIds; + } + + public String getAuditUserComment() + { + return _auditUserComment; + } + + public void setAuditUserComment(String auditUserComment) + { + _auditUserComment = auditUserComment; + } + + public Map getAuditRecordMap() + { + Map map = new LinkedHashMap<>(); + map.put("Name", getName()); + if (!StringUtils.isEmpty(getDescription())) + map.put("Description", getDescription()); + if (!StringUtils.isEmpty(getStatus())) + map.put("Status", getStatus()); + String autoCopyTargetContainerId = getAutoCopyTargetContainer() != null ? getAutoCopyTargetContainer().getEntityId() : getAutoCopyTargetContainerId(); + if (!StringUtils.isEmpty(autoCopyTargetContainerId)) + map.put("AutoCopyTargetContainer", autoCopyTargetContainerId); + if (!StringUtils.isEmpty(getAutoLinkCategory())) + map.put("AutoLinkCategory", getAutoLinkCategory()); + map.put("SaveScriptFiles", isSaveScriptFiles()); + map.put("IsEditableResults", isEditableResults()); + map.put("IsEditableRuns", isEditableRuns()); + map.put("IsBackgroundUpload", isBackgroundUpload()); + map.put("IsQcEnabled", isQcEnabled()); + map.put("IsPlateMetadataEnabled", isPlateMetadata()); + + return map; + } + + +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java b/api/src/org/labkey/api/gwt/client/model/GWTConditionalFormat.java similarity index 89% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java rename to api/src/org/labkey/api/gwt/client/model/GWTConditionalFormat.java index 8200dd1bcc9..d23647d510f 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTConditionalFormat.java @@ -1,52 +1,51 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import com.google.gwt.user.client.rpc.IsSerializable; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; - -import java.io.Serializable; - -@Getter -@Setter -@EqualsAndHashCode -public class GWTConditionalFormat implements Serializable, IsSerializable -{ - public static final String COLOR_REGEX = "[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]"; - public static final String DATA_REGION_NAME = "format"; - public static final String COLUMN_NAME = "column"; - - private boolean bold = false; - private boolean italic = false; - private boolean strikethrough = false; - private String textColor = null; - private String backgroundColor = null; - private String filter; - - public GWTConditionalFormat() {} - - public GWTConditionalFormat(GWTConditionalFormat f) - { - setBold(f.isBold()); - setItalic(f.isItalic()); - setStrikethrough(f.isStrikethrough()); - setTextColor(f.getTextColor()); - setBackgroundColor(f.getBackgroundColor()); - setFilter(f.getFilter()); - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode +public class GWTConditionalFormat implements Serializable +{ + public static final String COLOR_REGEX = "[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]"; + public static final String DATA_REGION_NAME = "format"; + public static final String COLUMN_NAME = "column"; + + private boolean bold = false; + private boolean italic = false; + private boolean strikethrough = false; + private String textColor = null; + private String backgroundColor = null; + private String filter; + + public GWTConditionalFormat() {} + + public GWTConditionalFormat(GWTConditionalFormat f) + { + setBold(f.isBold()); + setItalic(f.isItalic()); + setStrikethrough(f.isStrikethrough()); + setTextColor(f.getTextColor()); + setBackgroundColor(f.getBackgroundColor()); + setFilter(f.getFilter()); + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTContainer.java b/api/src/org/labkey/api/gwt/client/model/GWTContainer.java similarity index 95% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTContainer.java rename to api/src/org/labkey/api/gwt/client/model/GWTContainer.java index 69be6fdd666..f78158b3b33 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTContainer.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTContainer.java @@ -1,70 +1,70 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import java.io.Serializable; - -/** - * User: jeckels - * Date: Jan 28, 2011 - */ -public class GWTContainer implements Serializable -{ - private String _entityId; - private int _rowId; - private String _path; - private String _name; - - /** Required for GWT serialization */ - @SuppressWarnings({"UnusedDeclaration"}) - public GWTContainer() - { - } - - public GWTContainer(String entityId, int rowId, String path, String name) - { - _entityId = entityId; - _rowId = rowId; - _path = path; - _name = name; - } - - public String getEntityId() - { - return _entityId; - } - - public int getRowId() - { - return _rowId; - } - - public String getName() - { - return _name; - } - - public String getPath() - { - return _path; - } - - @Override - public String toString() - { - return getPath(); - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +import java.io.Serializable; + +/** + * User: jeckels + * Date: Jan 28, 2011 + */ +public class GWTContainer implements Serializable +{ + private String _entityId; + private int _rowId; + private String _path; + private String _name; + + /** Required for GWT serialization */ + @SuppressWarnings({"UnusedDeclaration"}) + public GWTContainer() + { + } + + public GWTContainer(String entityId, int rowId, String path, String name) + { + _entityId = entityId; + _rowId = rowId; + _path = path; + _name = name; + } + + public String getEntityId() + { + return _entityId; + } + + public int getRowId() + { + return _rowId; + } + + public String getName() + { + return _name; + } + + public String getPath() + { + return _path; + } + + @Override + public String toString() + { + return getPath(); + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java b/api/src/org/labkey/api/gwt/client/model/GWTDomain.java similarity index 92% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java rename to api/src/org/labkey/api/gwt/client/model/GWTDomain.java index 7fc753de0e5..198faefe6e6 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTDomain.java @@ -1,335 +1,323 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.gwt.user.client.rpc.IsSerializable; -import lombok.Getter; -import lombok.Setter; -import org.labkey.api.gwt.client.DefaultValueType; -import org.labkey.api.gwt.client.util.PropertyUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class GWTDomain implements IsSerializable -{ - private String _ts; - @Getter @Setter private int domainId; - @Getter @Setter private String name; - @Getter @Setter private String domainURI; - @Getter @Setter private String domainKindName; - @Getter @Setter private String description; - @Getter @Setter private String container; - @Getter @Setter private boolean allowFileLinkProperties; - @Getter @Setter private boolean allowAttachmentProperties; - @Getter @Setter private boolean allowTextChoiceProperties; - @Getter @Setter private boolean allowMultiChoiceProperties; - @Getter @Setter private boolean allowSampleSubjectProperties; - @Getter @Setter private boolean allowTimepointProperties; - @Getter @Setter private boolean allowUniqueConstraintProperties; - @Getter @Setter private boolean allowCalculatedFields; - @Getter @Setter private boolean showDefaultValueSettings; - private DefaultValueType defaultDefaultValueType = null; - private DefaultValueType[] defaultValueOptions = new DefaultValueType[0]; - private List fields = new ArrayList<>(); - private List standardFields = null; - private List calculatedFields = null; - @Getter @Setter private List indices = new ArrayList<>(); - private String defaultValuesURL = null; - private Set mandatoryPropertyDescriptorNames = new HashSet<>(); - private Set reservedFieldNames = new HashSet<>(); - private Set reservedFieldNamePrefixes = new HashSet<>(); - private Set phiNotAllowedFieldNames = new HashSet<>(); - private Set excludeFromExportFieldNames = new HashSet<>(); - @Getter @Setter private boolean provisioned = false; - @Getter @Setter private List disabledSystemFields; - - // schema,query,template are not part of the domain, but it's handy to pass - // these values to the PropertiedEditor along with the GWTDomain. - // NOTE queryName is not necessarily == name - @Getter @Setter private String schemaName = null; - @Getter @Setter private String queryName = null; - @Getter @Setter private String templateDescription = null; // null if no template - @Getter @Setter private String instructions = null; - @Getter @Setter private boolean supportsPhiLevel = false; - - public GWTDomain() - { - } - - // deep clone constructor - public GWTDomain(GWTDomain src) - { - _ts = src._ts; - this.domainId = src.domainId; - this.name = src.name; - this.domainURI = src.domainURI; - this.domainKindName = src.domainKindName; - this.description = src.description; - this.disabledSystemFields = src.disabledSystemFields; - this.container = src.container; - this.allowFileLinkProperties = src.allowFileLinkProperties; - this.allowAttachmentProperties = src.allowAttachmentProperties; - this.allowTextChoiceProperties = src.allowTextChoiceProperties; - this.allowMultiChoiceProperties = src.allowMultiChoiceProperties; - this.allowSampleSubjectProperties = src.allowSampleSubjectProperties; - this.allowTimepointProperties = src.allowTimepointProperties; - this.allowUniqueConstraintProperties = src.allowUniqueConstraintProperties; - this.allowCalculatedFields = src.allowCalculatedFields; - this.showDefaultValueSettings = src.showDefaultValueSettings; - this.defaultDefaultValueType = src.defaultDefaultValueType; - this.defaultValueOptions = src.defaultValueOptions; - this.defaultValuesURL = src.defaultValuesURL; - this.provisioned = src.provisioned; - this.supportsPhiLevel = src.supportsPhiLevel; - - if (src.indices != null) - { - for (int i = 0; i < src.indices.size(); i++) - this.indices.add(src.indices.get(i).copy()); - } - - // include all fields here (standard and calculated) in the copy - if (src.getFields(true) == null) - return; - for (int i=0 ; i getFields(boolean includeCalculated) - { - if (includeCalculated) - return fields; - else - return getFields(); - } - - public List getFields() - { - if (standardFields == null) - standardFields = fields.stream().filter(f -> f.getValueExpression() == null).toList(); - return standardFields; - } - - public List getCalculatedFields() - { - if (calculatedFields == null) - calculatedFields = fields.stream().filter(f -> f.getValueExpression() != null).toList(); - return calculatedFields; - } - - public void setFields(List list) - { - fields = list; - - // reset the cached lists of fields so they will be recalculated on next call to getters - standardFields = null; - calculatedFields = null; - } - - public FieldType getFieldByName(String name) - { - for (FieldType field : getFields(true)) - { - if (field.getName() != null && field.getName().equalsIgnoreCase(name)) - return field; - } - return null; - } - - /** - * @return Indicates that the property can't be removed from the domain. The property may or may not be nullable. - */ - public boolean isMandatoryField(FieldType field) - { - if (mandatoryPropertyDescriptorNames == null || field.getName() == null) - { - return false; - } - return mandatoryPropertyDescriptorNames.contains(field.getName().toLowerCase()); - } - - public boolean isEditable(FieldType field) - { - return true; - } - - /** - * @return Indicates that the property is not allowed to be set as PHI - */ - public boolean allowsPhi(FieldType field) - { - return !(getPhiNotAllowedFieldNames() != null && field.getName() != null && getPhiNotAllowedFieldNames().contains(field.getName().toLowerCase())); - } - - /** - * @param mandatoryFieldNames names of property descriptors that must be present in this domain. Does not indicate that they must be non-nullable. - */ - public void setMandatoryFieldNames(Set mandatoryFieldNames) - { - this.mandatoryPropertyDescriptorNames = new HashSet<>(); - for (String mandatoryPropertyDescriptor : mandatoryFieldNames) - { - this.mandatoryPropertyDescriptorNames.add(mandatoryPropertyDescriptor.toLowerCase()); - } - } - - /** - * Get the list of property names that can't be removed from the domain. The set of mandatory fields is not modifiable in the designer. - */ - public Set getMandatoryFieldNames() - { - if (this.mandatoryPropertyDescriptorNames == null) - return Collections.emptySet(); - return Collections.unmodifiableSet(this.mandatoryPropertyDescriptorNames); - } - - public Set getReservedFieldNames() - { - return reservedFieldNames; - } - - /** - * @param reservedFieldNames can't create new fields with these names - */ - public void setReservedFieldNames(Set reservedFieldNames) - { - this.reservedFieldNames = new HashSet<>(); - for (String s : reservedFieldNames) - { - this.reservedFieldNames.add(s.toLowerCase()); - } - } - - public Set getReservedFieldNamePrefixes() - { - return this.reservedFieldNamePrefixes; - } - - public void setReservedFieldNamePrefixes(Set prefixes) - { - this.reservedFieldNamePrefixes = new HashSet<>(prefixes); - } - /** - * - * @param excludeFromExportFieldNames These fields will be suppressed from the export field list. Primary use case is to not export List key fields. - */ - public void setExcludeFromExportFieldNames(Set excludeFromExportFieldNames) - { - this.excludeFromExportFieldNames = new HashSet<>(); - for (String excludeFromExportFieldName : excludeFromExportFieldNames) - { - this.excludeFromExportFieldNames.add(excludeFromExportFieldName.toLowerCase()); - } - } - - public Set getExcludeFromExportFieldNames() - { - return excludeFromExportFieldNames; - } - - public boolean isExcludeFromExportField(FieldType field) - { - if (excludeFromExportFieldNames == null || field.getName() == null) - { - return false; - } - return excludeFromExportFieldNames.contains(field.getName().toLowerCase()); - } - - public Set getPhiNotAllowedFieldNames() - { - return phiNotAllowedFieldNames; - } - - public void setPhiNotAllowedFieldNames(Set phiNotAllowedFieldNames) - { - this.phiNotAllowedFieldNames = new HashSet<>(); - for (String fieldName : phiNotAllowedFieldNames) - { - this.phiNotAllowedFieldNames.add(fieldName.toLowerCase()); - } - } - - public DefaultValueType getDefaultDefaultValueType() - { - return defaultDefaultValueType; - } - - public DefaultValueType[] getDefaultValueOptions() - { - return defaultValueOptions; - } - - public void setDefaultValueOptions(DefaultValueType[] defaultOptions, DefaultValueType defaultDefault) - { - this.defaultDefaultValueType = defaultDefault; - this.defaultValueOptions = defaultOptions; - } - - public String getDefaultValuesURL() - { - if (defaultValuesURL == null) - return PropertyUtil.getRelativeURL("setDefaultValuesList", "list"); - return defaultValuesURL; - } - - public void setDefaultValuesURL(String defaultValuesURL) - { - this.defaultValuesURL = defaultValuesURL; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import org.labkey.api.gwt.client.DefaultValueType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class GWTDomain +{ + private String _ts; + @Getter @Setter private int domainId; + @Getter @Setter private String name; + @Getter @Setter private String domainURI; + @Getter @Setter private String domainKindName; + @Getter @Setter private String description; + @Getter @Setter private String container; + @Getter @Setter private boolean allowFileLinkProperties; + @Getter @Setter private boolean allowAttachmentProperties; + @Getter @Setter private boolean allowTextChoiceProperties; + @Getter @Setter private boolean allowMultiChoiceProperties; + @Getter @Setter private boolean allowSampleSubjectProperties; + @Getter @Setter private boolean allowTimepointProperties; + @Getter @Setter private boolean allowUniqueConstraintProperties; + @Getter @Setter private boolean allowCalculatedFields; + @Getter @Setter private boolean showDefaultValueSettings; + private DefaultValueType defaultDefaultValueType = null; + private DefaultValueType[] defaultValueOptions = new DefaultValueType[0]; + private List fields = new ArrayList<>(); + private List standardFields = null; + private List calculatedFields = null; + @Getter @Setter private List indices = new ArrayList<>(); + @Setter + @Getter + private String defaultValuesURL = null; + private Set mandatoryPropertyDescriptorNames = new HashSet<>(); + private Set reservedFieldNames = new HashSet<>(); + private Set reservedFieldNamePrefixes = new HashSet<>(); + private Set phiNotAllowedFieldNames = new HashSet<>(); + private Set excludeFromExportFieldNames = new HashSet<>(); + @Getter @Setter private boolean provisioned = false; + @Getter @Setter private List disabledSystemFields; + + // schema,query,template are not part of the domain, but it's handy to pass + // these values to the PropertiedEditor along with the GWTDomain. + // NOTE queryName is not necessarily == name + @Getter @Setter private String schemaName = null; + @Getter @Setter private String queryName = null; + @Getter @Setter private String templateDescription = null; // null if no template + @Getter @Setter private String instructions = null; + @Getter @Setter private boolean supportsPhiLevel = false; + + public GWTDomain() + { + } + + // deep clone constructor + public GWTDomain(GWTDomain src) + { + _ts = src._ts; + this.domainId = src.domainId; + this.name = src.name; + this.domainURI = src.domainURI; + this.domainKindName = src.domainKindName; + this.description = src.description; + this.disabledSystemFields = src.disabledSystemFields; + this.container = src.container; + this.allowFileLinkProperties = src.allowFileLinkProperties; + this.allowAttachmentProperties = src.allowAttachmentProperties; + this.allowTextChoiceProperties = src.allowTextChoiceProperties; + this.allowMultiChoiceProperties = src.allowMultiChoiceProperties; + this.allowSampleSubjectProperties = src.allowSampleSubjectProperties; + this.allowTimepointProperties = src.allowTimepointProperties; + this.allowUniqueConstraintProperties = src.allowUniqueConstraintProperties; + this.allowCalculatedFields = src.allowCalculatedFields; + this.showDefaultValueSettings = src.showDefaultValueSettings; + this.defaultDefaultValueType = src.defaultDefaultValueType; + this.defaultValueOptions = src.defaultValueOptions; + this.defaultValuesURL = src.defaultValuesURL; + this.provisioned = src.provisioned; + this.supportsPhiLevel = src.supportsPhiLevel; + + if (src.indices != null) + { + for (int i = 0; i < src.indices.size(); i++) + this.indices.add(src.indices.get(i).copy()); + } + + // include all fields here (standard and calculated) in the copy + if (src.getFields(true) == null) + return; + for (int i=0 ; i getFields(boolean includeCalculated) + { + if (includeCalculated) + return fields; + else + return getFields(); + } + + public List getFields() + { + if (standardFields == null) + standardFields = fields.stream().filter(f -> f.getValueExpression() == null).toList(); + return standardFields; + } + + public List getCalculatedFields() + { + if (calculatedFields == null) + calculatedFields = fields.stream().filter(f -> f.getValueExpression() != null).toList(); + return calculatedFields; + } + + public void setFields(List list) + { + fields = list; + + // reset the cached lists of fields so they will be recalculated on next call to getters + standardFields = null; + calculatedFields = null; + } + + public FieldType getFieldByName(String name) + { + for (FieldType field : getFields(true)) + { + if (field.getName() != null && field.getName().equalsIgnoreCase(name)) + return field; + } + return null; + } + + /** + * @return Indicates that the property can't be removed from the domain. The property may or may not be nullable. + */ + public boolean isMandatoryField(FieldType field) + { + if (mandatoryPropertyDescriptorNames == null || field.getName() == null) + { + return false; + } + return mandatoryPropertyDescriptorNames.contains(field.getName().toLowerCase()); + } + + public boolean isEditable(FieldType field) + { + return true; + } + + /** + * @return Indicates that the property is not allowed to be set as PHI + */ + public boolean allowsPhi(FieldType field) + { + return !(getPhiNotAllowedFieldNames() != null && field.getName() != null && getPhiNotAllowedFieldNames().contains(field.getName().toLowerCase())); + } + + /** + * @param mandatoryFieldNames names of property descriptors that must be present in this domain. Does not indicate that they must be non-nullable. + */ + public void setMandatoryFieldNames(Set mandatoryFieldNames) + { + this.mandatoryPropertyDescriptorNames = new HashSet<>(); + for (String mandatoryPropertyDescriptor : mandatoryFieldNames) + { + this.mandatoryPropertyDescriptorNames.add(mandatoryPropertyDescriptor.toLowerCase()); + } + } + + /** + * Get the list of property names that can't be removed from the domain. The set of mandatory fields is not modifiable in the designer. + */ + public Set getMandatoryFieldNames() + { + if (this.mandatoryPropertyDescriptorNames == null) + return Collections.emptySet(); + return Collections.unmodifiableSet(this.mandatoryPropertyDescriptorNames); + } + + public Set getReservedFieldNames() + { + return reservedFieldNames; + } + + /** + * @param reservedFieldNames can't create new fields with these names + */ + public void setReservedFieldNames(Set reservedFieldNames) + { + this.reservedFieldNames = new HashSet<>(); + for (String s : reservedFieldNames) + { + this.reservedFieldNames.add(s.toLowerCase()); + } + } + + public Set getReservedFieldNamePrefixes() + { + return this.reservedFieldNamePrefixes; + } + + public void setReservedFieldNamePrefixes(Set prefixes) + { + this.reservedFieldNamePrefixes = new HashSet<>(prefixes); + } + /** + * + * @param excludeFromExportFieldNames These fields will be suppressed from the export field list. Primary use case is to not export List key fields. + */ + public void setExcludeFromExportFieldNames(Set excludeFromExportFieldNames) + { + this.excludeFromExportFieldNames = new HashSet<>(); + for (String excludeFromExportFieldName : excludeFromExportFieldNames) + { + this.excludeFromExportFieldNames.add(excludeFromExportFieldName.toLowerCase()); + } + } + + public Set getExcludeFromExportFieldNames() + { + return excludeFromExportFieldNames; + } + + public boolean isExcludeFromExportField(FieldType field) + { + if (excludeFromExportFieldNames == null || field.getName() == null) + { + return false; + } + return excludeFromExportFieldNames.contains(field.getName().toLowerCase()); + } + + public Set getPhiNotAllowedFieldNames() + { + return phiNotAllowedFieldNames; + } + + public void setPhiNotAllowedFieldNames(Set phiNotAllowedFieldNames) + { + this.phiNotAllowedFieldNames = new HashSet<>(); + for (String fieldName : phiNotAllowedFieldNames) + { + this.phiNotAllowedFieldNames.add(fieldName.toLowerCase()); + } + } + + public DefaultValueType getDefaultDefaultValueType() + { + return defaultDefaultValueType; + } + + public DefaultValueType[] getDefaultValueOptions() + { + return defaultValueOptions; + } + + public void setDefaultValueOptions(DefaultValueType[] defaultOptions, DefaultValueType defaultDefault) + { + this.defaultDefaultValueType = defaultDefault; + this.defaultValueOptions = defaultOptions; + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTFilterCriteria.java b/api/src/org/labkey/api/gwt/client/model/GWTFilterCriteria.java similarity index 83% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTFilterCriteria.java rename to api/src/org/labkey/api/gwt/client/model/GWTFilterCriteria.java index 0dc50ead244..50723f69de0 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTFilterCriteria.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTFilterCriteria.java @@ -1,6 +1,5 @@ package org.labkey.api.gwt.client.model; -import com.google.gwt.user.client.rpc.IsSerializable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -10,7 +9,7 @@ @Setter @Getter @EqualsAndHashCode -public class GWTFilterCriteria implements Serializable, IsSerializable +public class GWTFilterCriteria implements Serializable { private String name; private String op; diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTIndex.java b/api/src/org/labkey/api/gwt/client/model/GWTIndex.java similarity index 95% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTIndex.java rename to api/src/org/labkey/api/gwt/client/model/GWTIndex.java index 75e8030492d..60290fc4abf 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTIndex.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTIndex.java @@ -15,7 +15,6 @@ */ package org.labkey.api.gwt.client.model; -import com.google.gwt.user.client.rpc.IsSerializable; import org.apache.commons.lang3.StringUtils; import org.labkey.api.data.PropertyStorageSpec; @@ -30,7 +29,7 @@ * User: kevink * Date: 11/9/15 */ -public class GWTIndex implements IsSerializable, Serializable +public class GWTIndex implements Serializable { private List _columnNames; private boolean _unique; diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java b/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java similarity index 57% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java rename to api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java index 303fd309ede..7aa3bd46509 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java @@ -1,696 +1,692 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import com.google.gwt.user.client.rpc.IsSerializable; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import org.apache.commons.lang3.StringUtils; -import org.labkey.api.gwt.client.DefaultScaleType; -import org.labkey.api.gwt.client.DefaultValueType; -import org.labkey.api.gwt.client.LockedPropertyType; -import org.labkey.api.gwt.client.ui.PropertyType; -import org.labkey.api.gwt.client.util.BooleanProperty; -import org.labkey.api.gwt.client.util.IntegerProperty; -import org.labkey.api.gwt.client.util.StringProperty; - -import java.util.ArrayList; -import java.util.List; - -/** - * See {@link org.labkey.api.exp.PropertyDescriptor} - */ -@EqualsAndHashCode -public class GWTPropertyDescriptor implements IsSerializable -{ - private final IntegerProperty propertyId = new IntegerProperty(0); - private final StringProperty propertyURI = new StringProperty(); - private final StringProperty container = new StringProperty(); - private final StringProperty name = new StringProperty(); - private final StringProperty description = new StringProperty(); - private final StringProperty rangeURI = new StringProperty("http://www.w3.org/2001/XMLSchema#string"); - private final StringProperty conceptURI = new StringProperty(); - private final StringProperty label = new StringProperty(); - private final StringProperty format = new StringProperty(); - private final BooleanProperty required = new BooleanProperty(false); - private final BooleanProperty hidden = new BooleanProperty(false); - private final StringProperty lookupContainer = new StringProperty(); - private final StringProperty lookupSchema = new StringProperty(); - private final StringProperty lookupQuery = new StringProperty(); - private final BooleanProperty lookupIsValid = new BooleanProperty(true); - private String defaultValueType = null; - private final StringProperty defaultValue = new StringProperty(); - private final StringProperty defaultDisplayValue = new StringProperty("[none]"); - private final BooleanProperty mvEnabled = new BooleanProperty(false); - private final StringProperty importAliases = new StringProperty(); - private final StringProperty url = new StringProperty(); - private final StringProperty urlTarget = new StringProperty(); - private final BooleanProperty shownInInsertView = new BooleanProperty(true); - private final BooleanProperty shownInUpdateView = new BooleanProperty(true); - private final BooleanProperty shownInDetailsView = new BooleanProperty(true); - private final BooleanProperty measure = new BooleanProperty(); - private final BooleanProperty dimension = new BooleanProperty(); - private final BooleanProperty recommendedVariable = new BooleanProperty(false); - private final StringProperty defaultScale = new StringProperty(DefaultScaleType.LINEAR.name()); - private final StringProperty facetingBehaviorType = new StringProperty(); - private final StringProperty phi = new StringProperty("NotPHI"); // Must match PHI.NotPHI and tableInfo.xsd enum PHIType.NotPHI - private final BooleanProperty isExcludeFromShifting = new BooleanProperty(); - private final BooleanProperty isPreventReordering = new BooleanProperty(); - private final BooleanProperty isDisableEditing = new BooleanProperty(); - private final IntegerProperty scale = new IntegerProperty(4000); - private final StringProperty principalConceptCode = new StringProperty(); - private final StringProperty sourceOntology = new StringProperty(); - private final StringProperty conceptSubtree = new StringProperty(); - private final StringProperty conceptImportColumn = new StringProperty(); - private final StringProperty conceptLabelColumn = new StringProperty(); - private final StringProperty redactedText = new StringProperty(); - private final StringProperty derivationDataScope = new StringProperty(); - private final BooleanProperty isPrimaryKey = new BooleanProperty(false); - private final StringProperty lockType = new StringProperty(LockedPropertyType.NotLocked.name()); - private final BooleanProperty scannable = new BooleanProperty(false); - private final StringProperty valueExpression = new StringProperty(); - - @Getter @Setter private List conditionalFormats = new ArrayList<>(); - @Getter @Setter private List filterCriteria = new ArrayList<>(); - @Getter @Setter private List propertyValidators = new ArrayList<>(); - - public GWTPropertyDescriptor() - { - } - - public GWTPropertyDescriptor(String name, String rangeURI) - { - setName(name); - setRangeURI(rangeURI); - } - - public GWTPropertyDescriptor(GWTPropertyDescriptor s) - { - this(s, false); - } - - public GWTPropertyDescriptor(GWTPropertyDescriptor s, boolean isNew) - { - if (!isNew) - { - setPropertyId(s.getPropertyId()); - setPropertyURI(s.getPropertyURI()); - } - - setContainer(s.getContainer()); - setName(s.getName()); - setDescription(s.getDescription()); - setRangeURI(s.getRangeURI()); - setConceptURI(s.getConceptURI()); - setLabel(s.getLabel()); - setFormat(s.getFormat()); - setRequired(s.isRequired()); - setHidden(s.isHidden()); - setShownInDetailsView(s.isShownInDetailsView()); - setShownInInsertView(s.isShownInInsertView()); - setShownInUpdateView(s.isShownInUpdateView()); - setMvEnabled(s.getMvEnabled()); - setMeasure(s.isMeasure()); - setDimension(s.isDimension()); - setRecommendedVariable(s.isRecommendedVariable()); - setDefaultScale(s.getDefaultScale()); - setLookupContainer(s.getLookupContainer()); - setLookupIsValid(s.getLookupIsValid()); - setLookupSchema(s.getLookupSchema()); - setLookupQuery(s.getLookupQuery()); - setDefaultValueType(s.getDefaultValueType()); - setDefaultValue(s.getDefaultValue()); - setDefaultDisplayValue(s.getDefaultDisplayValue()); - setImportAliases(s.getImportAliases()); - setURL(s.getURL()); - setURLTarget(s.getURLTarget()); - setFacetingBehaviorType(s.getFacetingBehaviorType()); - setPHI(s.getPHI()); - setExcludeFromShifting(s.isExcludeFromShifting()); - setPreventReordering(s.getPreventReordering()); - setDisableEditing(s.getDisableEditing()); - setScale(s.getScale()); - setRedactedText(s.getRedactedText()); - setIsPrimaryKey(s.getIsPrimaryKey()); - setLockType(s.getLockType()); - setPrincipalConceptCode(s.getPrincipalConceptCode()); - setSourceOntology(s.getSourceOntology()); - setConceptSubtree(s.getConceptSubtree()); - setConceptImportColumn(s.getConceptImportColumn()); - setConceptLabelColumn(s.getConceptLabelColumn()); - setDerivationDataScope(s.getDerivationDataScope()); - setScannable(s.isScannable()); - setValueExpression(s.getValueExpression()); - - for (GWTPropertyValidator v : s.getPropertyValidators()) - { - GWTPropertyValidator gpv = new GWTPropertyValidator(v); - if (isNew) - { - gpv.setRowId(0); - gpv.setNew(true); - } - propertyValidators.add(gpv); - } - - for (GWTConditionalFormat f : s.getConditionalFormats()) - { - conditionalFormats.add(new GWTConditionalFormat(f)); - } - - for (GWTFilterCriteria fc : s.getFilterCriteria()) - { - filterCriteria.add(new GWTFilterCriteria(fc)); - } - } - - public GWTPropertyDescriptor copy() - { - return new GWTPropertyDescriptor(this); - } - - public String getContainer() - { - return container.getString(); - } - - public void setContainer(String container) - { - this.container.set(container); - } - - public String getLookupContainer() - { - return lookupContainer.getString(); - } - - public void setLookupContainer(String lookupContainer) - { - this.lookupContainer.set(lookupContainer); - } - - public String getLookupSchema() - { - return lookupSchema.getString(); - } - - public void setLookupSchema(String lookupSchema) - { - this.lookupSchema.set(lookupSchema); - } - - public String getLookupQuery() - { - return lookupQuery.getString(); - } - - public void setLookupQuery(String lookupQuery) - { - this.lookupQuery.set(lookupQuery); - } - - public boolean getLookupIsValid() - { - return lookupIsValid.getBoolean(); - } - - public void setLookupIsValid(boolean lookupIsValid) - { - this.lookupIsValid.set(lookupIsValid); - } - - public int getPropertyId() - { - return propertyId.getInt(); - } - - public void setPropertyId(int rowId) - { - this.propertyId.setInt(rowId); - } - - public String getPropertyURI() - { - return propertyURI.getString(); - } - - public void setPropertyURI(String propertyURI) - { - this.propertyURI.set(propertyURI); - } - - public String getName() - { - return name.getString(); - } - - public void setName(String name) - { - this.name.set(name); - } - - public String getDescription() - { - return description.getString(); - } - - public void setDescription(String description) - { - this.description.set(description); - } - - public String getRangeURI() - { - return rangeURI.getString(); - } - - public void setRangeURI(String dataTypeURI) - { - this.rangeURI.set(dataTypeURI); - } - - public void guessMeasureAndDimension() - { - boolean plottableType = PropertyType.xsdInt.getURI().equals(getRangeURI()) || - PropertyType.xsdDouble.getURI().equals(getRangeURI()); - boolean isMeasure = plottableType && getLookupQuery() == null && !isHidden(); - setMeasure(isMeasure); - - setDimension(getLookupQuery() != null && !isHidden()); - } - - public String getConceptURI() - { - return conceptURI.getString(); - } - - public void setConceptURI(String conceptURI) - { - this.conceptURI.set(conceptURI); - } - - public String getLabel() - { - return label.getString(); - } - - public void setLabel(String label) - { - this.label.set(label); - } - - public String getFormat() - { - return format.getString(); - } - - public void setFormat(String format) - { - this.format.set(format); - } - - public boolean isRequired() - { - return required.getBool(); - } - - public void setRequired(boolean required) - { - this.required.setBool(required); - } - - public boolean isHidden() - { - return hidden.getBool(); - } - - public void setHidden(boolean hidden) - { - this.hidden.setBool(hidden); - } - - public boolean isShownInInsertView() - { - return shownInInsertView.getBool(); - } - - public void setShownInInsertView(boolean shown) - { - shownInInsertView.setBool(shown); - } - - public boolean isShownInUpdateView() - { - return shownInUpdateView.getBool(); - } - - public void setShownInUpdateView(boolean shown) - { - shownInUpdateView.setBool(shown); - } - - public boolean isShownInDetailsView() - { - return shownInDetailsView.getBool(); - } - - public void setShownInDetailsView(boolean shown) - { - shownInDetailsView.setBool(shown); - } - - public boolean isSetMeasure() - { - return measure.getBoolean() != null; - } - - public boolean isMeasure() - { - return measure.booleanValue(); - } - - public void setMeasure(boolean isMeasure) - { - measure.setBool(isMeasure); - } - - public boolean isSetDimension() - { - return dimension.getBoolean() != null; - } - - public boolean isDimension() - { - return dimension.booleanValue(); - } - - public void setDimension(boolean isDimension) - { - dimension.setBool(isDimension); - } - - public boolean isRecommendedVariable() - { - return recommendedVariable.booleanValue(); - } - - public void setRecommendedVariable(boolean isRecommendedVariable) - { - recommendedVariable.setBool(isRecommendedVariable); - } - - public String getDefaultScale() - { - return defaultScale.getString(); - } - - public void setDefaultScale(String defaultScale) - { - this.defaultScale.set(defaultScale); - } - - public boolean getMvEnabled() - { - return mvEnabled.getBool(); - } - - public void setMvEnabled(boolean mvEnabled) - { - this.mvEnabled.setBool(mvEnabled); - } - - public DefaultValueType getDefaultValueType() - { - return null==defaultValueType ? null : DefaultValueType.valueOf(defaultValueType); - } - - public void setDefaultValueType(DefaultValueType defaultValueType) - { - this.defaultValueType = null==defaultValueType ? null : defaultValueType.name(); - } - - public String getDefaultValue() - { - return defaultValue.getString(); - } - - public void setDefaultValue(String defaultValue) - { - this.defaultValue.set(defaultValue); - } - - public String getDefaultDisplayValue() - { - return defaultDisplayValue.toString(); - } - - public void setDefaultDisplayValue(String defaultDisplayValue) - { - this.defaultDisplayValue.set(defaultDisplayValue); - } - - public String getFacetingBehaviorType() - { - return facetingBehaviorType.getString(); - } - - public void setFacetingBehaviorType(String facetingBehavior) - { - this.facetingBehaviorType.set(facetingBehavior); - } - - public String getPHI() - { - return phi.getString(); - } - - public void setPHI(String phi) - { - this.phi.set(phi); - } - - public boolean isSetExcludeFromShifting() - { - return isExcludeFromShifting.getBoolean() != null; - } - - public boolean isExcludeFromShifting() - { - return isExcludeFromShifting.booleanValue(); - } - - public void setExcludeFromShifting(boolean isExcludeFromShifting) - { - this.isExcludeFromShifting.setBool(isExcludeFromShifting); - } - - public boolean getPreventReordering() - { - return isPreventReordering.booleanValue(); - } - - public void setPreventReordering(boolean preventReordering) - { - isPreventReordering.setBool(preventReordering); - } - - public boolean getDisableEditing() - { - return isDisableEditing.booleanValue(); - } - - public void setDisableEditing(boolean disableEditing) - { - isDisableEditing.setBool(disableEditing); - } - - public Integer getScale() - { - return this.scale.getInteger(); - } - - public void setScale(Integer value) - { - this.scale.set(value); - } - - public boolean isScannable() - { - return scannable.getBoolean(); - } - - public void setScannable(boolean scannable) - { - this.scannable.setBool(scannable); - } - - public String getPrincipalConceptCode() { return this.principalConceptCode.getString(); } - - public void setPrincipalConceptCode(String code) { this.principalConceptCode.set(code); } - - public String getSourceOntology() - { - return sourceOntology.getString(); - } - - public void setSourceOntology(String sourceOntology) - { - this.sourceOntology.set(sourceOntology); - } - - public String getConceptSubtree() - { - return this.conceptSubtree.getString(); - } - - public void setConceptSubtree(String path) - { - this.conceptSubtree.set(path); - } - - public String getConceptImportColumn() - { - return conceptImportColumn.getString(); - } - - public void setConceptImportColumn(String conceptImportColumn) - { - this.conceptImportColumn.set(conceptImportColumn); - } - - public String getConceptLabelColumn() - { - return conceptLabelColumn.getString(); - } - - public void setConceptLabelColumn(String conceptLabelColumn) - { - this.conceptLabelColumn.set(conceptLabelColumn); - } - - public String getRedactedText() - { - return redactedText.getString(); - } - - public void setRedactedText(String redactedText) - { - this.redactedText.set(redactedText); - } - - public String getDerivationDataScope() - { - return derivationDataScope.getString(); - } - - public void setDerivationDataScope(String derivationDataScope) - { - this.derivationDataScope.set(derivationDataScope); - } - - public String getValueExpression() - { - return valueExpression.getString(); - } - - public void setValueExpression(String valueExpression) - { - this.valueExpression.set(valueExpression); - } - - public boolean getIsPrimaryKey() - { - return isPrimaryKey.booleanValue(); - } - - /** This method is for informational purpose only so that the client can identify column as a PK column. - * Setting PK on a column via this method will not get preserved in the domain's table. - */ - public void setIsPrimaryKey(boolean isPrimaryKey) - { - this.isPrimaryKey.setBool(isPrimaryKey); - } - - public String getLockType() - { - return lockType.getString(); - } - - /** This method is for informational purpose only so that the client can identify column's locked type. - * Setting lock type on a column via this method will not get preserved in the domain's table. - */ - public void setLockType(String lockType) - { - this.lockType.set(lockType); - } - - public String debugString() - { - return getName() + " " + getLabel() + " " + getRangeURI() + " " + isRequired() + " " + getDescription(); - } - - public String getImportAliases() - { - return importAliases.toString(); - } - - public void setImportAliases(String importAliases) - { - this.importAliases.set(importAliases); - } - - public String getURL() - { - return url.toString(); - } - - public void setURL(String url) - { - this.url.set(url); - } - - public String getURLTarget() - { - return urlTarget.toString(); - } - - public void setURLTarget(String urlTarget) - { - this.urlTarget.set(urlTarget); - } - - public String getLookupDescription() - { - if (StringUtils.isEmpty(getLookupSchema()) || StringUtils.isEmpty(getLookupQuery())) - return "(none)"; - - return getLookupSchema() + "." + getLookupQuery(); - } - - @Override - public String toString() - { - return name.getString() + ": " + rangeURI.getString(); - } - - public boolean isFileType() - { - return "http://cpas.fhcrc.org/exp/xml#fileLink".equals(getRangeURI()) || - "http://www.labkey.org/exp/xml#attachment".equals(getRangeURI()); - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.labkey.api.gwt.client.DefaultScaleType; +import org.labkey.api.gwt.client.DefaultValueType; +import org.labkey.api.gwt.client.LockedPropertyType; +import org.labkey.api.gwt.client.ui.PropertyType; + +import java.util.ArrayList; +import java.util.List; + +/** + * See {@link org.labkey.api.exp.PropertyDescriptor} + */ +@EqualsAndHashCode +public class GWTPropertyDescriptor +{ + private int propertyId = 0; + private String propertyURI; + private String container; + private String name; + private String description; + private String rangeURI = "http://www.w3.org/2001/XMLSchema#string"; + private String conceptURI; + private String label; + private String format; + private boolean required = false; + private boolean hidden = false; + private String lookupContainer; + private String lookupSchema; + private String lookupQuery; + private boolean lookupIsValid = true; + private String defaultValueType = null; + private String defaultValue; + private String defaultDisplayValue = "[none]"; + private boolean mvEnabled = false; + private String importAliases; + private String url; + private String urlTarget; + private boolean shownInInsertView = true; + private boolean shownInUpdateView = true; + private boolean shownInDetailsView = true; + private Boolean measure; + private Boolean dimension; + private boolean recommendedVariable = false; + private String defaultScale = DefaultScaleType.LINEAR.name(); + private String facetingBehaviorType; + private String phi = "NotPHI"; // Must match PHI.NotPHI and tableInfo.xsd enum PHIType.NotPHI + private Boolean isExcludeFromShifting; + private boolean isPreventReordering = false; + private boolean isDisableEditing = false; + private Integer scale = 4000; + private String principalConceptCode; + private String sourceOntology; + private String conceptSubtree; + private String conceptImportColumn; + private String conceptLabelColumn; + private String redactedText; + private String derivationDataScope; + private boolean isPrimaryKey = false; + private String lockType = LockedPropertyType.NotLocked.name(); + private boolean scannable = false; + private String valueExpression; + + @Getter @Setter private List conditionalFormats = new ArrayList<>(); + @Getter @Setter private List filterCriteria = new ArrayList<>(); + @Getter @Setter private List propertyValidators = new ArrayList<>(); + + public GWTPropertyDescriptor() + { + } + + public GWTPropertyDescriptor(String name, String rangeURI) + { + setName(name); + setRangeURI(rangeURI); + } + + public GWTPropertyDescriptor(GWTPropertyDescriptor s) + { + this(s, false); + } + + public GWTPropertyDescriptor(GWTPropertyDescriptor s, boolean isNew) + { + if (!isNew) + { + setPropertyId(s.getPropertyId()); + setPropertyURI(s.getPropertyURI()); + } + + setContainer(s.getContainer()); + setName(s.getName()); + setDescription(s.getDescription()); + setRangeURI(s.getRangeURI()); + setConceptURI(s.getConceptURI()); + setLabel(s.getLabel()); + setFormat(s.getFormat()); + setRequired(s.isRequired()); + setHidden(s.isHidden()); + setShownInDetailsView(s.isShownInDetailsView()); + setShownInInsertView(s.isShownInInsertView()); + setShownInUpdateView(s.isShownInUpdateView()); + setMvEnabled(s.getMvEnabled()); + setMeasure(s.isMeasure()); + setDimension(s.isDimension()); + setRecommendedVariable(s.isRecommendedVariable()); + setDefaultScale(s.getDefaultScale()); + setLookupContainer(s.getLookupContainer()); + setLookupIsValid(s.getLookupIsValid()); + setLookupSchema(s.getLookupSchema()); + setLookupQuery(s.getLookupQuery()); + setDefaultValueType(s.getDefaultValueType()); + setDefaultValue(s.getDefaultValue()); + setDefaultDisplayValue(s.getDefaultDisplayValue()); + setImportAliases(s.getImportAliases()); + setURL(s.getURL()); + setURLTarget(s.getURLTarget()); + setFacetingBehaviorType(s.getFacetingBehaviorType()); + setPHI(s.getPHI()); + setExcludeFromShifting(s.isExcludeFromShifting()); + setPreventReordering(s.getPreventReordering()); + setDisableEditing(s.getDisableEditing()); + setScale(s.getScale()); + setRedactedText(s.getRedactedText()); + setIsPrimaryKey(s.getIsPrimaryKey()); + setLockType(s.getLockType()); + setPrincipalConceptCode(s.getPrincipalConceptCode()); + setSourceOntology(s.getSourceOntology()); + setConceptSubtree(s.getConceptSubtree()); + setConceptImportColumn(s.getConceptImportColumn()); + setConceptLabelColumn(s.getConceptLabelColumn()); + setDerivationDataScope(s.getDerivationDataScope()); + setScannable(s.isScannable()); + setValueExpression(s.getValueExpression()); + + for (GWTPropertyValidator v : s.getPropertyValidators()) + { + GWTPropertyValidator gpv = new GWTPropertyValidator(v); + if (isNew) + { + gpv.setRowId(0); + gpv.setNew(true); + } + propertyValidators.add(gpv); + } + + for (GWTConditionalFormat f : s.getConditionalFormats()) + { + conditionalFormats.add(new GWTConditionalFormat(f)); + } + + for (GWTFilterCriteria fc : s.getFilterCriteria()) + { + filterCriteria.add(new GWTFilterCriteria(fc)); + } + } + + public GWTPropertyDescriptor copy() + { + return new GWTPropertyDescriptor(this); + } + + public String getContainer() + { + return container; + } + + public void setContainer(String container) + { + this.container = container; + } + + public String getLookupContainer() + { + return lookupContainer; + } + + public void setLookupContainer(String lookupContainer) + { + this.lookupContainer = lookupContainer; + } + + public String getLookupSchema() + { + return lookupSchema; + } + + public void setLookupSchema(String lookupSchema) + { + this.lookupSchema = lookupSchema; + } + + public String getLookupQuery() + { + return lookupQuery; + } + + public void setLookupQuery(String lookupQuery) + { + this.lookupQuery = lookupQuery; + } + + public boolean getLookupIsValid() + { + return lookupIsValid; + } + + public void setLookupIsValid(boolean lookupIsValid) + { + this.lookupIsValid = lookupIsValid; + } + + public int getPropertyId() + { + return propertyId; + } + + public void setPropertyId(int rowId) + { + this.propertyId = rowId; + } + + public String getPropertyURI() + { + return propertyURI; + } + + public void setPropertyURI(String propertyURI) + { + this.propertyURI = propertyURI; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public String getRangeURI() + { + return rangeURI; + } + + public void setRangeURI(String dataTypeURI) + { + this.rangeURI = dataTypeURI; + } + + public void guessMeasureAndDimension() + { + boolean plottableType = PropertyType.xsdInt.getURI().equals(getRangeURI()) || + PropertyType.xsdDouble.getURI().equals(getRangeURI()); + boolean isMeasure = plottableType && getLookupQuery() == null && !isHidden(); + setMeasure(isMeasure); + + setDimension(getLookupQuery() != null && !isHidden()); + } + + public String getConceptURI() + { + return conceptURI; + } + + public void setConceptURI(String conceptURI) + { + this.conceptURI = conceptURI; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public String getFormat() + { + return format; + } + + public void setFormat(String format) + { + this.format = format; + } + + public boolean isRequired() + { + return required; + } + + public void setRequired(boolean required) + { + this.required = required; + } + + public boolean isHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public boolean isShownInInsertView() + { + return shownInInsertView; + } + + public void setShownInInsertView(boolean shown) + { + this.shownInInsertView = shown; + } + + public boolean isShownInUpdateView() + { + return shownInUpdateView; + } + + public void setShownInUpdateView(boolean shown) + { + this.shownInUpdateView = shown; + } + + public boolean isShownInDetailsView() + { + return shownInDetailsView; + } + + public void setShownInDetailsView(boolean shown) + { + this.shownInDetailsView = shown; + } + + public boolean isSetMeasure() + { + return measure != null; + } + + public boolean isMeasure() + { + return measure != null && measure; + } + + public void setMeasure(boolean isMeasure) + { + this.measure = isMeasure; + } + + public boolean isSetDimension() + { + return dimension != null; + } + + public boolean isDimension() + { + return dimension != null && dimension; + } + + public void setDimension(boolean isDimension) + { + this.dimension = isDimension; + } + + public boolean isRecommendedVariable() + { + return recommendedVariable; + } + + public void setRecommendedVariable(boolean isRecommendedVariable) + { + this.recommendedVariable = isRecommendedVariable; + } + + public String getDefaultScale() + { + return defaultScale; + } + + public void setDefaultScale(String defaultScale) + { + this.defaultScale = defaultScale; + } + + public boolean getMvEnabled() + { + return mvEnabled; + } + + public void setMvEnabled(boolean mvEnabled) + { + this.mvEnabled = mvEnabled; + } + + public DefaultValueType getDefaultValueType() + { + return null==defaultValueType ? null : DefaultValueType.valueOf(defaultValueType); + } + + public void setDefaultValueType(DefaultValueType defaultValueType) + { + this.defaultValueType = null==defaultValueType ? null : defaultValueType.name(); + } + + public String getDefaultValue() + { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) + { + this.defaultValue = defaultValue; + } + + public String getDefaultDisplayValue() + { + return defaultDisplayValue; + } + + public void setDefaultDisplayValue(String defaultDisplayValue) + { + this.defaultDisplayValue = defaultDisplayValue; + } + + public String getFacetingBehaviorType() + { + return facetingBehaviorType; + } + + public void setFacetingBehaviorType(String facetingBehavior) + { + this.facetingBehaviorType = facetingBehavior; + } + + public String getPHI() + { + return phi; + } + + public void setPHI(String phi) + { + this.phi = phi; + } + + public boolean isSetExcludeFromShifting() + { + return isExcludeFromShifting != null; + } + + public boolean isExcludeFromShifting() + { + return isExcludeFromShifting != null && isExcludeFromShifting; + } + + public void setExcludeFromShifting(boolean isExcludeFromShifting) + { + this.isExcludeFromShifting = isExcludeFromShifting; + } + + public boolean getPreventReordering() + { + return isPreventReordering; + } + + public void setPreventReordering(boolean preventReordering) + { + this.isPreventReordering = preventReordering; + } + + public boolean getDisableEditing() + { + return isDisableEditing; + } + + public void setDisableEditing(boolean disableEditing) + { + this.isDisableEditing = disableEditing; + } + + public Integer getScale() + { + return scale; + } + + public void setScale(Integer value) + { + this.scale = value; + } + + public boolean isScannable() + { + return scannable; + } + + public void setScannable(boolean scannable) + { + this.scannable = scannable; + } + + public String getPrincipalConceptCode() { return principalConceptCode; } + + public void setPrincipalConceptCode(String code) { this.principalConceptCode = code; } + + public String getSourceOntology() + { + return sourceOntology; + } + + public void setSourceOntology(String sourceOntology) + { + this.sourceOntology = sourceOntology; + } + + public String getConceptSubtree() + { + return conceptSubtree; + } + + public void setConceptSubtree(String path) + { + this.conceptSubtree = path; + } + + public String getConceptImportColumn() + { + return conceptImportColumn; + } + + public void setConceptImportColumn(String conceptImportColumn) + { + this.conceptImportColumn = conceptImportColumn; + } + + public String getConceptLabelColumn() + { + return conceptLabelColumn; + } + + public void setConceptLabelColumn(String conceptLabelColumn) + { + this.conceptLabelColumn = conceptLabelColumn; + } + + public String getRedactedText() + { + return redactedText; + } + + public void setRedactedText(String redactedText) + { + this.redactedText = redactedText; + } + + public String getDerivationDataScope() + { + return derivationDataScope; + } + + public void setDerivationDataScope(String derivationDataScope) + { + this.derivationDataScope = derivationDataScope; + } + + public String getValueExpression() + { + return valueExpression; + } + + public void setValueExpression(String valueExpression) + { + this.valueExpression = valueExpression; + } + + public boolean getIsPrimaryKey() + { + return isPrimaryKey; + } + + /** This method is for informational purpose only so that the client can identify column as a PK column. + * Setting PK on a column via this method will not get preserved in the domain's table. + */ + public void setIsPrimaryKey(boolean isPrimaryKey) + { + this.isPrimaryKey = isPrimaryKey; + } + + public String getLockType() + { + return lockType; + } + + /** This method is for informational purpose only so that the client can identify column's locked type. + * Setting lock type on a column via this method will not get preserved in the domain's table. + */ + public void setLockType(String lockType) + { + this.lockType = lockType; + } + + public String debugString() + { + return getName() + " " + getLabel() + " " + getRangeURI() + " " + isRequired() + " " + getDescription(); + } + + public String getImportAliases() + { + return importAliases; + } + + public void setImportAliases(String importAliases) + { + this.importAliases = importAliases; + } + + public String getURL() + { + return url; + } + + public void setURL(String url) + { + this.url = url; + } + + public String getURLTarget() + { + return urlTarget; + } + + public void setURLTarget(String urlTarget) + { + this.urlTarget = urlTarget; + } + + public String getLookupDescription() + { + if (StringUtils.isEmpty(getLookupSchema()) || StringUtils.isEmpty(getLookupQuery())) + return "(none)"; + + return getLookupSchema() + "." + getLookupQuery(); + } + + @Override + public String toString() + { + return name + ": " + rangeURI; + } + + public boolean isFileType() + { + return "http://cpas.fhcrc.org/exp/xml#fileLink".equals(getRangeURI()) || + "http://www.labkey.org/exp/xml#attachment".equals(getRangeURI()); + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyValidator.java b/api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java similarity index 90% rename from api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyValidator.java rename to api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java index 2c0d9a24729..c3d82774b53 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyValidator.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java @@ -1,200 +1,200 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import com.google.gwt.user.client.rpc.IsSerializable; -import org.labkey.api.gwt.client.util.PropertyUtil; -import org.labkey.api.gwt.client.util.StringUtils; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/* -* User: Karl Lum -* Date: Aug 8, 2008 -* Time: 3:49:42 PM -*/ -public class GWTPropertyValidator implements Serializable, IsSerializable -{ - private long _rowId; - private String _name; - private PropertyValidatorType _type; - private String _description; - private String _expression; - private String _errorMessage; - private boolean _isNew; - - private Map _properties = new HashMap<>(); - private Map> _extraProperties = new HashMap<>(); - - public GWTPropertyValidator() - { - _isNew = true; - } - - public GWTPropertyValidator(GWTPropertyValidator s) - { - _copyProperties(s, this); - } - - public void copy(GWTPropertyValidator s) - { - _copyProperties(s, this); - } - - private static void _copyProperties(GWTPropertyValidator s, GWTPropertyValidator d) - { - d.setRowId(s.getRowId()); - d.setName(s.getName()); - d.setType(s.getType()); - d.setDescription(s.getDescription()); - d.setExpression(s.getExpression()); - d.setErrorMessage(s.getErrorMessage()); - d.getProperties().putAll(s.getProperties()); - d.setNew(s.isNew()); - } - - public long getRowId() - { - return _rowId; - } - - public void setRowId(long rowId) - { - _rowId = rowId; - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public PropertyValidatorType getType() - { - return _type; - } - - public void setType(PropertyValidatorType type) - { - _type = type; - } - - public String getDescription() - { - return _description; - } - - public void setDescription(String description) - { - _description = description; - } - - public String getExpression() - { - return _expression; - } - - public void setExpression(String expression) - { - _expression = expression; - } - - public String getErrorMessage() - { - return _errorMessage; - } - - public void setErrorMessage(String errorMessage) - { - _errorMessage = errorMessage; - } - - public Map getProperties() - { - return _properties; - } - - public void setProperties(Map properties) - { - _properties = properties; - } - - public Map> getExtraProperties() - { - return _extraProperties; - } - - public void setExtraProperties(Map> extraProperties) - { - _extraProperties = extraProperties; - } - - public boolean isNew() - { - return _isNew; - } - - public void setNew(boolean aNew) - { - _isNew = aNew; - } - - public void validate(List errors) - { - if (StringUtils.trimToNull(getName()) == null) - errors.add("Validator Name cannot be blank"); - if (StringUtils.trimToNull(getExpression()) == null) - errors.add("Validator Expression cannot be blank"); - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null) return false; - if (!(o instanceof GWTPropertyValidator that)) - return false; - - if (getRowId() != that.getRowId()) return false; - if (!StringUtils.equals(getName(), that.getName())) return false; - if (!PropertyUtil.nullSafeEquals(getType(), that.getType())) return false; - if (!StringUtils.equals(getDescription(), that.getDescription())) return false; - if (!StringUtils.equals(getExpression(), that.getExpression())) return false; - if (!StringUtils.equals(getErrorMessage(), that.getErrorMessage())) return false; - if (!getProperties().equals(that.getProperties())) return false; - - return true; - } - - public int hashCode() - { - int result = (int)getRowId(); - result = 31 * result + (getName() != null ? getName().hashCode() : 0); - result = 31 * result + (getType() != null ? getType().hashCode() : 0); - result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0); - result = 31 * result + (getExpression() != null ? getExpression().hashCode() : 0); - result = 31 * result + (getErrorMessage() != null ? getErrorMessage().hashCode() : 0); - result = 31 * result + (getProperties() != null ? getProperties().hashCode() : 0); - - return result; - } +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +import com.google.api.client.util.Objects; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* +* User: Karl Lum +* Date: Aug 8, 2008 +* Time: 3:49:42 PM +*/ +public class GWTPropertyValidator implements Serializable +{ + private long _rowId; + private String _name; + private PropertyValidatorType _type; + private String _description; + private String _expression; + private String _errorMessage; + private boolean _isNew; + + private Map _properties = new HashMap<>(); + private Map> _extraProperties = new HashMap<>(); + + public GWTPropertyValidator() + { + _isNew = true; + } + + public GWTPropertyValidator(GWTPropertyValidator s) + { + _copyProperties(s, this); + } + + public void copy(GWTPropertyValidator s) + { + _copyProperties(s, this); + } + + private static void _copyProperties(GWTPropertyValidator s, GWTPropertyValidator d) + { + d.setRowId(s.getRowId()); + d.setName(s.getName()); + d.setType(s.getType()); + d.setDescription(s.getDescription()); + d.setExpression(s.getExpression()); + d.setErrorMessage(s.getErrorMessage()); + d.getProperties().putAll(s.getProperties()); + d.setNew(s.isNew()); + } + + public long getRowId() + { + return _rowId; + } + + public void setRowId(long rowId) + { + _rowId = rowId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public PropertyValidatorType getType() + { + return _type; + } + + public void setType(PropertyValidatorType type) + { + _type = type; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public String getExpression() + { + return _expression; + } + + public void setExpression(String expression) + { + _expression = expression; + } + + public String getErrorMessage() + { + return _errorMessage; + } + + public void setErrorMessage(String errorMessage) + { + _errorMessage = errorMessage; + } + + public Map getProperties() + { + return _properties; + } + + public void setProperties(Map properties) + { + _properties = properties; + } + + public Map> getExtraProperties() + { + return _extraProperties; + } + + public void setExtraProperties(Map> extraProperties) + { + _extraProperties = extraProperties; + } + + public boolean isNew() + { + return _isNew; + } + + public void setNew(boolean aNew) + { + _isNew = aNew; + } + + public void validate(List errors) + { + if (StringUtils.trimToNull(getName()) == null) + errors.add("Validator Name cannot be blank"); + if (StringUtils.trimToNull(getExpression()) == null) + errors.add("Validator Expression cannot be blank"); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof GWTPropertyValidator that)) + return false; + + if (getRowId() != that.getRowId()) return false; + if (!StringUtils.equals(getName(), that.getName())) return false; + Object o1 = getType(); + if (!Objects.equal(o1, that.getType())) return false; + if (!StringUtils.equals(getDescription(), that.getDescription())) return false; + if (!StringUtils.equals(getExpression(), that.getExpression())) return false; + if (!StringUtils.equals(getErrorMessage(), that.getErrorMessage())) return false; + if (!getProperties().equals(that.getProperties())) return false; + + return true; + } + + public int hashCode() + { + int result = (int)getRowId(); + result = 31 * result + (getName() != null ? getName().hashCode() : 0); + result = 31 * result + (getType() != null ? getType().hashCode() : 0); + result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0); + result = 31 * result + (getExpression() != null ? getExpression().hashCode() : 0); + result = 31 * result + (getErrorMessage() != null ? getErrorMessage().hashCode() : 0); + result = 31 * result + (getProperties() != null ? getProperties().hashCode() : 0); + + return result; + } } \ No newline at end of file diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/PropertyValidatorType.java b/api/src/org/labkey/api/gwt/client/model/PropertyValidatorType.java similarity index 60% rename from api/gwtsrc/org/labkey/api/gwt/client/model/PropertyValidatorType.java rename to api/src/org/labkey/api/gwt/client/model/PropertyValidatorType.java index 845826d6466..70b1803782c 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/PropertyValidatorType.java +++ b/api/src/org/labkey/api/gwt/client/model/PropertyValidatorType.java @@ -1,143 +1,109 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import org.labkey.api.gwt.client.ui.HelpPopup; - -/** - * User: jeckels - * Date: Oct 10, 2010 - */ -public enum PropertyValidatorType -{ - RegEx - { - @Override - public String getLabel() - { - return "RegEx Validator"; - } - - @Override - public HelpPopup createHelpPopup() - { - return new HelpPopup(getLabel(), "RegEx validators allow you to specify a regular expression that defines what string values are valid"); - } - }, - Range - { - @Override - public String getLabel() - { - return "Range Validator"; - } - - @Override - public HelpPopup createHelpPopup() - { - return new HelpPopup(getLabel(), "Range validators allow you to specify numeric comparisons that must be satisfied, such as the value must be greater than or less than some constant"); - } - }, - Lookup - { - @Override - public String getLabel() - { - return "Lookup Validator"; - } - - @Override - public HelpPopup createHelpPopup() - { - return new HelpPopup(getLabel(), "Lookup validators allow you to require that any value is present in the lookup's target table or query"); - } - - @Override - public boolean isConfigurable() - { - return false; - } - }, - TextLength - { - @Override - public String getLabel() - { - return "Length Validator"; - } - - @Override - public HelpPopup createHelpPopup() - { - return new HelpPopup(getLabel(), "Length validators allow you to validate the length of a text field"); - } - @Override - public boolean isConfigurable() - { - return false; - } - @Override - public boolean isHidden() - { - return true; - } - }, - TextChoice - { - @Override - public String getLabel() - { - return "Text Choice Validator"; - } - - @Override - public HelpPopup createHelpPopup() - { - return new HelpPopup(getLabel(), "Text Choice validators allow you to specify a set of text values that are used to constrain values for a given domain field, like a light-weight lookup."); - } - }; - - public abstract HelpPopup createHelpPopup(); - - public abstract String getLabel(); - - public String getTypeName() - { - return toString().toLowerCase(); - } - - public boolean isConfigurable() - { - return true; - } - - public boolean isHidden() - { - return false; - } - - public static PropertyValidatorType getType(String typeName) - { - for (PropertyValidatorType type : values()) - { - if (type.getTypeName().equals(typeName)) - { - return type; - } - } - return null; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +/** + * User: jeckels + * Date: Oct 10, 2010 + */ +public enum PropertyValidatorType +{ + RegEx + { + @Override + public String getLabel() + { + return "RegEx Validator"; + } + }, + Range + { + @Override + public String getLabel() + { + return "Range Validator"; + } + }, + Lookup + { + @Override + public String getLabel() + { + return "Lookup Validator"; + } + + @Override + public boolean isConfigurable() + { + return false; + } + }, + TextLength + { + @Override + public String getLabel() + { + return "Length Validator"; + } + @Override + public boolean isConfigurable() + { + return false; + } + @Override + public boolean isHidden() + { + return true; + } + }, + TextChoice + { + @Override + public String getLabel() + { + return "Text Choice Validator"; + } + }; + + public abstract String getLabel(); + + public String getTypeName() + { + return toString().toLowerCase(); + } + + public boolean isConfigurable() + { + return true; + } + + public boolean isHidden() + { + return false; + } + + public static PropertyValidatorType getType(String typeName) + { + for (PropertyValidatorType type : values()) + { + if (type.getTypeName().equals(typeName)) + { + return type; + } + } + return null; + } +} diff --git a/api/gwtsrc/org/labkey/api/gwt/client/ui/PropertyType.java b/api/src/org/labkey/api/gwt/client/ui/PropertyType.java similarity index 100% rename from api/gwtsrc/org/labkey/api/gwt/client/ui/PropertyType.java rename to api/src/org/labkey/api/gwt/client/ui/PropertyType.java diff --git a/api/src/org/labkey/api/gwt/server/BaseRemoteService.java b/api/src/org/labkey/api/gwt/server/BaseRemoteService.java deleted file mode 100644 index 16ef58e9145..00000000000 --- a/api/src/org/labkey/api/gwt/server/BaseRemoteService.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2008-2019 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.api.gwt.server; - -import com.google.gwt.user.server.rpc.jakarta.RemoteServiceServlet; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.module.ModuleLoader; -import org.labkey.api.security.User; -import org.labkey.api.settings.LookAndFeelProperties; -import org.labkey.api.util.ExceptionUtil; -import org.labkey.api.view.ActionURL; -import org.labkey.api.view.UnauthorizedException; -import org.labkey.api.view.ViewContext; - -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Enumeration; -import java.util.Vector; - -/** - * User: brittp - * Date: Feb 2, 2007 - * Time: 2:30:51 PM - */ -public abstract class BaseRemoteService extends RemoteServiceServlet -{ - protected ViewContext _context; - public BaseRemoteService(ViewContext context) - { - _context = context; - } - - public Container getContainer() - { - return _context.getContainer(); - } - - public User getUser() - { - return _context.getUser(); - } - - public ActionURL getActionURL() - { - return _context.getActionURL(); - } - - @Override - public String getServletName() - { - return getServletConfig().getServletName(); - } - - @Override - public ServletConfig getServletConfig() - { - return new ServletConfig() - { - @Override - public String getInitParameter(String string) - { - return null; - } - - @Override - public Enumeration getInitParameterNames() - { - return new Vector().elements(); - } - - @Override - public ServletContext getServletContext() - { - return ModuleLoader.getServletContext(); - } - - @Override - public String getServletName() - { - return "BaseRemoteService"; - } - }; - } - - @Override - protected void doUnexpectedFailure(Throwable failure) - { - failure = ExceptionUtil.unwrapException(failure); - ExceptionUtil.logExceptionToMothership(getThreadLocalRequest(), failure); - - HttpServletResponse response = getThreadLocalResponse(); - try - { - int status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - if (failure instanceof UnauthorizedException) - { - response.setHeader("WWW-Authenticate", "Basic realm=\"" + LookAndFeelProperties.getInstance(ContainerManager.getRoot()).getDescription() + "\""); - status = HttpServletResponse.SC_UNAUTHORIZED; - } - response.setContentType("text/plain"); - response.setStatus(status); - response.getWriter().write("There was an error processing the request."); - if (failure.getMessage() != null) - { - response.getWriter().write("\n" + failure.getMessage()); - } - response.getWriter().write("\n(Error type: " + failure.getClass().getName() + ")"); - } - catch (IOException e) - { - // Give up - } - catch (IllegalStateException e) - { - // Give up, response is already committed - } - - } - - public ViewContext getViewContext() - { - return _context; - } -} diff --git a/api/src/org/labkey/api/ontology/Ontology.java b/api/src/org/labkey/api/ontology/Ontology.java index 90afb47df33..9f2275148fb 100644 --- a/api/src/org/labkey/api/ontology/Ontology.java +++ b/api/src/org/labkey/api/ontology/Ontology.java @@ -1,9 +1,9 @@ package org.labkey.api.ontology; +import org.jetbrains.annotations.NotNull; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; -import jakarta.validation.constraints.NotNull; import java.util.Date; public interface Ontology // extends Entity diff --git a/api/src/org/labkey/api/pipeline/PipelineJob.java b/api/src/org/labkey/api/pipeline/PipelineJob.java index 534d5ad3c57..9f8678cc278 100644 --- a/api/src/org/labkey/api/pipeline/PipelineJob.java +++ b/api/src/org/labkey/api/pipeline/PipelineJob.java @@ -40,7 +40,6 @@ import org.labkey.api.assay.AssayFileWriter; import org.labkey.api.data.Container; import org.labkey.api.exp.api.ExpRun; -import org.labkey.api.gwt.client.util.PropertyUtil; import org.labkey.api.pipeline.file.FileAnalysisJobSupport; import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryKey; @@ -1977,28 +1976,28 @@ public List compareJobs(PipelineJob job2) { PipelineJob job1 = this; List errors = new ArrayList<>(); - if (!PropertyUtil.nullSafeEquals(job1._activeTaskId, job2._activeTaskId)) + if (!com.google.api.client.util.Objects.equal(job1._activeTaskId, job2._activeTaskId)) errors.add("_activeTaskId"); if (job1._activeTaskRetries != job2._activeTaskRetries) errors.add("_activeTaskRetries"); - if (!PropertyUtil.nullSafeEquals(job1._activeTaskStatus, job2._activeTaskStatus)) + if (!com.google.api.client.util.Objects.equal(job1._activeTaskStatus, job2._activeTaskStatus)) errors.add("_activeTaskStatus"); if (job1._errors != job2._errors) errors.add("_errors"); if (job1._interrupted != job2._interrupted) errors.add("_interrupted"); - if (!PropertyUtil.nullSafeEquals(job1._jobGUID, job2._jobGUID)) + if (!com.google.api.client.util.Objects.equal(job1._jobGUID, job2._jobGUID)) errors.add("_jobGUID"); - if (!PropertyUtil.nullSafeEquals(job1._logFile, job2._logFile)) + if (!com.google.api.client.util.Objects.equal(job1._logFile, job2._logFile)) { if (null == job1._logFile || null == job2._logFile) errors.add("_logFile"); else if (!FileUtil.getAbsoluteCaseSensitiveFile(job1._logFile.toFile()).getAbsolutePath().equalsIgnoreCase(FileUtil.getAbsoluteCaseSensitiveFile(job2._logFile.toFile()).getAbsolutePath())) errors.add("_logFile"); } - if (!PropertyUtil.nullSafeEquals(job1._parentGUID, job2._parentGUID)) + if (!com.google.api.client.util.Objects.equal(job1._parentGUID, job2._parentGUID)) errors.add("_parentGUID"); - if (!PropertyUtil.nullSafeEquals(job1._provider, job2._provider)) + if (!com.google.api.client.util.Objects.equal(job1._provider, job2._provider)) errors.add("_provider"); if (job1._submitted != job2._submitted) errors.add("_submitted"); diff --git a/api/src/org/labkey/api/query/CrosstabExcelWriter.java b/api/src/org/labkey/api/query/CrosstabExcelWriter.java index 64c57ca45cd..d842c99a2ce 100644 --- a/api/src/org/labkey/api/query/CrosstabExcelWriter.java +++ b/api/src/org/labkey/api/query/CrosstabExcelWriter.java @@ -19,6 +19,7 @@ import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; import org.apache.poi.ss.usermodel.Sheet; +import org.jetbrains.annotations.NotNull; import org.labkey.api.data.CrosstabMember; import org.labkey.api.data.CrosstabTableInfo; import org.labkey.api.data.DisplayColumn; @@ -27,7 +28,6 @@ import org.labkey.api.data.ResultsFactory; import org.labkey.api.util.Pair; -import jakarta.validation.constraints.NotNull; import java.util.List; /** diff --git a/api/src/org/labkey/api/util/ExceptionUtil.java b/api/src/org/labkey/api/util/ExceptionUtil.java index 402eddb58b7..0a28c89becf 100644 --- a/api/src/org/labkey/api/util/ExceptionUtil.java +++ b/api/src/org/labkey/api/util/ExceptionUtil.java @@ -163,7 +163,7 @@ public static Throwable unwrapException(@NotNull Throwable ex) ex = cause; cause = null; - if (ex.getClass() == RuntimeException.class || ex.getClass() == UnexpectedException.class || ex.getClass() == RuntimeSQLException.class || ex instanceof InvocationTargetException || ex instanceof com.google.gwt.user.server.rpc.UnexpectedException) + if (ex.getClass() == RuntimeException.class || ex.getClass() == UnexpectedException.class || ex.getClass() == RuntimeSQLException.class || ex instanceof InvocationTargetException) { cause = ex.getCause(); } @@ -1611,7 +1611,7 @@ public void sendRedirect(String s) } @Override - public void sendRedirect(String s, int i, boolean b) throws IOException + public void sendRedirect(String s, int i, boolean b) { redirect = s; } diff --git a/api/src/org/labkey/api/util/OptionBuilder.java b/api/src/org/labkey/api/util/OptionBuilder.java index 8c2f3733ff9..58c4ab0efa9 100644 --- a/api/src/org/labkey/api/util/OptionBuilder.java +++ b/api/src/org/labkey/api/util/OptionBuilder.java @@ -1,6 +1,6 @@ package org.labkey.api.util; -import jakarta.validation.constraints.NotNull; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class OptionBuilder implements HasHtmlString, SafeToRender diff --git a/api/src/org/labkey/api/view/GWTView.java b/api/src/org/labkey/api/view/GWTView.java deleted file mode 100644 index 2d1da04ae7f..00000000000 --- a/api/src/org/labkey/api/view/GWTView.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2008-2019 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.api.view; - -import com.google.gwt.core.client.EntryPoint; -import org.labkey.api.compliance.ComplianceService; -import org.labkey.api.usageMetrics.SimpleMetricsService; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Wrapper around a GWT (Google Web Toolkit) module that knows how to render into the overall HTML of the page. - */ -public class GWTView extends JspView -{ - public static final String PROPERTIES_OBJECT_NAME = "LABKEY.GWTProperties"; - - public static class GWTViewBean - { - private final String _moduleName; - private final Map _properties; - - public GWTViewBean(String moduleName, Map properties) - { - _moduleName = moduleName; - _properties = new HashMap<>(properties); - } - - public void init(ViewContext context) - { - // Use Core as the catch-all to consolidate reporting, even though GWT uses are distributed across modules - SimpleMetricsService.get().increment("Core", "GWTView", context.getActionURL().getController() + "-" + context.getActionURL().getAction()); - - _properties.put("container", context.getContainer().getPath()); - _properties.put("controller", context.getActionURL().getController()); - _properties.put("action", context.getActionURL().getAction()); - _properties.put("queryString", context.getActionURL().getQueryString()); - _properties.put("contextPath", context.getContextPath()); - _properties.put("header1Size", "16px"); - _properties.put("loadingStyle", ""); - - _properties.put("maxAllowedPhi", ComplianceService.get().getMaxAllowedPhi(context.getContainer(), context.getUser()).name()); - } - - public String getModuleName() - { - return _moduleName; - } - - public Map getProperties() - { - return _properties; - } - - public String getLoadingStyleName() - { - return _properties.get("loadingStyle"); - } - - public void setLoadingStyleName(String name) - { - _properties.put("loadingStyle", name); - } - } - - private static String convertClassToModuleName(Class c) - { - String name = c.getName(); - int index = name.indexOf(".client."); - if (index != -1) - { - name = name.substring(0, index) + name.substring(index + ".client.".length() - 1); - } - return name; - } - - public GWTView(Class moduleClass, Map properties) - { - this(convertClassToModuleName(moduleClass), properties); - } - - public GWTView(String moduleName, Map properties) - { - super("/org/labkey/api/view/GWTView.jsp", new GWTViewBean(moduleName, properties)); - getModelBean().init(getViewContext()); - getModulesForRootContext().add(moduleName); - } - - public static Set getModulesForRootContext() - { - //Not synchronized since view rendering is single threaded - Set gwtModules = (Set) HttpView.getRootContext().get("gwtModules"); - if (null == gwtModules) - { - gwtModules = new HashSet<>(); - HttpView.getRootContext().put("gwtModules", gwtModules); - } - - return gwtModules; - } -} diff --git a/api/src/org/labkey/api/view/GWTView.jsp b/api/src/org/labkey/api/view/GWTView.jsp deleted file mode 100644 index fc3da72a77f..00000000000 --- a/api/src/org/labkey/api/view/GWTView.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<% -/* - * Copyright (c) 2008-2019 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. - */ -%> -<%@ page import="org.json.JSONObject" %> -<%@ page import="org.labkey.api.view.GWTView" %> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page extends="org.labkey.api.jsp.JspBase"%> -<% - HttpView me = HttpView.currentView(); - GWTView.GWTViewBean bean = me.getModelBean(); -%> -
-<% -String jsPath = bean.getModuleName() + "/" + bean.getModuleName() + ".nocache.js"; -%> - -<%=getScriptTag(jsPath)%> - diff --git a/assay/gwtsrc/gwt/AssayApplication.gwt.xml b/assay/gwtsrc/gwt/AssayApplication.gwt.xml deleted file mode 100644 index 0e66d7c545b..00000000000 --- a/assay/gwtsrc/gwt/AssayApplication.gwt.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/assay/gwtsrc/gwt/client/org/labkey/assay/AssayApplication.java b/assay/gwtsrc/gwt/client/org/labkey/assay/AssayApplication.java deleted file mode 100644 index fcda0059d86..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/assay/AssayApplication.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2010-2019 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 gwt.client.org.labkey.assay; - -import com.google.gwt.core.client.EntryPoint; -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.RunAsyncCallback; -import com.google.gwt.user.client.ui.RootPanel; -import org.labkey.api.gwt.client.util.ErrorDialogAsyncCallback; -import org.labkey.api.gwt.client.util.PropertyUtil; - -import java.util.HashSet; -import java.util.Set; - -/** - * User: matthewb - * Date: Mar 31, 2010 - * Time: 1:59:56 PM - */ -public class AssayApplication implements EntryPoint -{ - /** - * As of 2.4.0, the GWT compiler NPEs if you pass an enum value in to GWT.runAsync(). Therefore, this is enum-like - * but not an actual enum anymore. - */ - public abstract static class GWTModule implements RunAsyncCallback - { - public final String className; - - private static final Set MODULES = new HashSet<>(); - - static - { - MODULES.add(new TemplateDesigner()); - } - - GWTModule(String clss) - { - this.className = clss; - } - - abstract EntryPoint getEntryPoint(); - - // - // RunAsyncCallback - // - @Override - public void onFailure(Throwable caught) - { - ErrorDialogAsyncCallback.showDialog(caught, "Failed to load code for module: " + getClass()); - } - - public static Set values() - { - return MODULES; - } - } - - public static class TemplateDesigner extends GWTModule - { - public TemplateDesigner() - { - super("gwt.client.org.labkey.plate.designer.client.TemplateDesigner"); - } - - @Override - public void onSuccess() - { - new gwt.client.org.labkey.plate.designer.client.TemplateDesigner().onModuleLoad(); - } - - @Override - EntryPoint getEntryPoint() - { - return new gwt.client.org.labkey.plate.designer.client.TemplateDesigner(); - } - } - - - public static RootPanel getRootPanel() - { - String name = PropertyUtil.getServerProperty("RootPanel"); - if (null == name) - name = "gwt.AssayApplication-Root"; - return RootPanel.get(name); - } - - - @Override - public void onModuleLoad() - { - RootPanel panel = getRootPanel(); - if (null != panel) - { - panel.getElement().setInnerHTML(""); - } - - final String moduleName = PropertyUtil.getServerProperty("GWTModule"); - - if ("TemplateDesigner".equalsIgnoreCase(moduleName)) - { - GWT.runAsync(new TemplateDesigner()); - } - else - { - throw new IllegalArgumentException("Unknown module: " + moduleName); - } - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListener.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListener.java deleted file mode 100644 index 04ac813d65f..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListener.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; - -/** - * User: brittp - * Date: Feb 7, 2007 - * Time: 4:17:33 PM - */ -public interface GroupChangeListener -{ - void groupAdded(GWTWellGroup group); - - void groupRemoved(GWTWellGroup group); - - void activeGroupChanged(GWTWellGroup previouslyActive, GWTWellGroup currentlyActive); - - void activeGroupTypeChanged(String type); -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListenerAdapter.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListenerAdapter.java deleted file mode 100644 index a6cb185bea0..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListenerAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; - -/** - * User: brittp - * Date: Feb 9, 2007 - * Time: 12:28:27 PM - */ -public abstract class GroupChangeListenerAdapter implements GroupChangeListener -{ - @Override - public void activeGroupChanged(GWTWellGroup previouslyActive, GWTWellGroup currentlyActive) - { - } - - @Override - public void groupAdded(GWTWellGroup group) - { - } - - @Override - public void groupRemoved(GWTWellGroup group) - { - } - - @Override - public void activeGroupTypeChanged(String type) - { - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanel.java deleted file mode 100644 index 54c7c43509f..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanel.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright (c) 2010-2018 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.dom.client.KeyDownEvent; -import com.google.gwt.event.dom.client.KeyDownHandler; -import com.google.gwt.event.dom.client.KeyUpEvent; -import com.google.gwt.event.dom.client.KeyUpHandler; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.DeferredCommand; -import com.google.gwt.user.client.IncrementalCommand; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.DialogBox; -import com.google.gwt.user.client.ui.FlexTable; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.ListBox; -import com.google.gwt.user.client.ui.ScrollPanel; -import com.google.gwt.user.client.ui.TextBox; -import com.google.gwt.user.client.ui.Widget; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import org.labkey.api.gwt.client.ui.BoundTextBox; -import org.labkey.api.gwt.client.ui.ImageButton; -import org.labkey.api.gwt.client.util.StringProperty; - -import java.util.List; -import java.util.Map; - -public class GroupTypePanel extends ScrollPanel implements GroupChangeListener -{ - private final TemplateView _view; - private final String _type; - - private String _newGroupFieldValue; - private ImageButton _createButton; - private ImageButton _multiCreateButton; - private boolean _allowNewGroups; - - public GroupTypePanel(TemplateView view, String type) - { - _type = type; - _view = view; - _view.addGroupListener(this); - redraw(); - } - - public void redraw() - { - clear(); - List wellgroups = _view.getPlate().getTypeToGroupsMap().get(_type); - FlexTable groupList = new FlexTable(); - groupList.setCellPadding(5); - if (wellgroups != null) - { - int i=0; - _allowNewGroups = wellgroups.isEmpty(); - for (GWTWellGroup group : wellgroups) - { - if (!_allowNewGroups && group.isAllowNewGroups()) - _allowNewGroups = true; - - GroupTypePanelRow row = new GroupTypePanelRow(_view, group); - row.attach(groupList, i++); - } - } - else - { - _allowNewGroups = true; - groupList.setWidget(0, 1, new Label("No groups defined.")); - } - add(groupList); - - KeyDownHandler fieldKeyDownListener = new KeyDownHandler() - { - @Override - public void onKeyDown(KeyDownEvent event) - { - if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) - { - // we might want to remove this capability (create wellgroup using keyboard ENTER), the later - // versions of GWT don't allow access to the raw value and the current behavior is components - // don't update their value until a blur event - _view.createWellGroup(_newGroupFieldValue, _type); - } - } - }; - - KeyUpHandler fieldKeyUpListener = new KeyUpHandler() - { - @Override - public void onKeyUp(KeyUpEvent event) - { - _createButton.setEnabled(true); - _multiCreateButton.setEnabled(true); - } - }; - - _createButton = new ImageButton("Create"); - _createButton.setEnabled(false); - _createButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent event) - { - _view.createWellGroup(_newGroupFieldValue, _type); - } - }); - - _multiCreateButton = new ImageButton("Create multiple..."); - _multiCreateButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent event) - { - String defaultBaseName = _newGroupFieldValue; - - MultiCreatePopupPanel multiCreatePanel = new MultiCreatePopupPanel(_view, defaultBaseName, _type); - multiCreatePanel.center(); - multiCreatePanel.show(); - } - }); - - // if the group type is furnishing any default group names, populate them into a combo box - // else just use a text field - Map> typeToGroupMap = _view.getPlate().getTypesToDefaultGroups(); - Widget newGroupField; - - if (!typeToGroupMap.containsKey(_type)) - { - final TextBox textField = new TextBox(); - DOM.setElementAttribute(textField.getElement(), "name", "wellGroupName"); - DOM.setElementAttribute(textField.getElement(), "data-type", _type); - - textField.addKeyDownHandler(fieldKeyDownListener); - textField.addKeyUpHandler(fieldKeyUpListener); - textField.addChangeHandler(changeEvent -> { - TextBox source = (TextBox) changeEvent.getSource(); - _newGroupFieldValue = source.getValue(); - }); - - newGroupField = textField; - } - else - { - List defaults = typeToGroupMap.get(_type); - final ListBox selector = new ListBox(); - - selector.addKeyDownHandler(fieldKeyDownListener); - selector.addKeyUpHandler(fieldKeyUpListener); - selector.addChangeHandler(changeEvent -> { - ListBox source = (ListBox)changeEvent.getSource(); - _newGroupFieldValue = source.getSelectedValue(); - - boolean enable = (!_newGroupFieldValue.isEmpty()); - - _createButton.setEnabled(enable); - _multiCreateButton.setEnabled(enable); - }); - - for (String name : defaults) - selector.addItem(name); - - selector.setItemSelected(0, true); - _newGroupFieldValue = defaults.get(0); - _createButton.setEnabled(true); - - newGroupField = selector; - } - - if (_allowNewGroups) - { - groupList.setWidget(groupList.getRowCount(), 0, new Label("New:")); - groupList.setWidget(groupList.getRowCount() - 1, 1, newGroupField); - groupList.setWidget(groupList.getRowCount() - 1, 2, _createButton); - groupList.setWidget(groupList.getRowCount() - 1, 3, _multiCreateButton); - } - } - - private static class MultiCreatePopupPanel extends DialogBox - { - StringProperty _count = new StringProperty("2"); - StringProperty _baseName = new StringProperty(); - public MultiCreatePopupPanel(final TemplateView view, final String baseName, final String type) - { - super(true, false); - _baseName.set(baseName); - setText("Create Multiple Groups"); - int row = 0; - FlexTable options = new FlexTable(); - - ImageButton createButton = new ImageButton("Create"); - createButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent event) - { - try - { - final int count = Integer.parseInt(_count.getString()); - MultiCreatePopupPanel.this.clear(); - final Label status = new Label("Creating " + _baseName + " 1..."); - MultiCreatePopupPanel.this.add(status); - // Use a deferred command to allow the UI to update after each well group is added: - DeferredCommand.addCommand(new IncrementalCommand() - { - private int _created = 0; - @Override - public boolean execute() - { - String groupName = _baseName + " " + (_created + 1); - view.createWellGroup(groupName, type); - if (++_created < count) - { - // Set the status for the next item. The UI will refresh now, after we return, but before - // the next item is created. The more intuitive approach of setting the status right before - // the call to createWellGroup doesn't work, since the UI won't refresh between the calls. - status.setText("Creating " + _baseName + " " + (_created + 1) + "..."); - return true; - } - else - { - MultiCreatePopupPanel.this.hide(); - return false; - } - } - }); - } - catch (NumberFormatException e) - { - Window.alert("\"" + _count.getString() + "\" is not a valid count."); - } - } - }); - - ImageButton cancelButton = new ImageButton("Cancel"); - cancelButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent event) - { - MultiCreatePopupPanel.this.hide(); - } - }); - - options.setWidget(row, 0, new Label("Base Name")); - options.setWidget(row++, 1, new BoundTextBox("Base Name", "baseName", _baseName)); - - options.setWidget(row, 0, new Label("Count")); - options.setWidget(row++, 1, new BoundTextBox("Count", "createCount", _count) - { - @Override - protected String validateValue(String text) - { - try - { - int count = Integer.parseInt(text); - return super.validateValue(text); - } - catch (NumberFormatException e) - { - return "\"" + text + "\" is not a valid count."; - } - } - }); - - HorizontalPanel buttonBar = new HorizontalPanel(); - buttonBar.add(cancelButton); - buttonBar.add(createButton); - options.setWidget(row++, 1, buttonBar); - add(options); - } - - } - - - @Override - public void activeGroupChanged(GWTWellGroup previouslyActive, GWTWellGroup currentlyActive) - { - } - - @Override - public void activeGroupTypeChanged(String type) - { - } - - @Override - public void groupAdded(GWTWellGroup group) - { - redraw(); - } - - @Override - public void groupRemoved(GWTWellGroup group) - { - redraw(); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanelRow.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanelRow.java deleted file mode 100644 index 2701dc33343..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanelRow.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.ui.*; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.Element; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import org.labkey.api.gwt.client.ui.ImageButton; -import org.labkey.api.gwt.client.ui.TextBoxDialogBox; - -/** - * User: brittp - * Date: Feb 7, 2007 - * Time: 3:28:08 PM - */ -public class GroupTypePanelRow extends GroupChangeListenerAdapter -{ - private final TemplateView _view; - private final GWTWellGroup _group; - private FocusPanel _colorPanel; - private Element _tableRowElement; - private RadioButton _radioButton; - private ImageButton _deleteButton; - private ImageButton _renameButton; - - public GroupTypePanelRow(TemplateView view, GWTWellGroup group) - { - _view = view; - _group = group; - } - - public void attach(FlexTable parent, int row) - { - int col = 0; - - _view.addGroupListener(this); - _colorPanel = new FocusPanel(); - DOM.setStyleAttribute(_colorPanel.getElement(), "border", "1px solid white"); - DOM.setStyleAttribute(_colorPanel.getElement(), "backgroundColor", "#" + _view.getColor(_group)); - _colorPanel.setSize("35px", "25px"); - parent.setWidget(row, col++, _colorPanel); - - MouseListener hoverListener = new MouseListenerAdapter() - { - @Override - public void onMouseEnter(Widget sender) - { - DOM.setStyleAttribute(_colorPanel.getElement(), "border", "1px solid black"); - _view.setHighlightGroup(_group, true); - } - - @Override - public void onMouseLeave(Widget sender) - { - DOM.setStyleAttribute(_colorPanel.getElement(), "border", "1px solid white"); - _view.setHighlightGroup(_group, false); - } - }; - - ClickListener clickListener = new ClickListener() - { - @Override - public void onClick(Widget sender) - { - _view.setActiveGroup(_group); - } - }; - - _colorPanel.addClickListener(clickListener); - - _radioButton = new RadioButton("wellGroup", _group.getName()); - FocusPanel radioButtonFocusPanel = new FocusPanel(); - radioButtonFocusPanel.addMouseListener(hoverListener); - radioButtonFocusPanel.add(_radioButton); - parent.setWidget(row, col++, radioButtonFocusPanel); - _radioButton.addClickListener(clickListener); - - _colorPanel.addMouseListener(hoverListener); - - _deleteButton = new ImageButton("Delete"); - _deleteButton.setVisible(false); - _deleteButton.addClickListener(new ClickListener() - { - @Override - public void onClick(Widget sender) - { - if (Window.confirm("Delete well group \"" + _group.getName() + "\"?")) - _view.deleteWellGroup(_group); - } - }); - parent.setWidget(row, col++, _deleteButton); - - _renameButton = new ImageButton("Rename"); - _renameButton.setVisible(false); - _renameButton.addClickListener(new ClickListener() - { - @Override - public void onClick(Widget sender) - { - TextBoxDialogBox dialog = new TextBoxDialogBox("Rename Well Group", "Name") - { - @Override - protected boolean commit(String value) - { - if (value.isEmpty()) - { - Window.alert("You must specify a well group name."); - return false; - } - _group.setName(value); - _radioButton.setText(value); - _view.markAsDirty(); - return true; - } - }; - dialog.show(_group.getName()); - } - }); - parent.setWidget(row, col++, _renameButton); - - _tableRowElement = parent.getRowFormatter().getElement(row); - } - - @Override - public void activeGroupChanged(GWTWellGroup previouslyActive, GWTWellGroup currentlyActive) - { - if (_group == currentlyActive) - { - _radioButton.setChecked(true); - // if this group is effectively not editable never show delete and rename buttons - if (_group.isAllowNewGroups()) - { - _deleteButton.setVisible(true); - _renameButton.setVisible(true); - } - DOM.setStyleAttribute(_tableRowElement, "backgroundColor", "#DDDDDD"); - DOM.setStyleAttribute(_tableRowElement, "border", "1px solid black"); - } - else if (_group == previouslyActive) - { - _radioButton.setChecked(false); - _deleteButton.setVisible(false); - _renameButton.setVisible(false); - DOM.setStyleAttribute(_tableRowElement, "backgroundColor", "#FFFFFF"); - DOM.setStyleAttribute(_tableRowElement, "border", "1px solid white"); - } - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypesTabPanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypesTabPanel.java deleted file mode 100644 index 23365ed08c4..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypesTabPanel.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2010-2016 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.event.dom.client.MouseDownEvent; -import com.google.gwt.event.dom.client.MouseDownHandler; -import com.google.gwt.event.dom.client.MouseMoveEvent; -import com.google.gwt.event.dom.client.MouseMoveHandler; -import com.google.gwt.event.dom.client.MouseUpEvent; -import com.google.gwt.event.dom.client.MouseUpHandler; -import com.google.gwt.user.client.ui.FocusPanel; -import com.google.gwt.user.client.ui.HasHorizontalAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.TabPanel; -import com.google.gwt.user.client.ui.TabListener; -import com.google.gwt.user.client.ui.SourcesTabEvents; -import com.google.gwt.user.client.ui.Grid; - -import java.util.List; - -/** - * User: brittp - * Date: Feb 7, 2007 - * Time: 11:14:32 AM - */ -public class GroupTypesTabPanel extends TabPanel -{ - private final TemplateView _view; - private final List _types; - private int _gridTab = -1; - - public GroupTypesTabPanel(TemplateView view) - { - setHeight("100%"); - _view = view; - _types = view.getPlate().getGroupTypes(); - for (String type : _types) - { - Grid tabPanelGrid = new Grid(2, 1); - HorizontalPanel lowerPane = new HorizontalPanel(); - lowerPane.setWidth("100%"); - lowerPane.add(new GroupTypePanel(view, type)); - ShiftPanel shifter = new ShiftPanel(view); - lowerPane.add(shifter); - lowerPane.setCellHorizontalAlignment(shifter, HasHorizontalAlignment.ALIGN_RIGHT); - tabPanelGrid.setWidget(1, 0, lowerPane); - add(tabPanelGrid, type); - } - selectTab(0); - addTabListener(new TabListener() - { - @Override - public boolean onBeforeTabSelected(SourcesTabEvents sender, int tabIndex) - { - return true; - } - - @Override - public void onTabSelected(SourcesTabEvents sender, int tabIndex) - { - _view.setActiveType(_types.get(tabIndex)); - moveGrid(tabIndex); - } - }); - } - - @Override - public void selectTab(int index) - { - super.selectTab(index); - moveGrid(index); - } - - private void moveGrid(int selectedTab) - { - if (_gridTab >= 0) - { - Grid prevPanel = (Grid) getDeckPanel().getWidget(_gridTab); - prevPanel.remove(_view.getGrid()); - } - Grid currentPanel = (Grid) getDeckPanel().getWidget(selectedTab); - - TemplateGrid wellGrid = _view.getGrid(); - // Use a focus panel to prevent mouse events within the grid from being propagated to the browser. - // This is necessary to enable click and drag functionality that doesn't perform browser-provided text selection. - FocusPanel focusPanel = new FocusPanel(wellGrid); - focusPanel.addMouseDownHandler(new MouseDownHandler() - { - @Override - public void onMouseDown(MouseDownEvent event) - { - event.preventDefault(); - } - }); - focusPanel.addMouseUpHandler(new MouseUpHandler() - { - @Override - public void onMouseUp(MouseUpEvent event) - { - event.preventDefault(); - } - }); - focusPanel.addMouseMoveHandler(new MouseMoveHandler() - { - @Override - public void onMouseMove(MouseMoveEvent event) - { - event.preventDefault(); - } - }); - - currentPanel.setWidget(0, 0, focusPanel); - _gridTab = selectedTab; - } - - public void redraw() - { - Grid panel = (Grid) getWidget(getTabBar().getSelectedTab()); - GroupTypePanel typesPanel = (GroupTypePanel) panel.getWidget(1, 0); - typesPanel.redraw(); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataService.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataService.java deleted file mode 100644 index a94e4956645..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataService.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2010-2015 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.rpc.RemoteService; -import com.google.gwt.user.client.rpc.SerializableException; -import gwt.client.org.labkey.plate.designer.client.model.GWTPlate; - -/** - * User: brittp -* Date: Jan 31, 2007 -* Time: 2:37:12 PM -*/ -public interface PlateDataService extends RemoteService -{ - GWTPlate getTemplateDefinition(String templateName, int plateId, String assayTypeName, String templateTypeName, int rowCount, int columnCount, boolean copyTemplate) throws SerializableException, Exception; - - long saveChanges(GWTPlate plate, boolean replaceIfExisting) throws SerializableException, Exception; - -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataServiceAsync.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataServiceAsync.java deleted file mode 100644 index 34e0431f2d0..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataServiceAsync.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2010-2015 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import gwt.client.org.labkey.plate.designer.client.model.GWTPlate; - -/** - * User: brittp - * Date: Jan 31, 2007 - * Time: 2:37:25 PM - */ -public interface PlateDataServiceAsync -{ - void getTemplateDefinition(String templateName, int plateId, String assayTypeName, String templateTypeName, int rowCount, int columnCount, boolean copyTemplate, AsyncCallback async); - - void saveChanges(GWTPlate plate, boolean replaceIfExisting, AsyncCallback async); -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlatePropertyPanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlatePropertyPanel.java deleted file mode 100644 index 28dc45a9fd2..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlatePropertyPanel.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -/** - * User: jeckels - * Date: Apr 19, 2007 - */ -public class PlatePropertyPanel extends PropertyPanel -{ - public PlatePropertyPanel(TemplateView templateView) - { - super(templateView); - redraw(templateView.getPlate().getPlateProperties()); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyCreationDialog.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyCreationDialog.java deleted file mode 100644 index be63aad1b90..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyCreationDialog.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.Window; -import org.labkey.api.gwt.client.ui.TextBoxDialogBox; - -/** - * User: jeckels - * Date: Apr 20, 2007 - */ -public class PropertyCreationDialog extends TextBoxDialogBox -{ - private final PropertyPanel _panel; - - public PropertyCreationDialog(PropertyPanel panel) - { - super("Add Property", "Name"); - _panel = panel; - } - - @Override - protected boolean commit(String propName) - { - if (propName.isEmpty()) - { - Window.alert("You must specify a property name."); - return false; - } - return _panel.addProperty(propName); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyPanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyPanel.java deleted file mode 100644 index 283feb3f454..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyPanel.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (c) 2010-2011 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.ui.*; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.DOM; - -import java.util.*; - -import org.labkey.api.gwt.client.ui.ImageButton; -import org.labkey.api.gwt.client.util.PropertyUtil; - -/** - * User: brittp - * Date: Feb 8, 2007 - * Time: 1:23:51 PM - */ -@SuppressWarnings("Convert2Diamond") -public abstract class PropertyPanel extends DockPanel -{ - private final TemplateView _view; - private final FlexTable _propertyTable; - private Map _propertyTextBoxes; - - private final ChangeListener _changeListener = new ChangeListener() - { - @Override - public void onChange(Widget sender) - { - _view.markAsDirty(); - } - }; - - private final KeyboardListener _keyboardListener = new KeyboardListenerAdapter() - { - @Override - public void onKeyPress(Widget sender, char keyCode, int modifiers) - { - _view.markAsDirty(); - } - }; - - private final MouseListenerAdapter _tooltipListener = new MouseListenerAdapter() - { - private final PopupPanel _tooltip = new PopupPanel(true); - { - _tooltip.add(new Label("Delete")); - _tooltip.setStyleName("labkey-form-label"); - } - - @Override - public void onMouseEnter(Widget sender) - { - _tooltip.show(); - int width = _tooltip.getOffsetWidth(); - _tooltip.setPopupPosition(sender.getAbsoluteLeft() - width - 10, sender.getAbsoluteTop()); - } - - @Override - public void onMouseLeave(Widget sender) - { - _tooltip.hide(); - } - }; - private final ImageButton _addPropertyButton; - - public PropertyPanel(TemplateView view) - { - _view = view; - _propertyTable = new FlexTable(); - add(_propertyTable, CENTER); - - _addPropertyButton = new ImageButton("Add a new property"); - _addPropertyButton.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - PropertyCreationDialog dialog = new PropertyCreationDialog(PropertyPanel.this); - dialog.show(); - } - }); - } - - public void redraw(String message) - { - _propertyTable.clear(); - _propertyTextBoxes = new HashMap(); - _propertyTable.setWidget(0, 0, new Label(message)); - addNewPropertyButton(1); - _addPropertyButton.setEnabled(false); - } - - public Map getProperties() - { - Map result = new HashMap(); - for (String name : _propertyTextBoxes.keySet()) - result.put(name, ((TextBox) _propertyTextBoxes.get(name)).getText()); - return result; - } - - public void redraw(Map properties) - { - _propertyTable.clear(); - while (_propertyTable.getRowCount() > 0) - { - _propertyTable.removeRow(0); - } - _propertyTextBoxes = new HashMap(); - int row = 0; - if (properties != null) - { - List names = new ArrayList(properties.keySet()); - Collections.sort(names, String.CASE_INSENSITIVE_ORDER); - for (String name : names) - { - Object value = properties.get(name); - insertPropertyRow(row, name, value); - row++; - } - } - addNewPropertyButton(row); - _addPropertyButton.setEnabled(true); - } - - private void addNewPropertyButton(int row) - { - _propertyTable.setWidget(row, 0, _addPropertyButton); - _propertyTable.getFlexCellFormatter().setColSpan(row, 0, 3); - } - - private void insertPropertyRow(int row, final String name, Object value) - { - _propertyTable.setWidget(row, 0, new Label(name)); - _propertyTable.getFlexCellFormatter().setStyleName(row, 0, "labkey-form-label"); - TextBox textBox = new TextBox(); - DOM.setElementAttribute(textBox.getElement(), "id", "property-" + name); - textBox.addChangeListener(_changeListener); - textBox.addKeyboardListener(_keyboardListener); - if (value != null) - { - textBox.setText(value.toString()); - } - _propertyTable.setWidget(row, 1, textBox); - final Image image = new Image(PropertyUtil.getContextPath() + "/_images/partdelete.gif"); - image.addMouseListener(_tooltipListener); - image.addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent e) - { - deleteProperty(name); - } - }); - _propertyTable.setWidget(row, 2, image); - _propertyTextBoxes.put(name, textBox); - } - - private void deleteProperty(String name) - { - for (int i = 0; i < _propertyTable.getRowCount(); i++) - { - Widget widget = _propertyTable.getWidget(i, 0); - if (widget instanceof Label && ((Label)widget).getText().equals(name)) - { - _propertyTextBoxes.remove(name); - _propertyTable.removeRow(i); - _view.markAsDirty(); - return; - } - } - } - - public boolean addProperty(String propName) - { - if (getProperties().containsKey(propName)) - { - Window.alert("There is already a property with that name."); - return false; - } - - int rowIndex = _propertyTable.insertRow(_propertyTable.getRowCount() - 1); - insertPropertyRow(rowIndex, propName, ""); - _view.markAsDirty(); - return true; - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/ShiftPanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/ShiftPanel.java deleted file mode 100644 index a5fbe9896c5..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/ShiftPanel.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2010-2012 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.ui.FlexTable; -import com.google.gwt.user.client.ui.HasHorizontalAlignment; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.Widget; -import gwt.client.org.labkey.plate.designer.client.model.GWTPosition; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import org.labkey.api.gwt.client.ui.ImageButton; - -/** - * User: brittp - * Date: Aug 9, 2010 5:56:35 PM - */ -public class ShiftPanel extends FlexTable -{ - private final TemplateView _view; - - private class ShiftButton extends ImageButton - { - public ShiftButton(String text, final int vertical, final int horizontal) - { - super(text); - addClickHandler(new ClickHandler() - { - @Override - public void onClick(ClickEvent event) - { - shift(vertical, horizontal); - } - }); - } - } - - public ShiftPanel(TemplateView view) - { - _view = view; - addCenteredWidget(0, 1, new ShiftButton("Up", 1, 0)); - addCenteredWidget(2, 1, new ShiftButton("Down", -1, 0)); - addCenteredWidget(1, 2, new ShiftButton("Right", 0, -1)); - addCenteredWidget(1, 0, new ShiftButton("Left", 0, 1)); - Label shiftLabel = new Label("Shift"); - shiftLabel .setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER); - addCenteredWidget(1, 1,shiftLabel); - } - - private void addCenteredWidget(int row, int column, Widget widget) - { - setWidget(row, column, widget); - getFlexCellFormatter().setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER); - } - - private void shift(int verticalShift, int horizontalShift) - { - TemplateGrid grid = _view.getGrid(); - GWTWellGroup[][] snapshot = new GWTWellGroup[grid.getTemplateGridRowCount()][grid.getTemplateGridColumnCount()]; - for (int row = 0; row < snapshot.length; row++) - { - for (int col = 0; col < snapshot[row].length; col++) - { - snapshot[row][col] = grid.getTemplateGridCell(new GWTPosition(row, col)).getActiveGroup(); - } - } - - for (int row = 0; row < snapshot.length; row++) - { - for (int col = 0; col < snapshot[row].length; col++) - { - int sourceRow = (row + verticalShift) % grid.getTemplateGridRowCount(); - if (sourceRow < 0) - sourceRow += grid.getTemplateGridRowCount(); - int sourceCol = (col + horizontalShift) % grid.getTemplateGridColumnCount(); - if (sourceCol < 0) - sourceCol += grid.getTemplateGridColumnCount(); - GWTWellGroup newGroup = snapshot[sourceRow][sourceCol]; - grid.getTemplateGridCell(new GWTPosition(row, col)).replaceActiveGroup(newGroup); - } - } - _view.refreshWarningsAsync(); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/StatusBar.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/StatusBar.java deleted file mode 100644 index 45ba5191c79..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/StatusBar.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2010-2012 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.SimplePanel; -import org.labkey.api.gwt.client.ui.SaveButtonBar; -import org.labkey.api.gwt.client.ui.Saveable; -import org.labkey.api.gwt.client.ui.WindowUtil; -import org.labkey.api.gwt.client.util.ErrorDialogAsyncCallback; -import org.labkey.api.gwt.client.util.PropertyUtil; - -/** - * User: brittp - * Date: Feb 8, 2007 - * Time: 3:35:16 PM - */ -public class StatusBar extends HorizontalPanel implements Saveable -{ - private final String _doneLink; - private final SaveButtonBar _saveButtonBar; - - private final TemplateView _view; - private final Label _statusLabel; - private final Timer _clearTimer; - private boolean _dirty; - - public StatusBar(TemplateView view, final String doneLink) - { - _doneLink = doneLink; - - _view = view; - - _saveButtonBar = new SaveButtonBar(this); - add(_saveButtonBar); - - _statusLabel = new Label(); - SimplePanel spacer = new SimplePanel(); - spacer.setWidth("10px"); - add(spacer); - - add(_statusLabel); - setCellVerticalAlignment(_statusLabel, ALIGN_MIDDLE); - _clearTimer = new Timer() - { - @Override - public void run() - { - _statusLabel.setText(""); - } - }; - setDirty(false); - } - - @Override - public void cancel() - { - // We're already listening for navigation if the dirty bit is set, - // so no extra handling is needed. - String loc = PropertyUtil.getContextPath() + "/Project" + PropertyUtil.getContainerPath() + "/begin.view"; - WindowUtil.setLocation(loc); - } - - @Override - public void save(final SaveListener listener) - { - _view.saveChanges(new ErrorDialogAsyncCallback() - { - @Override - public void onSuccess(Long result) - { - setDirty(false); - if (listener != null) - listener.saveSuccessful(listener, PropertyUtil.getCurrentURL()); - } - }); - - } - - @Override - public void finish() - { - save(new SaveListener() - { - @Override - public void saveSuccessful(Object result, String designerUrl) - { - if (_doneLink != null && !_doneLink.isEmpty()) - WindowUtil.setLocation(_doneLink); - else - cancel(); - } - }); - } - - @Override - public String getCurrentURL() - { - return PropertyUtil.getCurrentURL(); - } - - @Override - public void save() - { - save(null); - } - - public void setDirty(boolean dirty) - { - _dirty = dirty; - _saveButtonBar.setAllowSave(dirty); - } - - @Override - public boolean isDirty() - { - return _dirty; - } - - public void setStatus(String status) - { - setStatus(status, 5); - } - - public void setStatus(String status, int secondsToDisplay) - { - _statusLabel.setText(status); - if (secondsToDisplay > 0) - _clearTimer.schedule(secondsToDisplay * 1000); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateDesigner.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateDesigner.java deleted file mode 100644 index 2ec383eb78b..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateDesigner.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2010-2015 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.core.client.EntryPoint; -import com.google.gwt.user.client.ui.RootPanel; -import gwt.client.org.labkey.assay.AssayApplication; -import org.labkey.api.gwt.client.util.PropertyUtil; - -/** - * User: brittp - * Date: Jan 30, 2007 - * Time: 1:59:01 PM - */ -public class TemplateDesigner implements EntryPoint -{ - private int getIntProperty(String name, int defaultValue) - { - try - { - return Integer.parseInt(PropertyUtil.getServerProperty(name)); - } - catch (NumberFormatException e) - { - return defaultValue; - } - } - - @Override - public void onModuleLoad() - { - RootPanel panel = AssayApplication.getRootPanel(); - String templateName = PropertyUtil.getServerProperty("templateName"); - int plateId = PropertyUtil.getServerProperty("plateId") != null ? Integer.parseInt(PropertyUtil.getServerProperty("plateId")) : 0; - String assayTypeName = PropertyUtil.getServerProperty("assayTypeName"); - String templateTypeName = PropertyUtil.getServerProperty("templateTypeName"); - int rowCount = getIntProperty("templateRowCount", 8); - int colCount = getIntProperty("templateColumnCount", 12); - TemplateView view = new TemplateView(panel, plateId, templateName, assayTypeName, templateTypeName, rowCount, colCount); - view.showAsync(); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGrid.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGrid.java deleted file mode 100644 index ed98de91521..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGrid.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2010-2012 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.ui.*; -import com.google.gwt.user.client.DOM; - -import java.util.Set; -import java.util.List; -import java.util.ArrayList; - -import gwt.client.org.labkey.plate.designer.client.model.GWTPosition; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; - -/** - * User: brittp - * Date: Feb 6, 2007 - * Time: 3:58:52 PM - */ -@SuppressWarnings("Convert2Diamond") -public class TemplateGrid extends Grid -{ - public static final char[] ALPHABET = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; - - public TemplateGrid(TemplateView view, String activeType) - { - super(view.getPlate().getRows() + 1, view.getPlate().getCols() + 1); - setBorderWidth(0); - setCellPadding(0); - setCellSpacing(3); - // Arbitrary sizes to decide what constitutes a "big" plate (rendered with smaller wells) - boolean bigPlate = view.getPlate().getCols() > 15 || view.getPlate().getRows() > 10; - String cellWidth = bigPlate ? "25px" : "30px"; - String cellHeight = bigPlate ? "20px" : "25px"; - for (int row = 0; row < getRowCount(); row++) - { - Widget cellWidget = null; - for (int col = 0; col < getColumnCount(); col++) - { - if (row == 0 && col > 0) - cellWidget = new Label("" + col); - else if (col == 0 && row > 0) - cellWidget = new Label("" + ALPHABET[row - 1]); - else if (col > 0 && row > 0) - { - GWTPosition position = new GWTPosition(row - 1, col - 1); - Set groups = (Set) view.getPlate().getPositionToGroupsMap().get(position); - cellWidget = new TemplateGridCell(view, position, groups, activeType); - - // add a marker class - DOM.setElementAttribute(cellWidget.getElement(), "class", "Cell-" + ALPHABET[row-1] + col); - } - if (cellWidget != null) - { - cellWidget.setSize(cellWidth, cellHeight); - setWidget(row, col, cellWidget); - DOM.setStyleAttribute(cellWidget.getElement(), "textAlign", "center"); - } - } - } - } - - public void highlightGroup(GWTWellGroup group, boolean on) - { - for (Object o : group.getPositions()) - { - GWTPosition position = (GWTPosition) o; - getTemplateGridCell(position).setHighlight(on); - } - } - - public TemplateGridCell getTemplateGridCell(GWTPosition position) - { - return (TemplateGridCell) getWidget(position.getRow() + 1, position.getCol() + 1); - } - - public int getTemplateGridColumnCount() - { - // The column count of this grid includes the column containing row labels, which we - // don't want to include in our count of well group columns. - return getColumnCount() - 1; - } - - public int getTemplateGridRowCount() - { - // The row count of this grid includes the row containing column labels, which we - // don't want to include in our count of well group rows. - return getRowCount() - 1; - } - - - - public List getAllCells() - { - List cells = new ArrayList(); - for (int row = 0; row < getRowCount(); row++) - { - for (int col = 0; col < getColumnCount(); col++) - { - if (col > 0 && row > 0) - cells.add((TemplateGridCell) getWidget(row, col)); - } - } - return cells; - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGridCell.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGridCell.java deleted file mode 100644 index 5d8a6c995ac..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGridCell.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.event.dom.client.*; -import com.google.gwt.user.client.ui.*; -import com.google.gwt.user.client.DOM; - -import java.util.*; - -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import gwt.client.org.labkey.plate.designer.client.model.GWTPosition; - -/** - * User: brittp - * Date: Feb 6, 2007 - * Time: 7:14:17 PM - */ -public class TemplateGridCell extends FocusPanel -{ - private Set _groups; - private String _activeType; - private final TemplateView _view; - private final GWTPosition _position; - - public TemplateGridCell(TemplateView view, GWTPosition position, Set groups, String activeType) - { - _view = view; - _position = position; - _groups = groups; - _activeType = activeType; - addMouseOverHandler(new MouseOverHandler() - { - @Override - public void onMouseOver(MouseOverEvent event) - { - GWTWellGroup activeGroup = getActiveGroup(); - if (activeGroup != null) - _view.setStatus("Well " + TemplateGrid.ALPHABET[_position.getRow()] + (_position.getCol() + 1) + ": " + activeGroup.getName()); - setHighlight(true); - _view.onMouseOverCell((TemplateGridCell) event.getSource()); - } - }); - - addMouseOutHandler(new MouseOutHandler() - { - @Override - public void onMouseOut(MouseOutEvent event) - { - _view.setStatus(""); - setHighlight(false); - } - }); - - addMouseDownHandler(new MouseDownHandler() - { - @Override - public void onMouseDown(MouseDownEvent event) - { - _view.onMouseDownCell((TemplateGridCell) event.getSource()); - } - }); - - _view.addGroupListener(new GroupChangeListenerAdapter() - { - @Override - public void activeGroupTypeChanged(String type) - { - _activeType = type; - redraw(); - } - - @Override - public void groupRemoved(GWTWellGroup group) - { - if (_groups.contains(group)) - { - getGroups().remove(group); - redraw(); - } - } - }); - - redraw(); - } - - public void setHighlight(boolean on) - { - Widget rowLabel = ((Grid) getParent()).getWidget(_position.getRow() + 1, 0); - Widget colLabel = ((Grid) getParent()).getWidget(0, _position.getCol() + 1); - DOM.setStyleAttribute(rowLabel.getElement(), "backgroundColor", on ? "#DDDDDD" : "#FFFFFF"); - DOM.setStyleAttribute(colLabel.getElement(), "backgroundColor", on ? "#DDDDDD" : "#FFFFFF"); - DOM.setStyleAttribute(getElement(), "border", on ? "1px solid black" : "1px solid gray"); - } - - private Set getGroups() - { - if (_groups == null) - _groups = new HashSet<>(); - return _groups; - } - - private void redraw() - { - String color = null; - for (Iterator it = getGroups().iterator(); it.hasNext() && color == null;) - { - GWTWellGroup group = (GWTWellGroup) it.next(); - if (group.getType().equals(_activeType)) - color = _view.getColor(group); - } - if (color == null) - color = "FFFFFF"; - DOM.setStyleAttribute(getElement(), "backgroundColor", "#" + color); - DOM.setStyleAttribute(getElement(), "border", "1px solid gray"); - } - - public void replaceActiveGroup(GWTWellGroup group) - { - GWTWellGroup activeGroup = getActiveGroup(); - if (activeGroup != null) - { - getGroups().remove(activeGroup); - activeGroup.removePosition(_position); - } - if (group != null) - { - group.addPosition(_position); - getGroups().add(group); - } - redraw(); - } - - public List getWarnings() - { - boolean replicate = false; - boolean specimen = false; - boolean control = false; - for (GWTWellGroup group : getGroups()) - { - if (group.getType().equals("SPECIMEN")) - specimen = true; - else if (group.getType().equals("CONTROL")) - control = true; - else if (group.getType().equals("REPLICATE")) - replicate = true; - } - List warnings = null; - if (replicate && !(specimen || control)) - warnings = addWarning(warnings, "Well is a replicate, but is not part of a specimen or control group."); - if (control && specimen) - warnings = addWarning(warnings, "Well is in both a specimen and a control group."); - return warnings; - } - - private List addWarning(List warnings, String warning) - { - if (warnings == null) - warnings = new ArrayList<>(); - warnings.add(warning); - return warnings; - } - - public GWTWellGroup getActiveGroup() - { - for (GWTWellGroup group : getGroups()) - { - if (group.getType().equals(_activeType)) - return group; - } - return null; - } - - public GWTPosition getPosition() - { - return _position; - } - - public String toString() - { - return TemplateGrid.ALPHABET[getPosition().getRow()] + "" + (getPosition().getCol() + 1); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateView.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateView.java deleted file mode 100644 index 722dbbdaedb..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateView.java +++ /dev/null @@ -1,649 +0,0 @@ -/* - * Copyright (c) 2010-2016 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.event.dom.client.MouseDownEvent; -import com.google.gwt.event.dom.client.MouseDownHandler; -import com.google.gwt.event.dom.client.MouseUpEvent; -import com.google.gwt.event.dom.client.MouseUpHandler; -import com.google.gwt.http.client.UrlBuilder; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.WindowCloseListener; -import com.google.gwt.user.client.WindowResizeListener; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.ChangeListener; -import com.google.gwt.user.client.ui.FocusPanel; -import com.google.gwt.user.client.ui.HasVerticalAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.KeyboardListenerAdapter; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RootPanel; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.TabPanel; -import com.google.gwt.user.client.ui.TextBox; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Widget; -import gwt.client.org.labkey.plate.designer.client.model.GWTPlate; -import gwt.client.org.labkey.plate.designer.client.model.GWTPosition; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import org.labkey.api.gwt.client.util.ColorGenerator; -import org.labkey.api.gwt.client.util.PropertyUtil; -import org.labkey.api.gwt.client.util.ServiceUtil; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * User: brittp - * Date: Feb 6, 2007 - * Time: 3:56:25 PM - */ -@SuppressWarnings("Convert2Diamond") -public class TemplateView extends HorizontalPanel -{ - private final RootPanel _rootPanel; - private final int _plateId; - private final String _templateName; - private TemplateGrid _grid; - private final List _groupListeners = new ArrayList<>(); - private GWTWellGroup _activeGroup; - private StatusBar _statusBar; - private GWTPlate _plate; - private GroupTypesTabPanel _typePanel; - private final ColorGenerator _colorGenerator = new ColorGenerator(); - private final Map _groupToColorMap = new HashMap<>(); - private PlateDataServiceAsync _testService; - private boolean _dirty = false; - private Set _existingTemplateNames; - private String _originalTemplateName; - private TextBox _nameBox; - private boolean _copyMode; - private final Map> _cellWarnings = new HashMap<>(); - private TabPanel _propertyTabPanel; - private boolean _showWarningPanel; - private WarningPanel _warningPanel; - private WellGroupPropertyPanel _wellGroupPropertyPanel; - private PlatePropertyPanel _platePropertyPanel; - private final String _assayTypeName; - private final String _templateTypeName; - private final int _rowCount; - private final int _columnCount; - private boolean _mouseDown = false; - private TemplateGridCell _selectionStartCell; - private TemplateGridCell _prevSelectionEndCell; - private boolean _setSelected; - private GWTWellGroup[][] _assignmentShapshot; - private final List _updatedCellList = new ArrayList<>(); - private final Timer _warningUpdateTimer = new Timer() - { - @Override - public void run() - { - synchronized (_updatedCellList) - { - if (!_updatedCellList.isEmpty()) - refreshWarnings(_updatedCellList); - _updatedCellList.clear(); - } - } - }; - - public TemplateView(RootPanel rootPanel, int plateId, String plateName, String plateTypeName, String templateName, int rowCount, int columnCount) - { - _rootPanel = rootPanel; - _plateId = plateId; - _templateName = plateName; - _assayTypeName = plateTypeName; - _templateTypeName = templateName; - _rowCount = rowCount; - _columnCount = columnCount; - _copyMode = Boolean.valueOf(PropertyUtil.getServerProperty("copyTemplate")).booleanValue(); - } - - public void showAsync() - { - _rootPanel.clear(); - _rootPanel.add(new Label("Loading...")); - getService().getTemplateDefinition(_templateName, _plateId, _assayTypeName, _templateTypeName, _rowCount, _columnCount, _copyMode, new AsyncCallback() - { - @Override - public void onFailure(Throwable throwable) - { - VerticalPanel mainPanel = new VerticalPanel(); - mainPanel.add(new Label("Unable to load plate template: " + throwable.getMessage())); - _rootPanel.add(mainPanel); - } - - @Override - public void onSuccess(GWTPlate plate) - { - _plate = plate; - show(); - } - }); - } - - private PlateDataServiceAsync getService() - { - if (_testService == null) - { - _testService = GWT.create(PlateDataService.class); - ServiceUtil.configureEndpoint(_testService, "designerService"); - } - return _testService; - } - - private void resize(int width, int height) - { - // Set the size of main body scroll panel so that it fills the browser. - int fullWidth = Math.max(width - getAbsoluteLeft() - 20, 0); - int rightPanelWidth = Math.min(fullWidth - _typePanel.getOffsetWidth(), 500); - -// setSize(null, Math.max(height - getAbsoluteTop() - 10, 0) + "px"); - - _propertyTabPanel.setWidth(rightPanelWidth + "px"); - } - - private void show() - { - VerticalPanel mainPanel = new VerticalPanel(); - List groupTypes = _plate.getGroupTypes(); - _originalTemplateName = _plate.getName(); - _showWarningPanel = _plate.isShowWarningPanel(); - - // status bar - _statusBar = new StatusBar(this, PropertyUtil.getRelativeURL("begin")); - mainPanel.add(_statusBar); - mainPanel.setCellHeight(_statusBar, "30px"); - - // plate name textbox: - _nameBox = new TextBox(); - _nameBox.setVisibleLength(40); - DOM.setElementAttribute(_nameBox.getElement(), "id", "templateName"); - String plateName = PropertyUtil.getServerProperty("defaultPlateName"); - if (plateName == null) - { - plateName = ""; - } - _nameBox.setText(plateName); - _plate.setName(plateName); - _nameBox.addKeyboardListener(new KeyboardListenerAdapter() - { - @Override - public void onKeyPress(Widget sender, char keyCode, int modifiers) - { - setDirty(true); - } - }); - - _nameBox.addChangeListener(new ChangeListener() - { - @Override - public void onChange(Widget sender) - { - String newName = _nameBox.getText(); - _plate.setName(newName.trim()); - if (!_nameBox.getText().equals(_plate.getName())) - { - _nameBox.setText(_plate.getName()); - } - } - }); - - HorizontalPanel namePanel = new HorizontalPanel(); - Label templateNameLabel = new Label("Template Name: "); - namePanel.add(templateNameLabel); - SimplePanel spacer = new SimplePanel(); - spacer.setWidth("5px"); - namePanel.add(spacer); - namePanel.add(_nameBox); - mainPanel.add(namePanel); - namePanel.setCellVerticalAlignment(templateNameLabel, HasVerticalAlignment.ALIGN_MIDDLE); - mainPanel.setCellHeight(namePanel, "30px"); - - // well groups and well grid - _grid = new TemplateGrid(this, (String) groupTypes.get(0)); - _typePanel = new GroupTypesTabPanel(this); - HorizontalPanel hpanel = new HorizontalPanel(); - - // Wrap the tabPanel in a focuspanel so we can track mouse events: - FocusPanel focusPanel = new FocusPanel(_typePanel); - focusPanel.addMouseDownHandler(new MouseDownHandler() - { - @Override - public void onMouseDown(MouseDownEvent event) - { - _mouseDown = true; - } - }); - focusPanel.addMouseUpHandler(new MouseUpHandler() - { - @Override - public void onMouseUp(MouseUpEvent event) - { - _mouseDown = false; - } - }); - - hpanel.add(focusPanel); - mainPanel.add(hpanel); - mainPanel.setCellHeight(hpanel, "100%"); - - add(mainPanel); - - _propertyTabPanel = new TabPanel(); - _propertyTabPanel.setWidth("500px"); - - _platePropertyPanel = new PlatePropertyPanel(this); - //_propertyTabPanel.add(_platePropertyPanel, "Plate Properties"); - - _wellGroupPropertyPanel = new WellGroupPropertyPanel(this); - _propertyTabPanel.add(_wellGroupPropertyPanel, "Well Group Properties"); - - if (_showWarningPanel) - { - _warningPanel = new WarningPanel(); - _propertyTabPanel.add(_warningPanel, "Warnings"); - } - _propertyTabPanel.selectTab(0); - - SimplePanel spacer2 = new SimplePanel(); - spacer2.setWidth("5px"); - add(spacer2); - - add(_propertyTabPanel); - - refreshWarnings(_grid.getAllCells()); - _rootPanel.clear(); - _rootPanel.add(this); - resize(Window.getClientWidth(), Window.getClientHeight()); - Window.addWindowResizeListener(new WindowResizeListener() - { - @Override - public void onWindowResized(int width, int height) - { - resize(width, height); - } - - }); - Window.addWindowCloseListener(new WindowCloseListener() - { - @Override - public void onWindowClosed() - { - } - - @Override - public String onWindowClosing() - { - if (_dirty) - return "Changes have not been saved and will be discarded."; - else - return null; - } - }); - - if (_copyMode) - setDirty(true); - } - - private Set getAllTemplateNames() - { - if (_existingTemplateNames == null) - { - String current; - _existingTemplateNames = new HashSet<>(); - int idx = 0; - do - { - current = PropertyUtil.getServerProperty("templateName[" + idx++ + "]"); - if (current != null) - _existingTemplateNames.add(current); - } while (current != null); - } - return _existingTemplateNames; - } - - private void setDirty(boolean dirty) - { - _dirty = dirty; - _statusBar.setDirty(dirty); - } - - public void markAsDirty() - { - setDirty(true); - } - - public void saveChanges(final AsyncCallback callback) - { - String templateName = _nameBox.getText().trim(); - if (templateName == null || templateName.isEmpty()) - { - Window.alert("A plate name must be specified."); - return; - } - if (_copyMode || !templateName.equals(_originalTemplateName)) - { - Set allTemplateNames = getAllTemplateNames(); - if (allTemplateNames.contains(templateName)) - { - Window.alert("A plate template with this name already exists. Please choose another name."); - return; - } - } - _plate.setPlateProperties(_platePropertyPanel.getProperties()); - if (_activeGroup != null) - { - _activeGroup.setProperties(_wellGroupPropertyPanel.getProperties()); - } - setStatus("Saving..."); - getService().saveChanges(_plate, !_copyMode, new AsyncCallback() - { - @Override - public void onFailure(Throwable throwable) - { - setStatus("Save failed: " + throwable.getMessage()); - setDirty(true); - callback.onFailure(throwable); - } - - @Override - public void onSuccess(Long newPlateId) - { - UrlBuilder urlBuilder = Window.Location.createUrlBuilder(); - String oldUrl = urlBuilder.buildString(); - - // remove all parameters (e.g, copy, rows, cols, ...) - for (String param : Window.Location.getParameterMap().keySet()) - { - urlBuilder.removeParameter(param); - } - - // add the necessary parameters for loading the template again if the user refreshes the page - urlBuilder.setParameter("templateName", _plate.getName()); - urlBuilder.setParameter("plateId", String.valueOf(newPlateId)); - - // if the URL has changed, replace it in history - String newUrl = urlBuilder.buildString(); - if (!newUrl.equals(oldUrl)) - { - replaceState(newUrl); - } - - _copyMode = false; - setStatus("Saved."); - setDirty(false); - callback.onSuccess(newPlateId); - } - }); - } - - // https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState - public static native void replaceState(String newURL) /*-{ - $wnd.history.replaceState(null, '', newURL); - }-*/; - - public void setStatus(String status) - { - _statusBar.setStatus(status); - } - - public void onMouseOverCell(TemplateGridCell cell) - { - if (_mouseDown) - { - setStatus("Selection: " + _selectionStartCell + " to " + cell); - // we appear to be doing a drag operation to select multiple cells- snapshot the well group assignments - // now, so we can revert wells as the user drags around: - if (_assignmentShapshot == null) - _assignmentShapshot = snapshotWellAssignments(); - updateSelection(_selectionStartCell, cell, _prevSelectionEndCell, _setSelected); - _prevSelectionEndCell = cell; - } - else - { - _selectionStartCell = null; - _prevSelectionEndCell = null; - _assignmentShapshot = null; - } - } - - private GWTWellGroup[][] snapshotWellAssignments() - { - GWTWellGroup[][] assignments = new GWTWellGroup[_grid.getTemplateGridRowCount()][_grid.getTemplateGridColumnCount()]; - for (int row = 0; row < assignments.length; row++) - { - for (int col = 0; col < assignments[row].length; col++) - assignments[row][col] = _grid.getTemplateGridCell(new GWTPosition(row, col)).getActiveGroup(); - } - return assignments; - } - - public void onMouseDownCell(TemplateGridCell cell) - { - _selectionStartCell = cell; - _setSelected = (cell.getActiveGroup() != _activeGroup); - if (_activeGroup != null) - { - if (cell.getActiveGroup() == _activeGroup) - cell.replaceActiveGroup(null); - else - cell.replaceActiveGroup(_activeGroup); - setDirty(true); - List cellList = new ArrayList(); - cellList.add(cell); - refreshWarnings(cellList); - } - } - - public void refreshWarningsAsync() - { - refreshWarningsAsync(_grid.getAllCells()); - } - - public void refreshWarningsAsync(List updatedCellList) - { - synchronized (_updatedCellList) - { - _updatedCellList.clear(); - _updatedCellList.addAll(updatedCellList); - _warningUpdateTimer.cancel(); - _warningUpdateTimer.schedule(1000); - } - } - - private void updateSelection(TemplateGridCell startCell, TemplateGridCell endCell, TemplateGridCell prevEndCell, boolean setSelected) - { - // First, set the selection state of all cells in the current selection range: - int startCol = Math.min(startCell.getPosition().getCol(), endCell.getPosition().getCol()); - int endCol = Math.max(startCell.getPosition().getCol(), endCell.getPosition().getCol()); - List cellList = new ArrayList(); - for (int col = startCol; col <= endCol; col++) - { - int startRow = Math.min(startCell.getPosition().getRow(), endCell.getPosition().getRow()); - int endRow = Math.max(startCell.getPosition().getRow(), endCell.getPosition().getRow()); - for (int row = startRow; row <= endRow; row++) - { - TemplateGridCell cell = this.getGrid().getTemplateGridCell(new GWTPosition(row, col)); - if (setSelected) - cell.replaceActiveGroup(_activeGroup); - else - cell.replaceActiveGroup(null); - cellList.add(cell); - } - } - refreshWarningsAsync(cellList); - - // Second, de-select any cells that were previously selected in this same drag but which are no longer - // selected. (This can happen if the user drags a few columns/rows in one direction, overshoots, and backs - // off one column/row.) We know that the user has backed off if (and only if) the previous selection end is - // no longer inside the rectangle defined by the current selection. - if (prevEndCell != null && !prevEndCell.getPosition().inside(startCell.getPosition(), endCell.getPosition())) - { - for (int row = 0; row < _grid.getTemplateGridRowCount(); row++) - { - for (int col = 0; col < _grid.getTemplateGridColumnCount(); col++) - { - GWTPosition pos = new GWTPosition(row, col); - // Any cells that were previously included in the selection range but which are now excluded need to be - // reset to their original state: - if (pos.inside(startCell.getPosition(), prevEndCell.getPosition()) && !pos.inside(startCell.getPosition(), endCell.getPosition())) - _grid.getTemplateGridCell(pos).replaceActiveGroup(_assignmentShapshot[row][col]); - } - } - } - } - - private void refreshWarnings(List modifiedCells) - { - boolean warningsChanged = false; - for (TemplateGridCell cell : modifiedCells) - { - List warnings = cell.getWarnings(); - String key = cell.toString(); - if (warnings == null) - { - if (_cellWarnings.containsKey(key)) - { - _cellWarnings.remove(key); - warningsChanged = true; - } - } - else - { - _cellWarnings.put(key, warnings); - warningsChanged = true; - } - } - if (warningsChanged && _showWarningPanel) - { - _warningPanel.update(_cellWarnings); - int currentTab = _propertyTabPanel.getTabBar().getSelectedTab(); - _propertyTabPanel.remove(_warningPanel); - if (_cellWarnings.isEmpty()) - { - _propertyTabPanel.add(_warningPanel, "Warnings"); - } - else - { - _propertyTabPanel.add(_warningPanel, "Warnings (" + _cellWarnings.size() + ")", true); - } - _propertyTabPanel.selectTab(currentTab); - } - } - - public void deleteWellGroup(GWTWellGroup group) - { - setDirty(true); - _plate.removeGroup(group); - List listenersCopy = new ArrayList(_groupListeners); - for (GroupChangeListener listener : listenersCopy) - listener.groupRemoved(group); - _groupToColorMap.remove(group); - setActiveGroup(null); - List cells = new ArrayList(); - for (GWTPosition position : group.getPositions()) - cells.add(_grid.getTemplateGridCell(position)); - refreshWarnings(cells); - } - - public void createWellGroup(String groupName, String type) - { - Map properties = getPropertiesForType(type); - - int wellGroupCount = _plate.getWellGroupCount() + 1; - GWTWellGroup group = new GWTWellGroup(-1 * wellGroupCount, type, groupName, new ArrayList(), properties); - if (_plate.addGroup(group)) - { - setDirty(true); - List listenersCopy = new ArrayList(_groupListeners); - for (GroupChangeListener listener : listenersCopy) - listener.groupAdded(group); - setActiveGroup(group); - } - } - - public void setActiveType(String activeType) - { - setActiveGroup(null); - List listenersCopy = new ArrayList(_groupListeners); - for (GroupChangeListener listener : listenersCopy) - listener.activeGroupTypeChanged(activeType); - } - - public void addGroupListener(GroupChangeListener listener) - { - _groupListeners.add(listener); - } - - public void removeGroupListener(GroupChangeListener listener) - { - _groupListeners.remove(listener); - } - - public void setActiveGroup(GWTWellGroup group) - { - GWTWellGroup previous = _activeGroup; - _activeGroup = group; - List listenersCopy = new ArrayList(_groupListeners); - for (GroupChangeListener listener : listenersCopy) - listener.activeGroupChanged(previous, group); - } - - public GWTPlate getPlate() - { - return _plate; - } - - public String getColor(GWTWellGroup group) - { - String color = _groupToColorMap.get(group); - if (color == null) - { - color = _colorGenerator.next(); - _groupToColorMap.put(group, color); - } - return color; - } - - private Map getPropertiesForType(String type) - { - List groups = _plate.getTypeToGroupsMap().get(type); - if (groups != null && !groups.isEmpty()) - return groups.iterator().next().getProperties(); - else - return new HashMap(); - } - - public TemplateGrid getGrid() - { - return _grid; - } - - public void setHighlightGroup(GWTWellGroup group, boolean on) - { - _grid.highlightGroup(group, on); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WarningPanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WarningPanel.java deleted file mode 100644 index bd505648a9c..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WarningPanel.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.DOM; - -import java.util.*; - -/** - * User: brittp - * Date: Mar 1, 2007 - * Time: 4:52:29 PM - */ -public class WarningPanel extends VerticalPanel -{ - public WarningPanel() - { - setWidth("100%"); - update(null); - } - - public void update(Map cellToWarnings) - { - clear(); - if (cellToWarnings != null && !cellToWarnings.isEmpty()) - { - List keys = new ArrayList(cellToWarnings.keySet()); - Collections.sort(keys); - for (Iterator cellIt = keys.iterator(); cellIt.hasNext(); ) - { - String cell = (String) cellIt.next(); - List warnings = (List) cellToWarnings.get(cell); - Label cellLabel = new Label("Well " + cell + ":"); - DOM.setStyleAttribute(cellLabel.getElement(), "fontWeight", "bold"); - add(cellLabel); - for (Iterator warnIt = warnings.iterator(); warnIt.hasNext(); ) - { - String warning = (String) warnIt.next(); - Label warningText = new Label(warning); - add(warningText); - DOM.setStyleAttribute(warningText.getElement(), "paddingLeft", "5px"); - } - } - } - else - { - Label header = new Label("No warnings."); - add(header); - } - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WellGroupPropertyPanel.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WellGroupPropertyPanel.java deleted file mode 100644 index b50141905fa..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WellGroupPropertyPanel.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client; - -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; - -/** - * User: jeckels - * Date: Apr 19, 2007 - */ -public class WellGroupPropertyPanel extends PropertyPanel implements GroupChangeListener -{ - public WellGroupPropertyPanel(TemplateView view) - { - super(view); - view.addGroupListener(this); - activeGroupChanged(null, null); - } - - @Override - public void activeGroupChanged(GWTWellGroup previouslyActive, GWTWellGroup currentlyActive) - { - if (previouslyActive != null) - { - previouslyActive.setProperties(getProperties()); - } - if (currentlyActive == null) - { - redraw("No well group selected."); - } - else - { - redraw(currentlyActive.getProperties()); - } - } - - @Override - public void activeGroupTypeChanged(String type) - { - } - - @Override - public void groupAdded(GWTWellGroup group) - { - } - - @Override - public void groupRemoved(GWTWellGroup group) - { - } - -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPlate.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPlate.java deleted file mode 100644 index 02aa209fbae..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPlate.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (c) 2010-2012 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 gwt.client.org.labkey.plate.designer.client.model; - -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.IsSerializable; - -import java.util.*; - -@SuppressWarnings("Convert2Diamond") -public class GWTPlate implements IsSerializable -{ - private long _rowId; - private String _name; - private String _type; - private int _rows; - private int _cols; - private Map> _groups = new HashMap>(); - private Set _allGroups = new HashSet<>(); - private boolean _showWarningPanel; - - private List _groupTypes; - private Map _plateProperties = new HashMap(); - private Map> _typesToDefaultGroups = new HashMap>(); - private transient Map> _positionToGroups = null; - - public GWTPlate() - { - } - - public GWTPlate(long rowId, String name, String type, int rows, int cols, List groupTypes, boolean showWarningPanel) - { - _rowId = rowId; - _name = name; - _type = type; - _rows = rows; - _cols = cols; - _groupTypes = groupTypes; - _showWarningPanel = showWarningPanel; - } - - public long getRowId() - { - return _rowId; - } - - public Map getPositionToGroupsMap() - { - if (_positionToGroups == null) - { - _positionToGroups = new HashMap>(); - for (List typeGroups : _groups.values()) - { - for (GWTWellGroup group : typeGroups) - { - for (GWTPosition position : group.getPositions()) - { - Set groupList = _positionToGroups.get(position); - if (groupList == null) - { - groupList = new HashSet(); - _positionToGroups.put(position, groupList); - } - groupList.add(group); - } - } - } - } - return _positionToGroups; - } - - public Map> getTypeToGroupsMap() - { - return Collections.unmodifiableMap(_groups); - } - - public void removeGroup(GWTWellGroup group) - { - if (_groups.containsKey(group.getType())) - { - _groups.get(group.getType()).remove(group); - _positionToGroups = null; - } - _allGroups.remove(group); - } - - public boolean addGroup(GWTWellGroup group) - { - if (!_groups.containsKey(group.getType())) - _groups.put(group.getType(), new ArrayList<>()); - - // verify the group rowId is unique - if (_allGroups.contains(group)) - { - Window.alert("Group '" + group.getName() + "' already exists."); - return false; - } - _allGroups.add(group); - - // verify the group name is unique within the group type - List groups = _groups.get(group.getType()); - for (GWTWellGroup g : groups) - { - if (group.getName() != null && group.getName().equals(g.getName())) - { - Window.alert("Group '" + group.getName() + "' already exists."); - return false; - } - } - groups.add(group); - - _positionToGroups = null; - return true; - } - - public int getCols() - { - return _cols; - } - - public int getWellGroupCount() - { - return _allGroups.size(); - } - - public Set getGroups() - { - return Collections.unmodifiableSet(_allGroups); - } - - public int getRows() - { - return _rows; - } - - public List getGroupTypes() - { - return _groupTypes; - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public void setGroups(List groups) - { - for (GWTWellGroup group : groups) - addGroup(group); - } - - public void setPlateProperties(Map plateProperties) - { - _plateProperties = plateProperties; - } - - public Map getPlateProperties() - { - return _plateProperties; - } - - public String getType() - { - return _type; - } - - public Map> getTypesToDefaultGroups() - { - return _typesToDefaultGroups; - } - - public void setTypesToDefaultGroups(Map> typesToDefaultGroups) - { - _typesToDefaultGroups = typesToDefaultGroups; - } - - public boolean isShowWarningPanel() - { - return _showWarningPanel; - } - - public void setShowWarningPanel(boolean showWarningPanel) - { - _showWarningPanel = showWarningPanel; - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPosition.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPosition.java deleted file mode 100644 index a7b514ba9d1..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPosition.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2010 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 gwt.client.org.labkey.plate.designer.client.model; - -import com.google.gwt.user.client.rpc.IsSerializable; - -/** - * User: brittp - * Date: Feb 2, 2007 - * Time: 3:04:10 PM - */ -public class GWTPosition implements IsSerializable -{ - private int _row; - private int _col; - - public GWTPosition() - { - // empty constructor for serialization - } - - public GWTPosition(int row, int col) - { - _row = row; - _col = col; - } - - public int getCol() - { - return _col; - } - - public int getRow() - { - return _row; - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (!(o instanceof GWTPosition)) - return false; - return ((GWTPosition) o).getRow() == getRow() && - ((GWTPosition) o).getCol() == getCol(); - } - - public int hashCode() - { - int result; - result = _row; - result = 31 * result + _col; - return result; - } - - /** - * Returns a boolean indicating whether this position is inside the grid defined by the two parameter positions. - * Containment is inclusive of the rows/columns occupied by the positions themselves. - * @param first The upper-left or lower-right position of the containment grid. - * @param second The upper-left or lower-right position of the containment grid. - * @return Boolean indicating whether this point is inside the grid defined by the two parameter positions. - */ - public boolean inside(GWTPosition first, GWTPosition second) - { - GWTPosition upperLeft = new GWTPosition(Math.min(first.getRow(), second.getRow()), - Math.min(first.getCol(), second.getCol())); - GWTPosition lowerRight = new GWTPosition(Math.max(first.getRow(), second.getRow()), - Math.max(first.getCol(), second.getCol())); - return upperLeft.getRow() <= getRow() && - lowerRight.getRow() >= getRow() && - upperLeft.getCol() <= getCol() && - lowerRight.getCol() >= getCol(); - } -} diff --git a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTWellGroup.java b/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTWellGroup.java deleted file mode 100644 index 28c0fd343bd..00000000000 --- a/assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTWellGroup.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2010-2012 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 gwt.client.org.labkey.plate.designer.client.model; - -import com.google.gwt.user.client.rpc.IsSerializable; - -import java.util.List; -import java.util.Map; - -/** - * User: brittp - * Date: Feb 2, 2007 - * Time: 3:16:30 PM - */ -public class GWTWellGroup implements IsSerializable -{ - private int _rowId; - private String _type; - private String _name; - private List _positions; - private boolean _allowNewGroups = true; // issue 47210 : disallow creating/editing of well groups for specific types - - /** - * This field is a Map that must always contain Strings. - */ - private Map _properties; - - public GWTWellGroup() - { - // no-arg constructor for deserialization - } - - public GWTWellGroup(int rowId, String type, String name, List positions, Map properties) - { - _rowId = rowId; - _type = type; - _name = name; - _positions = positions; - _properties = properties; - } - - public int getRowId() - { - return _rowId; - } - - public void removePosition(GWTPosition position) - { - _positions.remove(position); - } - - public void addPosition(GWTPosition position) - { - if (!_positions.contains(position)) - _positions.add(position); - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public String getType() - { - return _type; - } - - public List getPositions() - { - return _positions; - } - - public Map getProperties() - { - return _properties; - } - - public void setProperties(Map properties) - { - _properties = properties; - } - - public boolean isAllowNewGroups() - { - return _allowNewGroups; - } - - public void setAllowNewGroups(boolean allowNewGroups) - { - _allowNewGroups = allowNewGroups; - } - - @Override - public int hashCode() - { - return Integer.hashCode(_rowId); - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - GWTWellGroup that = (GWTWellGroup) o; - - return this._rowId == that._rowId; - } -} diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 9f2f465697c..362c014cc1c 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -22,7 +22,6 @@ import org.labkey.api.action.FormHandlerAction; import org.labkey.api.action.JsonInputLimit; import org.labkey.api.action.FormViewAction; -import org.labkey.api.action.GWTServiceAction; import org.labkey.api.action.Marshal; import org.labkey.api.action.Marshaller; import org.labkey.api.action.MutatingApiAction; @@ -46,7 +45,6 @@ import org.labkey.api.data.ContainerManager; import org.labkey.api.data.TSVWriter; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; -import org.labkey.api.gwt.server.BaseRemoteService; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.FieldKey; import org.labkey.api.query.ValidationException; @@ -76,7 +74,6 @@ import org.labkey.api.view.NavTree; import org.labkey.api.view.NotFoundException; import org.labkey.api.writer.ZipFile; -import org.labkey.assay.plate.PlateDataServiceImpl; import org.labkey.assay.plate.PlateImpl; import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.PlateSetExport; @@ -85,7 +82,6 @@ import org.labkey.assay.plate.WellGroupImpl; import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.ReformatOptions; -import org.labkey.assay.view.AssayGWTView; import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.web.servlet.ModelAndView; @@ -216,17 +212,6 @@ public void addNavTrail(NavTree root) } } - /** Delete soon! */ - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public static class DesignerServiceAction extends GWTServiceAction - { - @Override - protected BaseRemoteService createService() throws IllegalStateException - { - return new PlateDataServiceImpl(getViewContext()); - } - } - public static class RowIdForm { private int _rowId; @@ -608,63 +593,6 @@ public void addNavTrail(NavTree root) } } - /** Delete soon! */ - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public class DesignerGwtAction extends SimpleViewAction - { - @Override - public ModelAndView getView(DesignerForm form, BindException errors) - { - Map properties = new HashMap<>(); - String templateName = null; - Long plateId = null; - - if (form.getTemplateName() != null) - { - plateId = form.getPlateId(); - templateName = form.getTemplateName(); - } - else if (form.getPlateId() != null) - { - Plate plate = PlateManager.get().getPlate(getContainer(), form.getPlateId()); - if (plate != null) - { - plateId = plate.getRowId(); - templateName = plate.getName(); - } - } - - if (templateName != null) - { - properties.put("copyTemplate", Boolean.toString(form.isCopy())); - properties.put("templateName", templateName); - if (plateId != null) - properties.put("plateId", String.valueOf(plateId)); - if (form.isCopy()) - properties.put("defaultPlateName", getUniqueName(getContainer(), templateName)); - else - properties.put("defaultPlateName", templateName); - } - - if (form.getAssayType() != null) - properties.put("assayTypeName", form.getAssayType()); - if (form.getTemplateType() != null) - properties.put("templateTypeName", form.getTemplateType()); - - properties.put("templateRowCount", String.valueOf(form.getRowCount())); - properties.put("templateColumnCount", String.valueOf(form.getColCount())); - - return new AssayGWTView(gwt.client.org.labkey.plate.designer.client.TemplateDesigner.class, properties); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("editPlateTemplate"); - root.addChild("Plate Editor (GWT)"); - } - } - @RequiresAnyOf({DeletePermission.class, DesignAssayPermission.class}) public static class DeleteAction extends FormHandlerAction { diff --git a/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java b/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java index 5b1102e114f..565dbfd097d 100644 --- a/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java +++ b/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java @@ -4,7 +4,6 @@ import org.labkey.api.data.generator.DataGenerator; import org.labkey.api.exp.query.ExpSchema; import org.labkey.api.exp.query.SamplesSchema; -import org.labkey.api.gwt.client.assay.AssayException; import org.labkey.api.gwt.client.assay.model.GWTProtocol; import org.labkey.api.gwt.client.model.GWTDomain; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; @@ -28,7 +27,7 @@ public AssayDesignGenerator(PipelineJob job, Config config) super(job, config); } - public void generateAssayDesigns(String namePrefix) throws ValidationException, AssayException + public void generateAssayDesigns(String namePrefix) throws ValidationException { int numAssayDesigns = _config.getNumAssayDesigns(); if (numAssayDesigns <= 0) @@ -154,7 +153,7 @@ public boolean isAssayDesignPlateSupport() public static class Driver implements DataGenerationDriver { @Override - public List generateData(PipelineJob job, Properties properties) throws ValidationException, AssayException + public List generateData(PipelineJob job, Properties properties) throws ValidationException { AssayDesignGenerator generator = new AssayDesignGenerator(job, new AssayDesignGenerator.Config(properties)); generator.generateAssayDesigns("Assay Design "); diff --git a/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java b/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java index 883445f2c3c..2d362f85ae2 100644 --- a/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java +++ b/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java @@ -13,7 +13,6 @@ import org.labkey.api.exp.property.Domain; import org.labkey.api.exp.property.DomainProperty; import org.labkey.api.exp.query.SamplesSchema; -import org.labkey.api.gwt.client.assay.AssayException; import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.query.SchemaKey; import org.labkey.api.query.ValidationException; @@ -225,7 +224,7 @@ public void setMaxRowsPerRun(int maxRowsPerRun) public static class Driver implements DataGenerationDriver { @Override - public List generateData(PipelineJob job, Properties properties) throws ValidationException, AssayException, ExperimentException + public List generateData(PipelineJob job, Properties properties) throws ValidationException, ExperimentException { AssayRunDataGenerator generator = new AssayRunDataGenerator(job, new AssayRunDataGenerator.Config(properties)); generator.generateAssayRunData(); diff --git a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java deleted file mode 100644 index af614c9465f..00000000000 --- a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (c) 2008-2019 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.assay.plate; - -import gwt.client.org.labkey.plate.designer.client.PlateDataService; -import gwt.client.org.labkey.plate.designer.client.model.GWTPlate; -import gwt.client.org.labkey.plate.designer.client.model.GWTPosition; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.labkey.api.assay.plate.Plate; -import org.labkey.api.assay.plate.PlateLayoutHandler; -import org.labkey.api.assay.plate.PlateService; -import org.labkey.api.assay.plate.PlateType; -import org.labkey.api.assay.plate.Position; -import org.labkey.api.assay.plate.WellGroup; -import org.labkey.api.gwt.server.BaseRemoteService; -import org.labkey.api.query.BatchValidationException; -import org.labkey.api.query.ValidationException; -import org.labkey.api.view.ViewContext; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * User: brittp - * Date: Jan 31, 2007 - * Time: 2:38:17 PM - */ -public class PlateDataServiceImpl extends BaseRemoteService implements PlateDataService -{ - private static final Logger LOG = LogManager.getLogger(PlateDataServiceImpl.class); - - public PlateDataServiceImpl(ViewContext context) - { - super(context); - } - - @Override - public GWTPlate getTemplateDefinition(String templateName, int plateId, String assayTypeName, String templateTypeName, int rowCount, int columnCount, boolean copyTemplate) throws Exception - { - try - { - Plate template; - PlateLayoutHandler handler; - - if (templateName != null) - { - // existing template - template = PlateService.get().getPlate(getContainer(), plateId); - if (template == null) - throw new Exception("Plate " + templateName + " does not exist."); - - handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); - if (handler == null) - throw new Exception("Plate template type " + template.getAssayType() + " does not exist."); - } - else - { - // new default template - handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); - if (handler == null) - throw new Exception("Plate template type " + assayTypeName + " does not exist."); - - PlateType plateType = PlateService.get().getPlateType(rowCount, columnCount); - if (plateType == null) - throw new Exception("The plate type : (" + rowCount + " x " + columnCount + ") does not exist"); - template = handler.createPlate(templateTypeName, getContainer(), plateType); - } - - // Translate PlateTemplate to GWTPlate - List groups = template.getWellGroups(); - List translated = new ArrayList<>(); - for (int i = 0; i < groups.size(); i++) - { - WellGroup group = groups.get(i); - List positions = new ArrayList<>(group.getPositions().size()); - for (Position position : group.getPositions()) - positions.add(new GWTPosition(position.getRow(), position.getColumn())); - Map groupProperties = new HashMap<>(); - for (String propName : group.getPropertyNames()) - { - groupProperties.put(propName, group.getProperty(propName)); - } - - // NOTE: Use negative rowId for unsaved well groups to support GWTWellGroup.equals() - int wellGroupId = copyTemplate || group.getRowId() == null ? -1 * (i+1) : group.getRowId(); - GWTWellGroup gwtWellGroup = new GWTWellGroup(wellGroupId, group.getType().name(), group.getName(), positions, groupProperties); - gwtWellGroup.setAllowNewGroups(handler.canCreateNewGroups(group.getType())); - translated.add(gwtWellGroup); - } - - long newPlateId = copyTemplate || template.getRowId() == null ? -1 : template.getRowId(); - GWTPlate plate = new GWTPlate(newPlateId, - template.getName(), template.getAssayType(), template.getRows(), - template.getColumns(), getTypeList(template), handler.showEditorWarningPanel()); - plate.setGroups(translated); - plate.setTypesToDefaultGroups(handler.getDefaultGroupsForTypes()); - - Map templateProperties = new HashMap<>(); - for (String propName : template.getPropertyNames()) - { - templateProperties.put(propName, template.getProperty(propName) == null ? null : template.getProperty(propName).toString()); - } - plate.setPlateProperties(templateProperties); - return plate; - } - catch (SQLException e) - { - LOG.error("Error create plate from template", e); - throw new Exception(e); - } - } - - private List getTypeList(Plate template) - { - List wellTypes = Arrays.asList( - WellGroup.Type.CONTROL, WellGroup.Type.SPECIMEN, - WellGroup.Type.REPLICATE, WellGroup.Type.OTHER); - - PlateLayoutHandler handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); - if (handler != null) - wellTypes = handler.getWellGroupTypes(); - - List types = new ArrayList<>(); - for (WellGroup.Type type : wellTypes) - types.add(type.name()); - return types; - } - - @Override - public long saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exception - { - try - { - boolean updateExisting = false; - Plate plate; - if (gwtPlate.getRowId() > 0) - { - plate = PlateManager.get().getPlate(getContainer(), gwtPlate.getRowId()); - if (plate == null) - throw new Exception("Plate template not found: " + gwtPlate.getRowId()); - - // check another plate of the same name doesn't already exist - if (PlateManager.get().isDuplicatePlateName(getContainer(), getUser(), gwtPlate.getName(), null) && !replaceIfExisting) - throw new Exception("A plate template with name '" + gwtPlate.getName() + "' already exists."); - - if (!plate.getAssayType().equals(gwtPlate.getType())) - throw new Exception("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + gwtPlate.getName() + "'"); - - if (plate.getRows() != gwtPlate.getRows() || plate.getColumns() != gwtPlate.getCols()) - throw new Exception("Plate template dimensions cannot be changed for '" + gwtPlate.getName() + "'"); - - // TODO: Use a version column to avoid concurrent updates - - updateExisting = true; - } - else - { - // check another plate of the same name doesn't already exist - Plate other = PlateManager.get().getPlateByName(getContainer(), gwtPlate.getName()); - if (other != null) - { - if (!replaceIfExisting) - throw new Exception("A plate template with name '" + gwtPlate.getName() + "' already exists."); - - // delete the existing plate first - PlateService.get().deletePlate(getContainer(), getUser(), other.getRowId()); - } - - PlateType plateType = PlateService.get().getPlateType(gwtPlate.getRows(), gwtPlate.getCols()); - if (plateType == null) - throw new Exception("The plate type : (" + gwtPlate.getRows() + " x " + gwtPlate.getCols() + ") does not exist"); - plate = PlateManager.get().createPlate(getContainer(), gwtPlate.getType(), plateType); - } - - plate.setName(gwtPlate.getName()); - plate.setProperties(gwtPlate.getPlateProperties()); - - // first, mark well groups not submitted for saving as deleted - Set groups = gwtPlate.getGroups(); - List existingWellGroups = plate.getWellGroups(); - for (WellGroup existingWellGroup : existingWellGroups) - { - if (groups.stream().noneMatch(g-> g.getRowId() == existingWellGroup.getRowId())) - ((PlateImpl)plate).markWellGroupForDeletion(existingWellGroup); - } - - // next, update positions on existing well groups or create new well groups - for (GWTWellGroup gwtGroup : groups) - { - WellGroup.Type groupType = WellGroup.Type.valueOf(gwtGroup.getType()); - List positions = new ArrayList<>(); - for (GWTPosition gwtPosition : gwtGroup.getPositions()) - positions.add(plate.getPosition(gwtPosition.getRow(), gwtPosition.getCol())); - - WellGroupImpl group; - if (updateExisting && gwtGroup.getRowId() > 0) - { - group = findExistingWellGroup(existingWellGroups, gwtGroup.getRowId()); - if (group == null) - throw new Exception("Well group " + gwtGroup.getRowId() + " wasn't found"); - if (group.getType() != groupType) - throw new Exception("Well group cannot be changed: " + gwtGroup.getName()); - - group.setName(gwtGroup.getName()); - group.setPositions(positions); - - ((PlateImpl)plate).storeWellGroup(group); - } - else - { - assert gwtGroup.getRowId() <= 0 : "Updating existing well group on a new template"; - group = (WellGroupImpl) plate.addWellGroup(gwtGroup.getName(), groupType, positions); - } - - group.setProperties(gwtGroup.getProperties()); - } - - PlateManager.get().getPlateLayoutHandler(plate.getAssayType()).validatePlate(getContainer(), getUser(), plate); - return PlateService.get().save(getContainer(), getUser(), plate); - } - catch (BatchValidationException | ValidationException e) - { - LOG.error("Error saving plate", e); - throw new Exception(e); - } - } - - private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) - { - for (WellGroup wellGroup : wellGroups) - { - if (wellGroup.getRowId() != null && wellGroup.getRowId() == rowId) - return (WellGroupImpl) wellGroup; - } - return null; - } -} diff --git a/assay/src/org/labkey/assay/view/AssayGWTView.java b/assay/src/org/labkey/assay/view/AssayGWTView.java deleted file mode 100644 index 822aee9d815..00000000000 --- a/assay/src/org/labkey/assay/view/AssayGWTView.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2010-2013 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.assay.view; - -import com.google.gwt.core.client.EntryPoint; -import gwt.client.org.labkey.assay.AssayApplication; -import org.labkey.api.view.GWTView; - -import java.util.Map; - -/** - * User: matthewb - * Date: Mar 31, 2010 - * Time: 2:21:17 PM - */ -public class AssayGWTView extends GWTView -{ - public AssayGWTView(AssayApplication.GWTModule module, Map properties) - { - super("gwt.AssayApplication", properties); - getModelBean().getProperties().put("GWTModule", module.getClass().getSimpleName()); - } - - public AssayGWTView(Class clss, Map properties) - { - super("gwt.AssayApplication", properties); - for (AssayApplication.GWTModule m : AssayApplication.GWTModule.values()) - { - if (m.className.equals(clss.getName())) - { - getModelBean().getProperties().put("GWTModule", m.getClass().getSimpleName()); - return; - } - } - throw new IllegalArgumentException(clss.getName()); - } -} diff --git a/experiment/src/org/labkey/experiment/api/ExpDataClassDataTestCase.jsp b/experiment/src/org/labkey/experiment/api/ExpDataClassDataTestCase.jsp index 26381bc8d2f..efbcf19c39a 100644 --- a/experiment/src/org/labkey/experiment/api/ExpDataClassDataTestCase.jsp +++ b/experiment/src/org/labkey/experiment/api/ExpDataClassDataTestCase.jsp @@ -107,8 +107,8 @@ <%@ page import="java.util.concurrent.TimeUnit" %> <%@ page import="java.util.stream.Collectors" %> <%@ page import="static org.labkey.api.util.PageFlowUtil.encodeURIComponent" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asInteger" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asLong" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asInteger" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asLong" %> <%@ page import="static org.hamcrest.Matchers.containsString" %> <%@ page extends="org.labkey.api.jsp.JspTest.BVT" %> diff --git a/experiment/src/org/labkey/experiment/api/ExpSampleTypeTestCase.jsp b/experiment/src/org/labkey/experiment/api/ExpSampleTypeTestCase.jsp index 10e13d4a0ea..04332ea21c8 100644 --- a/experiment/src/org/labkey/experiment/api/ExpSampleTypeTestCase.jsp +++ b/experiment/src/org/labkey/experiment/api/ExpSampleTypeTestCase.jsp @@ -82,8 +82,8 @@ <%@ page import="java.util.concurrent.TimeUnit" %> <%@ page import="org.jetbrains.annotations.NotNull" %> <%@ page import="org.labkey.api.dataiterator.MapDataIterator" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asInteger" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asLong" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asInteger" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asLong" %> <%@ page import="static java.util.Collections.emptyList" %> <%@ page import="org.jetbrains.annotations.Nullable" %> <%@ page import="org.labkey.api.view.ActionURL" %> diff --git a/query/src/org/labkey/query/MultiValueTest.jsp b/query/src/org/labkey/query/MultiValueTest.jsp index c2f90629e65..ebe587b1f4c 100644 --- a/query/src/org/labkey/query/MultiValueTest.jsp +++ b/query/src/org/labkey/query/MultiValueTest.jsp @@ -33,8 +33,8 @@ <%@ page import="java.util.Map" %> <%@ page import="java.util.Set" %> <%@ page import="org.labkey.api.util.JsonUtil" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asInteger" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asLong" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asInteger" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asLong" %> <%@ page extends="org.labkey.api.jsp.JspTest.DRT" %> <%! final String aliasPrefix = "MultiValueTest-"; diff --git a/query/src/org/labkey/query/model/MetadataTableJSONMixin.java b/query/src/org/labkey/query/model/MetadataTableJSONMixin.java index 426541613fb..536cecdf6a9 100644 --- a/query/src/org/labkey/query/model/MetadataTableJSONMixin.java +++ b/query/src/org/labkey/query/model/MetadataTableJSONMixin.java @@ -1,11 +1,10 @@ package org.labkey.query.model; import com.fasterxml.jackson.annotation.JsonProperty; -import org.labkey.api.gwt.client.util.StringProperty; public abstract class MetadataTableJSONMixin { - MetadataTableJSONMixin(@JsonProperty("URL") StringProperty url) + MetadataTableJSONMixin(@JsonProperty("URL") String url) { } @JsonProperty("URL") diff --git a/study/src/org/labkey/study/controllers/CohortController.java b/study/src/org/labkey/study/controllers/CohortController.java index fc2463fa34d..119bb1b78a1 100644 --- a/study/src/org/labkey/study/controllers/CohortController.java +++ b/study/src/org/labkey/study/controllers/CohortController.java @@ -15,6 +15,7 @@ */ package org.labkey.study.controllers; +import com.google.api.client.util.Objects; import org.apache.commons.lang3.Strings; import org.labkey.api.action.FormHandlerAction; import org.labkey.api.action.FormViewAction; @@ -26,7 +27,6 @@ import org.labkey.api.data.DataRegion; import org.labkey.api.data.DbScope; import org.labkey.api.data.TableInfo; -import org.labkey.api.gwt.client.util.PropertyUtil; import org.labkey.api.query.QueryUpdateForm; import org.labkey.api.query.ValidationError; import org.labkey.api.query.ValidationException; @@ -427,7 +427,8 @@ public boolean handlePost(EditCohortForm form, BindException errors) boolean labelChanged = (newLabel != null && !cohort.getLabel().equals(newLabel)); boolean enrolledChanged = cohort.isEnrolled() != newEnrolled; - boolean subjectCountChanged = !PropertyUtil.nullSafeEquals(cohort.getSubjectCount(), newSubjectCount); + Object o1 = cohort.getSubjectCount(); + boolean subjectCountChanged = !Objects.equal(o1, newSubjectCount); boolean desciprtionChanged = !Strings.CS.equals(cohort.getDescription(), newDescription); if (labelChanged || enrolledChanged || subjectCountChanged || desciprtionChanged) diff --git a/study/src/org/labkey/study/query/CohortUpdateService.java b/study/src/org/labkey/study/query/CohortUpdateService.java index 9f5b04136e2..42a623f4e94 100644 --- a/study/src/org/labkey/study/query/CohortUpdateService.java +++ b/study/src/org/labkey/study/query/CohortUpdateService.java @@ -15,15 +15,14 @@ */ package org.labkey.study.query; +import com.google.api.client.util.Objects; import org.apache.commons.beanutils.converters.IntegerConverter; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; import org.labkey.api.data.DbScope; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; -import org.labkey.api.gwt.client.util.PropertyUtil; import org.labkey.api.query.AbstractQueryUpdateService; import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.ValidationException; @@ -138,8 +137,9 @@ protected Map updateRow(User user, Container container, Map updateRow(User user, Container container, Map <%@ page import="static org.labkey.api.util.PageFlowUtil.jsString" %> <%@ page import="java.util.Objects" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asInteger" %> -<%@ page import="static org.labkey.api.exp.api.ExperimentService.asLong" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asInteger" %> +<%@ page import="static org.labkey.api.util.IntegerUtils.asLong" %> <%@ page import="org.labkey.api.collections.LongHashMap" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> From e35f1d99f725889e1dcdc4dac6fa6f89e1f8f416 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 29 Apr 2026 20:02:55 -0700 Subject: [PATCH 12/18] Delete GWT --- .gitattributes | 69 ++++++++++---------------------------------------- 1 file changed, 13 insertions(+), 56 deletions(-) diff --git a/.gitattributes b/.gitattributes index 78a592374d5..96e0d2042b3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -51,39 +51,19 @@ announcements/src/org/labkey/announcements/respond.jsp -text announcements/src/org/labkey/announcements/rss.jsp -text announcements/src/org/labkey/announcements/SendMessageAction.java -text announcements/src/org/labkey/announcements/update.jsp -text -api/gwtsrc/org/labkey/api/gwt/client/assay/AssayException.java -text -api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java -text -api/gwtsrc/org/labkey/api/gwt/client/assay/model/GWTProtocol.java -text -api/gwtsrc/org/labkey/api/gwt/client/AuditBehaviorType.java -text -api/gwtsrc/org/labkey/api/gwt/client/DefaultScaleType.java -text -api/gwtsrc/org/labkey/api/gwt/client/DefaultValueType.java -text -api/gwtsrc/org/labkey/api/gwt/client/FacetingBehaviorType.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/GWTContainer.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/GWTIndex.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyValidator.java -text -api/gwtsrc/org/labkey/api/gwt/client/model/PropertyValidatorType.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/BoundTextBox.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/DirtyCallback.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/domain/CancellationException.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/HelpPopup.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/ImageButton.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/TextBoxDialogBox.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/WebPartPanel.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/WidgetUpdatable.java -text -api/gwtsrc/org/labkey/api/gwt/client/ui/WindowUtil.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/BooleanProperty.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/ColorGenerator.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/ErrorDialogAsyncCallback.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/IntegerProperty.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/IPropertyWrapper.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/PropertyUtil.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/ServiceUtil.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/StringProperty.java -text -api/gwtsrc/org/labkey/api/gwt/client/util/StringUtils.java -text -api/gwtsrc/org/labkey/api/gwt/Internal.gwt.xml -text +api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java -text +api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java -text +api/src/org/labkey/api/gwt/client/AuditBehaviorType.java -text +api/src/org/labkey/api/gwt/client/DefaultScaleType.java -text +api/src/org/labkey/api/gwt/client/DefaultValueType.java -text +api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java -text +api/src/org/labkey/api/gwt/client/model/GWTConditionalFormat.java -text +api/src/org/labkey/api/gwt/client/model/GWTContainer.java -text +api/src/org/labkey/api/gwt/client/model/GWTDomain.java -text +api/src/org/labkey/api/gwt/client/model/GWTIndex.java -text +api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java -text +api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java -text +api/src/org/labkey/api/gwt/client/model/PropertyValidatorType.java -text api/module.properties -text api/schemas/apiTest.xsd -text api/schemas/clientLibrary.xsd -text @@ -1398,8 +1378,6 @@ api/src/org/labkey/api/view/DefaultModelAndView.java -text api/src/org/labkey/api/view/FolderManagement.java -text api/src/org/labkey/api/view/FolderTab.java -text api/src/org/labkey/api/view/ForbiddenProjectException.java -text -api/src/org/labkey/api/view/GWTView.java -text -api/src/org/labkey/api/view/GWTView.jsp -text api/src/org/labkey/api/view/JspTemplate.java -text api/src/org/labkey/api/view/jspTemplateTest.jsp -text api/src/org/labkey/api/view/menu/ContainerMenu.java -text @@ -1592,27 +1570,6 @@ assay/api-src/org/labkey/api/assay/plate/Well.java -text assay/api-src/org/labkey/api/assay/plate/WellData.java -text assay/api-src/org/labkey/api/assay/plate/WellGroup.java -text assay/api-src/org/labkey/api/assay/query/RunListDetailsQueryView.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListener.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupChangeListenerAdapter.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanel.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypePanelRow.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/GroupTypesTabPanel.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPlate.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTPosition.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/model/GWTWellGroup.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataService.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlateDataServiceAsync.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PlatePropertyPanel.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyCreationDialog.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/PropertyPanel.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/ShiftPanel.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/StatusBar.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateDesigner.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGrid.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateGridCell.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/TemplateView.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WarningPanel.java -text -assay/gwtsrc/gwt/client/org/labkey/plate/designer/client/WellGroupPropertyPanel.java -text assay/schemas/assayProvider.xsd -text assay/src/org/labkey/api/assay/nab/view/controlSummary.jsp -text assay/src/org/labkey/api/assay/nab/view/cutoffDilutions.jsp -text From 7e0e4a9fd7c47a5d7c0351e2c723866e5a4a8575 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 29 Apr 2026 20:10:44 -0700 Subject: [PATCH 13/18] Delete GWT --- .../api/gwt/client/DefaultValueType.java | 89 ++- .../api/gwt/client/FacetingBehaviorType.java | 119 ++-- .../api/gwt/client/model/GWTDomain.java | 622 +++++++++--------- .../client/model/GWTPropertyDescriptor.java | 450 +++---------- .../api/gwt/client/ui/PropertyType.java | 19 - 5 files changed, 469 insertions(+), 830 deletions(-) diff --git a/api/src/org/labkey/api/gwt/client/DefaultValueType.java b/api/src/org/labkey/api/gwt/client/DefaultValueType.java index 259c59bc19a..9f2798e8b10 100644 --- a/api/src/org/labkey/api/gwt/client/DefaultValueType.java +++ b/api/src/org/labkey/api/gwt/client/DefaultValueType.java @@ -1,47 +1,44 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client; - -import java.io.Serializable; - -public enum DefaultValueType implements Serializable -{ - FIXED_EDITABLE("Editable default", "An editable default value will be entered for the user. The default value will be the same for every user for every insert."), - FIXED_NON_EDITABLE("Fixed value", "Fixed values cannot be edited by the user. This option is used to save fixed data with each inserted data row."), - LAST_ENTERED("Last entered", "An editable default value will be entered for the user's first use of the form. During subsequent inserts, the user will see their last entered value as the default."); - - private String _label; - private String _helpText; - - // Needed for GWT serialization to work correctly, at least in dev mode - DefaultValueType() {} - - DefaultValueType(String label, String helpText) - { - _label = label; - _helpText = helpText; - } - - public String getLabel() - { - return _label; - } - - public String getHelpText() - { - return _helpText; - } +/* + * Copyright (c) 2018-2019 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.api.gwt.client; + +import java.io.Serializable; + +public enum DefaultValueType implements Serializable +{ + FIXED_EDITABLE("Editable default", "An editable default value will be entered for the user. The default value will be the same for every user for every insert."), + FIXED_NON_EDITABLE("Fixed value", "Fixed values cannot be edited by the user. This option is used to save fixed data with each inserted data row."), + LAST_ENTERED("Last entered", "An editable default value will be entered for the user's first use of the form. During subsequent inserts, the user will see their last entered value as the default."); + + private final String _label; + private final String _helpText; + + DefaultValueType(String label, String helpText) + { + _label = label; + _helpText = helpText; + } + + public String getLabel() + { + return _label; + } + + public String getHelpText() + { + return _helpText; + } } \ No newline at end of file diff --git a/api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java b/api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java index 5994d34761c..3b08697d4ff 100644 --- a/api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java +++ b/api/src/org/labkey/api/gwt/client/FacetingBehaviorType.java @@ -1,69 +1,50 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client; - -/** - * User: klum - * Date: Apr 11, 2012 - */ -public enum FacetingBehaviorType -{ - AUTOMATIC("Auto detect", - "The column will be examined to determine if the faceted filter panel can be shown. A column is a faceting candidate if it is a " + - "lookup, dimension or of type: (Boolean, Integer, Text, DateTime)."), - ALWAYS_ON("On", - "The faceted filter panel will be shown by default."), - ALWAYS_OFF("Off", - "The faceted filter panel will not be shown by default."); - - private final String _label; - private final String _helpText; - - private static String _helpPopupHtml; - - FacetingBehaviorType(String label, String helpText) - { - _label = label; - _helpText = helpText; - } - - public String getLabel() - { - return _label; - } - - public String getHelpText() - { - return _helpText; - } - - public static String getHelpPopupHtml() - { - if (_helpPopupHtml == null) - { - StringBuilder helpString = new StringBuilder(); - for (int i = 0; i < values().length; i++) - { - FacetingBehaviorType type = values()[i]; - helpString.append("").append(type.getLabel()).append(": ").append(type.getHelpText()); - if (i < values().length - 1) - helpString.append("

"); - } - _helpPopupHtml = helpString.toString(); - } - return _helpPopupHtml; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client; + +/** + * User: klum + * Date: Apr 11, 2012 + */ +public enum FacetingBehaviorType +{ + AUTOMATIC("Auto detect", + "The column will be examined to determine if the faceted filter panel can be shown. A column is a faceting candidate if it is a " + + "lookup, dimension or of type: (Boolean, Integer, Text, DateTime)."), + ALWAYS_ON("On", + "The faceted filter panel will be shown by default."), + ALWAYS_OFF("Off", + "The faceted filter panel will not be shown by default."); + + private final String _label; + private final String _helpText; + + FacetingBehaviorType(String label, String helpText) + { + _label = label; + _helpText = helpText; + } + + public String getLabel() + { + return _label; + } + + public String getHelpText() + { + return _helpText; + } +} diff --git a/api/src/org/labkey/api/gwt/client/model/GWTDomain.java b/api/src/org/labkey/api/gwt/client/model/GWTDomain.java index 198faefe6e6..920ab61b0fc 100644 --- a/api/src/org/labkey/api/gwt/client/model/GWTDomain.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTDomain.java @@ -1,323 +1,299 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; -import lombok.Setter; -import org.labkey.api.gwt.client.DefaultValueType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class GWTDomain -{ - private String _ts; - @Getter @Setter private int domainId; - @Getter @Setter private String name; - @Getter @Setter private String domainURI; - @Getter @Setter private String domainKindName; - @Getter @Setter private String description; - @Getter @Setter private String container; - @Getter @Setter private boolean allowFileLinkProperties; - @Getter @Setter private boolean allowAttachmentProperties; - @Getter @Setter private boolean allowTextChoiceProperties; - @Getter @Setter private boolean allowMultiChoiceProperties; - @Getter @Setter private boolean allowSampleSubjectProperties; - @Getter @Setter private boolean allowTimepointProperties; - @Getter @Setter private boolean allowUniqueConstraintProperties; - @Getter @Setter private boolean allowCalculatedFields; - @Getter @Setter private boolean showDefaultValueSettings; - private DefaultValueType defaultDefaultValueType = null; - private DefaultValueType[] defaultValueOptions = new DefaultValueType[0]; - private List fields = new ArrayList<>(); - private List standardFields = null; - private List calculatedFields = null; - @Getter @Setter private List indices = new ArrayList<>(); - @Setter - @Getter - private String defaultValuesURL = null; - private Set mandatoryPropertyDescriptorNames = new HashSet<>(); - private Set reservedFieldNames = new HashSet<>(); - private Set reservedFieldNamePrefixes = new HashSet<>(); - private Set phiNotAllowedFieldNames = new HashSet<>(); - private Set excludeFromExportFieldNames = new HashSet<>(); - @Getter @Setter private boolean provisioned = false; - @Getter @Setter private List disabledSystemFields; - - // schema,query,template are not part of the domain, but it's handy to pass - // these values to the PropertiedEditor along with the GWTDomain. - // NOTE queryName is not necessarily == name - @Getter @Setter private String schemaName = null; - @Getter @Setter private String queryName = null; - @Getter @Setter private String templateDescription = null; // null if no template - @Getter @Setter private String instructions = null; - @Getter @Setter private boolean supportsPhiLevel = false; - - public GWTDomain() - { - } - - // deep clone constructor - public GWTDomain(GWTDomain src) - { - _ts = src._ts; - this.domainId = src.domainId; - this.name = src.name; - this.domainURI = src.domainURI; - this.domainKindName = src.domainKindName; - this.description = src.description; - this.disabledSystemFields = src.disabledSystemFields; - this.container = src.container; - this.allowFileLinkProperties = src.allowFileLinkProperties; - this.allowAttachmentProperties = src.allowAttachmentProperties; - this.allowTextChoiceProperties = src.allowTextChoiceProperties; - this.allowMultiChoiceProperties = src.allowMultiChoiceProperties; - this.allowSampleSubjectProperties = src.allowSampleSubjectProperties; - this.allowTimepointProperties = src.allowTimepointProperties; - this.allowUniqueConstraintProperties = src.allowUniqueConstraintProperties; - this.allowCalculatedFields = src.allowCalculatedFields; - this.showDefaultValueSettings = src.showDefaultValueSettings; - this.defaultDefaultValueType = src.defaultDefaultValueType; - this.defaultValueOptions = src.defaultValueOptions; - this.defaultValuesURL = src.defaultValuesURL; - this.provisioned = src.provisioned; - this.supportsPhiLevel = src.supportsPhiLevel; - - if (src.indices != null) - { - for (int i = 0; i < src.indices.size(); i++) - this.indices.add(src.indices.get(i).copy()); - } - - // include all fields here (standard and calculated) in the copy - if (src.getFields(true) == null) - return; - for (int i=0 ; i getFields(boolean includeCalculated) - { - if (includeCalculated) - return fields; - else - return getFields(); - } - - public List getFields() - { - if (standardFields == null) - standardFields = fields.stream().filter(f -> f.getValueExpression() == null).toList(); - return standardFields; - } - - public List getCalculatedFields() - { - if (calculatedFields == null) - calculatedFields = fields.stream().filter(f -> f.getValueExpression() != null).toList(); - return calculatedFields; - } - - public void setFields(List list) - { - fields = list; - - // reset the cached lists of fields so they will be recalculated on next call to getters - standardFields = null; - calculatedFields = null; - } - - public FieldType getFieldByName(String name) - { - for (FieldType field : getFields(true)) - { - if (field.getName() != null && field.getName().equalsIgnoreCase(name)) - return field; - } - return null; - } - - /** - * @return Indicates that the property can't be removed from the domain. The property may or may not be nullable. - */ - public boolean isMandatoryField(FieldType field) - { - if (mandatoryPropertyDescriptorNames == null || field.getName() == null) - { - return false; - } - return mandatoryPropertyDescriptorNames.contains(field.getName().toLowerCase()); - } - - public boolean isEditable(FieldType field) - { - return true; - } - - /** - * @return Indicates that the property is not allowed to be set as PHI - */ - public boolean allowsPhi(FieldType field) - { - return !(getPhiNotAllowedFieldNames() != null && field.getName() != null && getPhiNotAllowedFieldNames().contains(field.getName().toLowerCase())); - } - - /** - * @param mandatoryFieldNames names of property descriptors that must be present in this domain. Does not indicate that they must be non-nullable. - */ - public void setMandatoryFieldNames(Set mandatoryFieldNames) - { - this.mandatoryPropertyDescriptorNames = new HashSet<>(); - for (String mandatoryPropertyDescriptor : mandatoryFieldNames) - { - this.mandatoryPropertyDescriptorNames.add(mandatoryPropertyDescriptor.toLowerCase()); - } - } - - /** - * Get the list of property names that can't be removed from the domain. The set of mandatory fields is not modifiable in the designer. - */ - public Set getMandatoryFieldNames() - { - if (this.mandatoryPropertyDescriptorNames == null) - return Collections.emptySet(); - return Collections.unmodifiableSet(this.mandatoryPropertyDescriptorNames); - } - - public Set getReservedFieldNames() - { - return reservedFieldNames; - } - - /** - * @param reservedFieldNames can't create new fields with these names - */ - public void setReservedFieldNames(Set reservedFieldNames) - { - this.reservedFieldNames = new HashSet<>(); - for (String s : reservedFieldNames) - { - this.reservedFieldNames.add(s.toLowerCase()); - } - } - - public Set getReservedFieldNamePrefixes() - { - return this.reservedFieldNamePrefixes; - } - - public void setReservedFieldNamePrefixes(Set prefixes) - { - this.reservedFieldNamePrefixes = new HashSet<>(prefixes); - } - /** - * - * @param excludeFromExportFieldNames These fields will be suppressed from the export field list. Primary use case is to not export List key fields. - */ - public void setExcludeFromExportFieldNames(Set excludeFromExportFieldNames) - { - this.excludeFromExportFieldNames = new HashSet<>(); - for (String excludeFromExportFieldName : excludeFromExportFieldNames) - { - this.excludeFromExportFieldNames.add(excludeFromExportFieldName.toLowerCase()); - } - } - - public Set getExcludeFromExportFieldNames() - { - return excludeFromExportFieldNames; - } - - public boolean isExcludeFromExportField(FieldType field) - { - if (excludeFromExportFieldNames == null || field.getName() == null) - { - return false; - } - return excludeFromExportFieldNames.contains(field.getName().toLowerCase()); - } - - public Set getPhiNotAllowedFieldNames() - { - return phiNotAllowedFieldNames; - } - - public void setPhiNotAllowedFieldNames(Set phiNotAllowedFieldNames) - { - this.phiNotAllowedFieldNames = new HashSet<>(); - for (String fieldName : phiNotAllowedFieldNames) - { - this.phiNotAllowedFieldNames.add(fieldName.toLowerCase()); - } - } - - public DefaultValueType getDefaultDefaultValueType() - { - return defaultDefaultValueType; - } - - public DefaultValueType[] getDefaultValueOptions() - { - return defaultValueOptions; - } - - public void setDefaultValueOptions(DefaultValueType[] defaultOptions, DefaultValueType defaultDefault) - { - this.defaultDefaultValueType = defaultDefault; - this.defaultValueOptions = defaultOptions; - } -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import org.labkey.api.gwt.client.DefaultValueType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class GWTDomain +{ + private String _ts; + @Getter @Setter private int domainId; + @Getter @Setter private String name; + @Getter @Setter private String domainURI; + @Getter @Setter private String domainKindName; + @Getter @Setter private String description; + @Getter @Setter private String container; + @Getter @Setter private boolean allowFileLinkProperties; + @Getter @Setter private boolean allowAttachmentProperties; + @Getter @Setter private boolean allowTextChoiceProperties; + @Getter @Setter private boolean allowMultiChoiceProperties; + @Getter @Setter private boolean allowSampleSubjectProperties; + @Getter @Setter private boolean allowTimepointProperties; + @Getter @Setter private boolean allowUniqueConstraintProperties; + @Getter @Setter private boolean allowCalculatedFields; + @Getter @Setter private boolean showDefaultValueSettings; + @Getter + private DefaultValueType defaultDefaultValueType = null; + @Getter + private DefaultValueType[] defaultValueOptions = new DefaultValueType[0]; + private List fields = new ArrayList<>(); + private List standardFields = null; + private List calculatedFields = null; + @Getter @Setter private List indices = new ArrayList<>(); + @Setter + @Getter + private String defaultValuesURL = null; + private Set mandatoryPropertyDescriptorNames = new HashSet<>(); + @Getter + private Set reservedFieldNames = new HashSet<>(); + @Getter + private Set reservedFieldNamePrefixes = new HashSet<>(); + @Getter + private Set phiNotAllowedFieldNames = new HashSet<>(); + @Getter + private Set excludeFromExportFieldNames = new HashSet<>(); + @Getter @Setter private boolean provisioned = false; + @Getter @Setter private List disabledSystemFields; + + // schema,query,template are not part of the domain, but it's handy to pass + // these values to the PropertiedEditor along with the GWTDomain. + // NOTE queryName is not necessarily == name + @Getter @Setter private String schemaName = null; + @Getter @Setter private String queryName = null; + @Getter @Setter private String templateDescription = null; // null if no template + @Getter @Setter private String instructions = null; + @Getter @Setter private boolean supportsPhiLevel = false; + + public GWTDomain() + { + } + + // deep clone constructor + public GWTDomain(GWTDomain src) + { + _ts = src._ts; + this.domainId = src.domainId; + this.name = src.name; + this.domainURI = src.domainURI; + this.domainKindName = src.domainKindName; + this.description = src.description; + this.disabledSystemFields = src.disabledSystemFields; + this.container = src.container; + this.allowFileLinkProperties = src.allowFileLinkProperties; + this.allowAttachmentProperties = src.allowAttachmentProperties; + this.allowTextChoiceProperties = src.allowTextChoiceProperties; + this.allowMultiChoiceProperties = src.allowMultiChoiceProperties; + this.allowSampleSubjectProperties = src.allowSampleSubjectProperties; + this.allowTimepointProperties = src.allowTimepointProperties; + this.allowUniqueConstraintProperties = src.allowUniqueConstraintProperties; + this.allowCalculatedFields = src.allowCalculatedFields; + this.showDefaultValueSettings = src.showDefaultValueSettings; + this.defaultDefaultValueType = src.defaultDefaultValueType; + this.defaultValueOptions = src.defaultValueOptions; + this.defaultValuesURL = src.defaultValuesURL; + this.provisioned = src.provisioned; + this.supportsPhiLevel = src.supportsPhiLevel; + + if (src.indices != null) + { + for (int i = 0; i < src.indices.size(); i++) + this.indices.add(src.indices.get(i).copy()); + } + + // include all fields here (standard and calculated) in the copy + if (src.getFields(true) == null) + return; + for (int i=0 ; i getFields(boolean includeCalculated) + { + if (includeCalculated) + return fields; + else + return getFields(); + } + + public List getFields() + { + if (standardFields == null) + standardFields = fields.stream().filter(f -> f.getValueExpression() == null).toList(); + return standardFields; + } + + public List getCalculatedFields() + { + if (calculatedFields == null) + calculatedFields = fields.stream().filter(f -> f.getValueExpression() != null).toList(); + return calculatedFields; + } + + public void setFields(List list) + { + fields = list; + + // reset the cached lists of fields so they will be recalculated on next call to getters + standardFields = null; + calculatedFields = null; + } + + public FieldType getFieldByName(String name) + { + for (FieldType field : getFields(true)) + { + if (field.getName() != null && field.getName().equalsIgnoreCase(name)) + return field; + } + return null; + } + + /** + * @return Indicates that the property can't be removed from the domain. The property may or may not be nullable. + */ + public boolean isMandatoryField(FieldType field) + { + if (mandatoryPropertyDescriptorNames == null || field.getName() == null) + { + return false; + } + return mandatoryPropertyDescriptorNames.contains(field.getName().toLowerCase()); + } + + public boolean isEditable(FieldType field) + { + return true; + } + + /** + * @return Indicates that the property is not allowed to be set as PHI + */ + public boolean allowsPhi(FieldType field) + { + return !(getPhiNotAllowedFieldNames() != null && field.getName() != null && getPhiNotAllowedFieldNames().contains(field.getName().toLowerCase())); + } + + /** + * @param mandatoryFieldNames names of property descriptors that must be present in this domain. Does not indicate that they must be non-nullable. + */ + public void setMandatoryFieldNames(Set mandatoryFieldNames) + { + this.mandatoryPropertyDescriptorNames = new HashSet<>(); + for (String mandatoryPropertyDescriptor : mandatoryFieldNames) + { + this.mandatoryPropertyDescriptorNames.add(mandatoryPropertyDescriptor.toLowerCase()); + } + } + + /** + * Get the list of property names that can't be removed from the domain. The set of mandatory fields is not modifiable in the designer. + */ + public Set getMandatoryFieldNames() + { + if (this.mandatoryPropertyDescriptorNames == null) + return Collections.emptySet(); + return Collections.unmodifiableSet(this.mandatoryPropertyDescriptorNames); + } + + /** + * @param reservedFieldNames can't create new fields with these names + */ + public void setReservedFieldNames(Set reservedFieldNames) + { + this.reservedFieldNames = new HashSet<>(); + for (String s : reservedFieldNames) + { + this.reservedFieldNames.add(s.toLowerCase()); + } + } + + public void setReservedFieldNamePrefixes(Set prefixes) + { + this.reservedFieldNamePrefixes = new HashSet<>(prefixes); + } + /** + * + * @param excludeFromExportFieldNames These fields will be suppressed from the export field list. Primary use case is to not export List key fields. + */ + public void setExcludeFromExportFieldNames(Set excludeFromExportFieldNames) + { + this.excludeFromExportFieldNames = new HashSet<>(); + for (String excludeFromExportFieldName : excludeFromExportFieldNames) + { + this.excludeFromExportFieldNames.add(excludeFromExportFieldName.toLowerCase()); + } + } + + public boolean isExcludeFromExportField(FieldType field) + { + if (excludeFromExportFieldNames == null || field.getName() == null) + { + return false; + } + return excludeFromExportFieldNames.contains(field.getName().toLowerCase()); + } + + public void setPhiNotAllowedFieldNames(Set phiNotAllowedFieldNames) + { + this.phiNotAllowedFieldNames = new HashSet<>(); + for (String fieldName : phiNotAllowedFieldNames) + { + this.phiNotAllowedFieldNames.add(fieldName.toLowerCase()); + } + } + + public void setDefaultValueOptions(DefaultValueType[] defaultOptions, DefaultValueType defaultDefault) + { + this.defaultDefaultValueType = defaultDefault; + this.defaultValueOptions = defaultOptions; + } +} diff --git a/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java b/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java index 7aa3bd46509..9527d25e5ad 100644 --- a/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java @@ -23,7 +23,6 @@ import org.labkey.api.gwt.client.DefaultScaleType; import org.labkey.api.gwt.client.DefaultValueType; import org.labkey.api.gwt.client.LockedPropertyType; -import org.labkey.api.gwt.client.ui.PropertyType; import java.util.ArrayList; import java.util.List; @@ -34,51 +33,128 @@ @EqualsAndHashCode public class GWTPropertyDescriptor { + @Setter + @Getter private int propertyId = 0; + @Setter + @Getter private String propertyURI; + @Setter + @Getter private String container; + @Setter + @Getter private String name; + @Setter + @Getter private String description; + @Setter + @Getter private String rangeURI = "http://www.w3.org/2001/XMLSchema#string"; + @Setter + @Getter private String conceptURI; + @Setter + @Getter private String label; + @Setter + @Getter private String format; + @Setter + @Getter private boolean required = false; + @Setter + @Getter private boolean hidden = false; + @Getter + @Setter private String lookupContainer; + @Setter + @Getter private String lookupSchema; + @Setter + @Getter private String lookupQuery; + @Setter private boolean lookupIsValid = true; private String defaultValueType = null; + @Setter + @Getter private String defaultValue; + @Setter + @Getter private String defaultDisplayValue = "[none]"; + @Setter private boolean mvEnabled = false; + @Setter + @Getter private String importAliases; private String url; private String urlTarget; + @Setter + @Getter private boolean shownInInsertView = true; + @Setter + @Getter private boolean shownInUpdateView = true; + @Setter + @Getter private boolean shownInDetailsView = true; private Boolean measure; private Boolean dimension; + @Setter + @Getter private boolean recommendedVariable = false; + @Setter + @Getter private String defaultScale = DefaultScaleType.LINEAR.name(); + @Setter + @Getter private String facetingBehaviorType; private String phi = "NotPHI"; // Must match PHI.NotPHI and tableInfo.xsd enum PHIType.NotPHI private Boolean isExcludeFromShifting; + @Setter private boolean isPreventReordering = false; + @Setter private boolean isDisableEditing = false; + @Setter + @Getter private Integer scale = 4000; + @Setter + @Getter private String principalConceptCode; + @Setter + @Getter private String sourceOntology; + @Setter + @Getter private String conceptSubtree; + @Setter + @Getter private String conceptImportColumn; + @Setter + @Getter private String conceptLabelColumn; + @Setter + @Getter private String redactedText; + @Setter + @Getter private String derivationDataScope; private boolean isPrimaryKey = false; + /** + * -- SETTER -- + * This method is for informational purpose only so that the client can identify column's locked type. + * Setting lock type on a column via this method will not get preserved in the domain's table. + */ + @Setter + @Getter private String lockType = LockedPropertyType.NotLocked.name(); + @Setter + @Getter private boolean scannable = false; + @Setter + @Getter private String valueExpression; @Getter @Setter private List conditionalFormats = new ArrayList<>(); @@ -180,196 +256,11 @@ public GWTPropertyDescriptor copy() return new GWTPropertyDescriptor(this); } - public String getContainer() - { - return container; - } - - public void setContainer(String container) - { - this.container = container; - } - - public String getLookupContainer() - { - return lookupContainer; - } - - public void setLookupContainer(String lookupContainer) - { - this.lookupContainer = lookupContainer; - } - - public String getLookupSchema() - { - return lookupSchema; - } - - public void setLookupSchema(String lookupSchema) - { - this.lookupSchema = lookupSchema; - } - - public String getLookupQuery() - { - return lookupQuery; - } - - public void setLookupQuery(String lookupQuery) - { - this.lookupQuery = lookupQuery; - } - public boolean getLookupIsValid() { return lookupIsValid; } - public void setLookupIsValid(boolean lookupIsValid) - { - this.lookupIsValid = lookupIsValid; - } - - public int getPropertyId() - { - return propertyId; - } - - public void setPropertyId(int rowId) - { - this.propertyId = rowId; - } - - public String getPropertyURI() - { - return propertyURI; - } - - public void setPropertyURI(String propertyURI) - { - this.propertyURI = propertyURI; - } - - public String getName() - { - return name; - } - - public void setName(String name) - { - this.name = name; - } - - public String getDescription() - { - return description; - } - - public void setDescription(String description) - { - this.description = description; - } - - public String getRangeURI() - { - return rangeURI; - } - - public void setRangeURI(String dataTypeURI) - { - this.rangeURI = dataTypeURI; - } - - public void guessMeasureAndDimension() - { - boolean plottableType = PropertyType.xsdInt.getURI().equals(getRangeURI()) || - PropertyType.xsdDouble.getURI().equals(getRangeURI()); - boolean isMeasure = plottableType && getLookupQuery() == null && !isHidden(); - setMeasure(isMeasure); - - setDimension(getLookupQuery() != null && !isHidden()); - } - - public String getConceptURI() - { - return conceptURI; - } - - public void setConceptURI(String conceptURI) - { - this.conceptURI = conceptURI; - } - - public String getLabel() - { - return label; - } - - public void setLabel(String label) - { - this.label = label; - } - - public String getFormat() - { - return format; - } - - public void setFormat(String format) - { - this.format = format; - } - - public boolean isRequired() - { - return required; - } - - public void setRequired(boolean required) - { - this.required = required; - } - - public boolean isHidden() - { - return hidden; - } - - public void setHidden(boolean hidden) - { - this.hidden = hidden; - } - - public boolean isShownInInsertView() - { - return shownInInsertView; - } - - public void setShownInInsertView(boolean shown) - { - this.shownInInsertView = shown; - } - - public boolean isShownInUpdateView() - { - return shownInUpdateView; - } - - public void setShownInUpdateView(boolean shown) - { - this.shownInUpdateView = shown; - } - - public boolean isShownInDetailsView() - { - return shownInDetailsView; - } - - public void setShownInDetailsView(boolean shown) - { - this.shownInDetailsView = shown; - } - public boolean isSetMeasure() { return measure != null; @@ -400,36 +291,11 @@ public void setDimension(boolean isDimension) this.dimension = isDimension; } - public boolean isRecommendedVariable() - { - return recommendedVariable; - } - - public void setRecommendedVariable(boolean isRecommendedVariable) - { - this.recommendedVariable = isRecommendedVariable; - } - - public String getDefaultScale() - { - return defaultScale; - } - - public void setDefaultScale(String defaultScale) - { - this.defaultScale = defaultScale; - } - public boolean getMvEnabled() { return mvEnabled; } - public void setMvEnabled(boolean mvEnabled) - { - this.mvEnabled = mvEnabled; - } - public DefaultValueType getDefaultValueType() { return null==defaultValueType ? null : DefaultValueType.valueOf(defaultValueType); @@ -440,36 +306,6 @@ public void setDefaultValueType(DefaultValueType defaultValueType) this.defaultValueType = null==defaultValueType ? null : defaultValueType.name(); } - public String getDefaultValue() - { - return defaultValue; - } - - public void setDefaultValue(String defaultValue) - { - this.defaultValue = defaultValue; - } - - public String getDefaultDisplayValue() - { - return defaultDisplayValue; - } - - public void setDefaultDisplayValue(String defaultDisplayValue) - { - this.defaultDisplayValue = defaultDisplayValue; - } - - public String getFacetingBehaviorType() - { - return facetingBehaviorType; - } - - public void setFacetingBehaviorType(String facetingBehavior) - { - this.facetingBehaviorType = facetingBehavior; - } - public String getPHI() { return phi; @@ -500,115 +336,11 @@ public boolean getPreventReordering() return isPreventReordering; } - public void setPreventReordering(boolean preventReordering) - { - this.isPreventReordering = preventReordering; - } - public boolean getDisableEditing() { return isDisableEditing; } - public void setDisableEditing(boolean disableEditing) - { - this.isDisableEditing = disableEditing; - } - - public Integer getScale() - { - return scale; - } - - public void setScale(Integer value) - { - this.scale = value; - } - - public boolean isScannable() - { - return scannable; - } - - public void setScannable(boolean scannable) - { - this.scannable = scannable; - } - - public String getPrincipalConceptCode() { return principalConceptCode; } - - public void setPrincipalConceptCode(String code) { this.principalConceptCode = code; } - - public String getSourceOntology() - { - return sourceOntology; - } - - public void setSourceOntology(String sourceOntology) - { - this.sourceOntology = sourceOntology; - } - - public String getConceptSubtree() - { - return conceptSubtree; - } - - public void setConceptSubtree(String path) - { - this.conceptSubtree = path; - } - - public String getConceptImportColumn() - { - return conceptImportColumn; - } - - public void setConceptImportColumn(String conceptImportColumn) - { - this.conceptImportColumn = conceptImportColumn; - } - - public String getConceptLabelColumn() - { - return conceptLabelColumn; - } - - public void setConceptLabelColumn(String conceptLabelColumn) - { - this.conceptLabelColumn = conceptLabelColumn; - } - - public String getRedactedText() - { - return redactedText; - } - - public void setRedactedText(String redactedText) - { - this.redactedText = redactedText; - } - - public String getDerivationDataScope() - { - return derivationDataScope; - } - - public void setDerivationDataScope(String derivationDataScope) - { - this.derivationDataScope = derivationDataScope; - } - - public String getValueExpression() - { - return valueExpression; - } - - public void setValueExpression(String valueExpression) - { - this.valueExpression = valueExpression; - } - public boolean getIsPrimaryKey() { return isPrimaryKey; @@ -622,34 +354,6 @@ public void setIsPrimaryKey(boolean isPrimaryKey) this.isPrimaryKey = isPrimaryKey; } - public String getLockType() - { - return lockType; - } - - /** This method is for informational purpose only so that the client can identify column's locked type. - * Setting lock type on a column via this method will not get preserved in the domain's table. - */ - public void setLockType(String lockType) - { - this.lockType = lockType; - } - - public String debugString() - { - return getName() + " " + getLabel() + " " + getRangeURI() + " " + isRequired() + " " + getDescription(); - } - - public String getImportAliases() - { - return importAliases; - } - - public void setImportAliases(String importAliases) - { - this.importAliases = importAliases; - } - public String getURL() { return url; diff --git a/api/src/org/labkey/api/gwt/client/ui/PropertyType.java b/api/src/org/labkey/api/gwt/client/ui/PropertyType.java index 5cb59516f9d..64ee613d3c8 100644 --- a/api/src/org/labkey/api/gwt/client/ui/PropertyType.java +++ b/api/src/org/labkey/api/gwt/client/ui/PropertyType.java @@ -16,9 +16,7 @@ package org.labkey.api.gwt.client.ui; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -102,18 +100,6 @@ public boolean isLookupType() return _lookup; } - public static PropertyType fromURI(String uri) - { - for (PropertyType propertyType : values()) - { - if (propertyType.getURI().equals(uri)) - { - return propertyType; - } - } - return null; - } - public static PropertyType fromName(String type) { PropertyType t = synonyms.get(type); @@ -153,9 +139,4 @@ private static void _put(PropertyType t) synonyms.put("bool", xsdBoolean); synonyms.put("xsd:boolean", xsdBoolean); } - - public static List getBaseTypes() - { - return Arrays.asList(xsdString, expMultiLine, xsdBoolean, xsdInt, xsdDouble, xsdDateTime); - } } From ab2a11c940998f57c46e2977a324ed296a33b57c Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 29 Apr 2026 20:49:36 -0700 Subject: [PATCH 14/18] More cleanup --- .../assay/AbstractAssayTsvDataHandler.java | 3 +- api/src/org/labkey/api/exp/PropertyType.java | 36 +- .../labkey/api/exp/property/DomainUtil.java | 2 +- .../model/GWTPropertyDescriptorMixin.java | 48 - .../gwt/client/assay/model/GWTProtocol.java | 952 +++++++++--------- .../api/gwt/client/model/GWTDomain.java | 5 - .../client/model/GWTPropertyDescriptor.java | 13 + .../api/gwt/client/ui/PropertyType.java | 142 --- .../org/labkey/api/query/FilteredTable.java | 2 +- .../study/query/PublishResultsQueryView.java | 4 +- .../org/labkey/assay/TsvAssayProvider.java | 2 +- .../assay/actions/GetProtocolAction.java | 23 - .../data/generator/AssayDesignGenerator.java | 2 +- .../data/generator/AssayRunDataGenerator.java | 2 +- .../experiment/api/ExperimentServiceImpl.java | 95 +- .../api/property/GWTDomainMixin.java | 12 +- .../api/property/PropertyServiceImpl.java | 10 +- .../org/labkey/query/MetadataTableJSON.java | 16 +- .../query/controllers/QueryController.java | 5 +- .../query/model/MetadataTableJSONMixin.java | 12 - .../assay/AssayPublishConfirmAction.java | 2 +- .../study/assay/StudyPublishManager.java | 6 +- 22 files changed, 582 insertions(+), 812 deletions(-) delete mode 100644 api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java delete mode 100644 api/src/org/labkey/api/gwt/client/ui/PropertyType.java delete mode 100644 query/src/org/labkey/query/model/MetadataTableJSONMixin.java diff --git a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java index 9fa5dac7d8a..20b4050fe83 100644 --- a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java +++ b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java @@ -102,7 +102,6 @@ import org.labkey.api.view.ActionURL; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; import org.springframework.jdbc.BadSqlGrammarException; import java.io.File; @@ -127,7 +126,7 @@ import static java.util.stream.Collectors.toList; import static org.labkey.api.assay.AssayRunUploadContext.ReImportOption.MERGE_DATA; import static org.labkey.api.exp.OntologyManager.NO_OP_ROW_CALLBACK; -import static org.labkey.api.gwt.client.ui.PropertyType.SAMPLE_CONCEPT_URI; +import static org.labkey.api.exp.PropertyType.SAMPLE_CONCEPT_URI; import static org.labkey.api.util.IntegerUtils.asLongElseNull; public abstract class AbstractAssayTsvDataHandler extends AbstractExperimentDataHandler implements ValidationDataHandler diff --git a/api/src/org/labkey/api/exp/PropertyType.java b/api/src/org/labkey/api/exp/PropertyType.java index b3ab3c8017a..333c7b96a6a 100644 --- a/api/src/org/labkey/api/exp/PropertyType.java +++ b/api/src/org/labkey/api/exp/PropertyType.java @@ -43,7 +43,6 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TimeZone; @@ -96,7 +95,7 @@ protected void setValue(ObjectProperty property, Object value) boolValue = (Boolean)value; else if (null != value) boolValue = (Boolean) ConvertUtils.convert(value.toString(), Boolean.class); - property.floatValue = boolValue == null ? null : boolValue == Boolean.TRUE ? 1.0 : 0.0; + property.floatValue = boolValue == null ? null : boolValue ? 1.0 : 0.0; } @Override @@ -963,6 +962,11 @@ public Object getPreviewValue(@Nullable String prefix) } }; + public static final String PARTICIPANT_CONCEPT_URI = "http://cpas.labkey.com/Study#ParticipantId"; + public static final String VISIT_CONCEPT_URI = "http://cpas.labkey.com/Study#VisitId"; + public static final String SAMPLE_CONCEPT_URI = "http://www.labkey.org/exp/xml#sample"; + public static final String CALCULATED_CONCEPT_URI = "http://www.labkey.org/exp/xml#calculated"; + private final String typeURI; private final String xarName; private final char storageType; @@ -970,8 +974,8 @@ public Object getPreviewValue(@Nullable String prefix) private final @NotNull JdbcType jdbcType; private final int scale; private final String inputType; - private final Class javaType; - private final Class[] additionalTypes; + private final Class javaType; + private final Class[] additionalTypes; private static Map uriToProperty; private static Map xarToProperty = null; @@ -983,8 +987,8 @@ public Object getPreviewValue(@Nullable String prefix) int scale, String inputType, CellType excelCellType, - Class javaType, - Class... additionalTypes) + Class javaType, + Class... additionalTypes) { this.typeURI = typeURI; this.xarName = xarName; @@ -1116,7 +1120,7 @@ public static PropertyType getFromXarName(String xarName, PropertyType def) return null == p ? def : p; } - public static PropertyType getFromClass(Class clazz) + public static PropertyType getFromClass(Class clazz) { if (clazz == BigDecimal.class) clazz = Double.class; @@ -1134,7 +1138,7 @@ public static PropertyType getFromClass(Class clazz) { if (t.additionalTypes == null || t.additionalTypes.length == 0) continue; - for (Class type : t.additionalTypes) + for (Class type : t.additionalTypes) { if (type.isAssignableFrom(clazz)) return t; @@ -1201,16 +1205,12 @@ public static Object getFromExcelCell(Cell cell) throws ConversionException public String getValueTypeColumn() { - switch (this.getStorageType()) + return switch (this.getStorageType()) { - case 's': - return "stringValue"; - case 'd': - return "dateTimeValue"; - case 'f': - return "floatValue"; - default: - throw new IllegalArgumentException("Unknown property type: " + this); - } + case 's' -> "stringValue"; + case 'd' -> "dateTimeValue"; + case 'f' -> "floatValue"; + default -> throw new IllegalArgumentException("Unknown property type: " + this); + }; } } diff --git a/api/src/org/labkey/api/exp/property/DomainUtil.java b/api/src/org/labkey/api/exp/property/DomainUtil.java index c440d32c0ba..0152b74163d 100644 --- a/api/src/org/labkey/api/exp/property/DomainUtil.java +++ b/api/src/org/labkey/api/exp/property/DomainUtil.java @@ -117,7 +117,7 @@ import static org.labkey.api.data.ColumnRenderPropertiesImpl.TEXT_CHOICE_CONCEPT_URI; import static org.labkey.api.dataiterator.DetailedAuditLogDataIterator.AuditConfigs.AuditBehavior; -import static org.labkey.api.gwt.client.ui.PropertyType.CALCULATED_CONCEPT_URI; +import static org.labkey.api.exp.PropertyType.CALCULATED_CONCEPT_URI; import static org.labkey.api.util.StringExpressionFactory.SUBSTITUTION_EXP_PATTERN; public class DomainUtil diff --git a/api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java b/api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java deleted file mode 100644 index 16dc20bf5ca..00000000000 --- a/api/src/org/labkey/api/gwt/client/assay/model/GWTPropertyDescriptorMixin.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2019 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.api.gwt.client.assay.model; - - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Configures the fields that are not returned when serializing a GWTPropertyDescriptor. - * Ideally we would just add the @JsonIgnore annotations to GWTPropertyDescriptor directly, - * but the GWT compiler would need to have jackson on the classpath which isn't - * necessary. - */ -@JsonIgnoreProperties({ - "setMeasure", - "setDimension", - "setExcludeFromShifting", - "lookupDescription", - "fileType", - "updatedField", - "newField", - "renderUpdate" -}) -public abstract class GWTPropertyDescriptorMixin -{ - GWTPropertyDescriptorMixin(@JsonProperty("PHI") String phi, @JsonProperty("URL") String url, @JsonProperty("URLTarget") String URLTarget) - { } - @JsonProperty("PHI") - abstract void setPHI(String phi); // rename property on deserialize - @JsonProperty("URL") - abstract void setURL(String url); // rename property on deserialize - @JsonProperty("URLTarget") - abstract void setURLTarget(String urlTarget); // rename property on deserialize -} diff --git a/api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java b/api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java index 50ce1dd6a33..534a8dad52a 100644 --- a/api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java +++ b/api/src/org/labkey/api/gwt/client/assay/model/GWTProtocol.java @@ -1,476 +1,476 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.assay.model; - -import org.apache.commons.lang3.StringUtils; -import org.labkey.api.gwt.client.model.GWTContainer; -import org.labkey.api.gwt.client.model.GWTDomain; -import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * User: brittp - * Date: Jun 20, 2007 - * Time: 2:29:22 PM - */ -public class GWTProtocol -{ - private Long _protocolId; - private String _name; - private String _description; - private String _providerName; - private String domainKindName; - - private Map _protocolParameters; - - private List> _domains; - - private List _availablePlateTemplates; - private Map _availableMetadataInputFormats = new HashMap<>(); - private String _metadataInputFormatHelp; - - private String _selectedPlateTemplate; - private String _selectedMetadataInputFormat; - - /** Scripts defined in the module itself, associated with the assay provider */ - private List _moduleTransformScripts = new ArrayList<>(); - /** Scripts defined in the assay definition */ - private final List> _protocolTransformScripts = new ArrayList<>(); - - private List _availableDetectionMethods; - private String _selectedDetectionMethod; - - private boolean _allowBackgroundUpload; - private boolean _allowEditableResults; - private boolean _allowQCStates; - private boolean _allowTransformationScript; - private boolean _allowPlateMetadata; - - // UNDONE: update 'autoCopy' to 'autoLink' for the two members below and align ui-components and tests accordingly - private GWTContainer _autoCopyTargetContainer; - private String _autoCopyTargetContainerId; - private String _autoLinkCategory; - private boolean _saveScriptFiles; - private boolean _editableRuns; - private boolean _editableResults; - private boolean _backgroundUpload; - private boolean _qcEnabled; - private boolean _plateMetadata; - private String _status; - private List _excludedContainerIds; - private String _auditUserComment; - - public GWTProtocol() - { - } - - public Long getProtocolId() - { - return _protocolId; - } - - public void setProtocolId(Long protocolId) - { - _protocolId = protocolId; - } - - - public List> getDomains() - { - return _domains; - } - - public void setDomains(List> domains) - { - _domains = domains; - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public String getDescription() - { - return _description; - } - - public void setDescription(String description) - { - _description = description; - } - - public Map getProtocolParameters() - { - return _protocolParameters; - } - - public void setProtocolParameters(Map protocolParameters) - { - _protocolParameters = protocolParameters; - } - - public String getProviderName() - { - return _providerName; - } - - public void setProviderName(String providerName) - { - _providerName = providerName; - } - - public String getDomainKindName() - { - return domainKindName; - } - - public void setDomainKindName(String domainKindName) - { - this.domainKindName = domainKindName; - } - - public List getAvailablePlateTemplates() - { - return _availablePlateTemplates; - } - - public void setAvailablePlateTemplates(List availablePlateTemplates) - { - _availablePlateTemplates = availablePlateTemplates; - } - - public String getSelectedPlateTemplate() - { - return _selectedPlateTemplate; - } - - public void setSelectedPlateTemplate(String selectedPlateTemplate) - { - _selectedPlateTemplate = selectedPlateTemplate; - } - - public List getModuleTransformScripts() - { - return _moduleTransformScripts; - } - - public void setModuleTransformScripts(List moduleTransformScripts) - { - _moduleTransformScripts = moduleTransformScripts; - } - - public boolean isAllowTransformationScript() - { - return _allowTransformationScript; - } - - public void setAllowTransformationScript(boolean allowTransformationScript) - { - _allowTransformationScript = allowTransformationScript; - } - - public List> getProtocolTransformScripts() - { - return _protocolTransformScripts; - } - - private void handleMapTransformScripts(List> protocolTransformScripts) - { - for (Map map : protocolTransformScripts) - { - _protocolTransformScripts.add(Map.of( - "scriptPath", ((String) map.get("scriptPath")).trim(), - "runOnEdit", map.get("runOnEdit"), - "runOnImport", map.get("runOnImport") - )); - } - } - - private void handleStringTransformScripts(List protocolTransformScripts) - { - List> transformedScripts = new ArrayList<>(protocolTransformScripts.size()); - for (String script : protocolTransformScripts) - { - transformedScripts.add(Map.of( - "scriptPath", script.trim(), - "runOnEdit", false, - "runOnImport", true - )); - } - handleMapTransformScripts(transformedScripts); - } - - public void setProtocolTransformScripts(List protocolTransformScripts) - { - if (!protocolTransformScripts.isEmpty()) { - Object first = protocolTransformScripts.get(0); - if (first instanceof Map) { - handleMapTransformScripts((List>) protocolTransformScripts); - } else if (first instanceof String) { - handleStringTransformScripts((List) protocolTransformScripts); - } else { - throw new IllegalArgumentException("Unsupported type: " + first.getClass().getName()); - } - } - } - - public List getAvailableDetectionMethods() - { - return _availableDetectionMethods; - } - - public void setAvailableDetectionMethods(List availableDetectionMethods) - { - _availableDetectionMethods = availableDetectionMethods; - } - - public String getSelectedDetectionMethod() - { - return _selectedDetectionMethod; - } - - public void setSelectedDetectionMethod(String detectionMethod) - { - _selectedDetectionMethod = detectionMethod; - } - - public GWTContainer getAutoCopyTargetContainer() - { - return _autoCopyTargetContainer; - } - - public void setAutoCopyTargetContainer(GWTContainer autoCopyTargetContainer) - { - _autoCopyTargetContainer = autoCopyTargetContainer; - } - - public String getAutoCopyTargetContainerId() - { - return _autoCopyTargetContainerId; - } - - public void setAutoCopyTargetContainerId(String autoCopyTargetContainerId) - { - _autoCopyTargetContainerId = autoCopyTargetContainerId; - } - - public String getAutoLinkCategory() - { - return _autoLinkCategory; - } - - public void setAutoLinkCategory(String autoLinkCategory) - { - _autoLinkCategory = autoLinkCategory; - } - - public boolean isSaveScriptFiles() - { - return _saveScriptFiles; - } - - public void setSaveScriptFiles(boolean saveScriptFiles) - { - _saveScriptFiles = saveScriptFiles; - } - - public boolean isEditableRuns() - { - return _editableRuns; - } - - public void setEditableRuns(boolean editableRuns) - { - _editableRuns = editableRuns; - } - - public boolean isEditableResults() - { - return _editableResults; - } - - public void setEditableResults(boolean editableResults) - { - _editableResults = editableResults; - } - - public boolean isBackgroundUpload() - { - return _backgroundUpload; - } - - public void setBackgroundUpload(boolean backgroundUpload) - { - _backgroundUpload = backgroundUpload; - } - - public Map getAvailableMetadataInputFormats() - { - return _availableMetadataInputFormats; - } - - public void setAvailableMetadataInputFormats(Map availableMetadataInputFormats) - { - _availableMetadataInputFormats = availableMetadataInputFormats; - } - - public String getMetadataInputFormatHelp() - { - return _metadataInputFormatHelp; - } - - public void setMetadataInputFormatHelp(String metadataInputFormatHelp) - { - _metadataInputFormatHelp = metadataInputFormatHelp; - } - - public String getSelectedMetadataInputFormat() - { - return _selectedMetadataInputFormat; - } - - public void setSelectedMetadataInputFormat(String selectedMetadataInputFormat) - { - _selectedMetadataInputFormat = selectedMetadataInputFormat; - } - - public boolean isQcEnabled() - { - return _qcEnabled; - } - - public void setQcEnabled(boolean qcEnabled) - { - _qcEnabled = qcEnabled; - } - - public boolean isAllowQCStates() - { - return _allowQCStates; - } - - public void setAllowQCStates(boolean allowQCStates) - { - _allowQCStates = allowQCStates; - } - - public boolean isAllowEditableResults() - { - return _allowEditableResults; - } - - public void setAllowEditableResults(boolean allowEditableResults) - { - _allowEditableResults = allowEditableResults; - } - - public boolean isAllowBackgroundUpload() - { - return _allowBackgroundUpload; - } - - public void setAllowBackgroundUpload(boolean allowBackgroundUpload) - { - _allowBackgroundUpload = allowBackgroundUpload; - } - - public boolean isAllowPlateMetadata() - { - return _allowPlateMetadata; - } - - public void setAllowPlateMetadata(boolean allowPlateMetadata) - { - _allowPlateMetadata = allowPlateMetadata; - } - - public boolean isPlateMetadata() - { - return _plateMetadata; - } - - public void setPlateMetadata(boolean plateMetadata) - { - _plateMetadata = plateMetadata; - } - - public String getStatus() - { - return _status; - } - - public void setStatus(String status) - { - _status = status; - } - - public List getExcludedContainerIds() - { - return _excludedContainerIds; - } - - public void setExcludedContainerIds(List excludedContainerIds) - { - _excludedContainerIds = excludedContainerIds; - } - - public String getAuditUserComment() - { - return _auditUserComment; - } - - public void setAuditUserComment(String auditUserComment) - { - _auditUserComment = auditUserComment; - } - - public Map getAuditRecordMap() - { - Map map = new LinkedHashMap<>(); - map.put("Name", getName()); - if (!StringUtils.isEmpty(getDescription())) - map.put("Description", getDescription()); - if (!StringUtils.isEmpty(getStatus())) - map.put("Status", getStatus()); - String autoCopyTargetContainerId = getAutoCopyTargetContainer() != null ? getAutoCopyTargetContainer().getEntityId() : getAutoCopyTargetContainerId(); - if (!StringUtils.isEmpty(autoCopyTargetContainerId)) - map.put("AutoCopyTargetContainer", autoCopyTargetContainerId); - if (!StringUtils.isEmpty(getAutoLinkCategory())) - map.put("AutoLinkCategory", getAutoLinkCategory()); - map.put("SaveScriptFiles", isSaveScriptFiles()); - map.put("IsEditableResults", isEditableResults()); - map.put("IsEditableRuns", isEditableRuns()); - map.put("IsBackgroundUpload", isBackgroundUpload()); - map.put("IsQcEnabled", isQcEnabled()); - map.put("IsPlateMetadataEnabled", isPlateMetadata()); - - return map; - } - - -} +/* + * Copyright (c) 2018-2019 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.api.gwt.client.assay.model; + +import org.apache.commons.lang3.StringUtils; +import org.labkey.api.gwt.client.model.GWTContainer; +import org.labkey.api.gwt.client.model.GWTDomain; +import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * User: brittp + * Date: Jun 20, 2007 + * Time: 2:29:22 PM + */ +public class GWTProtocol +{ + private Long _protocolId; + private String _name; + private String _description; + private String _providerName; + private String domainKindName; + + private Map _protocolParameters; + + private List> _domains; + + private List _availablePlateTemplates; + private Map _availableMetadataInputFormats = new HashMap<>(); + private String _metadataInputFormatHelp; + + private String _selectedPlateTemplate; + private String _selectedMetadataInputFormat; + + /** Scripts defined in the module itself, associated with the assay provider */ + private List _moduleTransformScripts = new ArrayList<>(); + /** Scripts defined in the assay definition */ + private final List> _protocolTransformScripts = new ArrayList<>(); + + private List _availableDetectionMethods; + private String _selectedDetectionMethod; + + private boolean _allowBackgroundUpload; + private boolean _allowEditableResults; + private boolean _allowQCStates; + private boolean _allowTransformationScript; + private boolean _allowPlateMetadata; + + // UNDONE: update 'autoCopy' to 'autoLink' for the two members below and align ui-components and tests accordingly + private GWTContainer _autoCopyTargetContainer; + private String _autoCopyTargetContainerId; + private String _autoLinkCategory; + private boolean _saveScriptFiles; + private boolean _editableRuns; + private boolean _editableResults; + private boolean _backgroundUpload; + private boolean _qcEnabled; + private boolean _plateMetadata; + private String _status; + private List _excludedContainerIds; + private String _auditUserComment; + + public GWTProtocol() + { + } + + public Long getProtocolId() + { + return _protocolId; + } + + public void setProtocolId(Long protocolId) + { + _protocolId = protocolId; + } + + + public List> getDomains() + { + return _domains; + } + + public void setDomains(List> domains) + { + _domains = domains; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public Map getProtocolParameters() + { + return _protocolParameters; + } + + public void setProtocolParameters(Map protocolParameters) + { + _protocolParameters = protocolParameters; + } + + public String getProviderName() + { + return _providerName; + } + + public void setProviderName(String providerName) + { + _providerName = providerName; + } + + public String getDomainKindName() + { + return domainKindName; + } + + public void setDomainKindName(String domainKindName) + { + this.domainKindName = domainKindName; + } + + public List getAvailablePlateTemplates() + { + return _availablePlateTemplates; + } + + public void setAvailablePlateTemplates(List availablePlateTemplates) + { + _availablePlateTemplates = availablePlateTemplates; + } + + public String getSelectedPlateTemplate() + { + return _selectedPlateTemplate; + } + + public void setSelectedPlateTemplate(String selectedPlateTemplate) + { + _selectedPlateTemplate = selectedPlateTemplate; + } + + public List getModuleTransformScripts() + { + return _moduleTransformScripts; + } + + public void setModuleTransformScripts(List moduleTransformScripts) + { + _moduleTransformScripts = moduleTransformScripts; + } + + public boolean isAllowTransformationScript() + { + return _allowTransformationScript; + } + + public void setAllowTransformationScript(boolean allowTransformationScript) + { + _allowTransformationScript = allowTransformationScript; + } + + public List> getProtocolTransformScripts() + { + return _protocolTransformScripts; + } + + private void handleMapTransformScripts(List> protocolTransformScripts) + { + for (Map map : protocolTransformScripts) + { + _protocolTransformScripts.add(Map.of( + "scriptPath", ((String) map.get("scriptPath")).trim(), + "runOnEdit", map.get("runOnEdit"), + "runOnImport", map.get("runOnImport") + )); + } + } + + private void handleStringTransformScripts(List protocolTransformScripts) + { + List> transformedScripts = new ArrayList<>(protocolTransformScripts.size()); + for (String script : protocolTransformScripts) + { + transformedScripts.add(Map.of( + "scriptPath", script.trim(), + "runOnEdit", false, + "runOnImport", true + )); + } + handleMapTransformScripts(transformedScripts); + } + + public void setProtocolTransformScripts(List protocolTransformScripts) + { + if (!protocolTransformScripts.isEmpty()) { + Object first = protocolTransformScripts.getFirst(); + if (first instanceof Map) { + handleMapTransformScripts((List>) protocolTransformScripts); + } else if (first instanceof String) { + handleStringTransformScripts((List) protocolTransformScripts); + } else { + throw new IllegalArgumentException("Unsupported type: " + first.getClass().getName()); + } + } + } + + public List getAvailableDetectionMethods() + { + return _availableDetectionMethods; + } + + public void setAvailableDetectionMethods(List availableDetectionMethods) + { + _availableDetectionMethods = availableDetectionMethods; + } + + public String getSelectedDetectionMethod() + { + return _selectedDetectionMethod; + } + + public void setSelectedDetectionMethod(String detectionMethod) + { + _selectedDetectionMethod = detectionMethod; + } + + public GWTContainer getAutoCopyTargetContainer() + { + return _autoCopyTargetContainer; + } + + public void setAutoCopyTargetContainer(GWTContainer autoCopyTargetContainer) + { + _autoCopyTargetContainer = autoCopyTargetContainer; + } + + public String getAutoCopyTargetContainerId() + { + return _autoCopyTargetContainerId; + } + + public void setAutoCopyTargetContainerId(String autoCopyTargetContainerId) + { + _autoCopyTargetContainerId = autoCopyTargetContainerId; + } + + public String getAutoLinkCategory() + { + return _autoLinkCategory; + } + + public void setAutoLinkCategory(String autoLinkCategory) + { + _autoLinkCategory = autoLinkCategory; + } + + public boolean isSaveScriptFiles() + { + return _saveScriptFiles; + } + + public void setSaveScriptFiles(boolean saveScriptFiles) + { + _saveScriptFiles = saveScriptFiles; + } + + public boolean isEditableRuns() + { + return _editableRuns; + } + + public void setEditableRuns(boolean editableRuns) + { + _editableRuns = editableRuns; + } + + public boolean isEditableResults() + { + return _editableResults; + } + + public void setEditableResults(boolean editableResults) + { + _editableResults = editableResults; + } + + public boolean isBackgroundUpload() + { + return _backgroundUpload; + } + + public void setBackgroundUpload(boolean backgroundUpload) + { + _backgroundUpload = backgroundUpload; + } + + public Map getAvailableMetadataInputFormats() + { + return _availableMetadataInputFormats; + } + + public void setAvailableMetadataInputFormats(Map availableMetadataInputFormats) + { + _availableMetadataInputFormats = availableMetadataInputFormats; + } + + public String getMetadataInputFormatHelp() + { + return _metadataInputFormatHelp; + } + + public void setMetadataInputFormatHelp(String metadataInputFormatHelp) + { + _metadataInputFormatHelp = metadataInputFormatHelp; + } + + public String getSelectedMetadataInputFormat() + { + return _selectedMetadataInputFormat; + } + + public void setSelectedMetadataInputFormat(String selectedMetadataInputFormat) + { + _selectedMetadataInputFormat = selectedMetadataInputFormat; + } + + public boolean isQcEnabled() + { + return _qcEnabled; + } + + public void setQcEnabled(boolean qcEnabled) + { + _qcEnabled = qcEnabled; + } + + public boolean isAllowQCStates() + { + return _allowQCStates; + } + + public void setAllowQCStates(boolean allowQCStates) + { + _allowQCStates = allowQCStates; + } + + public boolean isAllowEditableResults() + { + return _allowEditableResults; + } + + public void setAllowEditableResults(boolean allowEditableResults) + { + _allowEditableResults = allowEditableResults; + } + + public boolean isAllowBackgroundUpload() + { + return _allowBackgroundUpload; + } + + public void setAllowBackgroundUpload(boolean allowBackgroundUpload) + { + _allowBackgroundUpload = allowBackgroundUpload; + } + + public boolean isAllowPlateMetadata() + { + return _allowPlateMetadata; + } + + public void setAllowPlateMetadata(boolean allowPlateMetadata) + { + _allowPlateMetadata = allowPlateMetadata; + } + + public boolean isPlateMetadata() + { + return _plateMetadata; + } + + public void setPlateMetadata(boolean plateMetadata) + { + _plateMetadata = plateMetadata; + } + + public String getStatus() + { + return _status; + } + + public void setStatus(String status) + { + _status = status; + } + + public List getExcludedContainerIds() + { + return _excludedContainerIds; + } + + public void setExcludedContainerIds(List excludedContainerIds) + { + _excludedContainerIds = excludedContainerIds; + } + + public String getAuditUserComment() + { + return _auditUserComment; + } + + public void setAuditUserComment(String auditUserComment) + { + _auditUserComment = auditUserComment; + } + + public Map getAuditRecordMap() + { + Map map = new LinkedHashMap<>(); + map.put("Name", getName()); + if (!StringUtils.isEmpty(getDescription())) + map.put("Description", getDescription()); + if (!StringUtils.isEmpty(getStatus())) + map.put("Status", getStatus()); + String autoCopyTargetContainerId = getAutoCopyTargetContainer() != null ? getAutoCopyTargetContainer().getEntityId() : getAutoCopyTargetContainerId(); + if (!StringUtils.isEmpty(autoCopyTargetContainerId)) + map.put("AutoCopyTargetContainer", autoCopyTargetContainerId); + if (!StringUtils.isEmpty(getAutoLinkCategory())) + map.put("AutoLinkCategory", getAutoLinkCategory()); + map.put("SaveScriptFiles", isSaveScriptFiles()); + map.put("IsEditableResults", isEditableResults()); + map.put("IsEditableRuns", isEditableRuns()); + map.put("IsBackgroundUpload", isBackgroundUpload()); + map.put("IsQcEnabled", isQcEnabled()); + map.put("IsPlateMetadataEnabled", isPlateMetadata()); + + return map; + } + + +} diff --git a/api/src/org/labkey/api/gwt/client/model/GWTDomain.java b/api/src/org/labkey/api/gwt/client/model/GWTDomain.java index 920ab61b0fc..dff37bf0349 100644 --- a/api/src/org/labkey/api/gwt/client/model/GWTDomain.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTDomain.java @@ -209,11 +209,6 @@ public boolean isMandatoryField(FieldType field) return mandatoryPropertyDescriptorNames.contains(field.getName().toLowerCase()); } - public boolean isEditable(FieldType field) - { - return true; - } - /** * @return Indicates that the property is not allowed to be set as PHI */ diff --git a/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java b/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java index 9527d25e5ad..d0feeeb6ccd 100644 --- a/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java @@ -16,6 +16,8 @@ package org.labkey.api.gwt.client.model; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -261,6 +263,7 @@ public boolean getLookupIsValid() return lookupIsValid; } + @JsonIgnore public boolean isSetMeasure() { return measure != null; @@ -276,6 +279,7 @@ public void setMeasure(boolean isMeasure) this.measure = isMeasure; } + @JsonIgnore public boolean isSetDimension() { return dimension != null; @@ -306,16 +310,19 @@ public void setDefaultValueType(DefaultValueType defaultValueType) this.defaultValueType = null==defaultValueType ? null : defaultValueType.name(); } + @JsonProperty("PHI") public String getPHI() { return phi; } + @JsonProperty("PHI") public void setPHI(String phi) { this.phi = phi; } + @JsonIgnore public boolean isSetExcludeFromShifting() { return isExcludeFromShifting != null; @@ -354,26 +361,31 @@ public void setIsPrimaryKey(boolean isPrimaryKey) this.isPrimaryKey = isPrimaryKey; } + @JsonProperty("URL") public String getURL() { return url; } + @JsonProperty("URL") public void setURL(String url) { this.url = url; } + @JsonProperty("URLTarget") public String getURLTarget() { return urlTarget; } + @JsonProperty("URLTarget") public void setURLTarget(String urlTarget) { this.urlTarget = urlTarget; } + @JsonIgnore public String getLookupDescription() { if (StringUtils.isEmpty(getLookupSchema()) || StringUtils.isEmpty(getLookupQuery())) @@ -388,6 +400,7 @@ public String toString() return name + ": " + rangeURI; } + @JsonIgnore public boolean isFileType() { return "http://cpas.fhcrc.org/exp/xml#fileLink".equals(getRangeURI()) || diff --git a/api/src/org/labkey/api/gwt/client/ui/PropertyType.java b/api/src/org/labkey/api/gwt/client/ui/PropertyType.java deleted file mode 100644 index 64ee613d3c8..00000000000 --- a/api/src/org/labkey/api/gwt/client/ui/PropertyType.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2018-2019 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.api.gwt.client.ui; - -import java.util.HashMap; -import java.util.Map; - -/** -* User: matthewb -* Date: Apr 22, 2010 - * - * TODO: Merge or replace with exp/PropertyType -*/ -public enum PropertyType -{ - // Treat expMultiLine the same as xsdString for lookup purposes, since it's really just a formatting distinction - expMultiLine("http://www.w3.org/2001/XMLSchema#multiLine", true, "Multi-Line Text", "String", "string"), - xsdString("http://www.w3.org/2001/XMLSchema#string", true, "Text (String)", "String", "string"), - xsdBoolean("http://www.w3.org/2001/XMLSchema#boolean", false, "Boolean", null, "boolean"), - xsdInt("http://www.w3.org/2001/XMLSchema#int", true, "Integer", null, "int"), - xsdDouble("http://www.w3.org/2001/XMLSchema#double", true, "Number (Double)", "Double", "float"), - xsdDateTime("http://www.w3.org/2001/XMLSchema#dateTime", true, "DateTime", null, "date"), - xsdDate("http://www.w3.org/2001/XMLSchema#date", true, "Date", null, "date"), - xsdTime("http://www.w3.org/2001/XMLSchema#time", true, "Time", null, "time"), - expFileLink("http://cpas.fhcrc.org/exp/xml#fileLink", false, "File"), - expAttachment("http://www.labkey.org/exp/xml#attachment", false, "Attachment"), - xsdFloat("http://www.w3.org/2001/XMLSchema#float", true, "Number (Float)", "Float", "float"), - xsdDecimal("http://www.w3.org/2001/XMLSchema#decimal", true, "Number (Decimal)", "Decimal", "float"), - xsdLong("http://www.w3.org/2001/XMLSchema#long", true, "Long Integer", "Long", "int"), - xsdBinary("http://www.w3.org/2001/XMLSchema#binary", false, "Byte Buffer", "Buffer", "string"); - - public static final String PARTICIPANT_CONCEPT_URI = "http://cpas.labkey.com/Study#ParticipantId"; - public static final String VISIT_CONCEPT_URI = "http://cpas.labkey.com/Study#VisitId"; - public static final String SPECIMEN_CONCEPT_URI = "http://cpas.labkey.com/Study#SpecimenId"; - public static final String SAMPLE_CONCEPT_URI = "http://www.labkey.org/exp/xml#sample"; - public static final String CALCULATED_CONCEPT_URI = "http://www.labkey.org/exp/xml#calculated"; - - private final String _uri; - private final String _display; - private final String _jsonType; - private final String _short; - private final boolean _lookup; - - PropertyType(String uri, boolean lookup, String display) - { - this(uri,lookup,display, display, null); - } - - PropertyType(String uri, boolean lookup, String display, String shortName, String jsonType) - { - _uri = uri; - _lookup = lookup; - _display = display; - _jsonType = jsonType; - _short = shortName == null ? display : shortName; - } - - public String getURI() - { - return _uri; - } - - @Override - public String toString() - { - return _uri; - } - - public String getDisplay() - { - return _display; - } - - public String getShortName() - { - return _short; - } - - public String getJsonType() - { - return _jsonType; - } - - public boolean isLookupType() - { - return _lookup; - } - - public static PropertyType fromName(String type) - { - PropertyType t = synonyms.get(type); - if (null == t) - t = synonyms.get(type.toLowerCase()); - return t; - } - - - private static final Map synonyms = new HashMap<>(); - private static void _put(PropertyType t) - { - synonyms.put(t.toString().toLowerCase(), t); - synonyms.put(t.getDisplay().toLowerCase(), t); - synonyms.put(t.getShortName().toLowerCase(), t); - } - static - { - for (PropertyType t : PropertyType.values()) - _put(t); - - synonyms.put("text", xsdString); - synonyms.put("xsd:string", xsdString); - - synonyms.put("int", xsdInt); - synonyms.put("integer", xsdInt); - synonyms.put("xsd:int", xsdInt); - - synonyms.put("number", xsdDouble); - synonyms.put("real", xsdDouble); - synonyms.put("float", xsdDouble); - synonyms.put("xsd:double", xsdDouble); - - synonyms.put("date", xsdDateTime); - synonyms.put("xsd:datetime", xsdDateTime); - - synonyms.put("bool", xsdBoolean); - synonyms.put("xsd:boolean", xsdBoolean); - } -} diff --git a/api/src/org/labkey/api/query/FilteredTable.java b/api/src/org/labkey/api/query/FilteredTable.java index 3219fcdab59..88c81138e08 100644 --- a/api/src/org/labkey/api/query/FilteredTable.java +++ b/api/src/org/labkey/api/query/FilteredTable.java @@ -62,7 +62,7 @@ import java.util.Set; import java.util.stream.Collectors; -import static org.labkey.api.gwt.client.ui.PropertyType.PARTICIPANT_CONCEPT_URI; +import static org.labkey.api.exp.PropertyType.PARTICIPANT_CONCEPT_URI; /** * A table that filters down to a particular set of rows from an underlying, wrapped table/subquery. A typical example diff --git a/api/src/org/labkey/api/study/query/PublishResultsQueryView.java b/api/src/org/labkey/api/study/query/PublishResultsQueryView.java index 1c2c6db2ceb..634db325c71 100644 --- a/api/src/org/labkey/api/study/query/PublishResultsQueryView.java +++ b/api/src/org/labkey/api/study/query/PublishResultsQueryView.java @@ -40,9 +40,9 @@ import org.labkey.api.data.Table; import org.labkey.api.data.UpdateColumn; import org.labkey.api.exp.ExperimentException; +import org.labkey.api.exp.PropertyType; import org.labkey.api.exp.api.ExpRun; import org.labkey.api.exp.api.ExperimentService; -import org.labkey.api.gwt.client.ui.PropertyType; import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryService; import org.labkey.api.query.QuerySettings; @@ -1041,7 +1041,7 @@ protected List getExtraColumns(Collection selectColum { //NOTE: the name of the assay PTID field might not always match ParticipantId. this allows us to also //support PARTICIPANT_CONCEPT_URI - ColumnInfo ptidCol = selectColumns.stream().filter(c -> PropertyType.PARTICIPANT_CONCEPT_URI.equals(c.getConceptURI())).findFirst().orElse(null); + ColumnInfo ptidCol = selectColumns.stream().filter(c -> org.labkey.api.exp.PropertyType.PARTICIPANT_CONCEPT_URI.equals(c.getConceptURI())).findFirst().orElse(null); if (ptidCol != null) linkedColumnMap.put(LinkToStudyKeys.ParticipantId, ptidCol); } diff --git a/assay/src/org/labkey/assay/TsvAssayProvider.java b/assay/src/org/labkey/assay/TsvAssayProvider.java index f44a544388c..cfad21024c7 100644 --- a/assay/src/org/labkey/assay/TsvAssayProvider.java +++ b/assay/src/org/labkey/assay/TsvAssayProvider.java @@ -285,7 +285,7 @@ protected Pair> createResultDomain(Container c specimenID.setImportAliasSet(specimenImportAliases); DomainProperty participantID = addProperty(dataDomain, PARTICIPANTID_PROPERTY_NAME, PARTICIPANTID_PROPERTY_CAPTION, PropertyType.STRING, "Used with either " + VISITID_PROPERTY_NAME + " or " + DATE_PROPERTY_NAME + " to identify subject and timepoint for assay."); - participantID.setConceptURI(org.labkey.api.gwt.client.ui.PropertyType.PARTICIPANT_CONCEPT_URI); + participantID.setConceptURI(PropertyType.PARTICIPANT_CONCEPT_URI); participantID.setImportAliasSet(participantImportAliases); DomainProperty visitID = addProperty(dataDomain, VISITID_PROPERTY_NAME, VISITID_PROPERTY_CAPTION, PropertyType.DOUBLE, "Used with " + PARTICIPANTID_PROPERTY_NAME + " to identify subject and timepoint for assay."); diff --git a/assay/src/org/labkey/assay/actions/GetProtocolAction.java b/assay/src/org/labkey/assay/actions/GetProtocolAction.java index 8846544911e..b1a0181459c 100644 --- a/assay/src/org/labkey/assay/actions/GetProtocolAction.java +++ b/assay/src/org/labkey/assay/actions/GetProtocolAction.java @@ -15,7 +15,6 @@ */ package org.labkey.assay.actions; -import com.fasterxml.jackson.databind.ObjectMapper; import org.labkey.api.action.ApiUsageException; import org.labkey.api.action.Marshal; import org.labkey.api.action.Marshaller; @@ -23,13 +22,10 @@ import org.labkey.api.assay.AssayDomainService; import org.labkey.api.exp.api.ExpProtocol; import org.labkey.api.exp.api.ExperimentService; -import org.labkey.api.gwt.client.assay.model.GWTPropertyDescriptorMixin; import org.labkey.api.gwt.client.assay.model.GWTProtocol; -import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.permissions.AssayReadPermission; import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.util.JsonUtil; import org.labkey.api.view.NotFoundException; import org.labkey.api.view.UnauthorizedException; import org.labkey.assay.AssayDomainServiceImpl; @@ -54,22 +50,6 @@ public void setCopy(boolean copy) } } - //Keeping both request and response object mappers to avoid serialization/deserialization issues - //as not sure if request object mapper is needed - @Override - protected ObjectMapper createRequestObjectMapper() - { - ObjectMapper mapper = JsonUtil.DEFAULT_MAPPER.copy(); - configureObjectMapper(mapper); - return mapper; - } - - @Override - protected ObjectMapper createResponseObjectMapper() - { - return this.createRequestObjectMapper(); - } - @Override public Object execute(DesignerForm form, BindException errors) throws Exception { @@ -109,7 +89,4 @@ else if (form.getProviderName() != null) } } - static void configureObjectMapper(ObjectMapper om) { - om.addMixIn(GWTPropertyDescriptor.class, GWTPropertyDescriptorMixin.class); - } } diff --git a/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java b/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java index 565dbfd097d..073f5b8f867 100644 --- a/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java +++ b/assay/src/org/labkey/assay/data/generator/AssayDesignGenerator.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Properties; -import static org.labkey.api.gwt.client.ui.PropertyType.SAMPLE_CONCEPT_URI; +import static org.labkey.api.exp.PropertyType.SAMPLE_CONCEPT_URI; public class AssayDesignGenerator extends DataGenerator { diff --git a/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java b/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java index 2d362f85ae2..b8fb4901755 100644 --- a/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java +++ b/assay/src/org/labkey/assay/data/generator/AssayRunDataGenerator.java @@ -27,7 +27,7 @@ import java.util.Properties; import java.util.stream.Collectors; -import static org.labkey.api.gwt.client.ui.PropertyType.SAMPLE_CONCEPT_URI; +import static org.labkey.api.exp.PropertyType.SAMPLE_CONCEPT_URI; public class AssayRunDataGenerator extends DataGenerator { diff --git a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java index 383e42b212d..62a3a24dc08 100644 --- a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java @@ -24,8 +24,10 @@ import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.commons.lang3.mutable.MutableLong; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.fhcrc.cpas.exp.xml.SimpleTypeNames; @@ -327,14 +329,14 @@ public class ExperimentServiceImpl implements ExperimentService, ObjectReference private static final Logger LOG = LogHelper.getLogger(ExperimentServiceImpl.class, "Experiment infrastructure including maintaining runs and lineage"); private final Cache PROTOCOL_ROW_ID_CACHE = DatabaseCache.get(getExpSchema().getScope(), CacheManager.UNLIMITED, CacheManager.HOUR, "Protocol by RowId", - (key, argument) -> getExpProtocol(new SimpleFilter(FieldKey.fromParts("RowId"), key))); + (key, _) -> getExpProtocol(new SimpleFilter(FieldKey.fromParts("RowId"), key))); private final Cache PROTOCOL_LSID_CACHE = DatabaseCache.get(getExpSchema().getScope(), CacheManager.UNLIMITED, CacheManager.HOUR, "Protocol by LSID", - (key, argument) -> getExpProtocol(new SimpleFilter(FieldKey.fromParts("LSID"), key))); + (key, _) -> getExpProtocol(new SimpleFilter(FieldKey.fromParts("LSID"), key))); private final Cache EXPERIMENT_RUN_CACHE = DatabaseCache.get(getExpSchema().getScope(), getTinfoExperimentRun().getCacheSize(), "Experiment Run by LSID", new ExperimentRunCacheLoader()); - private final Cache> dataClassCache = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Data classes", (containerId, argument) -> + private final Cache> dataClassCache = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Data classes", (containerId, _) -> { Container c = ContainerManager.getForId(containerId); if (c == null) @@ -536,7 +538,7 @@ public ExperimentRunType getExperimentRunType(@NotNull String description, @Null @Override public List getExpRuns(Container container, @Nullable ExpProtocol parentProtocol, @Nullable ExpProtocol childProtocol) { - return getExpRuns(container, parentProtocol, childProtocol, run -> true); + return getExpRuns(container, parentProtocol, childProtocol, _ -> true); } @Override @@ -921,7 +923,7 @@ public List getExpMaterialsByName(@NotNull Collection n return null; if (materials.size() > 1) throw new IllegalArgumentException("Expected 0 or 1 samples, got: " + materials.size()); - return materials.get(0); + return materials.getFirst(); } public List findIdsNotPermittedForOperation(List candidates, SampleTypeService.SampleOperations operation) @@ -1019,7 +1021,7 @@ public List getExpMaterialsByObjectId(ContainerFilter container @Override public void enumerateDocuments(SearchService.TaskIndexingQueue queue, final Date modifiedSince) { - queue.addRunnable((a) -> { + queue.addRunnable((_) -> { for (ExpSampleTypeImpl sampleType : getIndexableSampleTypes(queue.getContainer(), modifiedSince)) { sampleType.index(queue, null); @@ -1082,13 +1084,13 @@ private void indexMaterials(final @NotNull SearchService.TaskIndexingQueue queue materials.forEach(m -> { ExpMaterialImpl expMaterial = new ExpMaterialImpl(m); expMaterial.index(queue, null); - maxRowIdProcessed.setValue(Math.max(maxRowIdProcessed.getValue(), expMaterial.getRowId())); + maxRowIdProcessed.setValue(Math.max(maxRowIdProcessed.longValue(), expMaterial.getRowId())); }); if (materials.size() == INDEXING_LIMIT) { // Requeue for the next batch. This avoids overwhelming the indexer's queue with documents - queue.addRunnable((q) -> indexMaterials(q, modifiedSince, maxRowIdProcessed.getValue())); + queue.addRunnable((q) -> indexMaterials(q, modifiedSince, maxRowIdProcessed.longValue())); } } @@ -1113,20 +1115,20 @@ public void indexData(final @NotNull SearchService.TaskIndexingQueue queue, fina sql = getSchema().getSqlDialect().limitRows(sql, INDEXING_LIMIT); SqlSelector selector = new SqlSelector(getSchema(), sql); selector.setJdbcCaching(false); - MutableInt maxRowIdProcessed = new MutableInt(minRowId); + MutableLong maxRowIdProcessed = new MutableLong(minRowId); // Work in modest block sizes and fetch as a list so we don't keep the ResultSet open, which could lock the tables List data = selector.getArrayList(Data.class); data.forEach(d -> { ExpDataImpl expData = new ExpDataImpl(d); expData.index(queue, null); - maxRowIdProcessed.setValue(Math.max(maxRowIdProcessed.getValue(), expData.getRowId())); + maxRowIdProcessed.setValue(Math.max(maxRowIdProcessed.longValue(), expData.getRowId())); }); if (data.size() == INDEXING_LIMIT) { // Requeue for the next batch. This avoids overwhelming the indexer's queue with documents - queue.addRunnable((q) -> indexData(q, modifiedSince, maxRowIdProcessed.getValue())); + queue.addRunnable((q) -> indexData(q, modifiedSince, maxRowIdProcessed.longValue())); } } @@ -1298,7 +1300,7 @@ private void indexDataClassData(ExpDataClassImpl dataClass, SearchService.TaskIn .add(dataClass.getModified()); var scope = table.getSchema().getScope(); - scope.executeWithRetryReadOnly(tx -> + scope.executeWithRetryReadOnly(_ -> new SqlSelector(scope, sql).forEachBatch(Data.class, 1000, batch -> queue.addRunnable((q) -> batch.forEach(data -> new ExpDataImpl(data).index(q, null)) @@ -1387,7 +1389,7 @@ public ExpProtocolImpl getExpProtocol(String lsid) if (protocols.isEmpty()) return null; - return protocols.get(0); + return protocols.getFirst(); } private void uncacheProtocol(Protocol p) @@ -1945,7 +1947,7 @@ public ExpData findExpData( { Long rowId = asLong(cache.remap(ExpSchema.SCHEMA_EXP_DATA, dataClassName, user, c, getContainerFilterTypeForFind(c), dataName)); if (rowId != null) - return dataCache.computeIfAbsent(rowId, (x) -> getExpData(dataClass, rowId)); + return dataCache.computeIfAbsent(rowId, (_) -> getExpData(dataClass, rowId)); } catch (ConversionException e2) { @@ -1960,7 +1962,7 @@ public ExpData findExpData( Long rowId = ConvertHelper.convert(dataName, Long.class); // now attempt to resolve by rowId - return dataCache.computeIfAbsent(rowId, (x) -> getExpData(dataClass, rowId)); + return dataCache.computeIfAbsent(rowId, (_) -> getExpData(dataClass, rowId)); } catch (ConversionException e1) { @@ -2083,7 +2085,7 @@ public ExpExperiment createHiddenRunGroup(Container container, User user, ExpRun // We don't care which one we use. It's possible to have multiple matches if a run was deleted that was // already part of a hidden run group. - return new ExpExperimentImpl(exp.get(0)); + return new ExpExperimentImpl(exp.getFirst()); } else { @@ -2168,7 +2170,7 @@ public Set getMaterialInputRoles(Container container, User user, ExpProt return getInputRoles(container, ContainerFilter.current(container, user), getTinfoMaterialInput(), types); } - private Set getInputRoles(Container container, ContainerFilter filter, TableInfo table, ExpProtocol.ApplicationType... types) + private Set getInputRoles(Container ignoredContainer, ContainerFilter filter, TableInfo table, ExpProtocol.ApplicationType... types) { SQLFragment sql = new SQLFragment("SELECT role FROM "); sql.append(table, "t"); @@ -2491,7 +2493,7 @@ else if (parent instanceof ExpMaterial materialParent) type = sampleType.getName(); } - parentByType.computeIfAbsent(type, k -> new ArrayList<>()); + parentByType.computeIfAbsent(type, _ -> new ArrayList<>()); String parentName = parent.getName(); if (parentName.contains(",")) parentName = "\"" + parentName + "\""; @@ -2698,7 +2700,7 @@ private Pair getRunGraphCommonTableExpressions(SQLFragment ret, S String parentsInnerToken = ret.addCommonTableExpression(dialect, parentsInnerSelect, "org_lk_exp_PARENTS_INNER", parentsInnerSelectFrag, recursive); String parentsSelect = map.get("$PARENTS$"); - parentsSelect = StringUtils.replace(parentsSelect, "$PARENTS_INNER$", parentsInnerToken); + parentsSelect = Strings.CS.replace(parentsSelect, "$PARENTS_INNER$", parentsInnerToken); // don't use parentsSelect as key, it may not consolidate correctly because of parentsInnerToken parentsToken = ret.addCommonTableExpression(dialect, "$PARENTS$/" + Objects.toString(options.getExpTypeValue(), "ALL") + "/" + parentsInnerSelect, "org_lk_exp_PARENTS", SQLFragment.unsafe(parentsSelect), recursive); } @@ -2707,13 +2709,13 @@ private Pair getRunGraphCommonTableExpressions(SQLFragment ret, S if (options.isChildren()) { String childrenInnerSelect = map.get("$CHILDREN_INNER$"); - childrenInnerSelect = StringUtils.replace(childrenInnerSelect, "$EDGES$", edgesToken); + childrenInnerSelect = Strings.CS.replace(childrenInnerSelect, "$EDGES$", edgesToken); SQLFragment childrenInnerSelectFrag = SQLFragment.unsafe(childrenInnerSelect); childrenInnerSelectFrag.addAll(lsidsFrag.getParams()); String childrenInnerToken = ret.addCommonTableExpression(dialect, childrenInnerSelect, "org_lk_exp_CHILDREN_INNER", childrenInnerSelectFrag, recursive); String childrenSelect = map.get("$CHILDREN$"); - childrenSelect = StringUtils.replace(childrenSelect, "$CHILDREN_INNER$", childrenInnerToken); + childrenSelect = Strings.CS.replace(childrenSelect, "$CHILDREN_INNER$", childrenInnerToken); // don't use childrenSelect as key, it may not consolidate correctly because of childrenInnerToken childrenToken = ret.addCommonTableExpression(dialect, "$CHILDREN$/" + Objects.toString(options.getExpTypeValue(), "ALL") + "/" + childrenInnerSelect, "org_lk_exp_CHILDREN", SQLFragment.unsafe(childrenSelect), recursive); } @@ -2985,7 +2987,7 @@ private Long ensureOwnerObject(@Nullable String cpasType, @NotNull Map { + return cpasTypeToObjectId.computeIfAbsent(cpasType, (_) -> { // NOTE: We can't use OntologyManager.ensureObject() here (which caches) because we don't know what container the SampleType is defined in OntologyObject oo = OntologyManager.getOntologyObject(null, cpasType); @@ -3302,7 +3304,7 @@ SyncRunEdges doIncrementalClosureInvalidation(boolean i) void sync(@Nullable Map cpasTypeToObjectId) { DbScope expScope = getExpSchema().getScope(); - expScope.executeWithRetry((DbScope.RetryFn) tx -> { + expScope.executeWithRetry((DbScope.RetryFn) _ -> { syncInner(cpasTypeToObjectId); return null; }); @@ -3664,7 +3666,7 @@ public ProtocolApplication getStartingProtocolApplication(long runId) if (!protocolApplications.isEmpty()) { - protocolApplication = protocolApplications.get(0); + protocolApplication = protocolApplications.getFirst(); } return protocolApplication; } @@ -4110,7 +4112,7 @@ private Map> getRunInputMaterial(Map[] ma for (Map map : maps) { String runLSID = (String) map.get("RunLSID"); - List list = outputMap.computeIfAbsent(runLSID, k -> new ArrayList<>()); + List list = outputMap.computeIfAbsent(runLSID, _ -> new ArrayList<>()); Material m = f.fromMap(map); list.add(m); } @@ -4221,7 +4223,7 @@ public ExpDataImpl getExpDataByURL(String url, @Nullable Container c) if (data.isEmpty()) return null; - return new ExpDataImpl(data.get(0)); + return new ExpDataImpl(data.getFirst()); } public Lsid getDataClassLsid(Container container) @@ -4438,7 +4440,6 @@ private Collection getRelatedProtocolIds(Collection selectedProtocol while (!idsToCheck.isEmpty()) { String idsString = StringUtils.join(idsToCheck.iterator(), ", "); - idsToCheck = new HashSet<>(); StringBuilder sb = new StringBuilder(); sb.append("SELECT ParentProtocolId FROM exp.ProtocolAction WHERE ChildProtocolId IN ("); @@ -4446,7 +4447,7 @@ private Collection getRelatedProtocolIds(Collection selectedProtocol sb.append(")"); Long[] newIds = new SqlSelector(getExpSchema(), sb.toString()).getArray(Long.class); - idsToCheck.addAll(Arrays.asList(newIds)); + idsToCheck = new HashSet<>(Arrays.asList(newIds)); sb = new StringBuilder(); sb.append("SELECT ChildProtocolId FROM exp.ProtocolAction WHERE ParentProtocolId IN ("); @@ -4516,7 +4517,7 @@ public void deleteProtocolByRowIds(Container c, User user, @Nullable String audi List expProtocols = Arrays.stream(protocols).map(ExpProtocolImpl::new).collect(toList()); if (!c.hasPermission(user, AdminPermission.class) && !runs.isEmpty()) - throw new UnauthorizedException("You do not have sufficient permissions to delete '" + (expProtocols.size() == 1 ? expProtocols.get(0).getName() : "the protocols") + "'."); + throw new UnauthorizedException("You do not have sufficient permissions to delete '" + (expProtocols.size() == 1 ? expProtocols.getFirst().getName() : "the protocols") + "'."); AssayService assayService = AssayService.get(); @@ -4709,7 +4710,7 @@ public static ArrayList> includeLinkedToStudyText(List new HashSet<>()) + sampleTypeAliquotRoots.computeIfAbsent(sampleType, (_) -> new HashSet<>()) .add(material.getRootMaterialRowId()); } } @@ -5346,7 +5347,7 @@ public void deleteDataByRowIds(User user, Container container, Collection if (data.getClassId() != null) { - List byClass = rowIdsByClass.computeIfAbsent(data.getClassId(), k -> new ArrayList<>(10)); + List byClass = rowIdsByClass.computeIfAbsent(data.getClassId(), _ -> new ArrayList<>(10)); byClass.add(data.getRowId()); } allLsids.add(data.getLSID()); @@ -5631,7 +5632,7 @@ public void beforeDeleteData(User user, Container container, List d for (ExpData data : datas) { ExperimentDataHandler handler = data.findDataHandler(); - List list = handlers.computeIfAbsent(handler, k -> new ArrayList<>()); + List list = handlers.computeIfAbsent(handler, _ -> new ArrayList<>()); list.add(data); } for (Map.Entry> entry : handlers.entrySet()) @@ -6852,7 +6853,7 @@ public ExpRun saveSimpleExperimentRun( { throw new IllegalArgumentException("Protocol has the wrong number of steps for a simple protocol; it should have three."); } - ExpProtocolActionImpl action1 = actions.get(0); + ExpProtocolActionImpl action1 = actions.getFirst(); assert action1.getActionSequence() == SIMPLE_PROTOCOL_FIRST_STEP_SEQUENCE; assert action1.getChildProtocol().getRowId() == parentProtocol.getRowId(); @@ -7107,7 +7108,7 @@ private void _prepareRun(DeriveSamplesBulkHelper helper, ExpRunImpl run, SimpleR { throw new IllegalArgumentException("Protocol has the wrong number of steps for a simple protocol; it should have three."); } - ExpProtocolActionImpl action1 = actions.get(0); + ExpProtocolActionImpl action1 = actions.getFirst(); assert action1.getActionSequence() == SIMPLE_PROTOCOL_FIRST_STEP_SEQUENCE; assert action1.getChildProtocol().getRowId() == parentProtocol.getRowId(); @@ -7318,7 +7319,7 @@ private Map saveExpRunsBatch(Container c, List> p { // insert into exp.object List> expObjectParams = params.stream().map( - runParams -> List.of(/* LSID */ runParams.get(0), c.getId()) + runParams -> List.of(/* LSID */ runParams.getFirst(), c.getId()) ).collect(toList()); String expObjectSql = "INSERT INTO " + OntologyManager.getTinfoObject() + " (ObjectUri, Container) VALUES (?, ?)"; @@ -7331,7 +7332,7 @@ private Map saveExpRunsBatch(Container c, List> p Table.batchExecute(getExpSchema(), sql, params); - List runLsids = params.stream().map(p -> (String) p.get(0)).toList(); + List runLsids = params.stream().map(p -> (String) p.getFirst()).toList(); SimpleFilter filter = new SimpleFilter(FieldKey.fromParts(ExpExperimentTable.Column.LSID.name()), runLsids, IN); Map ret = new CaseInsensitiveHashMap<>(); getExperimentRuns(filter).forEach(er -> ret.put(er.getLSID(), er)); @@ -8191,7 +8192,7 @@ public void validateDataClassName(@NotNull Container c, @NotNull User u, String throw new ApiUsageException(reservedError); } - private void validateDataClassOptions(@NotNull Container c, @NotNull User u, @Nullable DataClassDomainKindProperties options) + private void validateDataClassOptions(@NotNull Container c, @NotNull User ignoredU, @Nullable DataClassDomainKindProperties options) { if (options == null) return; @@ -8474,7 +8475,7 @@ private void insertProtocolSteps(Protocol baseProtocol, @Nullable List getNameExpressionMetrics() return metrics; } - public @NotNull Pair, Set> getDataTypesWithRequiredLineage(Integer parentDataTypeRowId, boolean isSampleParent, Container container, User user) + public @NotNull Pair, Set> getDataTypesWithRequiredLineage(Integer parentDataTypeRowId, boolean isSampleParent, Container container, User ignoredUser) { Set sampleTypes = new HashSet<>(); Set dataClasses = new HashSet<>(); @@ -9529,7 +9530,7 @@ public boolean hasMissingRequiredParent(String parentCpasType, String childCpasT return totalCount > withParentCount; } - public String getInvalidRequiredImportAliasUpdate(String dataTypeLsid, boolean isSampleType, Map> newAliases, Set existingRequiredInputs, Container c, User u) + public String getInvalidRequiredImportAliasUpdate(String dataTypeLsid, boolean isSampleType, Map> newAliases, Set existingRequiredInputs, Container c, User ignoredU) { for (Map.Entry> newEntry : newAliases.entrySet()) { @@ -9589,7 +9590,7 @@ public Pair getCurrentAndCrossFolderDataCount(Collection .add(container.getId()) .append("\nAND RowId "); dialect.appendInClauseSql(currentFolderCountSql, rowIds); - int currentFolderSelectionCount = new SqlSelector(expSchema, currentFolderCountSql).getArrayList(Integer.class).get(0); + int currentFolderSelectionCount = new SqlSelector(expSchema, currentFolderCountSql).getArrayList(Integer.class).getFirst(); SQLFragment crossFolderCountSql = new SQLFragment() .append(" SELECT COUNT(*) FROM ") @@ -9598,7 +9599,7 @@ public Pair getCurrentAndCrossFolderDataCount(Collection .add(container.getId()) .append("\nAND RowId "); dialect.appendInClauseSql(crossFolderCountSql, rowIds); - int crossFolderSelectionCount = new SqlSelector(expSchema, crossFolderCountSql).getArrayList(Integer.class).get(0); + int crossFolderSelectionCount = new SqlSelector(expSchema, crossFolderCountSql).getArrayList(Integer.class).getFirst(); return new Pair<>(currentFolderSelectionCount, crossFolderSelectionCount); } @@ -9703,7 +9704,7 @@ public Map moveDataClassObjects( // move audit events associated with the sources that are moving int auditEventCount = QueryService.get().moveAuditEvents(targetContainer, dataIds, "exp.data", dataClassTable.getName()); - updateCounts.compute("sourceAuditEvents", (k, c) -> c == null ? auditEventCount : c + auditEventCount); + updateCounts.compute("sourceAuditEvents", (_, c) -> c == null ? auditEventCount : c + auditEventCount); // create summary audit entries for the source container only. The message is pretty generic, so having it // in both source and target doesn't help much. @@ -9776,7 +9777,7 @@ private Map moveDerivationRuns(Collection da Map> runIdData = new LongHashMap<>(); dataObjects.forEach(dataObject -> { if (dataObject.getRunId() != null) - runIdData.computeIfAbsent(dataObject.getRunId(), t -> new HashSet<>()).add(dataObject); + runIdData.computeIfAbsent(dataObject.getRunId(), _ -> new HashSet<>()).add(dataObject); }); // find the set of runs associated with data objects that are moving List toUpdate = new ArrayList<>(); @@ -9899,7 +9900,7 @@ public Map moveAssayRuns(@NotNull List assayR Map> protocolMap = new HashMap<>(); assayRuns.forEach(run -> - protocolMap.computeIfAbsent(run.getProtocol(), t -> new ArrayList<>()).add(run)); + protocolMap.computeIfAbsent(run.getProtocol(), _ -> new ArrayList<>()).add(run)); List runLsids = assayRuns.stream().map(ExpRun::getLSID).toList(); @@ -10416,7 +10417,7 @@ public void testRunInputProperties() throws Exception List materialRunInputs = pa.getMaterialInputs(); assertEquals(1, materialRunInputs.size()); - ExpMaterialRunInputImpl materialRunInput = (ExpMaterialRunInputImpl)materialRunInputs.get(0); + ExpMaterialRunInputImpl materialRunInput = (ExpMaterialRunInputImpl)materialRunInputs.getFirst(); assertEquals(sampleIn, materialRunInput.getMaterial()); assertEquals("Sample Goo", materialRunInput.getRole()); assertEquals(MaterialInput.NAMESPACE, materialRunInput.getLSIDNamespacePrefix()); diff --git a/experiment/src/org/labkey/experiment/api/property/GWTDomainMixin.java b/experiment/src/org/labkey/experiment/api/property/GWTDomainMixin.java index 7aba30e2013..0b4747b61a6 100644 --- a/experiment/src/org/labkey/experiment/api/property/GWTDomainMixin.java +++ b/experiment/src/org/labkey/experiment/api/property/GWTDomainMixin.java @@ -19,10 +19,11 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; /** - * Configures the fields that are not returned when serializing a GWTDomain. - * Ideally we would just add the @JsonIgnore annotations to GWTDomain directly, - * but the GWT compiler would need to have jackson on the classpath which isn't - * necessary. + * Scopes Jackson annotations to the ObjectMapper used by the listDomains action. + * The @JsonIgnoreProperties fields are excluded from list responses to reduce payload + * size, but are still needed by other callers (e.g. getDomainDetails for the domain + * designer). The @JsonFilter must also remain scoped here because only this ObjectMapper + * registers the "listDomainsActionFilter" filter. */ @JsonIgnoreProperties({ "_Ts", @@ -31,8 +32,7 @@ "excludeFromExportFieldNames", "phiNotAllowedFieldNames", "defaultValuesURL", - "provisioned", - "domainException" + "provisioned" }) @JsonFilter("listDomainsActionFilter") diff --git a/experiment/src/org/labkey/experiment/api/property/PropertyServiceImpl.java b/experiment/src/org/labkey/experiment/api/property/PropertyServiceImpl.java index 0ea06467b31..d9bbe8f368e 100644 --- a/experiment/src/org/labkey/experiment/api/property/PropertyServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/property/PropertyServiceImpl.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import org.apache.commons.beanutils.ConversionException; -import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.lang3.StringUtils; import org.fhcrc.cpas.exp.xml.DefaultType; import org.fhcrc.cpas.exp.xml.DomainDescriptorType; @@ -73,9 +72,7 @@ import org.labkey.api.exp.query.ExpSchema; import org.labkey.api.exp.xar.LsidUtils; import org.labkey.api.gwt.client.DefaultValueType; -import org.labkey.api.gwt.client.assay.model.GWTPropertyDescriptorMixin; import org.labkey.api.gwt.client.model.GWTDomain; -import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; import org.labkey.api.ontology.OntologyService; import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryService; @@ -491,7 +488,6 @@ public void configureObjectMapper(ObjectMapper om, @Nullable SimpleBeanPropertyF .addFilter("listDomainsActionFilter", gwtDomainPropertiesFilter); om.setFilterProvider(gwtDomainFilterProvider); om.addMixIn(GWTDomain.class, GWTDomainMixin.class); - om.addMixIn(GWTPropertyDescriptor.class, GWTPropertyDescriptorMixin.class); om.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); } @@ -684,7 +680,7 @@ public Set findVocabularyProperties(Container container, Set getUsageMetrics() DbSchema schema = ExperimentService.get().getSchema(); String lengthFn = schema.getSqlDialect().getVarcharLengthFunction(); - TableInfo t = new ExpSchema(User.getAdminServiceUser(), ContainerManager.getRoot()).getTable(ExpSchema.TableType.Fields.name(), ContainerFilter.getUnsafeEverythingFilter()); + TableInfo t = new ExpSchema(User.getAdminServiceUser(), ContainerManager.getRoot()).getTableOrThrow(ExpSchema.TableType.Fields.name(), ContainerFilter.getUnsafeEverythingFilter()); long storageColumnNameMismatches = new TableSelector(t, new SimpleFilter(FieldKey.fromParts("StorageColumnNameMatch"), false), null).getRowCount(); return Map.of( @@ -752,7 +748,7 @@ private Map stripUriPrefixes(@NotNull Map value key = key.split("#")[1]; } key = key.replace("xsd:", ""); - result.compute(key, (k, v) -> v == null ? value : v.intValue() + value); + result.compute(key, (_, v) -> v == null ? value : v.intValue() + value); } return result; } diff --git a/query/src/org/labkey/query/MetadataTableJSON.java b/query/src/org/labkey/query/MetadataTableJSON.java index 934c621caa6..8e795f33b5a 100644 --- a/query/src/org/labkey/query/MetadataTableJSON.java +++ b/query/src/org/labkey/query/MetadataTableJSON.java @@ -69,7 +69,7 @@ import java.util.Objects; import java.util.Set; -import static org.labkey.api.gwt.client.ui.PropertyType.CALCULATED_CONCEPT_URI; +import static org.labkey.api.exp.PropertyType.CALCULATED_CONCEPT_URI; /** * User: jeckels @@ -83,12 +83,6 @@ public class MetadataTableJSON extends GWTDomain private static final Logger log = LogHelper.getLogger(MetadataTableJSON.class, "Visual editor support for table/query metadata"); - @Override - public boolean isEditable(MetadataColumnJSON field) - { - return true; - } - public boolean isUserDefinedQuery() { return _userDefinedQuery; @@ -129,7 +123,7 @@ private static TableInfo getRawTableInfo(UserSchema schema, String tableName, @N ArrayList errors = new ArrayList<>(); TablesDocument doc = moduleQueryDef.getParsedMetadata().getTablesDocument(errors); if (!errors.isEmpty()) - throw UnexpectedException.wrap(errors.get(0)); + throw UnexpectedException.wrap(errors.getFirst()); if (doc != null) { @@ -139,7 +133,7 @@ private static TableInfo getRawTableInfo(UserSchema schema, String tableName, @N List tableTypes = Collections.singletonList(tables.getTableArray(0)); tableInfo.overlayMetadata(tableTypes, schema, errors); if (!errors.isEmpty()) - throw UnexpectedException.wrap(errors.get(0)); + throw UnexpectedException.wrap(errors.getFirst()); } } } @@ -484,7 +478,7 @@ else if (xmlColumn.isSetImportAliases()) fk.setFkColumnName(rawLookupColumnName); // Issue 48973: don't overwrite existing target FK columns that were set via source else if (fk.getFkColumnName() == null) - fk.setFkColumnName(pkCols.get(0)); + fk.setFkColumnName(pkCols.getFirst()); if (targetContainer != null) fk.setFkFolderPath(targetContainer.getPath()); @@ -751,7 +745,7 @@ else if (columnInfo.getValueExpression() != null) if (queryDefs != null && !queryDefs.isEmpty()) { // Use the last QueryDef's metadata -- this should be the user's metadata override in the database if it exists - QueryDef queryDef = queryDefs.get(queryDefs.size()-1); + QueryDef queryDef = queryDefs.getLast(); if (!container.getId().equals(queryDef.getContainerId())) { diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 14b4cfa9bce..f482caa62f4 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -291,7 +291,6 @@ import org.labkey.query.TableXML; import org.labkey.query.audit.QueryExportAuditProvider; import org.labkey.query.audit.QueryUpdateAuditProvider; -import org.labkey.query.model.MetadataTableJSONMixin; import org.labkey.query.persist.AbstractExternalSchemaDef; import org.labkey.query.persist.CstmView; import org.labkey.query.persist.ExternalSchemaDef; @@ -7903,9 +7902,7 @@ protected ObjectMapper createRequestObjectMapper() PropertyService propertyService = PropertyService.get(); if (null != propertyService) { - ObjectMapper mapper = JsonUtil.DEFAULT_MAPPER.copy(); - mapper.addMixIn(GWTPropertyDescriptor.class, MetadataTableJSONMixin.class); - return mapper; + return JsonUtil.DEFAULT_MAPPER.copy(); } else { diff --git a/query/src/org/labkey/query/model/MetadataTableJSONMixin.java b/query/src/org/labkey/query/model/MetadataTableJSONMixin.java deleted file mode 100644 index 536cecdf6a9..00000000000 --- a/query/src/org/labkey/query/model/MetadataTableJSONMixin.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.labkey.query.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public abstract class MetadataTableJSONMixin -{ - MetadataTableJSONMixin(@JsonProperty("URL") String url) - { } - - @JsonProperty("URL") - abstract void setURL(String url); // rename property on deserialize -} diff --git a/study/src/org/labkey/study/assay/AssayPublishConfirmAction.java b/study/src/org/labkey/study/assay/AssayPublishConfirmAction.java index 54b73e1db2d..53c177ee588 100644 --- a/study/src/org/labkey/study/assay/AssayPublishConfirmAction.java +++ b/study/src/org/labkey/study/assay/AssayPublishConfirmAction.java @@ -10,10 +10,10 @@ import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.Container; import org.labkey.api.data.JdbcType; +import org.labkey.api.exp.PropertyType; import org.labkey.api.exp.api.ExpProtocol; import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.property.DomainProperty; -import org.labkey.api.gwt.client.ui.PropertyType; import org.labkey.api.query.FieldKey; import org.labkey.api.query.QuerySettings; import org.labkey.api.query.QueryView; diff --git a/study/src/org/labkey/study/assay/StudyPublishManager.java b/study/src/org/labkey/study/assay/StudyPublishManager.java index f0a74f4cbf0..119b7ef05db 100644 --- a/study/src/org/labkey/study/assay/StudyPublishManager.java +++ b/study/src/org/labkey/study/assay/StudyPublishManager.java @@ -1356,7 +1356,7 @@ public ActionURL autoLinkResults(ExpProtocol protocol, AssayProvider provider, E for (ColumnInfo c : resultTable.getColumns()) { // Check for a column with the PTID concept URI instead - if (org.labkey.api.gwt.client.ui.PropertyType.PARTICIPANT_CONCEPT_URI.equals(c.getConceptURI())) + if (PropertyType.PARTICIPANT_CONCEPT_URI.equals(c.getConceptURI())) { ptidFK = c.getFieldKey(); } @@ -1717,7 +1717,7 @@ public Map getSamplePublishFieldKeys(User user, Conta if (col != null) { - if (org.labkey.api.gwt.client.ui.PropertyType.VISIT_CONCEPT_URI.equalsIgnoreCase(col.getConceptURI())) + if (PropertyType.VISIT_CONCEPT_URI.equalsIgnoreCase(col.getConceptURI())) { if (!fieldKeyMap.containsKey(LinkToStudyKeys.VisitId) && col.getJdbcType().isReal()) fieldKeyMap.put(LinkToStudyKeys.VisitId, ci.getFieldKey()); @@ -1727,7 +1727,7 @@ public Map getSamplePublishFieldKeys(User user, Conta fieldKeyMap.put(LinkToStudyKeys.VisitLabel, ci.getFieldKey()); } - if (!fieldKeyMap.containsKey(LinkToStudyKeys.ParticipantId) && org.labkey.api.gwt.client.ui.PropertyType.PARTICIPANT_CONCEPT_URI.equalsIgnoreCase(col.getConceptURI())) + if (!fieldKeyMap.containsKey(LinkToStudyKeys.ParticipantId) && PropertyType.PARTICIPANT_CONCEPT_URI.equalsIgnoreCase(col.getConceptURI())) { fieldKeyMap.put(LinkToStudyKeys.ParticipantId, ci.getFieldKey()); } From c1d4962021b13a7a4d82ca304d30f275df69291d Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 8 May 2026 10:06:11 -0700 Subject: [PATCH 15/18] Delete GWT --- .../src/org/labkey/assay/PlateController.java | 72 ----- .../assay/plate/PlateDataServiceImpl.java | 258 ------------------ 2 files changed, 330 deletions(-) delete mode 100644 assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 9f2f465697c..362c014cc1c 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -22,7 +22,6 @@ import org.labkey.api.action.FormHandlerAction; import org.labkey.api.action.JsonInputLimit; import org.labkey.api.action.FormViewAction; -import org.labkey.api.action.GWTServiceAction; import org.labkey.api.action.Marshal; import org.labkey.api.action.Marshaller; import org.labkey.api.action.MutatingApiAction; @@ -46,7 +45,6 @@ import org.labkey.api.data.ContainerManager; import org.labkey.api.data.TSVWriter; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; -import org.labkey.api.gwt.server.BaseRemoteService; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.FieldKey; import org.labkey.api.query.ValidationException; @@ -76,7 +74,6 @@ import org.labkey.api.view.NavTree; import org.labkey.api.view.NotFoundException; import org.labkey.api.writer.ZipFile; -import org.labkey.assay.plate.PlateDataServiceImpl; import org.labkey.assay.plate.PlateImpl; import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.PlateSetExport; @@ -85,7 +82,6 @@ import org.labkey.assay.plate.WellGroupImpl; import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.ReformatOptions; -import org.labkey.assay.view.AssayGWTView; import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.web.servlet.ModelAndView; @@ -216,17 +212,6 @@ public void addNavTrail(NavTree root) } } - /** Delete soon! */ - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public static class DesignerServiceAction extends GWTServiceAction - { - @Override - protected BaseRemoteService createService() throws IllegalStateException - { - return new PlateDataServiceImpl(getViewContext()); - } - } - public static class RowIdForm { private int _rowId; @@ -608,63 +593,6 @@ public void addNavTrail(NavTree root) } } - /** Delete soon! */ - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public class DesignerGwtAction extends SimpleViewAction - { - @Override - public ModelAndView getView(DesignerForm form, BindException errors) - { - Map properties = new HashMap<>(); - String templateName = null; - Long plateId = null; - - if (form.getTemplateName() != null) - { - plateId = form.getPlateId(); - templateName = form.getTemplateName(); - } - else if (form.getPlateId() != null) - { - Plate plate = PlateManager.get().getPlate(getContainer(), form.getPlateId()); - if (plate != null) - { - plateId = plate.getRowId(); - templateName = plate.getName(); - } - } - - if (templateName != null) - { - properties.put("copyTemplate", Boolean.toString(form.isCopy())); - properties.put("templateName", templateName); - if (plateId != null) - properties.put("plateId", String.valueOf(plateId)); - if (form.isCopy()) - properties.put("defaultPlateName", getUniqueName(getContainer(), templateName)); - else - properties.put("defaultPlateName", templateName); - } - - if (form.getAssayType() != null) - properties.put("assayTypeName", form.getAssayType()); - if (form.getTemplateType() != null) - properties.put("templateTypeName", form.getTemplateType()); - - properties.put("templateRowCount", String.valueOf(form.getRowCount())); - properties.put("templateColumnCount", String.valueOf(form.getColCount())); - - return new AssayGWTView(gwt.client.org.labkey.plate.designer.client.TemplateDesigner.class, properties); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("editPlateTemplate"); - root.addChild("Plate Editor (GWT)"); - } - } - @RequiresAnyOf({DeletePermission.class, DesignAssayPermission.class}) public static class DeleteAction extends FormHandlerAction { diff --git a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java deleted file mode 100644 index af614c9465f..00000000000 --- a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (c) 2008-2019 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.assay.plate; - -import gwt.client.org.labkey.plate.designer.client.PlateDataService; -import gwt.client.org.labkey.plate.designer.client.model.GWTPlate; -import gwt.client.org.labkey.plate.designer.client.model.GWTPosition; -import gwt.client.org.labkey.plate.designer.client.model.GWTWellGroup; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.labkey.api.assay.plate.Plate; -import org.labkey.api.assay.plate.PlateLayoutHandler; -import org.labkey.api.assay.plate.PlateService; -import org.labkey.api.assay.plate.PlateType; -import org.labkey.api.assay.plate.Position; -import org.labkey.api.assay.plate.WellGroup; -import org.labkey.api.gwt.server.BaseRemoteService; -import org.labkey.api.query.BatchValidationException; -import org.labkey.api.query.ValidationException; -import org.labkey.api.view.ViewContext; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * User: brittp - * Date: Jan 31, 2007 - * Time: 2:38:17 PM - */ -public class PlateDataServiceImpl extends BaseRemoteService implements PlateDataService -{ - private static final Logger LOG = LogManager.getLogger(PlateDataServiceImpl.class); - - public PlateDataServiceImpl(ViewContext context) - { - super(context); - } - - @Override - public GWTPlate getTemplateDefinition(String templateName, int plateId, String assayTypeName, String templateTypeName, int rowCount, int columnCount, boolean copyTemplate) throws Exception - { - try - { - Plate template; - PlateLayoutHandler handler; - - if (templateName != null) - { - // existing template - template = PlateService.get().getPlate(getContainer(), plateId); - if (template == null) - throw new Exception("Plate " + templateName + " does not exist."); - - handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); - if (handler == null) - throw new Exception("Plate template type " + template.getAssayType() + " does not exist."); - } - else - { - // new default template - handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); - if (handler == null) - throw new Exception("Plate template type " + assayTypeName + " does not exist."); - - PlateType plateType = PlateService.get().getPlateType(rowCount, columnCount); - if (plateType == null) - throw new Exception("The plate type : (" + rowCount + " x " + columnCount + ") does not exist"); - template = handler.createPlate(templateTypeName, getContainer(), plateType); - } - - // Translate PlateTemplate to GWTPlate - List groups = template.getWellGroups(); - List translated = new ArrayList<>(); - for (int i = 0; i < groups.size(); i++) - { - WellGroup group = groups.get(i); - List positions = new ArrayList<>(group.getPositions().size()); - for (Position position : group.getPositions()) - positions.add(new GWTPosition(position.getRow(), position.getColumn())); - Map groupProperties = new HashMap<>(); - for (String propName : group.getPropertyNames()) - { - groupProperties.put(propName, group.getProperty(propName)); - } - - // NOTE: Use negative rowId for unsaved well groups to support GWTWellGroup.equals() - int wellGroupId = copyTemplate || group.getRowId() == null ? -1 * (i+1) : group.getRowId(); - GWTWellGroup gwtWellGroup = new GWTWellGroup(wellGroupId, group.getType().name(), group.getName(), positions, groupProperties); - gwtWellGroup.setAllowNewGroups(handler.canCreateNewGroups(group.getType())); - translated.add(gwtWellGroup); - } - - long newPlateId = copyTemplate || template.getRowId() == null ? -1 : template.getRowId(); - GWTPlate plate = new GWTPlate(newPlateId, - template.getName(), template.getAssayType(), template.getRows(), - template.getColumns(), getTypeList(template), handler.showEditorWarningPanel()); - plate.setGroups(translated); - plate.setTypesToDefaultGroups(handler.getDefaultGroupsForTypes()); - - Map templateProperties = new HashMap<>(); - for (String propName : template.getPropertyNames()) - { - templateProperties.put(propName, template.getProperty(propName) == null ? null : template.getProperty(propName).toString()); - } - plate.setPlateProperties(templateProperties); - return plate; - } - catch (SQLException e) - { - LOG.error("Error create plate from template", e); - throw new Exception(e); - } - } - - private List getTypeList(Plate template) - { - List wellTypes = Arrays.asList( - WellGroup.Type.CONTROL, WellGroup.Type.SPECIMEN, - WellGroup.Type.REPLICATE, WellGroup.Type.OTHER); - - PlateLayoutHandler handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); - if (handler != null) - wellTypes = handler.getWellGroupTypes(); - - List types = new ArrayList<>(); - for (WellGroup.Type type : wellTypes) - types.add(type.name()); - return types; - } - - @Override - public long saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exception - { - try - { - boolean updateExisting = false; - Plate plate; - if (gwtPlate.getRowId() > 0) - { - plate = PlateManager.get().getPlate(getContainer(), gwtPlate.getRowId()); - if (plate == null) - throw new Exception("Plate template not found: " + gwtPlate.getRowId()); - - // check another plate of the same name doesn't already exist - if (PlateManager.get().isDuplicatePlateName(getContainer(), getUser(), gwtPlate.getName(), null) && !replaceIfExisting) - throw new Exception("A plate template with name '" + gwtPlate.getName() + "' already exists."); - - if (!plate.getAssayType().equals(gwtPlate.getType())) - throw new Exception("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + gwtPlate.getName() + "'"); - - if (plate.getRows() != gwtPlate.getRows() || plate.getColumns() != gwtPlate.getCols()) - throw new Exception("Plate template dimensions cannot be changed for '" + gwtPlate.getName() + "'"); - - // TODO: Use a version column to avoid concurrent updates - - updateExisting = true; - } - else - { - // check another plate of the same name doesn't already exist - Plate other = PlateManager.get().getPlateByName(getContainer(), gwtPlate.getName()); - if (other != null) - { - if (!replaceIfExisting) - throw new Exception("A plate template with name '" + gwtPlate.getName() + "' already exists."); - - // delete the existing plate first - PlateService.get().deletePlate(getContainer(), getUser(), other.getRowId()); - } - - PlateType plateType = PlateService.get().getPlateType(gwtPlate.getRows(), gwtPlate.getCols()); - if (plateType == null) - throw new Exception("The plate type : (" + gwtPlate.getRows() + " x " + gwtPlate.getCols() + ") does not exist"); - plate = PlateManager.get().createPlate(getContainer(), gwtPlate.getType(), plateType); - } - - plate.setName(gwtPlate.getName()); - plate.setProperties(gwtPlate.getPlateProperties()); - - // first, mark well groups not submitted for saving as deleted - Set groups = gwtPlate.getGroups(); - List existingWellGroups = plate.getWellGroups(); - for (WellGroup existingWellGroup : existingWellGroups) - { - if (groups.stream().noneMatch(g-> g.getRowId() == existingWellGroup.getRowId())) - ((PlateImpl)plate).markWellGroupForDeletion(existingWellGroup); - } - - // next, update positions on existing well groups or create new well groups - for (GWTWellGroup gwtGroup : groups) - { - WellGroup.Type groupType = WellGroup.Type.valueOf(gwtGroup.getType()); - List positions = new ArrayList<>(); - for (GWTPosition gwtPosition : gwtGroup.getPositions()) - positions.add(plate.getPosition(gwtPosition.getRow(), gwtPosition.getCol())); - - WellGroupImpl group; - if (updateExisting && gwtGroup.getRowId() > 0) - { - group = findExistingWellGroup(existingWellGroups, gwtGroup.getRowId()); - if (group == null) - throw new Exception("Well group " + gwtGroup.getRowId() + " wasn't found"); - if (group.getType() != groupType) - throw new Exception("Well group cannot be changed: " + gwtGroup.getName()); - - group.setName(gwtGroup.getName()); - group.setPositions(positions); - - ((PlateImpl)plate).storeWellGroup(group); - } - else - { - assert gwtGroup.getRowId() <= 0 : "Updating existing well group on a new template"; - group = (WellGroupImpl) plate.addWellGroup(gwtGroup.getName(), groupType, positions); - } - - group.setProperties(gwtGroup.getProperties()); - } - - PlateManager.get().getPlateLayoutHandler(plate.getAssayType()).validatePlate(getContainer(), getUser(), plate); - return PlateService.get().save(getContainer(), getUser(), plate); - } - catch (BatchValidationException | ValidationException e) - { - LOG.error("Error saving plate", e); - throw new Exception(e); - } - } - - private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) - { - for (WellGroup wellGroup : wellGroups) - { - if (wellGroup.getRowId() != null && wellGroup.getRowId() == rowId) - return (WellGroupImpl) wellGroup; - } - return null; - } -} From 41712c131a338060fc7749027dd76cb5cc9d7b92 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 8 May 2026 14:32:05 -0700 Subject: [PATCH 16/18] Fix saving an assay design --- .../org/labkey/assay/actions/SaveProtocolAction.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/assay/src/org/labkey/assay/actions/SaveProtocolAction.java b/assay/src/org/labkey/assay/actions/SaveProtocolAction.java index 4721d83058c..ee907987b59 100644 --- a/assay/src/org/labkey/assay/actions/SaveProtocolAction.java +++ b/assay/src/org/labkey/assay/actions/SaveProtocolAction.java @@ -15,15 +15,19 @@ */ package org.labkey.assay.actions; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import org.labkey.api.action.Marshal; import org.labkey.api.action.Marshaller; import org.labkey.api.action.MutatingApiAction; import org.labkey.api.assay.AssayDomainService; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.exp.api.ExpProtocol; +import org.labkey.api.exp.property.PropertyService; import org.labkey.api.gwt.client.assay.model.GWTProtocol; import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.util.JsonUtil; import org.labkey.api.view.NotFoundException; import org.labkey.api.view.UnauthorizedException; import org.labkey.assay.AssayDomainServiceImpl; @@ -34,6 +38,14 @@ @RequiresPermission(ReadPermission.class) public class SaveProtocolAction extends MutatingApiAction { + @Override + protected ObjectMapper createRequestObjectMapper() + { + ObjectMapper om = JsonUtil.DEFAULT_MAPPER.copy(); + PropertyService.get().configureObjectMapper(om, null); + return om; + } + @Override public Object execute(GWTProtocol protocol, BindException errors) throws Exception { From 56353fa6ce924584a5a89517b6ad0b58199a4ab5 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 8 May 2026 15:10:40 -0700 Subject: [PATCH 17/18] Minor cleanup --- .../api/gwt/client/model/GWTPropertyValidator.java | 13 +++++++------ .../labkey/assay/actions/SaveProtocolAction.java | 1 - .../experiment/api/ExperimentServiceImpl.java | 3 +-- .../labkey/study/controllers/CohortController.java | 8 ++++---- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java b/api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java index c3d82774b53..148a02dc191 100644 --- a/api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java +++ b/api/src/org/labkey/api/gwt/client/model/GWTPropertyValidator.java @@ -15,13 +15,14 @@ */ package org.labkey.api.gwt.client.model; -import com.google.api.client.util.Objects; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /* * User: Karl Lum @@ -174,12 +175,12 @@ public boolean equals(Object o) return false; if (getRowId() != that.getRowId()) return false; - if (!StringUtils.equals(getName(), that.getName())) return false; + if (!Strings.CS.equals(getName(), that.getName())) return false; Object o1 = getType(); - if (!Objects.equal(o1, that.getType())) return false; - if (!StringUtils.equals(getDescription(), that.getDescription())) return false; - if (!StringUtils.equals(getExpression(), that.getExpression())) return false; - if (!StringUtils.equals(getErrorMessage(), that.getErrorMessage())) return false; + if (!Objects.equals(o1, that.getType())) return false; + if (!Strings.CS.equals(getDescription(), that.getDescription())) return false; + if (!Strings.CS.equals(getExpression(), that.getExpression())) return false; + if (!Strings.CS.equals(getErrorMessage(), that.getErrorMessage())) return false; if (!getProperties().equals(that.getProperties())) return false; return true; diff --git a/assay/src/org/labkey/assay/actions/SaveProtocolAction.java b/assay/src/org/labkey/assay/actions/SaveProtocolAction.java index ee907987b59..b3a5e3c2581 100644 --- a/assay/src/org/labkey/assay/actions/SaveProtocolAction.java +++ b/assay/src/org/labkey/assay/actions/SaveProtocolAction.java @@ -15,7 +15,6 @@ */ package org.labkey.assay.actions; -import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.labkey.api.action.Marshal; import org.labkey.api.action.Marshaller; diff --git a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java index 62a3a24dc08..843bd1e491a 100644 --- a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java @@ -26,7 +26,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.math.NumberUtils; -import org.apache.commons.lang3.mutable.MutableInt; import org.apache.commons.lang3.mutable.MutableLong; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -1077,7 +1076,7 @@ private void indexMaterials(final @NotNull SearchService.TaskIndexingQueue queue sql = getSchema().getSqlDialect().limitRows(sql, INDEXING_LIMIT); SqlSelector selector = new SqlSelector(getSchema(), sql); selector.setJdbcCaching(false); - MutableInt maxRowIdProcessed = new MutableInt(minRowId); + MutableLong maxRowIdProcessed = new MutableLong(minRowId); // Work in modest block sizes and fetch as a list so we don't keep the ResultSet open, which could lock the tables List materials = selector.getArrayList(Material.class); diff --git a/study/src/org/labkey/study/controllers/CohortController.java b/study/src/org/labkey/study/controllers/CohortController.java index 119bb1b78a1..a62e09f6bec 100644 --- a/study/src/org/labkey/study/controllers/CohortController.java +++ b/study/src/org/labkey/study/controllers/CohortController.java @@ -15,7 +15,6 @@ */ package org.labkey.study.controllers; -import com.google.api.client.util.Objects; import org.apache.commons.lang3.Strings; import org.labkey.api.action.FormHandlerAction; import org.labkey.api.action.FormViewAction; @@ -60,6 +59,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import static org.labkey.api.util.IntegerUtils.asInteger; @@ -428,10 +428,10 @@ public boolean handlePost(EditCohortForm form, BindException errors) boolean labelChanged = (newLabel != null && !cohort.getLabel().equals(newLabel)); boolean enrolledChanged = cohort.isEnrolled() != newEnrolled; Object o1 = cohort.getSubjectCount(); - boolean subjectCountChanged = !Objects.equal(o1, newSubjectCount); - boolean desciprtionChanged = !Strings.CS.equals(cohort.getDescription(), newDescription); + boolean subjectCountChanged = !Objects.equals(o1, newSubjectCount); + boolean descriptionChanged = !Strings.CS.equals(cohort.getDescription(), newDescription); - if (labelChanged || enrolledChanged || subjectCountChanged || desciprtionChanged) + if (labelChanged || enrolledChanged || subjectCountChanged || descriptionChanged) { cohort = cohort.createMutable(); From b8a480b3edf02f4474ffbc0b0e52e3cf6ae0f044 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 8 May 2026 15:27:06 -0700 Subject: [PATCH 18/18] Remove GWT references --- .../labkey/test/pages/AssayQueryConfig.java | 65 ---- .../DesignerController/DesignerTester.java | 302 ------------------ 2 files changed, 367 deletions(-) delete mode 100644 study/test/src/org/labkey/test/pages/AssayQueryConfig.java delete mode 100644 study/test/src/org/labkey/test/pages/DesignerController/DesignerTester.java diff --git a/study/test/src/org/labkey/test/pages/AssayQueryConfig.java b/study/test/src/org/labkey/test/pages/AssayQueryConfig.java deleted file mode 100644 index 3afa3aac71a..00000000000 --- a/study/test/src/org/labkey/test/pages/AssayQueryConfig.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2017 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.test.pages; - -import org.labkey.test.Locator; -import org.labkey.test.util.Ext4Helper; -import org.openqa.selenium.WebDriver; - -public class AssayQueryConfig extends LabKeyPage -{ - public AssayQueryConfig(WebDriver driver, int index) - { - super(driver); - click(getEditLink(index)); - } - - private Locator getEditLink(int index) - { - return Locators.self.append(Locator.tagWithAttribute("span", "dataindex", String.valueOf(index))); - } - - public void setFolderName(String name) - { - setFormElement(Locators.folderName, name); - } - - public void setSchemaName(String name) - { - _ext4Helper.selectComboBoxItem("Schema name:", name); - waitForElementToDisappear(Ext4Helper.Locators.mask().index(1)); - } - - public void setQueryName(String name) - { - _ext4Helper.selectComboBoxItem("Query name:", name); - waitForElementToDisappear(Ext4Helper.Locators.mask().index(1)); - } - - public void save() - { - clickButton("Submit", 0); - } - - public static class Locators - { - public static Locator.XPathLocator self = Locator.xpath("//table[contains(@class, 'assay-summary')]"); - public static Locator.XPathLocator dialog = Locator.xpath("//div[contains(@class, 'labkey-assay-config')]"); - public static Locator.XPathLocator folderName = dialog.append(Locator.input("Folder")); - public static Locator.XPathLocator schemaName = dialog.append(Locator.input("Schema")); - public static Locator.XPathLocator queryName = dialog.append(Locator.input("listTable")); - } -} diff --git a/study/test/src/org/labkey/test/pages/DesignerController/DesignerTester.java b/study/test/src/org/labkey/test/pages/DesignerController/DesignerTester.java deleted file mode 100644 index f1194a73440..00000000000 --- a/study/test/src/org/labkey/test/pages/DesignerController/DesignerTester.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 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.test.pages.DesignerController; - -import org.jetbrains.annotations.Nullable; -import org.junit.Assert; -import org.labkey.test.BaseWebDriverTest; -import org.labkey.test.Locator; -import org.openqa.selenium.WebElement; - -import java.util.List; - -/** - * org.labkey.study.controllers.designer.DesignerController#DesignerAction - */ -public class DesignerTester -{ - private final BaseWebDriverTest _test; - - public DesignerTester(BaseWebDriverTest test) - { - _test = test; - - _test.waitForElement(Locator.css("input.gwt-TextBox")); - } - - public void editDesign() - { - _test.clickButton("Edit"); - waitForEditReady(); - } - - private void waitForEditReady() - { - _test.waitForTextToDisappear("Loading", BaseWebDriverTest.WAIT_FOR_JAVASCRIPT); - _test.waitForElement(Locator.gwtTextBoxByLabel("Protocol Name"), BaseWebDriverTest.WAIT_FOR_PAGE); - } - - public void saveDesign() - { - _test.waitForElementToDisappear(Locator.tag("a").withText("Save").withClass("labkey-disabled-button")); - - int revisions = _test.getElementCount(Locator.gwtListBoxByLabel("Revision").append("/option")); - - _test.clickButton("Save", 0); - Locator successText = Locator.tagWithClass("div", "gwt-Label").withText("Revision " + (revisions + 1) + " saved successfully."); - if (!_test.waitForElement(successText, 1000, false)) - { - _test.clickButton("Save", 0); // GWT retry - } - _test.waitForElement(successText); - _test.waitForElement(Locator.tag("a").withText("Save").withClass("labkey-disabled-button")); - } - - public void finishEditing() - { - _test.clickButton("Finished"); - } - - public void setName(String name) - { - _test.setFormElement(Locator.gwtTextBoxByLabel("Protocol Name"), name); - } - - public void setInvestigator(String investigator) - { - _test.setFormElement(Locator.gwtTextBoxByLabel("Investigator"), investigator); - } - - public void setGrant(String grant) - { - _test.setFormElement(Locator.gwtTextBoxByLabel("Grant"), grant); - } - - public void setSpecies(String species) - { - _test.setFormElement(Locator.gwtTextBoxByLabel("Species"), species); - } - - public void setDescription(String description) - { - _test.fireEvent(Locator.xpath("//div[contains(text(), 'Click to edit description')]"), BaseWebDriverTest.SeleniumEvent.focus); - _test.setFormElement(Locator.name("protocolDescription"), description); - } - - public void addImmunogen(String name, String type, String doseAndUnits, String route) - { - int newRowNumber = _test.getElementCount(Locators.immunogenGridRow()); - - setImmunogenName(newRowNumber, name); - setImmunogenType(newRowNumber, type); - setImmunogenDoseAndUnits(newRowNumber, doseAndUnits); - setImmunogenRoute(newRowNumber, route); - } - - public void setImmunogenName(int immunogenRowNumber, String name) - { - Locator immunogenNameField = Locators.immunogenGridRow().index(immunogenRowNumber - 1).append("/td[2]/input"); - _test.setFormElement(immunogenNameField, name); - _test.fireEvent(immunogenNameField, BaseWebDriverTest.SeleniumEvent.change); - } - - public void setImmunogenType(int immunogenRowNumber, String type) - { - Locator immunogenTypeField = Locators.immunogenGridRow().index(immunogenRowNumber - 1).append("/td[3]/select"); - _test.selectOptionByText(immunogenTypeField, type); - } - - public void setImmunogenDoseAndUnits(int immunogenRowNumber, String doseAndUnits) - { - Locator immunogenRouteField = Locators.immunogenGridRow().index(immunogenRowNumber - 1).append("/td[4]/input"); - _test.setFormElement(immunogenRouteField, doseAndUnits); - _test.fireEvent(immunogenRouteField, BaseWebDriverTest.SeleniumEvent.change); - } - - public void setImmunogenRoute(int immunogenRowNumber, String route) - { - Locator immunogenRouteField = Locators.immunogenGridRow().index(immunogenRowNumber - 1).append("/td[5]/select"); - _test.selectOptionByText(immunogenRouteField, route); - } - - public void addAntigen(String immunogenName, String gene, String subType) - { - throw new RuntimeException("Not yet implemented"); - } - - public void addAdjuvant(String name, @Nullable String doseAndUnits, @Nullable String route) - { - int newRowNumber = _test.getElementCount(Locators.adjuvantGridRow()); - - setAdjuvantName(newRowNumber, name); - if (null != doseAndUnits) - setAdjuvantDoseAndUnits(newRowNumber, doseAndUnits); - if (null != route) - setAdjuvantRoute(newRowNumber, route); - } - - public void setAdjuvantName(int adjuvantRowNumber, String name) - { - Locator adjuvantNameField = Locators.adjuvantGridRow().index(adjuvantRowNumber - 1).append("/td[2]/input"); - _test.setFormElement(adjuvantNameField, name); - _test.fireEvent(adjuvantNameField, BaseWebDriverTest.SeleniumEvent.change); - } - - public void setAdjuvantDoseAndUnits(int adjuvantRowNumber, String doseAndUnits) - { - Locator adjuvantRouteField = Locators.adjuvantGridRow().index(adjuvantRowNumber - 1).append("/td[3]/input"); - _test.setFormElement(adjuvantRouteField, doseAndUnits); - _test.fireEvent(adjuvantRouteField, BaseWebDriverTest.SeleniumEvent.change); - } - - public void setAdjuvantRoute(int adjuvantRowNumber, String route) - { - Locator adjuvantRouteField = Locators.adjuvantGridRow().index(adjuvantRowNumber - 1).append("/td[4]/select"); - _test.selectOptionByText(adjuvantRouteField, route); - } - - public void addImmunizationGroup(String name, @Nullable String count) - { - int newRowNumber = _test.getElementCount(Locators.immunizationGridRow()); - - _test.click(Locator.tagWithText("div", "Add New")); - if (!_test.waitForElement(Locator.id("DefineGroupDialog"), 1000, false)) - { - _test.click(Locator.tagWithText("div", "Add New")); - } - _test.waitForElement(Locator.id("DefineGroupDialog")); - _test.setFormElement(Locator.name("newName"), name); - _test.clickButton("OK", 0); - _test.waitForElementToDisappear(Locator.id("DefineGroupDialog"), BaseWebDriverTest.WAIT_FOR_JAVASCRIPT); - _test.waitForElement(Locators.immunizationGridRow().index(newRowNumber - 1)); - - if (null != count) - { - setImmunizationGroupCount(newRowNumber, count); - } - } - - public void setImmunizationGroupCount(int groupRowNumber, String count) - { - Locator immunizationGroupCountField = Locators.immunizationGridRow().index(groupRowNumber - 1).append("/td[3]/input"); - _test.setFormElementJS(immunizationGroupCountField, count); - _test.fireEvent(immunizationGroupCountField, BaseWebDriverTest.SeleniumEvent.change); - } - - public void addImmunizationTimepoint(String timepoint) - { - addImmunizationTimepoint(null, timepoint, null); - } - - public void addImmunizationTimepoint(@Nullable String name, String timepoint, @Nullable String unit) - { - _test.click(Locator.xpath("//table[@id='ImmunizationGrid']//div[contains(text(), 'Add Timepoint')]")); - if (null != name) _test.setFormElement(Locator.name("timepointName"), name); - _test.setFormElement(Locator.name("timepointCount"), timepoint); - if (null != unit) _test.selectOptionByText(Locator.name("timepointUnit"), unit); - _test.click(Locator.tagWithText("button", "OK")); - } - - public void defineFirstUndefinedImmunization(List immunogensAndAdjuvants) - { - _test.click(Locator.xpath("//div[contains(text(), '(none)')]")); - _test.waitForElement(Locator.css("div.gwt-DialogBox")); - - for (String immunogenOrAdjuvant : immunogensAndAdjuvants) - { - _test.checkCheckbox(Locator.xpath(String.format("//label[text()='%s']/../input", immunogenOrAdjuvant))); - } - - _test.click(Locator.tagWithText("button", "Done")); - _test.waitForElementToDisappear(Locator.css("div.gwt-DialogBox")); - } - - public void setAssayPlan(String description) - { - _test.fireEvent(Locator.xpath("//div[contains(text(), 'Click to type assay plan here')]"), BaseWebDriverTest.SeleniumEvent.focus); - _test.setFormElement(Locator.name("assayPlan"), description); - } - - public void addAssay(String assay, String lab) - { - int newRowNumber = _test.getElementCount(Locators.assayGridRow()); - - Locator assayNameField = Locators.assayGridRow().index(newRowNumber - 1).append("/td[2]/select"); - _test.selectOptionByText(assayNameField, assay); - - setAssayLab(newRowNumber, lab); - } - - public void setAssayLab(int assayRowNumber, String lab) - { - Locator assayLabField = Locators.assayGridRow().index(assayRowNumber - 1).append("/td[3]/select"); - _test.selectOptionByText(assayLabField, lab); - } - - public void addAssayTimepoint(@Nullable String name, String timepoint, @Nullable String unit) - { - _test.click(Locator.xpath("//table[@id='AssayGrid']//div[contains(text(), 'Add Timepoint')]")); - if (null != name) _test.setFormElement(Locator.name("timepointName"), name); - _test.setFormElement(Locator.name("timepointCount"), timepoint); - if (null != unit) _test.selectOptionByText(Locator.name("timepointUnit"), unit); - _test.click(Locator.tagWithText("button", "OK")); - } - - public void setAssayRequiredForTimepoint(String assayName, String timepointSubstring) - { - Locator.XPathLocator assayGridRow = Locators.assayGridRow().withDescendant(Locator.tagWithText("td", assayName)); - Locator.XPathLocator timepointColumnHeader = Locator.id("AssayGrid").append(Locator.tagWithAttribute("div", "title", "Click to modify or delete this timepoint.")); - int timepointIndex = -1; - - List timepointColumnHeaders = timepointColumnHeader.findElements(_test.getDriver()); - for (int i = 0; i < timepointColumnHeaders.size(); i++) - { - String timepointLabel = timepointColumnHeaders.get(i).getText(); - if (timepointLabel.contains(timepointSubstring)) - { - timepointIndex = i; - break; - } - } - - Assert.assertFalse("Could not find Assay timepoint: " + timepointSubstring, timepointIndex < 0); - - _test.checkCheckbox(assayGridRow.append(Locator.tagWithAttribute("input", "type", "checkbox")).index(timepointIndex)); - } - - public static class Locators - { - public static Locator.XPathLocator immunogenGridRow() - { - return Locator.xpath("//table[@id='ImmunogenGrid']/tbody/tr[./td[1][contains(@class, 'row-header')]]"); - } - - public static Locator.XPathLocator adjuvantGridRow() - { - return Locator.xpath("//table[@id='AdjuvantGrid']/tbody/tr[./td[1][contains(@class, 'row-header')]]"); - } - - public static Locator.XPathLocator immunizationGridRow() - { - return Locator.xpath("//table[@id='ImmunizationGrid']/tbody/tr[./td[1][contains(@class, 'row-header')]]"); - } - - public static Locator.XPathLocator assayGridRow() - { - return Locator.xpath("//table[@id='AssayGrid']/tbody/tr[./td[1][contains(@class, 'row-header')]]"); - } - } -}
{getRowLabel(row)}